今回は以下の問題をヒントに石取りゲームをつくります。

G – 石取りゲーム

N 個の石からなる山を用意します。一番最初に先手が石をとるときは 1 個以上 P 個以下の好きな個数だけ石をとれます。それ以降については、各プレーヤーは 1 個以上、直前にとられた石の個数 + 1 個以下の好きな個数だけ石をとれます(例:前の人が石を 3 個取った場合、次の人は 1 個以上 4 個以下の石を取ることができる)。

ただ実際に作ってみると必勝法があまりに簡単にわかってしまいました。

なので「1 個以上、直前にとられた石の個数 + 1 個以下」を「直前にとられた石の個数 + 3 個」に変更します。

HTML部分

HTML部分を示します。

style.css

グローバル変数と定数

Modeクラスの定義

現在どのような状態(ゲーム開始前、先手後手選択時、プレイヤーの手番、CPUの手番)なのかでボタンの表示・非表示を変えます。そのため Mode クラスを定義して現在の状態がわかるようにします。

ページが読み込まれたときの処理

ページが読み込まれたときにおこなわれる処理を示します。

ここでは[ゲームスタート]ボタンの表示、レンジスライダーでボリューム調整ができるようにする処理、イベントリスナの追加をおこなっています。

ボタンの表示・非表示

ゲームの状態に応じてボタンの表示・非表示を切りかえる処理を示します。ここではまずボタンをすべて非表示にしたあと、ゲームの状態に応じて表示すべきボタンだけ表示させる処理をしています。

ボリューム調整関連の処理

レンジスライダーでボリューム調整ができるようにする処理を示します。これはゲームをつくるときは毎度おなじみの処理です。

効果音を再生する処理を示します。

イベントリスナの追加

イベントリスナを追加する処理を示します。ボタンがクリックされたら対応する関数を呼び出します。

ゲーム開始時の処理

ゲーム開始時の処理を示します。

[スタート]ボタンがクリックされたら乱数で、石の総数と初手で取ることができる石の数の上限を決めます。そのあとナビゲーションや現在の石の数の表示、ユーザーが先手と後手どちらを選択するかを決めるボタンの表示をおこないます。

残りの石数を表示する処理を示します。

先手後手を選択する

ゲームが開始されたらユーザーに先手と後手のどちらを選択するかを決めさせるボタンが表示されます。このボタンをクリックしたときの処理を示します。

この段階でプレイヤーとCPUのどちらが先手になるかが決まるので、playerNumber と cpuNumber に適切な値を代入します。

先手選択時の処理

onPlayerTurn関数はプレイヤーにターンが回ってきたときにおこなう処理が定義されています。ここではナビゲーションの表示とプレイヤーが石を取るために操作するボタンを表示させています。removeCount はプレイヤーが取ろうとしている石の数を暫定的に格納するためのものです。キャンセルボタンをクリックしたらもとに戻せるように確定前は stones の値は変更させないようにしています。

後手選択時の処理

ユーザーが後手を選択したときの処理を示します。

後手を選択した場合は初手をCPUの着手させたあとプレイヤーの着手となります。

プレイヤーが石を取る処理

プレイヤーが石を取る処理を示します。removeCount をインクリメントして stones – removeCount の値を残りの石の数として表示させるのですが、このとき石の数が負数になったり max を超えてはいけません。不正な操作がおこなわれた場合は警告音を出します。

[キャンセル]ボタンがクリックされたときはプレイヤーにターンが回ってきたときと同じ状態に戻します。

確定後の処理

プレイヤーが石を取り[確定]ボタンをクリックしたときの処理を示します。

石をひとつも取っていない場合は不正な操作となります。そうでない場合は stones を removeCount だけ減らしてCPUが取ることができる石の数の上限を removeCount + addition に変更します。そのあと手番を CPU に移すのですが、ゲームセットになっているかもしれないのでそのチェックをおこないます。ゲームセットになっていない場合はCPUに着手させます。

着手後、勝負がついているかもしれないのでそれをチェックする処理を示します。それぞれが着手したあと石の数が 0 になっていれば着手した側の勝利です。

CPU の着手

CPUに着手させる処理を示します。勝つための手が存在する場合はそれを選択します。勝つための手が存在しない場合は乱数で適当な数の石を取ります。

そのあと残りの石数と取れる数の上限を更新します。CPUの着手で勝負がつかない場合は手番をプレイヤーに戻します。

メモ用の3次元配列の初期化

CPUが最善手を探すときにメモ化再帰の処理をおこないます。initArray 関数は メモ用の3次元配列 memo[2][rowCount][colCount] を生成する処理をおこないます。

CPU の最善手を探す

bestMove関数は第位置引数が 0 のときは先手の、1 のときは後手の最善手を探します。第二引数は残されている石の数、第三引数は取ることができる上限です。メモ用の配列を初期化して相手に手番を渡した場合、どうやっても相手が負けてしまう手を探し、結果を配列で返します。

メモ化再帰

win 関数は第位置引数が 0 のときは先手の、1 のときは後手の最善手を探します。第二引数は残されている石の数、第三引数は取ることができる上限です。

第一引数を変えて再帰呼び出しをすることで、相手に手番を渡した場合、どうやっても相手が負けてしまう手を探し、あれば true を返します。同じ引数で何度も呼び出されることがあるため、一度計算した結果は配列に保存してまた同じ引数で呼び出されたときは配列に格納されている値を返すことで処理を高速化しています。