ナイト配置ゲームとは、8×8のチェス盤のうえにふたりで交互にナイトを置いていくゲームです。すでに置かれているナイトに取られる位置には置けません。置くことができなくなったほうが負けです。
まずはとりあえず動くものをつくります。
Contents
HTML部分
|
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 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>ナイト配置ゲーム</title> <meta name = "viewport" content = "width=device-width, initial-scale = 1.0"> <link href="./style.css" rel="stylesheet" /> <style> </style> </head> <body> <div id = "container"> <h1>ナイト配置ゲーム</h1> <p>ナイトを置いていき、置けなくなったら負け</p> <div id="board"></div> <p id="status"></p> <button id = "start-first" class = "start">リセット(先手)</button> <button id = "start-second" class = "start">リセット(後手)</button> <button class = "start" style="display: none;" onclick="test()">test</button> <div id = "volume"></div> </div><!-- /#container --> <script src = "./index.js"></script> </body> </html> |
style.css
|
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 |
body { font-size: 18px; font-family: sans-serif; text-align: center; color: rgb(245 245 245); background-color: rgb(15 15 15); } p { font-size: 18px; } #board { display: grid; grid-template-columns: repeat(8, 45px); width: 360px; margin: 20px auto; } .cell { width: 45px; height: 45px; line-height: 50px; font-size: 30px; cursor: pointer; } .white { background: #eee; color: black;} .black { background: #666; color: white; } .forbidden { font-size: small; } .start { width: 120px; height: 50px; } |
BitBoardクラスの定義
8×8のチェス盤を二次元配列で扱ってもよいのですが、ここでは2つの整数で表します。JavaScript の最大整数値は Number. MAX_SAFE_INTEGER という定数で定義されており、その値は 9007199254740991 です。2の64乗-1まで扱うことができればいいのですが、ちょっと足りないのでふたつの整数を使います。
着手があれば対応する位置(+着手禁止点)のbitを立て、bitが立っているかどうかでその位置に着手することができるかどうかを判定するという感じです。
index.js
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class BitBoard { constructor(lo = 0, hi = 0) { this.lo = lo >> 0; this.hi = hi >> 0; } clone() { return new BitBoard(this.lo, this.hi); } setBit(pos) { if (pos < 32) this.lo |= (1 << pos); else this.hi |= (1 << (pos - 32)); } getBit(pos) { if (pos < 32) return (this.lo >> pos) & 1; else return (this.hi >> (pos - 32)) & 1; } } |
Stateクラスの定義
盤上の状態を管理するためにStateクラスを定義します。ゲームの進行上、問題になるのは、そこに駒は置かれているか?その地点は着手可能か?のふたつです。このふたつを occupied と forbidden で表します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class State { constructor(){ this.occupied = new BitBoard(); // 駒が置かれている this.forbidden = new BitBoard(); // 着手不能点 } clone(){ const s = new State(); s.occupied = this.occupied.clone(); s.forbidden = this.forbidden.clone(); return s; } } |
Gameクラスの定義
Gameクラスを定義します。
コンストラクタ
コンストラクタを示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
class Game { constructor(){ this.ROW_COUNT = 8; // 8×8マス this.COL_COUNT = 8; this.State = new State(); // 盤面の状態 this.PlayerTurn = true; this.WinMemo = new Map(); // いまは使わない。 // 効果音 this.SelectSound = new Audio('./sounds/select.mp3'); this.NgSound = new Audio('./sounds/ng.mp3'); this.CpuSound = new Audio('./sounds/cpu.mp3'); this.WinSound = new Audio('./sounds/win.mp3'); this.LoseSound = new Audio('./sounds/lose.mp3'); this.Sounds = [this.SelectSound, this.NgSound, this.CpuSound, this.WinSound, this.LoseSound]; } } |
合法手の全取得
すべての合法手を取得する処理を示します。
|
1 2 3 4 5 6 7 8 9 10 |
class Game { GetLegalMoves(state){ const res = []; for(let i = 0; i < this.ROW_COUNT * this.COL_COUNT; i++){ if(!state.forbidden.getBit(i)) res.push(i); } return res; } } |
着手後の盤面の状態の取得
着手後の盤面の状態を取得する処理を示します。
着手した地点に駒をおき、その地点とそこからナイトが移動できるすべての地点を着手禁止点に変更します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
class Game { Inside(row, col){ return row >= 0 && row < this.ROW_COUNT && col >= 0 && col < this.COL_COUNT; } MakeMove(state, pos){ const n = state.clone(); n.occupied.setBit(pos); const r = Math.floor(pos / this.COL_COUNT); const c = pos % this.COL_COUNT; const moves = [[2,1],[1,2],[-1,2],[-2,1],[-2,-1],[-1,-2],[1,-2],[2,-1],[0,0]]; for(let [dy,dx] of moves){ const nc = c + dx; const nr = r + dy; if(this.Inside(nr,nc)) n.forbidden.setBit(nr * this.COL_COUNT + nc); } return n; } } |
コンピュータ側の処理
コンピュータ側が着手する処理を示します。今回は着手可能な地点をランダムに選んで着手するだけにしています。はっきり言って弱いです。
|
1 2 3 4 5 6 7 |
class Game { AiMove(){ const moves = this.GetLegalMoves(this.State); const move = moves[Math.floor(Math.random() * moves.length)]; this.State = this.MakeMove(this.State, move); } } |
更新処理と描画
更新処理と描画の処理を示します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
class Game { Update(){ this.Draw(); const moves = this.GetLegalMoves(this.State); if(moves.length == 0){ if(this.PlayerTurn){ document.getElementById("status").textContent = "あなたの負け!"; this.LoseSound.play(); } else { document.getElementById("status").textContent = "あなたの勝ち!"; this.WinSound.play(); } return true; } else { document.getElementById("status").textContent = this.PlayerTurn ? `あなたのターン (残り ${moves.length} 箇所)` : "AI思考中..."; 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 46 47 48 49 50 51 52 53 54 |
class Game { Draw(){ const $board = document.getElementById("board"); $board.innerHTML=""; const state = this.State; for(let i = 0; i < this.ROW_COUNT * this.COL_COUNT; i++){ const cell = document.createElement("div"); const row = Math.floor(i / this.COL_COUNT); const col = i % this.COL_COUNT; cell.className = "cell"; cell.classList.add((row + col) % 2 ? "black" : "white"); if(state.forbidden.getBit(i)){ if(state.occupied.getBit(i)) cell.textContent="?"; else { cell.classList.add("forbidden"); cell.textContent="×" } } // プレイヤーの手番で着手可能なマスをクリックしたら着手の処理ができるようにする cell.onclick=()=>{ if(this.PlayerTurn && this.GetLegalMoves(state).includes(i)){ this.State = this.MakeMove(state, i); this.PlayerTurn = false; const ret = this.Update(); this.SelectSound.currentTime = 0; this.SelectSound.play(); if(ret) return; setTimeout(()=>{ this.AiMove(); this.CpuSound.play(); this.PlayerTurn = true; this.Update(); },500); } else { this.NgSound.currentTime = 0; this.NgSound.play(); } }; $board.appendChild(cell); } } } |
リセット時の処理
ゲーム開始時や終局後に再挑戦するために現在の状態をリセットする処理を示します。
引数が true なら先手、false なら後手です。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
class Game { ResetGame(is_first){ this.State = new State(); this.WinMemo.clear(); this.PlayerTurn = is_first; this.Update(); if(!is_first){ setTimeout(() => { this.State = this.MakeMove(this.State, 27); this.CpuSound.play(); this.PlayerTurn = true; this.Update(); }, 1000); } this.SelectSound.play(); } } |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
|
1 2 3 4 5 6 7 8 9 10 |
window.addEventListener('load', () => { const game = new Game(); document.getElementById('start-first')?.addEventListener('click', () => game.ResetGame(true)); document.getElementById('start-second')?.addEventListener('click', () => game.ResetGame(false)); game.Update(); initVolume('volume', game.Sounds); }); |
以下はレンジスライダーでボリュームを調整するための定番の処理です。
|
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 |
function initVolume(elementId, sounds){ let volume = 0.3; const savedVolume = localStorage.getItem('hatodemowakaru-volume'); if(savedVolume) volume = Number(savedVolume); const $element = document.getElementById(elementId); const $div = document.createElement('div'); const $span1 = document.createElement('span'); $span1.innerHTML = '音量'; $div.appendChild($span1); const $range = document.createElement('input'); $range.type = 'range'; $div.appendChild($range); const $span2 = document.createElement('span'); $div.appendChild($span2); $range.addEventListener('input', () => { const value = $range.value; $span2.innerText = value; volume = Number(value) / 100; setVolume(); }); $range.addEventListener('change', () => localStorage.setItem('hatodemowakaru-volume', volume.toString())); setVolume(); $span2.innerText = Math.round(volume * 100).toString(); $span2.style.marginLeft = '16px'; $range.value = Math.round(volume * 100).toString(); $range.style.width = '230px'; $range.style.verticalAlign = 'middle'; $element.appendChild($div); const $button = document.createElement('button'); $button.innerHTML = '音量テスト'; $button.style.width = '120px'; $button.style.height = '45px'; $button.style.marginTop = '12px'; $button.style.marginLeft = '32px'; $button.addEventListener('click', () => { sounds[0].currentTime = 0; sounds[0].play(); }); $element.appendChild($button); function setVolume(){ for(let i = 0; i < sounds.length; i++) sounds[i].volume = volume; } } |
