1. Fuyutsubaki

    Posted

    Fuyutsubaki
Changes in title
+NKODICEシミュレーターを実装してみた
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,166 @@
+## 動機
+NKODICEというゲームがある
+どういったゲームなのかはこの記事を読んで感じ取ってほしい。
+
+https://news.denfaminicogamer.jp/news/210601b
+
+分かったと思うので、Rubyで簡易的なシミュレータを実装してみる。
+
+
+## 実装方針
+
+- 小便は考慮しない(プロのNKODICEプレイヤーは小便しないので)
+- NUDGEはリロール相当の効果があるものとする(プロのNKODICEプレイヤーは少なくともNUDGEを無駄遣いしないので)
+- wordが完成しない場合、NUDGEする(ライフで受ける戦略や、完成wordが期待よりしょぼかった場合もNUDGEする戦略もあるが、今回はこの実装で行く)
+
+
+## Qiitaの規約の回避
+
+NKODICEで用いられる東洋の神秘的な文字列は、何故かQiitaの規約に抵触してしまう可能性がある
+この問題は出力にsortをかけることで回避できる。
+
+```
+> 5.times.map{%w(ん こ う ま ち お).sample}.sort.join
+=> "おちちんん"
+```
+
+また、sortすることにより役判定の高速化が望める。
+
+
+## 実装
+
+```ruby: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"
+```
+
+
+
+## 動作結果
+実行してみる。
+
+![out.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/33897/4ec9de91-7ebd-34ed-94ee-54df7966b379.gif)
+
+
+停止しないんだが??
+
+
+## 考察
+
+操作を繰り返した際、長期的に見て `「消費するNUDGE」=<「得られるNUDGE」`が成り立つならば、
+ダイスを終わりなく振り続けることができると思われる。。
+
+- 5個のダイスを振ったとき、wordが完成する確率は約41%である
+- 1個以上のwordが完成した際得られるNUDGEの数は平均は約1.58個である
+
+このとき、「1個以上のwordが完成するまでNUDGEし続けた場合に失うNUDGEの数の期待値」は約1.43個になり、
+`「消費するNUDGE」=<「得られるNUDGE」`が成り立つ。
+
+
+したがって、wordが1つ以上完成するまでNUDGEし続けるという戦略をとった場合、
+十分な数のNUDGEが残っているという仮定の下ではNUDGEは長期的には増えていくことが予想され、ゲームは停止しなくなる。
+
+
+実際はダイスの数が増えるの要素もあるので上の計算より高速にNUDGE数は増えるだろう。
+
+```ruby
+> 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
+
+# 個以上のwordが完成するまでNUDGEし続けた場合に失うNUDGEの数の期待値
+> (0..1000).sum{|i| i*(0.59**(i)*0.41) }
+=> 1.4390243902439022
+```
+
+
+## 結論
+- プロではない人間がNKODICE遊ぶ際、ゲームが終了するのは運や長期的な収束の結果ではなくプレイヤーの力量が原因である可能性が高い
+- 練習しろ