Scalaでコンプガチャ

NBUゆるゆるかれんだー Advent Calendar 2020の7日目の記事が空いたので穴埋めにどこでも使う予定のない記事を書いておく。

プログラミング入門 という情報メディア学科1年次後期開講科目を担当するようになって5年経ち、今回で担当を卒業することとなった。

開講初年次から様々なプログラム言語に軽く触れておくという方針で、プログラミングの初学者にはハードルの高い内容で何が何だか分からないまま受講を終えた学生も多かったように思う。

授業用の資料も年々継ぎ足してきて嵩が増し少々難しくなりすぎた感があり、最後の年は内容を削って扱う言語も減らした。具体的にはJavaScriptとPHPとAlogodooのThymeを削ってSwift Playgrounds扱う回を1回から3回へと増やし、iPadのショートカットを追加した。

扱う言語を剪定して選定するにあたりScalaを切ろうか迷ったがガチャガチャのシミュレーションはお気に入りのネタであり残すことにした。

資料を見直しているとコンプガチャのシミュレーションも追加したくなったが授業に間に合わなかったのでここでやっておく。

以下は第12回の資料の続き


余談3

2012年に話題になった コンプガチャ問題 のシミュレーション

準備

第12回の資料 の最初の方と同じ要領で sbt console を起動する。

ランダムを使えるようにする。

import scala.util.Random

ガチャの表を用意する。25%の確率でABCDEの5種類のうちどれか一つが当たる。外れは75%でFになる。各ABCDEの当たる確率は5%

val gacha_table = "A"*5 + "B"*5 + "C"*5 + "D"*5 + "E"*5 +"F"*75

結果:

gacha_table: String = AAAAABBBBBCCCCCDDDDDEEEEEFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF


ガチャを定義する。

def gacha = gacha_table(Random.nextInt(100))

ガチャを引いてみる。何回か試して様子を確かめる。

gacha

結果:

scala> gachares138: Char = Fscala> gachares139: Char = Cscala> gachares140: Char = E


ガチャを引いた結果を可変MAPに記録する準備をする。

import scala.collection.mutable.Map

コンプガチャを用意する。

ガチャを引いて結果を記録するコードはm(gacha)+=1の部分。ガチャの履歴をイテレーターから取得できるように定義する。

def compgacha = new Iterator[Map[Char,Int]] {

private var m = Map('A'->0,'B'->0, 'C'->0, 'D'->0, 'E'->0, 'F'->0)

def hasNext = true

def next = { m(gacha)+=1; m }

}

↑の定義はIteratorのメソッドを利用して以下のように書ける。

def compgacha = Iterator.iterate(Map('A'->0,'B'->0, 'C'->0, 'D'->0, 'E'->0, 'F'->0))(m => {m(gacha)+=1; m})

さらに初期値の部分は、こう書ける

def compgacha = Iterator.iterate(Map("ABCDEF".map((_,0)):_*))(m => {m(gacha)+=1; m})

さらに次のように書けることをTwitterで教えてもらった。

def compgacha = Iterator.iterate("ABCDEF".map(_->0).to(Map))(m => {m(gacha)+=1; m})


ABCDEFの6種類すべてが揃うまでガチャを引いた回数を求める。初回は0回目としてカウントする。

compgacha indexWhere (_.values.forall( _ > 0 ))

結果:

res142: Int = 61 これはひどい。res143: Int = 17 これぐらいで揃えばラッキー。18回目に全て揃った。


ガチャの履歴を観察する。Mapをそのまま表示してもABCDEFの順に並ばない。

compgacha indexWhere (m => {println(m);m.values.forall( _ > 0 )})

Map(D -> 0, A -> 0, C -> 0, F -> 1, E -> 0, B -> 0)Map(D -> 1, A -> 0, C -> 0, F -> 1, E -> 0, B -> 0)Map(D -> 1, A -> 0, C -> 0, F -> 2, E -> 0, B -> 0)Map(D -> 1, A -> 0, C -> 0, F -> 3, E -> 0, B -> 0)Map(D -> 1, A -> 0, C -> 0, F -> 4, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 4, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 5, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 6, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 7, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 8, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 9, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 10, E -> 0, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 10, E -> 1, B -> 0)Map(D -> 1, A -> 1, C -> 0, F -> 11, E -> 1, B -> 0)Map(D -> 1, A -> 1, C -> 1, F -> 11, E -> 1, B -> 0)Map(D -> 1, A -> 1, C -> 1, F -> 12, E -> 1, B -> 0)Map(D -> 1, A -> 1, C -> 1, F -> 13, E -> 1, B -> 0)Map(D -> 1, A -> 1, C -> 1, F -> 13, E -> 1, B -> 1) 最後にBが当たって揃ったres147: Int = 17


ガチャの履歴を種類順に並べ替えて観察する。

compgacha indexWhere (m => {println(m.toSeq.sorted);m.values.forall( _ > 0 )})

ArrayBuffer((A,0), (B,0), (C,0), (D,0), (E,1), (F,0))ArrayBuffer((A,0), (B,1), (C,0), (D,0), (E,1), (F,0))ArrayBuffer((A,0), (B,1), (C,0), (D,0), (E,2), (F,0))ArrayBuffer((A,0), (B,1), (C,0), (D,0), (E,2), (F,1))ArrayBuffer((A,0), (B,1), (C,0), (D,0), (E,2), (F,2))ArrayBuffer((A,0), (B,1), (C,0), (D,1), (E,2), (F,2))ArrayBuffer((A,0), (B,1), (C,0), (D,1), (E,2), (F,3))ArrayBuffer((A,0), (B,2), (C,0), (D,1), (E,2), (F,3))ArrayBuffer((A,0), (B,2), (C,0), (D,1), (E,2), (F,4))ArrayBuffer((A,0), (B,2), (C,1), (D,1), (E,2), (F,4))ArrayBuffer((A,0), (B,2), (C,1), (D,1), (E,2), (F,5))ArrayBuffer((A,0), (B,2), (C,1), (D,1), (E,2), (F,6))ArrayBuffer((A,0), (B,2), (C,1), (D,1), (E,2), (F,7))ArrayBuffer((A,0), (B,2), (C,1), (D,1), (E,2), (F,8))ArrayBuffer((A,0), (B,2), (C,1), (D,1), (E,2), (F,9))ArrayBuffer((A,0), (B,3), (C,1), (D,1), (E,2), (F,9))ArrayBuffer((A,0), (B,3), (C,1), (D,1), (E,2), (F,10))ArrayBuffer((A,1), (B,3), (C,1), (D,1), (E,2), (F,10)) 最後にAが当たって揃った。


100人がA~Fのすべてを揃えるまで引き続けて、引いた回数の多い順(不幸な順)に並べる。

Iterator.continually(compgacha indexWhere (_.values.forall( _ > 0 ))).take(100).toList.sorted.reverse

結果:

List(170, 132, 112, 111, 101, 98, 97, 88, 87, 83, 83, 81, 75, 75, 75, 75, 72, 71, 69, 68, 64, 63, 59, 59, 58, 58, 58, 56, 55, 54, 53, 53, 53, 52, 52, 51, 51, 49, 49, 49, 48, 48, 48, 48, 48, 48, 48, 47, 46, 44, 43, 43, 43, 43, 41, 41, 41, 38, 37, 37, 37, 37, 36, 35, 35, 34, 33, 32, 32, 32, 31, 30, 30, 30, 27, 27, 26, 26, 25, 23, 23, 23, 22, 21, 20, 20, 20, 20, 19, 18, 18, 17, 17, 16, 15, 14, 14, 13, 12, 12)

0.95^170 は0.000163 らしいので、1/6000くらいの不幸みたい。


さて、2012年当時のコンプガチャや1970年代の野球カードのカード合わせがどの様な排出率の組み合わせだったのかは知らないが、どのカードも同じような確率で入手できたとすると以下の様な表を使って試せばよい。

6種類すべてを揃えるためにどれくらいの無駄が生じるかは各自で確かめて欲しい。

val gacha_table = "A"*17+ "B"*17 + "C"*17 + "D"*17 + "E"*16 +"F"*16

↑のコードでは全種類揃う判定を forall で行っているが、すでに引いている種類についても判定している。

つまり、まだ引けていない種類のみチェックすればいいので無駄がある。

改良版:

import scala.collection.mutable.Set


def compgacha = Iterator.iterate((Set("ABCDEF".toSeq:_*),Map("ABCDEF".map((_,0)):_*))){case (s,m) => {val g=gacha; s-=g; m(g)+=1; (s,m)}}

Mapの初期化は次のようにしてもよい。

def compgacha = Iterator.iterate((Set("ABCDEF".toSeq:_*),Map.empty[Char,Int].withDefaultValue(0))){case (s,m) =>

{val g=gacha; s-=g; m(g)+=1; (s,m)}}


20回ほど引いてみる。

compgacha.take(20).foreach(println)

結果:

(HashSet(A, B, C, D, E, F),HashMap(A -> 0, B -> 0, C -> 0, D -> 0, E -> 0, F -> 0))(HashSet(A, B, C, D, E),HashMap(A -> 0, B -> 0, C -> 0, D -> 0, E -> 0, F -> 1))(HashSet(B, C, D, E),HashMap(A -> 1, B -> 0, C -> 0, D -> 0, E -> 0, F -> 1))(HashSet(B, C, D, E),HashMap(A -> 1, B -> 0, C -> 0, D -> 0, E -> 0, F -> 2))(HashSet(B, C, D, E),HashMap(A -> 1, B -> 0, C -> 0, D -> 0, E -> 0, F -> 3))(HashSet(B, C, D, E),HashMap(A -> 1, B -> 0, C -> 0, D -> 0, E -> 0, F -> 4))(HashSet(B, D, E),HashMap(A -> 1, B -> 0, C -> 1, D -> 0, E -> 0, F -> 4))(HashSet(B, D, E),HashMap(A -> 1, B -> 0, C -> 1, D -> 0, E -> 0, F -> 5))(HashSet(B, D, E),HashMap(A -> 2, B -> 0, C -> 1, D -> 0, E -> 0, F -> 5))(HashSet(B, D, E),HashMap(A -> 2, B -> 0, C -> 1, D -> 0, E -> 0, F -> 6))(HashSet(B, D, E),HashMap(A -> 2, B -> 0, C -> 1, D -> 0, E -> 0, F -> 7))(HashSet(D, E),HashMap(A -> 2, B -> 1, C -> 1, D -> 0, E -> 0, F -> 7))(HashSet(D, E),HashMap(A -> 2, B -> 1, C -> 1, D -> 0, E -> 0, F -> 8))(HashSet(D, E),HashMap(A -> 2, B -> 1, C -> 1, D -> 0, E -> 0, F -> 9))(HashSet(D, E),HashMap(A -> 2, B -> 1, C -> 1, D -> 0, E -> 0, F -> 10))(HashSet(D, E),HashMap(A -> 2, B -> 1, C -> 1, D -> 0, E -> 0, F -> 11))(HashSet(E),HashMap(A -> 2, B -> 1, C -> 1, D -> 1, E -> 0, F -> 11))(HashSet(E),HashMap(A -> 2, B -> 1, C -> 1, D -> 1, E -> 0, F -> 12))(HashSet(E),HashMap(A -> 2, B -> 1, C -> 1, D -> 1, E -> 0, F -> 13))(HashSet(E),HashMap(A -> 2, B -> 1, C -> 1, D -> 1, E -> 0, F -> 14)) Eがまだ引けていない。

全部揃うまで引く。

compgacha.indexWhere(_._1.isEmpty)

履歴を表示しながら全部揃うまで引く。

compgacha.indexWhere{case (s,m) =>{println(m); s.isEmpty}}

結果: 改良前のコードの出力

HashMap(A -> 0, B -> 0, C -> 0, D -> 0, E -> 0, F -> 0)HashMap(A -> 0, B -> 0, C -> 0, D -> 1, E -> 0, F -> 0)HashMap(A -> 0, B -> 0, C -> 0, D -> 1, E -> 0, F -> 1)HashMap(A -> 0, B -> 0, C -> 0, D -> 1, E -> 0, F -> 2)HashMap(A -> 0, B -> 0, C -> 0, D -> 1, E -> 0, F -> 3)HashMap(A -> 0, B -> 0, C -> 0, D -> 1, E -> 0, F -> 4)HashMap(A -> 0, B -> 1, C -> 0, D -> 1, E -> 0, F -> 4)HashMap(A -> 0, B -> 1, C -> 0, D -> 1, E -> 0, F -> 5)HashMap(A -> 0, B -> 1, C -> 0, D -> 1, E -> 0, F -> 6)HashMap(A -> 0, B -> 1, C -> 0, D -> 1, E -> 0, F -> 7)HashMap(A -> 0, B -> 1, C -> 0, D -> 1, E -> 0, F -> 8)HashMap(A -> 0, B -> 1, C -> 0, D -> 1, E -> 0, F -> 9)HashMap(A -> 0, B -> 1, C -> 0, D -> 2, E -> 0, F -> 9)HashMap(A -> 0, B -> 1, C -> 0, D -> 2, E -> 0, F -> 10)HashMap(A -> 0, B -> 1, C -> 0, D -> 2, E -> 1, F -> 10)HashMap(A -> 0, B -> 1, C -> 0, D -> 3, E -> 1, F -> 10)HashMap(A -> 0, B -> 1, C -> 0, D -> 3, E -> 1, F -> 11)HashMap(A -> 0, B -> 1, C -> 0, D -> 3, E -> 1, F -> 12)HashMap(A -> 0, B -> 1, C -> 0, D -> 3, E -> 1, F -> 13)HashMap(A -> 0, B -> 1, C -> 0, D -> 3, E -> 1, F -> 14)HashMap(A -> 0, B -> 1, C -> 0, D -> 3, E -> 1, F -> 15)HashMap(A -> 1, B -> 1, C -> 0, D -> 3, E -> 1, F -> 15)HashMap(A -> 1, B -> 1, C -> 0, D -> 3, E -> 1, F -> 16)HashMap(A -> 1, B -> 1, C -> 0, D -> 3, E -> 1, F -> 17)HashMap(A -> 1, B -> 1, C -> 0, D -> 3, E -> 1, F -> 18)HashMap(A -> 1, B -> 1, C -> 0, D -> 3, E -> 2, F -> 18)HashMap(A -> 1, B -> 1, C -> 0, D -> 3, E -> 2, F -> 19)HashMap(A -> 1, B -> 2, C -> 0, D -> 3, E -> 2, F -> 19)HashMap(A -> 1, B -> 2, C -> 0, D -> 3, E -> 2, F -> 20)HashMap(A -> 1, B -> 2, C -> 0, D -> 3, E -> 2, F -> 21)HashMap(A -> 1, B -> 2, C -> 1, D -> 3, E -> 2, F -> 21)res25: Int = 30