JavaScriptで三目並べをつくります。
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 32 33 34 35 36 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <title>鳩でもわかる三目並べ</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="./style.css" type="text/css"> </head> <body> <div id = "field"> <div> <img id = "cell-0" class = "cell"> <img id = "cell-1" class = "cell"> <img id = "cell-2" class = "cell"> </div> <div> <img id = "cell-3" class = "cell"> <img id = "cell-4" class = "cell"> <img id = "cell-5" class = "cell"> </div> <div> <img id = "cell-6" class = "cell"> <img id = "cell-7" class = "cell"> <img id = "cell-8" class = "cell"> </div> <div id = "message"></div> <button id = "start" onclick="start()">開始</button> <div id = "first-outer"><input type="checkbox" id = "first">先手</div> </div> <script src="./app.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 |
body { background-color: #000; color: #fff; } #field { margin: 30px 10px 10px 20px ; } .cell { width: 60px; height: 60px; border: 2px solid #fff; display: inline-block; margin-bottom: 5px; margin-right: 5px; } #start { width:150px; height:40px; } |
JavaScript部分
定数は以下のとおりです。
1 2 3 |
const CELL_STATUS_NONE = 0; const CELL_STATUS_MARU = 1; const CELL_STATUS_BATTEN = 2; |
Cellクラスを定義します。
1 2 3 4 5 6 7 |
class Cell { constructor(row, col){ this.Status = CELL_STATUS_NONE; this.Row = row; this.Col = col; } } |
ボタンや文字列を表示するために各要素を取得します。
1 2 3 4 |
let $start = document.getElementById('start'); let $first = document.getElementById('first'); let $firstOuter = document.getElementById('first-outer'); let $message = document.getElementById('message'); |
グローバル変数を示します。
1 2 3 4 5 6 7 |
let $cells = []; // セル描画用の要素の配列 let cells = []; // Cellオブジェクトを格納する配列 let eightLines = null; // 縦横斜めに一列そろっているCellオブジェクトが格納された配列の配列 let cornerCells = null;// Cellオブジェクトの角の部分が格納された配列 let isPlayerFirst = true; // プレーヤーが先手か? let ignoreClick = true; // ゲーム開始前やCPUの手番のときはクリックを無視する |
ページが読み込まれたときの処理
ページが読み込まれたときの処理を示します。
最初はプレーヤー側先手の設定にしたいのでチェックボックスがチェックされた状態にします。セルを描画するためのDOM要素を取得して配列に格納するとともにこれと対応するCellオブジェクトも生成します。同時に縦横斜めに一列そろっているCellオブジェクトが格納された配列の配列や、Cellオブジェクトの角の部分が格納された配列にも適切な値をセットします。
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 |
window.onload = (ev) => { $first.checked = true; for(let i = 0; i < 9; i++) $cells.push(document.getElementById(`cell-${i}`)); for(let row = 0; row < 3; row++){ for(let col = 0; col < 3; col++) cells.push(new Cell(row, col)); } eightLines = get8Lines(); // 後述 cornerCells = getCornerCells(); // 後述 initCells(); // 後述 // ユーザーがつぎに何をすればいいかわかるようにメッセージを表示する if($message != null) $message.innerHTML = 'ゲームを開始するときは[開始]ボタンをクリックしてください'; for(let i = 0; i < 9; i++){ $cells[i].addEventListener('click', async(ev) => { // ゲーム開始前やCPUが次の手を考えているときはなにもしない if(ignoreClick) return; ignoreClick = true; // 着手不能点に着手しようとしたときは着手できない旨を表示する if(cells[i].Status != CELL_STATUS_NONE){ if($message != null) $message.innerHTML = 'ここには置けません'; ignoreClick = false; return; } // 着手が成立したら○または×を表示する if(isPlayerFirst){ $cells[i].src = 'maru.png'; cells[i].Status = CELL_STATUS_MARU; } else { $cells[i].src = 'batsu.png'; cells[i].Status = CELL_STATUS_BATTEN; } if($message != null) $message.innerHTML = ''; // プレーヤー側の勝利が確定しているか? if(checkVictory()){ if($message != null) $message.innerHTML = 'あなたの勝ちです'; onGameEnd(); return; } // CPU側の着手 await thinkCpu(); // CPU側の勝利が確定しているか? if(checkDefeat()){ if($message != null) $message.innerHTML = 'あなたの負けです'; onGameEnd(); return; } // 引き分けであることが確定しているか? let emptyCells = getEmptyCells(); if(emptyCells.length == 0){ if($message != null) $message.innerHTML = '引き分けです'; onGameEnd(); return; } // 引き分けであることが確定しているか? ignoreClick = false; if($message != null) $message.innerHTML = '着手してください'; }); } } |
縦横斜めに一列そろっているCellオブジェクトの配列の配列を取得する処理を示します。
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 |
function get8Lines(){ let lines = []; for(let i = 0; i < 3; i++){ let arr = []; arr.push(cells[0 + 3 * i]); arr.push(cells[1 + 3 * i]); arr.push(cells[2 + 3 * i]); lines.push(arr); } for(let i = 0; i < 3; i++){ let arr = []; arr.push(cells[0 + i]); arr.push(cells[3 + i]); arr.push(cells[6 + i]); lines.push(arr); } { let arr = []; arr.push(cells[0]); arr.push(cells[4]); arr.push(cells[8]); lines.push(arr); } { let arr = []; arr.push(cells[2]); arr.push(cells[4]); arr.push(cells[6]); lines.push(arr); } return lines; } |
Cellオブジェクトの角の部分が格納された配列を取得する処理を示します。
1 2 3 4 5 6 7 8 |
function getCornerCells(){ let ret = []; ret.push(cells.filter(_ => _.Row == 0 && _.Col == 0)[0]); ret.push(cells.filter(_ => _.Row == 2 && _.Col == 2)[0]); ret.push(cells.filter(_ => _.Row == 0 && _.Col == 2)[0]); ret.push(cells.filter(_ => _.Row == 2 && _.Col == 0)[0]); return ret; } |
セルを初期化する処理を示します。セルに○も×も表示されていない状態にするとともに、すべてのCellオブジェクトのStatusプロパティをCELL_STATUS_NONEにセットします。
1 2 3 4 5 6 |
function initCells(){ for(let i = 0; i < 9; i++) $cells[i].src = 'none.png'; for(let i = 0; i < 9; i++) cells[i].Status = CELL_STATUS_NONE; } |
勝敗判定
着手によってプレーヤー側またはCPU側が勝利したことを調べる処理を示します。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
function checkVictory(){ if (isPlayerFirst) return checkLines(CELL_STATUS_MARU); else return checkLines(CELL_STATUS_BATTEN); } function checkDefeat(){ if (isPlayerFirst) return checkLines(CELL_STATUS_BATTEN); else return checkLines(CELL_STATUS_MARU); } |
checkLines関数は縦横ななめのいずれかで3つ並んでいるかどうかを調べるためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
function checkLines(cellStatus){ for(let i=0; i<8; i++){ let ok = true; for(let k=0; k<3; k++){ if(eightLines[i][k].Status != cellStatus){ ok = false; break; } } if(ok) return true; } return false; } |
対局が終了したらスタートボタンと先手後手設定用のチェックボックスを表示させます。
1 2 3 4 5 6 |
function onGameEnd(){ if($start != null) $start.style.display = 'block'; if($firstOuter != null) $firstOuter.style.display = 'block'; } |
AIに次の一手を考えさせる
AIに次の一手を考えさせる処理を示します。
リーチがかかっているときは勝負を決め、プレーヤー側にリーチがかかっている場合はこれを阻止します。これに該当しない場合は次に複数のリーチをする手段が存在する場合はこれを実行し、プレーヤー側が可能なときはこれを阻止します。CPU後攻でプレーヤー側の初手が角の場合は中央へ、中央のときは角に着手しないと負けてしまうので、そのように着手します。
これらに該当しない場合は適当に空いている場所に着手します。
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 |
async function thinkCpu(){ let emptyCells = getEmptyCells(); if(emptyCells.length == 0){ if($message != null) $message.innerHTML = '引き分けです'; return; } let own = isPlayerFirst ? CELL_STATUS_BATTEN : CELL_STATUS_MARU; let imgPath = isPlayerFirst ? 'batsu.png' : 'maru.png'; if($message != null) $message.innerHTML = '考えています'; await sleep(500); // リーチがかかっているときは勝負を決める if(checkReach()) return; // ダブルリーチが可能ならかける if (setDoubleReach()) return; // プレーヤーのダブルリーチが可能なら阻止する if (blockDoubleReach()) return; // プレーヤーの着手が悪手だったときは勝ちに行く if (getWin()) return; // 後攻で中央が空いているときは中央に着手 let center = cells.filter(_ => _.Row == 1 && _.Col == 1)[0]; if (isPlayerFirst && center.Status == CELL_STATUS_NONE){ center.Status = own; $cells[center.Row * 3 + center.Col].src = imgPath; return; } // プレーヤーが中央に着手したときは角に着手 let enemy = isPlayerFirst ? CELL_STATUS_MARU : CELL_STATUS_BATTEN; if (center.Status == enemy) { let arr = cornerCells.filter(_ => _.Status == CELL_STATUS_NONE); if(arr.length > 0){ let cell = arr[Math.floor(Math.random() * arr.length)]; cell.Status = own $cells[cell.Row * 3 + cell.Col].src = imgPath; return; } } let r = Math.floor(Math.random() * emptyCells.length); emptyCells[r].Status = own; $cells[emptyCells[r].Row * 3 + emptyCells[r].Col].src = imgPath; } |
getEmptyCells関数は空白のセルを探します。
1 2 3 |
function getEmptyCells(){ return cells.filter(_ => _.Status == CELL_STATUS_NONE); } |
sleep関数はプレーヤー側着手後、すぐにCPU側が着手すると違和感があるので少し処理を止めるためのものです。
1 2 3 |
async function sleep(interval){ await new Promise(resolve => setInterval(resolve, interval)) } |
checkReach関数は現在リーチがかかっているか調べ、CPU側がリーチの場合はそのまま着手し、プレーヤー側がリーチの場合はこれを阻止するためのものです。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function checkReach(){ let own = isPlayerFirst ? CELL_STATUS_BATTEN : CELL_STATUS_MARU; let enemy = isPlayerFirst ? CELL_STATUS_MARU : CELL_STATUS_BATTEN; let imgPath = isPlayerFirst ? 'batsu.png' : 'maru.png'; let reachs = getReach(own); if (reachs.length > 0) { let r = Math.floor(Math.random() * reachs.length); reachs[r].Status = own; $cells[reachs[r].Row * 3 + reachs[r].Col].src = imgPath; return true; } reachs = getReach(enemy); if (reachs.length > 0) { let r = Math.floor(Math.random() * reachs.length); reachs[r].Status = own; $cells[reachs[r].Row * 3 + reachs[r].Col].src = imgPath; return true; } return false; } |
getReach関数はここに着手すれば勝利できるセルがあれば、これを配列にして返します。
1 2 3 4 5 6 7 8 9 10 11 12 |
function getReach(cellStatus){ let ret = []; for(let i =0; i<eightLines.length; i++){ if(eightLines[i].filter(_ => _.Status == cellStatus).length != 2) continue; if(eightLines[i].filter(_ => _.Status == CELL_STATUS_NONE).length != 1) continue; ret.push(eightLines[i].filter(_ => _.Status == CELL_STATUS_NONE)[0]); } return ret; } |
setDoubleReach関数はここに着手すれば複数のリーチがかかる場合があるか調べ、そのような手が存在する場合は着手します。
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 |
function setDoubleReach(){ let nextList = cells.filter(_ => _.Status == CELL_STATUS_NONE); if(nextList.length == 0) return false; let own = isPlayerFirst ? CELL_STATUS_BATTEN : CELL_STATUS_MARU; let enemy = isPlayerFirst ? CELL_STATUS_MARU : CELL_STATUS_BATTEN; let imgPath = isPlayerFirst ? 'batsu.png' : 'maru.png'; let reachs = []; for(let i=0; i<nextList.length; i++){ nextList[i].Status = own; if (getReach(own).length > 1) reachs.push(nextList[i]); nextList[i].Status = CELL_STATUS_NONE; } if (reachs.length > 0) { let r = Math.floor(Math.random() * reachs.length); reachs[r].Status = own; $cells[reachs[r].Row * 3 + reachs[r].Col].src = imgPath; return true; } else return false; } |
blockDoubleReach関数はプレーヤー側が着手すれば複数のリーチがかかる場合があるか調べ、そのような手が存在する場合はこれを阻止します。
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 |
function blockDoubleReach(){ let nextList = cells.filter(_ => _.Status == CELL_STATUS_NONE); if(nextList.length == 0) return false; let own = isPlayerFirst ? CELL_STATUS_BATTEN : CELL_STATUS_MARU; let enemy = isPlayerFirst ? CELL_STATUS_MARU : CELL_STATUS_BATTEN; let imgPath = isPlayerFirst ? 'batsu.png' : 'maru.png'; // プレーヤーのダブルリーチが可能なら阻止する let reachs = []; for(let i=0; i<nextList.length; i++){ nextList[i].Status = enemy; if (getReach(enemy).length > 1) reachs.push(nextList[i]); nextList[i].Status = CELL_STATUS_NONE; } if (reachs.length == 0) return false; // 単純にその場所に先行すれば阻止できるわけではないので総当たりで調べる let ret = []; nextList = cells.filter(_ => _.Status == CELL_STATUS_NONE); for(let i=0; i<nextList.length; i++){ nextList[i].Status = own; let nextList2 = cells.filter(_ => _.Status == CELL_STATUS_NONE); let reachs2 = []; for(let k=0; k<nextList2.length; k++){ nextList2[k].Status = enemy; if (getReach(enemy).length > 1) { let reachs3 = getReach(own); // この場合はダブルリーチを阻止できていない if (reachs3.length == 0) reachs2.push(nextList2[k]); } nextList2[k].Status = CELL_STATUS_NONE; } nextList[i].Status = CELL_STATUS_NONE; // ダブルリーチを阻止できる手段をリストに格納する if (reachs2.length == 0) ret.push(nextList[i]); } // ダブルリーチを阻止する手段がない場合はなにもしないでfalseを返す if(ret.length == 0) return false; let r = Math.floor(Math.random() * ret.length); ret[r].Status = own; $cells[ret[r].Row * 3 + ret[r].Col].src = imgPath; return true; } |
プレーヤー側がミスをした場合、CPUが勝ちにいく処理を示します。
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 |
function getWin(){ if (cells.filter(_ => _.Status == CELL_STATUS_MARU).length != 1) return false; if(cells.filter(_ => _.Status == CELL_STATUS_BATTEN).length != 1) return false; let imgPath = isPlayerFirst ? 'batsu.png' : 'maru.png'; let center = cells.filter(_ => _.Row == 1 && _.Col == 1)[0]; let notCornerCells = []; notCornerCells.push(cells.filter(_ => _.Row == 0 && _.Col == 1)[0]); notCornerCells.push(cells.filter(_ => _.Row == 1 && _.Col == 0)[0]); notCornerCells.push(cells.filter(_ => _.Row == 1 && _.Col == 2)[0]); notCornerCells.push(cells.filter(_ => _.Row == 2 && _.Col == 1)[0]); notCornerCells = notCornerCells.filter(_ => _.Status == CELL_STATUS_BATTEN); if (notCornerCells.length == 0) return false; let enemyCell = notCornerCells[0]; if (enemyCell.Row == 0 || enemyCell.Row == 2){ let row = enemyCell.Row; let cell; if (center.Status == CELL_STATUS_MARU) cell = cornerCells.filter(_ => _.Row == row)[0]; else if (cornerCells.filter(_ => _.Row == row).filter(_ => _.Status == CELL_STATUS_MARU).length > 0) cell = center; else return false; cell.Status = CELL_STATUS_MARU; $cells[cell.Row * 3 + cell.Col].src = imgPath; return true; } if (enemyCell.Col == 0 || enemyCell.Col == 2) { let col = enemyCell.Col; let cell; if (center.Status == CELL_STATUS_MARU) cell = cornerCells.filter(_ => _.Col == col)[0]; else if (cornerCells.filter(_ => _.Col == col).filter(_ => _.Status == CELL_STATUS_MARU).length > 0) cell = center; else return false; cell.Status = CELL_STATUS_MARU; $cells[cell.Row * 3 + cell.Col].src = imgPath; 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 |
async function start(){ isPlayerFirst = $first.checked; // セルを初期化 initCells(); // スタートボタンと先手後手設定用のチェックボックスを非表示にする if($start != null) $start.style.display = 'none'; if($firstOuter != null) $firstOuter.style.display = 'none'; // CPU先攻の場合は初手を考える if(!isPlayerFirst) await thinkCpu(); // プレーヤー側に次の手を選択させる ignoreClick = false; if($message != null) $message.innerHTML = '着手してください'; } |