鳩でもわかるThe Simple Gameをつくる(1)の続きです。今回はゲーム開始以降の処理を実装します。

コンピュータの着手
ゲーム開始の処理の前提としてコンピュータが次の手を着手する処理を示します。
勝つための手が存在する場合はこれを選択し、ない場合は適当に次の手を選びます。以下のコードは鳩でもわかるThe Simple Gameをつくる(1)で示したC#のコードとほぼ同じですが、コンピュータが後手の場合もあるので引数で切り分けています。コンピュータが自力勝利できる場合はそのための手も取得しています(現在位置から移動可能なコンピュータ必勝点がわかればよい)。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 |
function think(now_first){ let a_wins = []; let b_wins = []; let a_froms = []; let b_froms = []; for(let i = 0; i < ROW_COUNT * COL_COUNT; i++){ a_wins.push(cells[i].Text == 'A'); a_froms.push(null); } for(let turn = 0; turn < remaining_turns; turn++){ // 先手必勝点ではない頂点に移動できる頂点はすべて後手必勝点である // 実際には先手必勝点ではない頂点から逆向きに移動できる頂点を後手必勝点としている // このとき後手必勝点であることが確定したらそこからどこへ移動すべきかも記録する b_wins = []; for(let i = 0; i < ROW_COUNT * COL_COUNT; i++){ b_wins.push(false); b_froms.push(null); } for(let i = 0; i < ROW_COUNT * COL_COUNT; i++){ if(!a_wins[i]) rE[i].forEach(idx => { b_wins[idx] = true; b_froms[idx] = i; }); } // 後手必勝点ではない頂点に移動できる頂点はすべて先手必勝点である // 以下同文 a_wins = []; a_froms = []; for(let i = 0; i < ROW_COUNT * COL_COUNT; i++){ a_wins.push(false); a_froms.push(null); } for(let i = 0; i < ROW_COUNT * COL_COUNT; i++){ if(!b_wins[i]) rE[i].forEach(idx => { a_wins[idx] = true; a_froms[idx] = i; }); } } if(now_first) return {result: a_wins[cur_index], best_move: a_froms[cur_index]}; else return {result: b_wins[cur_index], best_move: b_froms[cur_index]}; } |
取得された手を着手する処理を示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
async function cpu_move() { const ret = think(!is_player_first); if(ret.result){ // コンピュータが自力勝利する手が存在する cur_index = ret.best_move; select_sound.currentTime = 0; select_sound.play(); } else { // 自力勝利できる手がないので適当に選ぶ let mv = cells[cur_index].Nexts[0]; if(cells[cur_index].Nexts.length > 1){ const idx = Math.floor(Math.random() * cells[cur_index].Nexts.length); mv = cells[cur_index].Nexts[idx]; } cur_index = mv; select_sound.currentTime = 0; select_sound.play(); } } |
勝敗の判定
所定のターン数を双方が着手し終わったら勝敗判定します。最終的に選択されているマスに書かれている文字がプレイヤー勝利の文字であればプレイヤーの勝ち、そうでなければ負けです。
勝ちの場合は残り時間と勝利数からどれだけ加点するか計算して加点処理をおこないます。そのあとターン数を 1 増やして次のステージに進みます。負けの場合はゲームオーバー処理をおこないます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
const sleep = async(ms) => await new Promise(_ => setTimeout(_, ms)); async function check() { if(remaining_turns == 0){ const ch = is_player_first ? 'A' : 'B'; if(cells[cur_index].Text == ch){ clear_sound.currentTime = 0; clear_sound.play(); clear_count++; const add = Math.floor(remaining_time / 100 * clear_count); score += add; await sleep(2000); max_remaining_turns++; remaining_turns = max_remaining_turns; remaining_time = REMAINING_TIME_MAX; createQuestion(); showFirstSecondButtons(true); // 先手後手選択ボタンの再表示 return true; } else { gameover(); return true; } } return false; // そもそも勝敗判定が必要な局面でないなら false を返す } |
ゲーム開始の処理
ゲームを開始するための処理を示します。
最初は1ターンだけで決着する簡単な問題を出題します。ゲームスタートのボタンを非表示にして先手後手選択用のボタンを表示します。またスコア等の変数をリセットします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function gameStart(){ max_remaining_turns = 1; remaining_turns = max_remaining_turns; createQuestion(); showStartButtons(false); showFirstSecondButtons(true); playing = true; remaining_time = REMAINING_TIME_MAX; score = 0; clear_count = 0; select_sound.currentTime = 0; select_sound.play(); } |
先手後手選択用のボタンが押下されたときの処理を示します。
先手が選択されたときは先手であることを示すis_player_firstフラグを true にして着手不能であることを示すignore_clickフラグを false にします。後手が選択されたときはis_player_firstフラグを false にしてコンピュータの着手が終わるのを待ってからignore_clickフラグを false にします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
async function selectFirst(is_first){ if(is_first){ // 先手 ignore_click = false; is_player_first = true; showFirstSecondButtons(false); select_sound.currentTime = 0; select_sound.play(); } else { // 後手 is_player_first = false; showFirstSecondButtons(false); select_sound.currentTime = 0; select_sound.play(); await sleep(1000); await cpu_move(); ignore_click = false; } } |
移動の処理
プレイヤーが駒を移動させる処理を示します。
引数は移動先のマスのindexです。移動先が移動可能なマスかどうかチェックして移動可能なら移動処理をおこないます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 |
async function move(idx){ if(!playing || ignore_click) return; // 引数で指定されたマスは移動可能? let find = false; for(let i = 0; i < cells[cur_index].Nexts.length; i++){ if(cells[cur_index].Nexts[i] == idx){ find = true; break; } } if(!find){ // 移動できない ng_sound.currentTime = 0; ng_sound.play(); return; } // 移動可能なので移動処理開始 // 移動処理が二重におこなわれないようにする ignore_click = true; cur_index = idx; select_sound.currentTime = 0; select_sound.play(); // プレイヤー後手ならここで残りターンを減らす // 残りターン 0 なら勝敗判定 if(!is_player_first){ remaining_turns--; const ret = await check(); if(ret) return; } // 終局でないならコンピュータに着手させる await sleep(1000); await cpu_move(); // プレイヤー先手ならここで残りターンを減らす // 残りターン 0 なら勝敗判定 if(is_player_first){ remaining_turns--; const ret = await check(); if(ret) return; } // 終局でないならプレイヤーが着手可能な状態にする ignore_click = false; } |
キー操作でも駒を移動させることができるようにします。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
async function onKeyDown(ev){ if(ev.key != 'ArrowUp' && ev.key != 'ArrowDown' && ev.key != 'ArrowLeft' && ev.key != 'ArrowRight') return; if(playing) ev.preventDefault(); if(ev.key == 'ArrowUp' && Math.floor(cur_index / COL_COUNT) > 0) move(cur_index - COL_COUNT); if(ev.key == 'ArrowDown' && Math.floor(cur_index / COL_COUNT) < ROW_COUNT - 1) move(cur_index + COL_COUNT); if(ev.key == 'ArrowLeft' && cur_index % COL_COUNT > 0) await move(cur_index - 1); if(ev.key == 'ArrowRight' && cur_index % COL_COUNT < COL_COUNT - 1) move(cur_index + 1); } |
ゲームオーバー処理
ゲームオーバー時の処理を示します。このときスコアランキングに登録するデータの送信もおこないます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
function gameover(){ if(playing){ playing = false; ignore_click = true; saveScore($player_name.value, score); gameover_sound.currentTime = 0; gameover_sound.play(); setTimeout(() => { // しばらくしてから再挑戦用のボタンを表示する showStartButtons(true); }, 2000); } } function saveScore(player_name, score){ if(player_name == '') player_name = '名無しのゴンベ'; // JSON形式でPOST fetch('./ranking.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: player_name, score: score, }) }); } |
サーバー側の処理とスコアランキングを表示させる処理はゲーム開始以降の処理 鳩でもわかるXORパズルをつくる(2)と同じなので省略します。
