タンク・チェンジをゲームにする 外観・イベントリスナの追加までの続きです。今回は戦車を移動させる処理を実装します。
Contents
戦車を選択する処理
広場の上に表示されている戦車の画像がクリックされたときにおこなわれる処理を示します。
クリックされたら対応する位置にある戦車の番号をチェックします。青なら 0 ~ 2、赤なら 3 ~ 5、戦車がない位置であれば -1 です。
つぎにすることが移動させたい戦車の選択(phase が 0 なら青、2 なら 赤)である場合であれば選択された位置をグローバル変数 selectedPosition に保存し、phase を次に移すとともに選択されたことがわかるように効果音を鳴らします。
戦車を選ぶタイミングでなかったり、適切な戦車を選べていない場合は、ユーザーに操作が間違っていることを知らせるための文字列を表示し、効果音を鳴らします。
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 |
function onClickTank(row, col){ if(phase == 0){ // 今は青い戦車を選ぶフェイズ if(matTanks[row][col] != -1 && matTanks[row][col] < 3){ // 正常:青い戦車が選択された $naviText.innerText = (moveCount + 1) + '手目:青軍の戦車の移動先を選択せよ'; selectedPosition = new Position(row, col); playSelectSound(); phase = 1; } else { // 不正:青ではない戦車が選択された $naviText.innerText = (moveCount + 1) + '手目:青軍の戦車を選択してください'; playBadSound(); // 後述 } return; } if(phase == 2){ // 今は赤い戦車を選ぶフェイズ if(matTanks[row][col] >= 3){ // 正常:赤い戦車が選択された $naviText.innerText = (moveCount + 1) + '手目:赤軍の戦車の移動先を選択せよ'; selectedPosition = new Position(row, col); playSelectSound(); phase = 3; } // 不正:赤ではない戦車が選択された else { $naviText.innerText = (moveCount + 1) + '手目:赤軍の戦車を選択してください'; playBadSound(); // 後述 } return; } if(phase == 1){ // 不正:今は青い戦車を移動させるフェイズであって選ぶフェイズではない onFailureMoveBlue(); // 後述 return; } if(phase == 3){ // 不正:今は赤い戦車を移動させるフェイズであって選ぶフェイズではない onFailureMoveRed(); // 後述 return; } } function playSelectSound(){ selectSound.currentTime = 0; selectSound.play(); } |
戦車を選択するフェイズで移動先を選んでしまった場合や移動のフェイズで移動できない場所を選択するという間違った操作がされたときにおこなわれる処理を示します。
この場合は間違いを指摘するメッセージを表示して効果音を鳴らします。戦車を移動させるときに不正な操作をした場合もこの処理がおこなわれます(戦車の選択に戻ってやり直すことになる)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function onFailureMoveBlue(){ $naviText.innerText = '移動できません。もう一度移動させたい青軍の戦車を選択しなおしてください'; phase = 0; playBadSound(); } function onFailureMoveRed(){ $naviText.innerText = '移動できません。もう一度移動させたい赤軍の戦車を選択しなおしてください'; phase = 2; playBadSound(); } function playBadSound(){ badSound.currentTime = 0; badSound.play(); } |
戦車を移動させる処理
ゲームでは移動させたい戦車を選択したらつぎに移動先を選択することになります。ここからは移動させる処理を示します。
その前にこれに関係する処理をする関数を示します。
sleep は第一引数で与えられた時間(単位:ミリ秒)だけ待機する関数です。
1 2 3 |
async function sleep(ms){ await new Promise(resolve => setTimeout(resolve, ms)); } |
getTankPosition は第一引数で与えられた番号の戦車の位置を返す関数です。グローバル変数 matTanks から引数で渡れた値があるかを探しているだけです。
1 2 3 4 5 6 7 8 |
function getTankPosition(number){ for(let row = 0; row < matTanks.length; row++){ for(let col = 0; col < matTanks[0].length; col++){ if(matTanks[row][col] == number) return new Position(row, col); } } } |
戦車は移動可能か?
CanMoveTank は srow 行 scol 列目にある戦車がgrow 行 gcol 列目に移動できるかを返す関数です。二次元配列を斜めにたどっていって他の戦車にぶつかることなく目的地にたどり着けるかを調べています。移動後に敵の戦車と向かい合ってしまうと撃ち合いが発生して移動できないのですが、この判定処理は移動後におこないます。処理を分けることに特別な意味はなく単なる演出上のこだわりです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
function canMoveTank(srow, scol, grow, gcol){ const dx = [1, 1, -1, -1]; const dy = [1, -1, 1, -1]; for(let i = 0; i < 4; i++) { let r = srow; let c = scol; while(true){ r += dy[i]; c += dx[i]; // 配列の範囲外であったり他の戦車がある場合は処理を打ち切る if(r < 0 || c < 0 || r >= matMoveButtons.length || c >= matMoveButtons[0].length || matTanks[r][c] != -1) break; if(r == grow && c == gcol) return true; } } return false; } |
流れるように戦車を移動させる
戦車を移動させる処理を示します。一瞬で移動するのではなく流れるように移動させるために待機処理をいれて少しづつ移動させています。
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 |
async function moveSmooth(oldPosition, newPosition){ const tankNumber = matTanks[oldPosition.Row][oldPosition.Col]; // どの戦車を移動させるか? const image = tankImages[tankNumber]; // その戦車はどのimg要素か? // 移動元の座標を求めるために戦車を左上の隅に配置したときの画像の中心座標を求める image.style.left = '0px'; image.style.top = '0px'; const imageRect = image.getBoundingClientRect(); const imageCenterX = (imageRect.left + imageRect.right) / 2; const imageCenterY = (imageRect.top + imageRect.bottom) / 2; // 移動元の座標を求める(画像の中心が広場の中心になるようにする) const oldRect = matMoveButtons[oldPosition.Row][oldPosition.Col].getBoundingClientRect(); const oldCenterX = (oldRect.left + oldRect.right) / 2; const oldCenterY = (oldRect.top + oldRect.bottom) / 2; const oldX = oldCenterX - imageCenterX; const oldY = oldCenterY - imageCenterY; // 移動先の座標を求める const newRect = matMoveButtons[newPosition.Row][newPosition.Col].getBoundingClientRect(); const newCenterX = (newRect.left + newRect.right) / 2; const newCenterY = (newRect.top + newRect.bottom) / 2; const newX = newCenterX - imageCenterX; const newY = newCenterY - imageCenterY; // 移動元と移動先の座標からどの方向にどれだけ移動するかを決める const dx = newX > oldX ? BUTTON_PITCH_X / 4 : -BUTTON_PITCH_X / 4; const dy = newY > oldY ? BUTTON_PITCH_Y / 4 : -BUTTON_PITCH_Y / 4; const count = (newX - oldX) / dx - 1; // 待機処理を入れながら少しづつ移動させる for(let i = 0; i < count; i++){ image.style.left = oldX + dx * (i +1) + 'px'; image.style.top = oldY + dy * (i +1) + 'px'; await sleep(30); } // 小数によるズレがあるかもしれないので最後は確実に移動先の座標を設定する image.style.left = newX + 'px'; image.style.top = newY + 'px'; // 戦車の位置情報を記録している2次元配列を更新する matTanks[oldPosition.Row][oldPosition.Col] = -1; matTanks[newPosition.Row][newPosition.Col] = tankNumber; } |
戦車が撃たれた場合の判定とその後の処理
移動が完了したらその先は敵の射程範囲内で撃たれてしまうかもしれません。その場合は効果音を鳴らして戦車をもとの位置に戻し、その位置には移動できない旨を表示させます。
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 |
function isDead(number){ const from = getTankPosition(number); // その戦車はどこにいるか? let sr = from?.Row; let sc = from?.Col; // 敵の戦車がいないか斜めにたどって調べる const dx = [1,1,-1,-1]; const dy = [1,-1,1,-1]; for(let i = 0; i < 4; i++){ let r = sr; let c = sc; while(true){ r += dy[i]; c += dx[i]; if(r < 0 || c < 0 || r >= matMoveButtons.length || c >= matMoveButtons[0].length) break; // その戦車から見える位置に戦車がいた場合は敵軍の戦車かどうか調べる if(matTanks[r][c] != -1){ if(number < 3 && matTanks[r][c] >= 3) return true; if(number >= 3 && matTanks[r][c] < 3) return true; } } } return false; } |
isDead関数によって移動した戦車が撃たれた場合、その戦車はもとの位置に戻します。戻す処理は滑るように移動する処理はせず一瞬で移動させます。
1 2 3 4 5 6 7 8 9 |
function moveTank(oldPosition, newPosition){ // 対応する位置にいる戦車を特定して matTanks を更新する const tankNumber = matTanks[oldPosition.Row][oldPosition.Col]; matTanks[oldPosition.Row][oldPosition.Col] = -1; matTanks[newPosition.Row][newPosition.Col] = tankNumber; // そのあと画像を移動先の広場に対応する要素のうえに移動させる show(tankImages[tankNumber], matMoveButtons[newPosition.Row][newPosition.Col]); } |
戦車が撃たれてしまう場所に移動させてしまったときに移動できない旨を表示する処理を示します。同じタイミングで効果音も鳴らしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
function onDeadMoveBlue(){ $naviText.innerText = '移動できません。もう一度移動させたい青軍の戦車を選択しなおしてください'; phase = 0; playDeadSound(); } function onDeadMoveRed(){ $naviText.innerText = '移動できません。もう一度移動させたい赤軍の戦車を選択しなおしてください'; phase = 2; playDeadSound(); } function playDeadSound(){ deadSound.currentTime = 0; deadSound.play(); } |
クリア判定とクリア時の処理
クリア判定とクリア時の処理を示します。戦車の位置が開始時と完全に入れ替わっているかどうかがわかればよいので2次元配列 matTanks に記録されている戦車の番号を調べています。一番上の行と下の行をそれぞれ別に集めてソートし、前者が 3,4,5 であり、後者が 0,1,2 であればクリアしていることになります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function isFinish(){ const arr0 = []; arr0.push(matTanks[0][0]); arr0.push(matTanks[0][2]); arr0.push(matTanks[0][4]); arr0.sort((a, b) => a - b); const arr1 = []; arr1.push(matTanks[6][0]); arr1.push(matTanks[6][2]); arr1.push(matTanks[6][4]); arr1.sort((a, b) => a - b); if(arr0[0] == 3 && arr0[1] == 4 && arr0[2] == 5 && arr1[0] == 0 && arr1[1] == 1 && arr1[2] == 2) return true; else return false; } |
クリア時はクリアまでに何手でかかったかを表示したかを表示します。コードにあるとおり、クリアまでの最短は18手です。
参考:タンク・チェンジの問題を解く(Cマガ電脳クラブ 第127回)
そのあとスタートボタンを再表示させて降参ボタンを非表示にします。そしてignoreClick フラグをセットして広場や戦車をクリックしても反応しないようにします。
1 2 3 4 5 6 7 |
function onClear(){ clearSound.play(); $naviText.innerText = moveCount + '手でフィニッシュです。ちなみに最短手数は 18 手です。'; $startButton.style.display = 'block'; $giveupButton.style.display = 'none'; ignoreClick = true; } |
移動先をクリックしたときの処理
これまでに定義してきた関数をつかって移動先をクリックしたときの処理を実装します。phase が 1 と 3 のときだけ移動の処理をおこないます。それ以外のときは不正な操作です。
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 |
async function onClickMoveButton(row, col){ if(phase == 1){ // 移動させられるなら移動の処理をする const tankNumber = matTanks[selectedPosition.Row][selectedPosition.Col]; if(canMoveTank(selectedPosition.Row, selectedPosition.Col, row, col)){ await moveSmooth(selectedPosition, new Position(row, col)); playSelectSound(); // 移動先で撃たれてしまったら元の位置に戻す if(isDead(tankNumber)) { await sleep(500); onDeadMoveBlue(); await sleep(500); moveTank(new Position(row, col), selectedPosition); return; } moveCount++; // 正常な着手であれば手数のカウントを増やす phase = 2; // クリア判定。していない場合は赤い戦車を選択させる。 if(isFinish()) onClear(); else $naviText.innerText = (moveCount + 1) + '手目:移動させたい赤軍の戦車を選択せよ'; } else // 不正な操作(移動できない場所や戦車がいる場所をクリックした) onFailureMoveBlue(); return; } if(phase == 3){ const tankNumber = matTanks[selectedPosition.Row][selectedPosition.Col]; if(canMoveTank(selectedPosition.Row, selectedPosition.Col, row, col)){ await moveSmooth(selectedPosition, new Position(row, col)); playSelectSound(); if(isDead(tankNumber)) { await sleep(500); onDeadMoveRed(); await sleep(500); moveTank(new Position(row, col), selectedPosition); return; } phase = 0; moveCount++; if(isFinish()) onClear(); else $naviText.innerText = (moveCount + 1) + '手目:移動させたい青軍の戦車を選択せよ'; } else onFailureMoveRed(); return; } if(phase == 0){ // 不正な操作(いまは戦車を移動するフェイズではない) $naviText.innerText = '青軍の戦車を選択してください'; playBadSound(); return; } if(phase == 2){ // 不正な操作(いまは戦車を移動するフェイズではない) $naviText.innerText = '赤軍の戦車を選択してください'; playBadSound(); return; } } |
次回はギブアップしたときに正解手順を示す処理を実装します。