動機
NKODICEというゲームがある
どういったゲームなのかはこの記事を読んで感じ取ってほしい。
分かったと思うので、Rubyで簡易的なシミュレータを実装してみる。
実装方針
- 小便は考慮しない(プロのNKODICEプレイヤーは小便しないので)
- NUDGEはリロール相当の効果があるものとする(プロのNKODICEプレイヤーは少なくともNUDGEを無駄遣いしないので)
- wordが完成しない場合、NUDGEする(ライフで受ける戦略や、完成wordが期待よりしょぼかった場合もNUDGEする戦略もあるが、今回はこの実装で行く)
Qiitaの規約の回避
NKODICEで用いられる東洋の神秘的な文字列は、何故かQiitaの規約に抵触してしまう可能性がある
この問題は出力にsortをかけることで回避できる。
> 5.times.map{%w(ん こ う ま ち お).sample}.sort.join
=> "おちちんん"
また、sortすることにより役判定の高速化が望める。
実装
nkodice.rb
def solve(kotodama)
face_list = kotodama.chars
word_list = []
if (kotodama.match(/う.*ち.*ん/))
word_list<<"うちん"
end
if (kotodama.match(/う.*こ.*ん/))
word_list << "うこん"
end
if (kotodama.match(/お.*こ.*ま.*ん/))
word_list << "おこまん"
elsif (kotodama.match(/こ.*ま.*ん/))
word_list << "こまん"
end
if (kotodama.match(/こ.*ち.*ん/))
word_list << "こちん"
end
if (kotodama.match(/お.*ちち.*んん/))
word_list << "OCHICHINN"
elsif (kotodama.match(/ちち.*んん/))
word_list << "ちちんん"
end
tripile_list = %w(ん こ う ま ち お).each_with_object([]) do|c,memo|
m = kotodama.match(/#{c}{3,}/)
memo << [c, m[0].size] if m
end
[face_list, word_list, tripile_list]
end
life = 3
dice_num = 5
nudge_num = 5
score = 0
roll = 0
word_combo = {}
loop do
break if life == 0
life -= 1
roll += 1
print "ROLL:#{roll} life: #{life} nudge:#{nudge_num} dice:#{dice_num}\n"
(face_list, word_list, tripile_list) = [nil,nil,nil]
loop do
kotodama = dice_num.times.map{%w(ん こ う ま ち お).sample}.sort.join
(face_list, word_list, tripile_list) = solve(kotodama)
if word_list.size == 0 && nudge_num > 0
nudge_num-=1
print "nudge!(left: #{nudge_num})\n"
next
end
break
end
life += 1 if word_list.size >= 1
dice_num = 5
dice_num += word_list.size - 1 if word_list.size >= 2
nudge_num += word_list.size
score += face_list.sum{|c| {"ん"=>50,"こ"=>100,"う"=>500,"ま"=>500,"ち"=>500,"お"=>300}[c] }
word_list.each{|w|
base_point = {"うちん"=>1000,"うこん"=>1000,"おこまん"=>5000,"こまん"=>1000,"こちん"=>1000,"OCHICHINN"=>10000,"ちちんん"=>3000}[w]
score += base_point * 2**[word_combo[w]||0, 3].min
dice_num = 10 if w == "OCHICHINN"
}
word_combo = word_list.map{|w| [w, (word_combo[w] || 0) + 1] }.to_h
tripile_list.each{|c,n|
base = {"ん"=>-3,"こ"=>1.5,"う"=>2,"ま"=>2,"ち"=>2,"お"=>1.5}[c]
score *= (base + (base > 0 ? 1 : -1) * (n-3)).clamp(-4,4)
score = score.abs if c == "ん"
}
print "score: #{score}\twords: #{word_list}\n"
end
print "gameover\n"
print "score: #{score}\n"
動作結果
実行してみる。
停止しないんだが??
考察
操作を繰り返した際、長期的に見て 「消費するNUDGE」=<「得られるNUDGE」
が成り立つならば、
終わりなくダイスを振り続けることができると思われる。
- 5個のダイスを振ったとき、wordが完成する確率は約41%である
- 1個以上のwordが完成した際得られるNUDGEの数は平均は約1.58個である
このとき、「1個以上のwordが完成するまでNUDGEし続けた場合に失うNUDGEの数の期待値」は約1.43個になり、
「消費するNUDGE」=<「得られるNUDGE」
が成り立つ。
したがって、wordが1つ以上完成するまでNUDGEし続けるという戦略をとった場合、
十分な数のNUDGEが残っているという仮定の下ではNUDGEは長期的には増えていくことが予想され、ゲームは停止しなくなる。
実際はダイスの数が増える要素もあるので上の計算より高速にNUDGE数は増えるだろう。
> a=%w(ん こ う ま ち お).repeated_permutation(5).map{|kotodama|solve(kotodama.sort.join)[1].size}
# wordが完成する確率
> a.select{|x|x!=0}.size.to_f/a.size
=> 0.411522633744856
# 役が完成した際得られるNUDGEの数の平均
> b = a.select{|x|x!=0}
> b.sum(0.0)/b.size
=> 1.58125
# 1個以上のwordが完成するまでNUDGEし続けた場合に失うNUDGEの数の期待値
> (0..1000).sum{|i| i*(0.59**(i)*0.41) }
=> 1.4390243902439022
結論
- プロではない人間がNKODICE遊ぶ際、ゲームが終了するのは運や長期的な収束の結果ではなくプレイヤーの力量が原因である可能性が高い
- 練習しろ