F#で簡素なモゲマスコンプガチャシミュレータ
椎名林檎「自由へ道連れ」をヘビロテしすぎて脳内無限ループしている今日この頃ですが、皆様いかがお過ごしでしょうか。
時事ネタとしては旬を逃した感じですが、簡素なコンプガチャシミュレータをF#で書いてみました。
とは言っても、この記事で伝えたいことはコンプガチャの確率がどうのですとか、実社会におけるコンプガチャの問題点がどうのとかいうような話題を扱うものではなく、安直にモナド則を満たさないコンピューテーション式を作ってしまうよりかは、Identityモナド(恒等モナド)を使ってプログラミングをした方が、見通しが良くモジュール性の高いコードを書くことができるかもしれないよ、という話題を提供します。割とどうでもいい話題ですね。未だガラケーユーザーであり、スマホやソーシャルゲーとはほとんど縁のない私ですが(もちろんモゲマスもやったことない)、気が向いたのでちょっと書いてみました。なお、モゲマスおよびFSharpxのステマではありません。
シミュレートするコンプガチャの仕様
まずはシミュレートするコンプガチャの仕様について簡単に確認しておきましょう。今回実装してみるのは、モゲマスことモバゲー『アイドルマスター シンデレラガールズ』のコンプガチャ「パジャマパーティー」に近いコンプガチャのシミュレートを目的としてみます。
Google先生に聞いてきた モゲマス 「パジャマパーティー」コンプガチャの概要
時折行われる課金ガチャのイベント企画。1ガチャあたり300円を支払って利用するガチャです。1回に1枚のアイドルカードが得られます。実際の支払いは「モバコイン」ですが、モバコインは100G=100円で購入して利用するため、便宜上こちらではそのまま円とします。イベント期間中以下の「レア」パジャマアイドルが確率テーブルに5枚追加され、その5枚を全て集めると、特典として限定「Sレア」カード「[眠れる姫君]星井美希」を獲得できるというもの。
コンプ対象パジャマアイドル
・[パジャマパーティー]緒方智絵里 コスト12 攻2880 守1600 キュート攻中アップ
・[パジャマパーティー]間中美里 コスト08 攻1240 守1440
・[パジャマパーティー]黒川千秋 コスト10 攻1860 守1560 クール攻中アップ
・[パジャマパーティー]川島瑞樹 コスト09 攻1400 守1680
・[パジャマパーティー]若林智香 コスト12 攻1600 守2680 パッション守中アップ
なお、バンナムによる公式の発表はないが、いずれかのパジャマアイドルが出現する確率は12%程度とのこと。
※ガチャのセット販売も行われているが、ここでは1回ずつガチャを行うこととする。
※なお、ネットで適当に拾ってきた情報のため正確ではない可能性あり。
内部の実装の詳細はわかりませんが、「今すぐモゲマスPすべてにSレアを授けてみせろッ! ver. 0.141.33」というシミュレータが既にあるようです。
http://mugenmasakazu.plala.jp/cgi-bin/nijiuraIDOLMASTER/mogemaskillyourself.cgi
なお、コンプガチャがどうして危険と言われているのかの理由については、「コンプガチャの確率マジックを中学生にも分かるように説明するよ - てっく煮ブログ」の解説がわかりやすい。わたしが中学生にも分かるように説明するなら、例えばモンハンの素材であるところの「逆鱗(出現率2%)」が5種類あったとして、それらすべてを揃えないと作れない武器があったとき、それにかかる時間を想像してみると、コンプガチャへ挑む無謀さが割と想像しやすい、とか。欲しいと思う素材ほど出ないようになっているといわれる架空のシステム。いわゆる「センサー」の存在への疑い、とか。
簡素なコンプガチャシミュレータを愚直に書いてみる
まずは愚直に。細かいことは考えずにとりあえず実装してみたバージョン。
open System let tee x f = f x; x let (|>!) x f= tee x f let rand = new Random(DateTime.Now.Millisecond); type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象が出る確率 let probability = 0.12 let shuffle source = let array = List.toArray source let rec loop i = i |> function | 1 -> () | _ -> let i = i - 1 let j = rand.Next(i) let temp = array.[i] array.[i] <- array.[j] array.[j] <- temp; loop i loop source.Length [for x in array do yield x] let completeGacha lst count total = let items = let dummy p = let e = ((float comp.Length) / p) |> int [for i in 1..(e-comp.Length) do yield Other] let target = comp@dummy probability target |> shuffle let gacha () = rand.Next(1, items.Length) |> fun i -> items.[i] let rec gacha' count total = let newitem = gacha () let current = count + 1 if List.exists (fun x -> x = newitem) comp |> not then (* でねぇ!!!*) gacha' current total elif List.forall (fun x -> x = newitem |> not) lst |> not then (* ダブりかよ...orz *) gacha' current total else (* よっしゃー!なんという引きの良さ!!! *) lst@[newitem], current, (total + current), List.length (lst@[newitem]) = comp.Length gacha' count total let printGacha x = x |>! (fun (possession, n, total, complete) -> let g = sprintf "%d回:%d円 " n (300 * n) let sum = sprintf "合計%d円" (300 * total) let result = sprintf "%s" (if complete then "コンプ" else "未コンプ") printfn "%s %s %A %s" g sum possession result) let cut (a,b,c,d) = a,b,c completeGacha [] 0 0 |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |> fun _ -> printfn "[眠れる姫君]星井美希を手に入れた!" Console.ReadLine () |> ignore
ジェネリックもへったくれもない。いくら適当とはいえ愚直すぎて泣ける。
結果は当然実行ごとに毎回変わりますが、一応実行結果の例。
1回:300円 合計300円 [R "黒川千秋"] 未コンプ 8回:2400円 合計2700円 [R "黒川千秋"; R "緒方智絵里"] 未コンプ 38回:11400円 合計14100円 [R "黒川千秋"; R "緒方智絵里"; R "川島瑞樹"] 未コンプ 78回:23400円 合計37500円 [R "黒川千秋"; R "緒方智絵里"; R "川島瑞樹"; R "間中美里"] 未コンプ 108回:32400円 合計69900円 [R "黒川千秋"; R "緒方智絵里"; R "川島瑞樹"; R "間中美里"; R "若林智香"] コンプ [眠れる姫君]星井美希を手に入れた!
1回目でレアカード"黒川千秋"を引き当てるという強運を発揮するも、2枚目のレア"緒方智絵里"を引き当てるには8回かかる。
そして、あらあらまあまあ最終的には合計69900円のぶっこみ。バンナムにかなり貢ぎましたな。
簡素なコンプガチャシミュレータをコンピューテーション式で
愚直にもほどがあるので、もうちょっとなんとかしてみましょう。
completeGacha [] 0 0 |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |||> completeGacha |> printGacha |> cut |> fun _ -> printfn "[眠れる姫君]星井美希を手に入れた!"
上記部分に着目すると、なんだか順々に関数を適用していく流れが見えます。なんだかコンピューテーション式にできそうです。
ということで、とりあえずコンピューテーション式にしてみる。
namespace Library1 [<AutoOpen>] module CompleteGacha = open System let tee x f = f x; x let inline (|>!) x f= tee x f let rand = new Random(DateTime.Now.Millisecond); let shuffle source = let array = List.toArray source let rec loop i = i |> function | 1 -> () | _ -> let i = i - 1 let j = rand.Next(i) let temp = array.[i] array.[i] <- array.[j] array.[j] <- temp; loop i loop (List.length source) [for x in array do yield x] let completeGacha comp d probability (lst:'a list) count total = let items = let dummy p = let e = ((float <| List.length comp) / p) |> int [for i in 1..(e - (List.length comp)) do yield d] let target = comp@(dummy probability) target |> shuffle let gacha () = let i = rand.Next(1, (List.length items)) items.[i] let rec gacha' count total = let newitem = gacha () let current = count + 1 if List.exists (fun x -> x = newitem) comp |> not then (* でねぇ!!! *) gacha' current total elif List.forall (fun x -> x = newitem |> not) lst |> not then (* ダブりかよ...orz *) gacha' current total else (* よっしゃー!なんという引きの良さ!!! *) lst@[newitem], current, (total + current), List.length (lst@[newitem]) = List.length comp gacha' count total type CompGacha<'a> = CompGacha of 'a type CompGachaBuilder () = member this.Bind(m, f) : CompGacha<_> = let (CompGacha (comp, d, p, lst,count,total,complete)) = m let lst,count,total,complete = completeGacha comp d p lst count total f (comp,d, p, lst,count,total,complete) member this.Return x = CompGacha(x) member this.ReturnFrom x = x let cg = new CompGachaBuilder() let printGacha price unit f x = x |>! (fun (comp, d, p, possession, n, total, complete) -> let g = sprintf "%d回:%d%s" n (price * n) unit let sum = sprintf "合計%d%s" (price * total) unit let result = sprintf "%s" (if complete then "コンプ" else "未コンプ") printfn "%s %s %A %s" g sum possession result if List.length comp = List.length possession then f()) open FSharpx open Operators let inline returnM x = returnM cg x let inline (>>=) m f = bindM cg m f let inline (=<<) f m = bindM cg m f let inline ap m f = f <*> m let inline map f m = liftM cg f m let inline (<!>) f m = map f m let inline lift2 f a b = returnM f <*> a <*> b let inline (>>.) m f = bindM cg m (fun _ -> f) let inline (>=>) f g = fun x -> f x >>= g let inline (<=<) x = flip (>=>) x
利用側
open System open Library1 type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象アイテムが出る確率 let probability = 0.12 // 12% // 1ガチャあたり300円 let printg = printGacha 300 "円" (fun () -> printfn "[眠れる姫君]星井美希を手に入れた!") let mogemasu x = cg { return x } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } >>= fun x -> cg { return x |> printg } // 別の書き方 //let mogemasu x = // cg { let! x = cg { return x } // let! x = cg { return x |> printg } // let! x = cg { return x |> printg } // let! x = cg { return x |> printg } // let! x = cg { return x |> printg } // return x |> printg } (comp, other, probability, [], 0, 0, false) |> mogemasu |> ignore Console.ReadLine () |> ignore
とりあえずコンピューテーション式にしました以外の何物でもない。愚直版に比べるとそこそこ抽象化こそされているが、まだ不十分。コンプ対象カード中何枚揃えるまでガチャを行うかの部分がハードコーディングされている(この場合は5回のBindをすることで5枚揃えるまでガチャしている)。ちなみに、このコンピューテーション式は「Functor且つApplicative且つモナド」を満たさない。「コンピューテーション式がモナドである必要は必ずしもない」が、このような実装ではモジュール性の低下は否めない。
簡素なコンプガチャシミュレータをIdentityモナドで
HaskellでIdentityモナド(恒等モナド)と言えば、一般的にはモナド変換子からモナドを導出するために使われることで知られている。内部処理を伴わない単なる関数適用をモナドで表現する目的でIdentityモナドを使うことは、Haskellではあまりしないのかもしれない。しかし、まったく意味がないというわけではない。モナドを利用することで、モジュール性が高まりプログラムの見通しが良くなる。「Functor且つApplicative且つモナド」ではないコンピューテーション式をわざわざ作るよりかは、Identityモナドを使った実装の方が見通しの良いプログラムが書けるかもしれない。
ではやってみよう。
FSharpxには標準で実装されていないため、まずはIdentityモナドをつくる必要がある。
module Identity = type M<'T> = M of 'T let mreturn x : M<'T> = M x type IdentityBuilder () = member this.Return (x) = mreturn x member this.Bind ((M x),f) : M<'U> = f x let identity = IdentityBuilder () open FSharpx open Operators let inline returnM x = returnM identity x let inline (>>=) m f = bindM identity m f let inline (=<<) f m = bindM identity m f let inline (<*>) f m = applyM identity identity f m let inline ap m f = f <*> m let inline map f m = liftM identity f m let inline (<!>) f m = map f m let inline lift2 f a b = returnM f <*> a <*> b let inline ( *>) x y = lift2 (fun _ z -> z) x y let inline ( <*) x y = lift2 (fun z _ -> z) x y let inline (>>.) m f = bindM identity m (fun _ -> f) let inline (>=>) f g = fun x -> f x >>= g let inline (<=<) x = flip (>=>) x
利用側
open System open Library1 open Library1.Identity type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象アイテムが出る確率 let probability = 0.12 // 12% // 1ガチャあたり300円 let printg = printGacha 300 "円" (fun () -> printfn "[眠れる姫君]星井美希を手に入れた!") let compGacha x = identity { let comp,d,probability,lst,count,total,r = x let lst,count,total,r = completeGacha comp d probability lst count total return (comp,d,probability,lst,count,total,r ) |> printg } let mogemasu () = (comp, other, probability, [], 0, 0, false) |> fun x -> compGacha x >>= compGacha >>= compGacha >>= compGacha >>= compGacha // 別の書き方 //let mogemasu () = // (comp, other, probability, [], 0, 0, false) |> fun x -> // identity { let! x = compGacha x // let! x = compGacha x // let! x = compGacha x // let! x = compGacha x // let! x = compGacha x // return x } mogemasu () |> ignore System.Console.ReadLine () |> ignore Console.ReadLine () |> ignore
Identityモナドを用いて実装することにより、冗長なコンピューテーション式をわざわざ作らなくても、見通しがよいコードを書くことができた。しかも、これはモナドであるためモジュール性が高い。その証拠にモナド則3の結合則から「何枚揃えるまでガチャを行うか」についての抽象を導き出すことができる。
例えばこうだ。
open System open Library1 open Library1.Identity type Rarity = |R of string |Other // ガチャアイテム let a,b,c,d,e,other = R("緒方智絵里"), R("間中美里"), R("黒川千秋"), R("川島瑞樹"), R("若林智香"), Other // コンプ let comp = [a;b;c;d;e] // コンプ対象アイテムが出る確率 let probability = 0.12 // 12% // 1ガチャあたり300円 let printg = printGacha 300 "円" (fun () -> printfn "[眠れる姫君]星井美希を手に入れた!") let compGacha x = identity { let comp,d,probability,lst,count,total,r = x let lst,count,total,r = completeGacha comp d probability lst count total return (comp,d,probability,lst,count,total,r ) |> printg } let mogemasu n = (comp, other, probability, [], 0, 0, false) |> fun x -> let cg = [for i in 1..n-1 do yield compGacha] List.fold (fun m f -> m >>= f) (compGacha x) cg mogemasu 5 |> ignore System.Console.ReadLine () |> ignore Console.ReadLine () |> ignore
List.foldで必要回数分のモナドを結合することで、mogemasu関数を汎化することができた。
なお、List.foldでモナドを結合している部分は、下記のようにList.foldBackに書き直しても同様に動作する。このことからもモナド則3を満たしていることが確認できる。
let mogemasu n = (comp, other, probability, [], 0, 0, false) |> fun x -> let cg = [for i in 1..n-1 do yield compGacha] List.foldBack (fun m f -> f >>= m) cg (compGacha x)
まとめ
そのまま適用しただけでは何もしてくれないので、一見使いどころがなさそうなIdentityモナド。しかし、使えないようでいて実は割と使えるかもしれない、という話題でした。
読者の中には記事の誘導によってうまいこと騙されている人もいるかもしれないけど、
いや...、つーかさ。それ再帰で書けばいんじゃね?(核心
i l l ヽ ヽ\\ ヾy ‐-~~~ ヽ、 \ ヽ ヽ ィ ヽ~\ ヽ ヽ `、 / ー-、 \ `、 / ヽヾヽ\ ヽ\ ヽ、 、 // / |\ ヽ、 ヽ ヽ | l`、 / | | l , 、\\\\ \ | l 丶 | l |. 、! \ \ ー '''' ヽ、ヽ l | | ` . |.l | r'} 、 \,,、 、__,,、-‐''`ヽ | | | | l.l | ( { `ー''丶 '''ー- '´ |/ヽ | | | ii | l | ヽ; | |' i| l | | | i ヽ .l `i. i ノ, / / /// / __________ \. l ヽ. ヽ /`" / // |~ヽ / ヽ. ヽ _,,,,,,_ /r、 / / | | <またつまらぬコードを書いてしまった。 \ /llヽ ‐-、`' /1| ヽ / /| | \__________ / ||∧. / | | \-‐' | | _ ,、 -/l ||{ ヽ,,,,,,,,,/ .| | |ヽ、、 | | _,、- ' ´ |. ||{ | | |ヽ、 ゛| |、,,_
関連記事
FizzBuzz問題から学ぶモナド
http://d.hatena.ne.jp/zecl/20110711/p1