NKODICEシミュレーターを実装してみた

## 動機
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

# 1個以上のwordが完成するまでNUDGEし続けた場合に失うNUDGEの数の期待値
> (0..1000).sum{|i| i*(0.59**(i)*0.41) }
=> 1.4390243902439022
```


## 結論 
- プロではない人間がNKODICE遊ぶ際、ゲームが終了するのは運や長期的な収束の結果ではなくプレイヤーの力量が原因である可能性が高い
- 練習しろ