最短以外は不正解!8パズルでゲームをつくるの続きです。今回はゲーム開始以降の処理を実装します。
Contents
ゲーム開始時の処理
ゲームを開始するための処理を示します。
問題は全部で31手問題まであります。1手問題みたいなのは簡単すぎるので省略し、4手、8手、10手、それ以降は31手まで連続する数で問題をつくります。最初の配列 move_limits に値を格納しているのはそのための処理です。
そのあと前回の処理で取得した問題のなかから◯手問題をランダムに選択してこれを出題することにします。そのあとボタンの表示非表示の切り替えと各フラグの変更、消費時間を計測するための現在時刻の取得などをおこないます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
function gameStart(){ move_limits = [4, 8, 10]; for(let i = 11; i <= 31; i++) move_limits.push(i); getQuestion(); // 出題する問題を取得して(後述) showQuestion(); // これを表示(後述) $start_buttons.style.display = 'none'; // 操作用のボタンの表示と非表示 $ctrl_buttons.style.display = 'block'; playing = true; timer_moving = true; // 計時の開始 start_time = Date.now(); time_sum = 0; // 前のステージまでのクリアにかかった時間の総和。最初はいうまでもなく 0 congratulation_y = INIT_CONGRATULATION_Y; select_sound.currentTime = 0; select_sound.play(); } |
出題する問題の取得
出題する問題を取得する処理を示します。
move_limitsに格納された要素の先頭をみればこれから出題すべき問題が何手問題かわかります。幅優先探索で問題を取得した以上、問題は辞書順に格納されているので、先頭に近いものは単調な問題である可能性があります。
そのため最初の2個は捨てていますが、最終問題は2パターンしかないのでこれだと31手問題が出題されないという問題が発生します。「どうせAIを使ったりチートしなければ20手問題くらいが限界でしょ」なんて考えていたら本当に「30手問題を正解したが31手問題が表示されない」というリプが届いてびっくり。テストはきちんとしましょう(自戒)。
|
1 2 3 4 5 6 7 8 |
function getQuestion(){ const move_limit = move_limits[0]; const rand = Math.floor(Math.random() * (questions[move_limit].length - 2) + 2); if(questions[move_limit].length > 2) cur_question = questions[move_limit][rand]; else cur_question = questions[move_limit][Math.floor(Math.random() * questions[move_limit].length)]; } |
問題を表示する処理を示します。移動回数を示すグローバル変数を0にリセットして先述のsetPieces関数で描画されるようにします。またcanvasのうえにあるdiv要素に現在何手問題かと現在の移動回数を表示させます。
|
1 2 3 4 5 6 |
function showQuestion(){ move_count = 0; setPieces(cur_question); $question.innerHTML = `${move_limits[0]} 手問題`; $move_count.innerHTML = `現在 ${move_count} 手`; } |
ピースを移動させる処理
ピースを移動させる処理を示します。移動はキー操作とcanvasのクリック、タップでおこなえるようにします。
まずmove関数を示します。引数はピースを移動させる方向を示します(’U’, ‘D’, ‘L’, ‘R’のいずれか)。
まず9番ピースはどのオブジェクトなのかを配列 pieces のなかから探し、これと引数から実際に移動させるピースを特定することができます。移動対象が存在しない場合は不正な操作がおこなわれようとしたことになるので警告音を鳴らして処理を終了します。そうでない場合は移動中であることを示すフラグ moving をセットして移動処理を開始します。
移動処理はいきなり場所を入れ替えるのではなくスライドしているように見せるために数回にわけて座標を変更する非同期処理でおこないます。移動の結果、パズルを完成させることができたら次のステージに移動し、上限回数に達しても完成していない場合は失敗なのでそのステージを最初からやりなおさせる処理をおこないます。
|
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 |
const sleep = async(ms) => await new Promise(_ => setTimeout(_, ms)); async function move(dir){ if(!playing || moving) // ゲーム開始前や移動処理中であればなにもしない return false; // 9番ピースはどれ? let row9 = -1; let col9 = -1; let piece9 = null; for (let i = 0; i < 9; i++) { if(pieces[i].Number == 9){ row9 = pieces[i].Y / PIECE_SIZE; col9 = pieces[i].X / PIECE_SIZE; piece9 = pieces[i]; break; } } if(piece9 == null) return false; // 9番ピースの位置から移動対象を求める let row = row9; let col = col9; if(dir == 'L') col++; if(dir == 'R') col--; if(dir == 'U') row++; if(dir == 'D') row--; if(row < 0 || row >= ROW_COL_COUNT || col < 0 || col >= ROW_COL_COUNT) return false; // 移動対象が特定できたら移動開始 let moveTarget = null; for (let i = 0; i < 9; i++) { if(pieces[i].Y / PIECE_SIZE == row && pieces[i].X / PIECE_SIZE == col){ moveTarget = pieces[i]; break; } } if(moveTarget == null) return false; moving = true; select_sound.currentTime = 0; select_sound.play(); // cnt回にわけて移動させスライドしているように見せる let dx = 0; let dy = 0; const cnt = 10; if(dir == 'L') dx = - PIECE_SIZE / cnt; if(dir == 'R') dx = PIECE_SIZE / cnt; if(dir == 'U') dy = - PIECE_SIZE / cnt; if(dir == 'D') dy = PIECE_SIZE / cnt; for(let i = 0; i < cnt; i++){ moveTarget.X += dx; moveTarget.Y += dy; await sleep(1000 / 60); } // 小数の誤差でズレるかもしれないので念の為 moveTarget.X = col9 * PIECE_SIZE; moveTarget.Y = row9 * PIECE_SIZE; // 9番ピースと場所を入れ替える piece9.X = col * PIECE_SIZE; piece9.Y = row * PIECE_SIZE; // ステージクリア? if(check()){ $move_count.innerHTML = `<span class = "aqua">クリア!!</span>`; timer_moving = false; // 次のステージが開始されるまで時間計測を停止 // そのステージをクリアするまでにかかった時間の総和を計算してスコアランキングに登録する const time = Date.now() - start_time; time_sum += time; const stage = move_limits[0]; const player_name = $player_name.value; saveScore(player_name, stage, time_sum); // スコアを送信(後述) clear_sound.currentTime = 0; clear_sound.play(); await sleep(2000); move_limits.shift(); // 先頭の要素を削除することで次は何手問題かがわかる if(move_limits.length > 0){ // 次の問題が存在するならそれを出題する getQuestion(); showQuestion(); start_time = Date.now(); timer_moving = true; } else { // 次の問題が存在しないなら「全問クリア」 const id = setInterval(() => { // 「全問クリア」の文字列を下から上へ流す congratulation_y -= 6; if(congratulation_y < -100){ clearInterval(id); congratulation_y = 100; } }, 1000 / 60); congratulation_sound.play(); await sleep(3000); playing = false; // プレイは終了なので再挑戦用のボタンを表示する $start_buttons.style.display = 'block'; } } else{ move_count++; // クリアでない場合は現在手数を加算し、手数を表示 if(move_count < move_limits[0]) $move_count.innerHTML = `現在 ${move_count} 手 (残り ${move_limits[0]-move_count} 手)`; else { // 手数オーバーの場合は失敗 $move_count.innerHTML = `<span class = "red">残念。失敗です</span>`; miss_sound.currentTime = 0; miss_sound.play(); await sleep(2000); showQuestion(); // 同じ問題でやり直しさせる } } moving = false; return true; } |
キー操作
キーを押下することで移動させる処理を示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
async function onKeyDown(ev){ if(ev.key != 'ArrowUp' && ev.key != 'ArrowDown' && ev.key != 'ArrowLeft' && ev.key != 'ArrowRight') return; if(!playing) return; ev.preventDefault(); let is_ok = false; if(ev.key == 'ArrowUp') is_ok = await move('U'); if(ev.key == 'ArrowDown') is_ok = await move('D'); if(ev.key == 'ArrowLeft') is_ok = await move('L'); if(ev.key == 'ArrowRight') is_ok = await move('R'); if(!is_ok){ ng_sound.currentTime = 0; ng_sound.play(); } } |
canvasがクリック・タップされたときに移動させる処理
canvasがクリック・タップされたときに移動させる処理を示します。クリックされたピースがどれか、9番ピースとの位置関係からその移動方向を求め、移動可能であればそれを移動させます。
|
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 |
async function onclickCanvas(ev){ if(!playing) return; const rect = $canvas.getBoundingClientRect(); const x = ev.clientX - rect.x; const y = ev.clientY - rect.y; const move_target = pieces.find(_ => _.X < x && x < _.X + PIECE_SIZE && _.Y < y && y < _.Y + PIECE_SIZE ); const piece9 = pieces.find(_ => _.Number == 9); let is_ok = false; if(move_target != null){ if(move_target.X == piece9.X){ if(move_target.Y - PIECE_SIZE == piece9.Y) is_ok = await move('U'); else if(move_target.Y + PIECE_SIZE == piece9.Y) is_ok = await move('D'); } else if(move_target.Y == piece9.Y){ if(move_target.X - PIECE_SIZE == piece9.X) is_ok = await move('L'); else if(move_target.X + PIECE_SIZE == piece9.X) is_ok = await move('R'); } if(!is_ok){ // 移動不可なら警告音 ng_sound.currentTime = 0; ng_sound.play(); } } } |
スコアランキングへの登録
ステージクリアしたらプレイヤー名とステージ番号、かかった時間をサーバーへ送ります。サーバー側でどのような処理がおこなわれるかは次回示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function saveScore(player_name, stage, time){ if(player_name == '') player_name = '名無しのゴンベ'; // JSON形式でPOST fetch('./ranking.php', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ name: player_name, stage: stage, time: time, }) }); } |
ギブアップ時の処理
ギブアップを選択したときにおこなわれる処理を示します。この場合は解を示してゲームオーバーとします。
解は幅優先探索で取得します。
|
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 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 |
async function showAnswer(){ moving = true; timer_moving = false; move_count = 0; setPieces(cur_question); $question.innerHTML = `${move_limits[0]} 手問題(答えを表示しています)`; $move_count.innerHTML = `現在 ${move_count} 手`; $ctrl_buttons.style.display = 'none'; const map = new Map(); const q = new QueueStack(); map.set(cur_question, ''); q.Enqueue(cur_question); while (q.Count() > 0){ const s = q.Dequeue(); const swaps = getSwapStrings(s); swaps.forEach(_ => { if (!map.has(_)) { q.Enqueue(_); map.set(_, s); } }); } let cur = '123456789'; const ans = []; while(true){ const from = map.get(cur); const cur_idx = cur.indexOf('9'); const cur_row = Math.floor(cur_idx / ROW_COL_COUNT); const cur_col = cur_idx % ROW_COL_COUNT; if(from == '') break; const from_idx = from.indexOf('9'); const from_row = Math.floor(from_idx / ROW_COL_COUNT); const from_col = from_idx % ROW_COL_COUNT; if(cur_col < from_col) ans.unshift({dir:'R', target:from[from_idx - 1]}); // 左を取得 else if(cur_col > from_col) ans.unshift({dir:'L', target:from[from_idx + 1]}); else if(cur_row < from_row) ans.unshift({dir:'D', target:from[from_idx - 3]}); else if(cur_row > from_row) ans.unshift({dir:'U', target:from[from_idx + 3]}); cur = from; } for(let i =0; i<ans.length; i++){ const dir = ans[i].dir; const target = ans[i].target; const move_target = pieces.find(_ => _.Number == target); let dx = 0; let dy = 0; const cnt = 10; if(dir == 'L') dx = - PIECE_SIZE / cnt; if(dir == 'R') dx = PIECE_SIZE / cnt; if(dir == 'U') dy = - PIECE_SIZE / cnt; if(dir == 'D') dy = PIECE_SIZE / cnt; for(let i=0; i<cnt; i++){ move_target.X += dx; move_target.Y += dy; await sleep(1000 / 60); } move_count++; $move_count.innerHTML = `現在 ${move_count} 手 (残り ${move_limits[0]-move_count} 手)`; await sleep(1000); } const piece9 = pieces.find(_ => _.Number == 9); piece9.X = (ROW_COL_COUNT - 1) * PIECE_SIZE; piece9.Y = (ROW_COL_COUNT - 1) * PIECE_SIZE; moving = false; playing = false; $start_buttons.style.display = 'block'; } |
