「五目並べ」はシンプルなルールながら、読み合いや戦略が深く、昔から多くの人を夢中にしてきた定番ゲームです。
この記事では、この五目並べを 「AI対戦ができるプログラム」 として作り上げる方法を、初心者にも分かりやすく解説します。
AIと聞くと難しく感じるかもしれませんが、ご安心ください。
今回作るのは HTML・CSS・JavaScriptだけで動く単一ファイルの五目並べのAI対戦です。
特別なライブラリや開発環境は一切必要ありません。ブラウザとテキストエディタだけでOKです。
「ロジックが難しそう」「理解できるか不安…」という方も大丈夫。
この記事では、完成したコードを そのままコピペして動かすだけ で、すぐにAIと対戦できる五目並べが完成します。
実際に手を動かして AI の挙動を見ながら学ぶことで、
「なぜこのロジックが必要なのか」が自然とつながり、理解がぐっと楽になります。
あなた自身の手で、AI対戦ができる五目並べを作り上げていきましょう!
五目並べAI対戦の基本仕様
まずは今回作成する 五目並べゲームのルール と AIがどこまで対応するのか を明確にしておきましょう。
最初に前提が整理されていると、後のロジック解説が一気に理解しやすくなります。
完成版を体験してみよう
五目並べの基本ルール
五目並べは非常にシンプルなルールで遊べますが、念のため基礎を表にまとめておきます。
| 項目 | 内容 |
|---|---|
| 盤面 | 15×15 の格子状の盤を使用します。 |
| プレイヤー | 黒(先手)・白(後手)が交互に石を置きます。 |
| 勝利条件 | 縦・横・斜めいずれかに 5 個の石を連続で並べた方が勝ち。 |
| 引き分け | 全てのマスが埋まり、勝者がいない場合は引き分け。 |
シンプルなルールですが、実際の対戦では「守り」や「先手有利」といった戦略的要素が強く、読み合いも奥深いゲームです。
今回つくるAI五目並べの仕様と特徴
今回のプロジェクトでは「ただ遊べるだけ」ではなく、
実際に考えて打つ“AI対戦”ができる五目並べ を作ります。
まずは全体仕様をわかりやすく表にまとめました
| 項目 | 仕様内容 | 補足 |
|---|---|---|
| 対戦形式 | プレイヤー vs AI | 先手・後手を自由に選択できます。 |
| AI難易度 | 初級・中級・上級 | ※ 探索深度 を変えることで強さを調整します。上級は「3手先」まで読みます。 |
| 禁じ手対応 | 黒の「長連」のみ対応 | ※ 連珠の三々・四々などは 今回は未対応(後半で拡張に触れます)。 |
| 強制手(重要) | 勝ち筋・負け筋を最優先でチェック | 相手の「生き三」など 2〜3手の必勝形を事前にブロックする仕組みです。 |
まずは動かしてみよう(コピペでOK)
AIの仕組みを理解するいちばんの近道は、コードを読むことではなく 実際に動かしてみること です。
この章では、完成した五目並べAI対戦を 1つのHTMLファイルだけで動かす方法 を紹介します。
ステップ1:ファイルを作成しよう
今回の五目並べAI対戦は HTML・CSS・JavaScriptをまとめた単一ファイル構成 なので、特別な環境は一切不要。
PCとブラウザがあれば、誰でもすぐに動かせます。
- 新しいファイルを作成
PCのデスクトップなど任意の場所に、gomoku_ai.htmlのような名前で新規ファイルを作成してください。 - エディタで開く
VS Code・Sublime Text・メモ帳など、お好きなテキストエディタで開いてください。
ステップ2:コードをコピペしよう
次の「完成コード」をすべてコピーして、gomoku_ai.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 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 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>五目並べ AI対戦</title> <style> /* CSSコードは省略 */ :root { --board-size: 15; --board-color: #d8b765; --line-color: #333; --black: #111; --white: #f8f8f8; --accent: #e57373; } * { box-sizing: border-box; } body { margin: 0; font-family: sans-serif; display: flex; align-items: center; justify-content: center; background: #f0f0f0; min-height: 100vh; padding: 10px; } .wrap { max-width: 750px; width: 95vw; text-align: center; background: #fff; padding: 5px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); } h1 { font-size: 1.5rem; margin-top: 0.5em; margin-bottom: 0.8em; } .board { position: relative; width: min(90vw, 600px); aspect-ratio: 1/1; margin: 20px auto; background-color: var(--board-color); border: 2px solid var(--line-color); } .grid { display: grid; grid-template-columns: repeat(var(--board-size), 1fr); grid-template-rows: repeat(var(--board-size), 1fr); width: 100%; height: 100%; } .cell { position: relative; display: flex; align-items: center; justify-content: center; cursor: pointer; } .cell::before, .cell::after { content: ''; position: absolute; background-color: var(--line-color); z-index: 1; } .cell::before { width: 1px; height: 100%; left: 50%; } .cell::after { height: 1px; width: 100%; top: 50%; } .cell:nth-child(15n)::before { width: 0; } .cell:nth-last-child(-n + 15)::after { height: 0; } .point { position: absolute; width: 80%; height: 80%; border-radius: 50%; z-index: 10; transition: transform 0.1s; } .point.black { background: var(--black); } .point.white { background: var(--white); border: 1px solid #aaa; } .hint::before { content: ''; position: absolute; width: 30%; height: 30%; border-radius: 50%; background-color: rgba(255, 255, 255, 0.7); z-index: 5; } .forbidden.hint::before { background-color: var(--accent); } .topbar, .controls { display: flex; flex-wrap: wrap; gap: 10px; align-items: center; justify-content: center; } .status { display: flex; gap: 15px; font-weight: bold; margin-top: 10px; } .msg { min-height: 2em; font-weight: 600; margin: 10px 0; color: #333; } .sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); border: 0; } button, select { padding: 8px 12px; border-radius: 6px; border: 1px solid #ccc; cursor: pointer; } button.primary { background: #4caf50; color: white; border-color: #4caf50; } @media (max-width: 600px) { .topbar { justify-content: space-between; } .controls { justify-content: flex-start; } .board { margin-top: 10px; } h1 { font-size: 1.2rem; } .status { font-size: 0.9rem; } } </style> </head> <body> <div class="wrap"> <h1>五目並べ AI対戦</h1> <div class="topbar"> <div class="controls"> <label for="first" class="sr-only">先手</label> <select id="first" title="先手"> <option value="black">あなた = 黒(先手)</option> <option value="white">あなた = 白(後手)</option> </select> <label for="level" class="sr-only">難易度</label> <select id="level" title="AIの強さ"> <option value="easy">初級</option> <option value="normal" selected>中級</option> <option value="hard">上級</option> </select> <button id="btnStart" class="primary">ゲーム開始</button> </div> </div> <div class="status"> </div> <div class="msg" id="message" aria-live="polite">設定を選んで「ゲーム開始」を押してください。</div> <div class="board"> <div class="grid" id="grid" role="grid"> </div> </div> </div> <script> document.addEventListener("DOMContentLoaded", () => { (() => { "use strict"; // ===== 基本定義 ===== const SIZE = 15; const EMPTY = 0, BLACK = 1, WHITE = 2; const DIRECTIONS = [[0,1],[1,0],[1,1],[1,-1]]; const MAX_DEPTH = { easy: 1, normal: 2, hard: 3 }; const WIN_SCORE = 100000000000; // ===== 状態 ===== let board = createEmptyBoard(); let turn = BLACK; // 黒先 let gameEnded = false; let isThinking = false; // ★AI思考中フラグ // ===== DOM参照(HTMLのIDに合わせる)===== const gridEl = document.getElementById("grid"); const msgEl = document.getElementById("message"); const levelEl = document.getElementById("level"); const firstEl = document.getElementById("first"); const startBtn = document.getElementById("btnStart"); // ===== イベント ===== startBtn.addEventListener("click", reset); firstEl.addEventListener("change", () => {/* 表示はreset時に更新 */}); // ===== ユーティリティ ===== function createEmptyBoard(){ return Array.from({length:SIZE},()=>Array(SIZE).fill(EMPTY)); } function cloneBoard(b){ return b.map(r=>r.slice()); } function opponentOf(c){ return c===BLACK?WHITE:BLACK; } function setStatus(html){ msgEl.innerHTML = html; } function isFull(b){ for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++) if(b[y][x]===EMPTY) return false; return true; } const sleep = (ms) => new Promise(res => setTimeout(res, ms)); // 人間/AIの色(セレクトの value は 'black'/'white') function humanColor(){ return (firstEl.value === "black") ? BLACK : WHITE; } function aiColor(){ return (firstEl.value === "black") ? WHITE : BLACK; } // ===== 盤の描画(.grid に出力)===== function renderBoard(){ gridEl.innerHTML = ""; for (let y=0; y<SIZE; y++){ for (let x=0; x<SIZE; x++){ const cell = document.createElement("div"); cell.className = "cell"; cell.dataset.y = y; cell.dataset.x = x; cell.addEventListener("click", onCellClick); // 禁じ手ヒント(人間=黒のみ)。副作用防止でクローン盤を渡す if (humanColor() === BLACK) { const temp = cloneBoard(board); if (isForbiddenOnBoard(temp, y, x, BLACK)) cell.classList.add("hint","forbidden"); else cell.classList.add("hint"); } // 既存石 const v = board[y][x]; if (v !== EMPTY){ const s = document.createElement("div"); s.className = "point " + (v===BLACK?"black":"white"); cell.appendChild(s); } gridEl.appendChild(cell); } } } function redrawStones(){ for (let y=0; y<SIZE; y++){ for (let x=0; x<SIZE; x++){ const idx = y*SIZE + x; const cell = gridEl.children[idx]; cell.innerHTML = ""; const v = board[y][x]; if (v !== EMPTY){ const s = document.createElement("div"); s.className = "point " + (v===BLACK?"black":"white"); cell.appendChild(s); } } } } // ===== クリック処理 ===== function onCellClick(e){ if (gameEnded) return; if (isThinking) return; // ★AI思考中は無効化 if (turn !== humanColor()) return; const y = +e.currentTarget.dataset.y; const x = +e.currentTarget.dataset.x; if (board[y][x] !== EMPTY) return; // 黒の禁じ手は「この局面の盤」で判定 if (turn === BLACK && isForbiddenOnBoard(board, y, x, turn)) { setStatus("その手は禁じ手(長連)です。別の場所に置いてください。"); return; } placeStone(y,x,turn); if (!gameEnded && turn === aiColor()) aiTurn(); // 非同期でOK } function placeStone(y,x,color){ board[y][x] = color; redrawStones(); if (checkWinOnBoard(board, y, x, color)) { gameEnded = true; setStatus(`${color===BLACK?"黒(●)":"白(○)"}の勝ち!`); return; } if (isFull(board)){ gameEnded = true; setStatus("引き分けです。"); return; } turn = opponentOf(color); } // ===== 勝敗・数え上げ ===== function countOneDir(b, y, x, dy, dx, color){ let c=0, ny=y+dy, nx=x+dx; while(ny>=0&&ny<SIZE&&nx>=0&&nx<SIZE&&b[ny][nx]===color){ c++; ny+=dy; nx+=dx; } return c; } function checkWinOnBoard(b, y, x, color){ for(const [dy,dx] of DIRECTIONS){ const c = 1 + countOneDir(b,y,x,dy,dx,color) + countOneDir(b,y,x,-dy,-dx,color); if (c >= 5) return true; } return false; } // ===== 禁じ手(黒の長連)— 必ず「判定したい盤面 b」を渡す ===== function isForbiddenOnBoard(b, y, x, color){ if (color !== BLACK) return false; if (b[y][x] !== EMPTY) return false; b[y][x] = BLACK; // 仮置き let over = false; for (const [dy,dx] of DIRECTIONS){ const c = 1 + countOneDir(b,y,x,dy,dx,BLACK) + countOneDir(b,y,x,-dy,-dx,BLACK); if (c >= 6){ over = true; break; } } b[y][x] = EMPTY; return over; } // ===== AI:強制手+ミニマックス(★1秒思考を追加)===== async function aiTurn(){ if (gameEnded) return; isThinking = true; setStatus("AI思考中…"); await sleep(500); // ★思考時間 const depth = MAX_DEPTH[levelEl.value]; const color = aiColor(); // 1) 強制手 const force = findImmediateMove(color); if (force){ placeStone(force.y, force.x, color); isThinking=false; return; } // 2) 探索候補 let moves = generateMoves(board, color); if (moves.length === 0){ isThinking=false; turn = opponentOf(turn); return; } moves.sort((a,b)=> scoreMoveHeuristic(board,b,color) - scoreMoveHeuristic(board,a,color)); let alpha = -Infinity, beta = Infinity; let bestMove = moves[0], bestScore = -Infinity; for(const m of moves){ const nb = cloneBoard(board); nb[m.y][m.x] = color; const score = minimax(nb, depth-1, false, alpha, beta, {y:m.y,x:m.x}, color); if (score > bestScore){ bestScore = score; bestMove = m; } if (score > alpha) alpha = score; if (beta <= alpha) break; } placeStone(bestMove.y, bestMove.x, color); isThinking = false; } // ★ 相手open-3を直接塞ぐ端点の列挙 function listOpenThreeBlocks(b, colorOpponent) { const blocks = []; const me = colorOpponent; for (let y=0; y<SIZE; y++) for (let x=0; x<SIZE; x++) { if (b[y][x] !== EMPTY) continue; if (me === BLACK && isForbiddenOnBoard(b, y, x, me)) continue; const tmp = cloneBoard(b); tmp[y][x] = me; // 相手がここに置いた想定 for (const [dy, dx] of DIRECTIONS) { const c1 = countLineOnBoard(tmp, y, x, dy, dx, me); const c2 = countLineOnBoard(tmp, y, x, -dy, -dx, me); const total = c1 + c2 + 1; if (total !== 4) continue; const end1y = y + dy * (c1 + 1), end1x = x + dx * (c1 + 1); const end2y = y - dy * (c2 + 1), end2x = x - dx * (c2 + 1); const open1 = (end1y>=0&&end1y<SIZE&&end1x>=0&&end1x<SIZE && tmp[end1y][end1x]===EMPTY); const open2 = (end2y>=0&&end2y<SIZE&&end2x>=0&&end2x<SIZE && tmp[end2y][end2x]===EMPTY); if (open1 && open2) { if (!blocks.some(m => m.y===y && m.x===x)) blocks.push({ y, x }); break; } } } return blocks; } // ★ 強制手:自分即勝 > 相手即勝ブロック > 相手open-3ブロック > 準即勝ブロック > 自分二重脅威 function findImmediateMove(color){ const opponent = opponentOf(color); let aiWin=null, blockNow=null, blockOpen3=null, blockPre=null, aiDouble=null; // 相手の1手勝ち const oppWins = []; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if (board[y][x] !== EMPTY) continue; if (opponent===BLACK && isForbiddenOnBoard(board, y, x, opponent)) continue; const tmp = cloneBoard(board); tmp[y][x] = opponent; if (checkWinOnBoard(tmp, y, x, opponent)) oppWins.push({y,x}); } // 相手の open-3 の急所(AIが先に打つべき点) const oppOpen3Blocks = listOpenThreeBlocks(board, opponent); // 空点走査 for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if (board[y][x] !== EMPTY) continue; if (color===BLACK && isForbiddenOnBoard(board, y, x, color)) continue; const move = {y,x}; // 自分即勝 const nb = cloneBoard(board); nb[y][x] = color; if (checkWinOnBoard(nb, y, x, color)) aiWin = move; // 相手即勝ブロック if (!blockNow && oppWins.some(w=>w.y===y&&w.x===x)) blockNow = move; // 相手 open-3 ブロック if (!blockOpen3 && oppOpen3Blocks.some(w=>w.y===y&&w.x===x)) blockOpen3 = move; // 準即勝ブロック(相手がここに置くと直後に即勝が発生) if (!blockPre){ const nb2 = cloneBoard(board); nb2[y][x] = opponent; if (countImmediateWinsAfter(nb2, opponent) >= 1) blockPre = move; } // 自分二重脅威 if (!aiDouble && countImmediateWinsAfter(nb, color) >= 2) aiDouble = move; } if (aiWin) return aiWin; if (blockNow) return blockNow; if (blockOpen3) return blockOpen3; if (blockPre) return blockPre; if (aiDouble) return aiDouble; return null; } // 即勝手の数(b に色 color が次に置ける“1手勝ち”が何箇所あるか) function countImmediateWinsAfter(b, color){ let cnt = 0; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if (b[y][x] !== EMPTY) continue; if (color===BLACK && isForbiddenOnBoard(b, y, x, color)) continue; const nb = cloneBoard(b); nb[y][x] = color; if (checkWinOnBoard(nb, y, x, color)){ cnt++; if (cnt>=2) return cnt; } } return cnt; } // 候補生成(近傍優先 + 禁じ手除外は「その盤面」で) function generateMoves(b, playerColor){ const moves = []; const seen = new Set(); let any=false; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++) if(b[y][x]!==EMPTY){ any=true; break; } if (!any) return [{y:Math.floor(SIZE/2), x:Math.floor(SIZE/2)}]; const add = (y,x,prio)=>{ if (y<0||y>=SIZE||x<0||x>=SIZE) return; if (b[y][x] !== EMPTY) return; if (playerColor===BLACK && isForbiddenOnBoard(b, y, x, playerColor)) return; const key = y+","+x; if (seen.has(key)) return; seen.add(key); moves.push({y,x,prio}); }; for (const R of [1,2,3]){ for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if (b[y][x] === EMPTY) continue; for(let dy=-R;dy<=R;dy++) for(let dx=-R;dx<=R;dx++){ const ny=y+dy, nx=x+dx; const md=Math.abs(dy)+Math.abs(dx); if (md<=0||md>R) continue; add(ny,nx, (R===1?5:R===2?3:1)); } } if (moves.length>=18) break; } const c=(SIZE-1)/2; moves.forEach(m=> m.prio += (SIZE*2 - (Math.abs(m.y-c)+Math.abs(m.x-c)))*0.01 ); moves.sort((a,b)=> b.prio - a.prio); return moves; } function scoreMoveHeuristic(b, move, color){ const nb = cloneBoard(b); nb[move.y][move.x] = color; return evaluateBoard(nb, {y:move.y,x:move.x}, color); } // ミニマックス(評価は aiClr 視点) function minimax(b, depth, isMaximizing, alpha, beta, lastMove, aiClr){ const player = isMaximizing ? aiClr : opponentOf(aiClr); if (lastMove){ const lastColor = b[lastMove.y][lastMove.x]; if (checkWinOnBoard(b, lastMove.y, lastMove.x, lastColor)) return evaluateBoard(b, lastMove, aiClr); } if (depth === 0) return evaluateBoard(b, lastMove, aiClr); let moves = generateMoves(b, player); if (moves.length === 0) return 0; moves.sort((m1,m2)=> scoreMoveHeuristic(b,m2,player) - scoreMoveHeuristic(b,m1,player)); if (isMaximizing){ let best = -Infinity; for (const m of moves){ const nb = cloneBoard(b); if (player===BLACK && isForbiddenOnBoard(nb, m.y, m.x, player)) continue; nb[m.y][m.x] = player; const val = minimax(nb, depth-1, false, alpha, beta, {y:m.y,x:m.x}, aiClr); if (val > best) best = val; if (val > alpha) alpha = val; if (beta <= alpha) break; } return best; }else{ let best = Infinity; for (const m of moves){ const nb = cloneBoard(b); if (player===BLACK && isForbiddenOnBoard(nb, m.y, m.x, player)) continue; nb[m.y][m.x] = player; const val = minimax(nb, depth-1, true, alpha, beta, {y:m.y,x:m.x}, aiClr); if (val < best) best = val; if (val < beta) beta = val; if (beta <= alpha) break; } return best; } } // 評価:AI視点固定 function evaluateBoard(b, lastMove, aiClr){ if (!lastMove) return 0; const lastColor = b[lastMove.y][lastMove.x]; if (checkWinOnBoard(b, lastMove.y, lastMove.x, lastColor)) return (lastColor === aiClr) ? WIN_SCORE : -WIN_SCORE; let total = 0; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ const v = b[y][x]; if (v===EMPTY) continue; const isAI = (v === aiClr); const ev = evaluateLine(b, y, x, v); let s = 0; if (isAI){ s += ev.open4*10000000 + ev.closed4*50000 + ev.open3*600000 + ev.closed3*5000; if (ev.open4>0) s *= 10; if (ev.open3>=2) s *= 3; }else{ s += ev.open4*100000 + ev.closed4*5000 + ev.open3*100000 + ev.closed3*1000; if (ev.open4>0) s *= 10; if (ev.open3>=2) s *= 3; } total += isAI ? s : -s; } const c=(SIZE-1)/2; total += (SIZE*2 - (Math.abs(lastMove.y-c)+Math.abs(lastMove.x-c))) * 2; return total; } // 連評価(始点のみ数える) function evaluateLine(b,y,x,player){ let open4=0, closed4=0, open3=0, closed3=0; for (const [dy,dx] of DIRECTIONS){ const py=y-dy, px=x-dx; if (py>=0&&py<SIZE&&px>=0&&px<SIZE&&b[py][px]===player) continue; const c1 = countLineOnBoard(b,y,x,dy,dx,player); const c2 = countLineOnBoard(b,y,x,-dy,-dx,player); const total = c1+c2+1; const end = (yy,xx,ddy,ddx,cnt)=>{ const ny=yy+ddy*(cnt+1), nx=xx+ddx*(cnt+1); if (ny<0||ny>=SIZE||nx<0||nx>=SIZE) return "wall"; const v=b[ny][nx]; return v===EMPTY?"open":(v===player?"self":"block"); }; const e1=end(y,x,dy,dx,c1), e2=end(y,x,-dy,-dx,c2); const oe=(e1==="open")+(e2==="open"); if (total===4){ if (oe===2) open4++; else if (oe===1) closed4++; } else if (total===3){ if (oe===2) open3++; else if (oe===1) closed3++; } } return {open3,closed3,open4,closed4}; } function countLineOnBoard(b,y,x,dy,dx,color){ let c=0, ny=y+dy, nx=x+dx; while(ny>=0&&ny<SIZE&&nx>=0&&nx<SIZE&&b[ny][nx]===color){ c++; ny+=dy; nx+=dx; } return c; } // ===== 初期メッセージ ===== setStatus("設定を選んで「ゲーム開始」を押してください。"); // ===== リセット(開始) ===== function reset(){ board = createEmptyBoard(); gameEnded = false; isThinking = false; turn = BLACK; // 常に黒先 renderBoard(); const you = humanColor()===BLACK ? "黒(●)" : "白(○)"; const ai = aiColor()===BLACK ? "黒(●)" : "白(○)"; setStatus(`黒(●)が先手です。あなたは <b>${you}</b>、AIは <b>${ai}</b> です。`); // 人間=白(後手)の場合はAIが先に打つ if (humanColor() === WHITE) aiTurn(); } })(); }); </script> </body> </html> |
ステップ3:ブラウザで開いてみよう
保存した gomoku_ai.html ファイルをダブルクリックすると、
お使いのブラウザで自動的に開きます。
ゲーム画面が表示されたら、まずは 上級(3手先読み) に設定して遊んでみましょう。
ここで、AIの強力な防御ロジックを確認するためのテストプレイをしてみましょう。
オススメのテスト方法(AIの強さを体感できます)
- 黒(先手)で4連を作る
→ AIがその両端のどちらかを即座にブロックします。
4連は、次の手で5連が確定する「即勝手」です。これを最優先で阻止します。 - 黒で「生き三」を作る
→ AIが急所を最優先で塞ぐ行動を取ればOK。
これを無視して別の場所を打つAIは弱いAIです。
本作のAIは、生き三の「準即勝形」をきちんと判断し最優先で対策します。
(※)生き三とは、両端が空いた3つの連なりを指します。
こうしたテストを行うことで、後で学ぶ
「なぜこのロジックが必要なのか?」
という理解が飛躍的に深まります。
まずは遊んでみることが何より大切です。防御ロジックが正しく動いているか、体感しながら読み進めていってくださいね!
HTMLの解説(盤面と情報表示をつくる)
AIロジックに進む前に、まずはゲームの「舞台」となる HTML構造 を理解しましょう。
今回の五目並べAIは 1ファイルで完結するため、<body> の中に必要な要素をすべて配置していきます。
HTMLの役割と基本構造
HTML部分は大きく分けて次の2つで構成されています。
- ゲームの表示エリア(盤面)
- 操作やメッセージを表示するエリア
それぞれの役割を表でまとめると、以下のようになります
| 役割 | HTML要素(id / class) | 説明 |
|---|---|---|
| 全体コンテナ | <div class="wrap"> | 中央配置や背景装飾のための外枠。 |
| 操作パネル | <div class="controls"> | 先手/後手選択(#first)、AIの強さ選択(#level)、開始ボタン(#btnStart)を配置。 |
| スコア・情報表示 | <div class="status"> / <div id="message"> | 黒石・白石の数や「黒の番です」などのメッセージを表示。 |
| 盤面コンテナ | <div class="board"> | 盤全体の枠要素。 |
| マス生成エリア | <div class="grid" id="grid"> | JavaScriptが 225マス(15×15)を自動生成するための入れ物。 |
盤面の中心となる <div id="grid">
盤面のマス(セル)はHTMLに直接225個書き込むのではなく、
JavaScriptで1つずつ生成し、grid内に追加する仕組みになっています。
この方式にすることで、
- マスを(x, y)座標として扱いやすい
- AIロジックとの連携がしやすい
- 盤面サイズを変える応用も簡単
といったメリットがあります。
JavaScriptとの連携ポイント
HTMLに振られた id は、JavaScriptから以下のように取得して使います。
|
1 2 |
const gridEl = document.getElementById("grid"); const msgEl = document.getElementById("message"); |
gridEl
→ マス(セル)を生成したり、クリックイベントを監視したりする中心的な存在。msgEl
→ 「黒の番です」「AIが思考中…」「黒の勝ち!」など、ゲーム状態を表示。
HTMLはあくまで「骨組み」。
ここにCSSとJavaScriptを組み合わせることで、ようやくゲームとして動き出します。
CSSの解説(盤面デザインとレスポンシブ対応)
HTMLで骨組みができたら、次は CSSで「五目並べらしい見た目」 を作っていきます。
特に重要なのは次の2点です。
- 15×15 の格子をきれいに描画すること
- スマホでもストレスなくプレイできるデザインにすること
この章では、そのために実装しているCSSのポイントをわかりやすく解説します。
CSS Grid で 15×15 の格子をつくる
五目並べの盤面は、CSSの Grid(グリッド)レイアウト が最適です。display: grid を指定し、repeat(15, 1fr) で等分されたマスを作るだけで、
美しく整列した 15 × 15 のセルが自動生成されます。
CSS変数 --board-size: 15; を使っているので、後から盤面サイズを変えるのも簡単です。
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 |
.grid { display: grid; /* 1fr を 15 回繰り返すことで、15 等分された格子を作る */ grid-template-columns: repeat(var(--board-size), 1fr); grid-template-rows: repeat(var(--board-size), 1fr); width: 100%; height: 100%; } .cell { position: relative; /* 格子線 */ display: flex; align-items: center; justify-content: center; cursor: pointer; } /* 格子線(縦・横)の作成 */ .cell::before, .cell::after { content: ''; position: absolute; background-color: var(--line-color); z-index: 1; } .cell::before { width: 1px; height: 100%; left: 50%; } .cell::after { height: 1px; width: 100%; top: 50%; } /* 盤の端の線は消す */ .cell:nth-child(15n)::before { width: 0; } .cell:nth-last-child(-n + 15)::after { height: 0; } |
石のデザインとヒント表示
石は、マス(.cell)の中に配置する 円形の要素(.point) で表現しています。
| 要素 | スタイル | 説明 |
|---|---|---|
.point.black | 黒い円 | 黒石 |
.point.white | 白い円(薄い枠つき) | 白石 |
.cell.hint::before | 半透明の円 | 石を置けるマスのガイド |
.forbidden.hint::before | 赤い円 | 黒の禁じ手(長連)となるマスの警告 |
ヒント表示があることで、プレイヤーは
「どこに置けるのか」「禁じ手はどこか」を直感的に把握できます。
レスポンシブ(スマホ対応)
五目並べはスマホでも遊ぶ人が多いため、画面サイズに応じて盤面が自動調整されるようにしています。
その中心となるのが min() と aspect-ratio の組み合わせです。
|
1 2 3 4 5 6 7 8 9 |
.board { position: relative; /* 画面幅の 90vw か 600px のうち小さい方を採用 */ width: min(90vw, 600px); aspect-ratio: 1/1; /* 縦横比 1:1 を強制し、正方形を維持 */ margin: 20px auto; background-color: var(--board-color); border: 2px solid var(--line-color); } |
aspect-ratio: 1/1 を指定することで、
画面の幅が変わっても「正方形の盤面」が崩れずに維持されます。
ボードゲーム系のUIでは非常に相性の良い手法です。
CSS Grid はボードゲーム開発と相性抜群です。repeat(15, 1fr) と指定するだけでマスが等間隔で並ぶので、
手作業でHTMLを並べる必要がありません!
JavaScriptの基本ロジックを理解する
HTMLとCSSで盤面の「見た目」が完成したら、いよいよJavaScriptでゲームに“命”を与えていきます。
この章では、AIロジックを理解する前段階として、盤面データの管理や手番の切り替えといった“基本の仕組み”を丁寧に解説します。
定数と盤面(ボード)の管理
五目並べでは、15×15 の盤面をどうデータとして扱うかが非常に重要です。
今回の実装では、盤面を 2次元配列(配列の中に配列) として表現しています。
使用する定数
| 定数名 | 値 | 意味 |
|---|---|---|
SIZE | 15 | 盤面のサイズ(15×15) |
EMPTY | 0 | 何も置かれていないマス |
BLACK | 1 | 黒石 |
WHITE | 2 | 白石 |
盤面配列の仕組み
盤面全体は board という2次元配列に保存します。
|
1 2 3 4 5 6 |
let board = createEmptyBoard(); // [[0, 0, 0, ...], [0, 0, 0, ...], ...] function createEmptyBoard() { // 15 行分、それぞれ 15 列の 0 (EMPTY) で初期化された配列を作成 return Array.from({ length: SIZE }, () => Array(SIZE).fill(EMPTY)); } |
このように作ることで、次のように直感的に扱えるようになります。
board[y][x]→ y 行 x 列のマスを示すboard[7][7] = BLACK;→ 盤面中央に黒石を置く
ポイント
五目並べは座標管理が複雑になりやすいため、
「board[y][x]」で明確に指定できる構造は非常に有効です。
手番の管理と石の配置
次に、ゲームの進行を管理します。
誰の番なのか、どの色の石を置くのかは次の変数で管理しています。
|
1 2 3 |
let currentPlayer = BLACK; // 常に黒(先手)からスタート let humanColor = BLACK; // プレイヤーの色 let aiColor = WHITE; // AIの色 |
設定画面で先手/後手を選んだ場合は、ここから書き換えられます。
プレイヤーがマスをクリックしたときの流れ
クリックイベントは onCellClick が処理します。
|
1 2 3 4 5 6 7 8 |
function onCellClick(e) { const y = +e.currentTarget.dataset.y; // クリックされた行 const x = +e.currentTarget.dataset.x; // クリックされた列 if (board[y][x] !== EMPTY) return; placeStone(y,x,turn); // ... } |
クリックされたマスを取得したら、次の処理が行われます。
1.座標の取得と合法手のチェックonCellClick はクリックされたマスの座標を取得し、そのマスが既に埋まっていないか、
黒番の場合に禁じ手(長連)ではないかをチェックします。
2.着手処理の実行
チェックが通ると、placeStone(y, x, humanColor) 関数を呼び出します。
この関数が、データの更新と画面の更新を一括して行います。
この placeStone 関数が、AIもプレイヤーも共通して利用する「石を置いて、次の状態へ移行する」ための核となる処理です。
placeStone 関数の動きは以下となります。
- 盤面データを更新します (board[y][x] = color;)。
- 画面表示を更新します(redrawStones();)。
- 勝利判定(checkWinOnBoard)を行い、勝敗が決していればゲーム終了処理を行います。
- 手番を相手に交代し(turn = opponentOf(color);)、AIの手番であれば aiTurn() へ移行します。
この仕組みにより、AIが選んだ手でもプレイヤーが選んだ手でも、placeStone 関数を呼び出すだけで、統一された流れで着手と画面表示が完了します。
AIロジックの仕組みを理解しよう(最重要!)
この章は、五目並べAIの「頭脳」にあたる部分です。
五目並べAIは、「ミニマックス法」と呼ばれる探索手法で最善手を決定しますが、それだけではありません。
最大の特徴:5段階の「強制手」ロジックを搭載
五目並べには「次に打たれたら絶対負ける」「ほぼ勝ちが決まる」といった局面が存在します。
これを見逃すと、どれだけ探索しても弱いAIになってしまいます。
そこで今回のAIは、 「今すぐ打たないといけない手」=強制手 を探し、最優先で着手します。
AIの思考フローは「強制手 → ミニマックス」の2段構え
AIは次の順序で手を決めます。
① 強制手があるか?(即勝・即負け阻止など)
↓ YES → その手を返す(探索なし)
↓ NO
② ミニマックス法で最善手を探索する(1〜3手先)
この2段構えにより、
- 即勝ちを確実に拾う
- 負ける手を絶対に見逃さない
- 加えて、深さ3手までの読みで攻め筋を評価できる
という 堅実かつ攻撃的なAI になります。
5段階の「強制手」優先順位(優先度1〜5)
強制手は次のように優先順位が決まっています。
この順序は findImmediateMove 関数にそのまま反映されています。
| 優先度 | 種類 | 内容 | 目的 |
|---|---|---|---|
| 1位 | 自分の即勝 (aiWin) | そのマスに置けば5連で勝ち | 最優先で勝利確定 |
| 2位 | 相手の即勝阻止 (blockNow) | 相手が次に置けば勝つマスをブロック | 1手負けを防ぐ |
| 3位 | オープン3急所阻止 (blockOpen3) | 相手の「生き三」を潰す | 2手先の必勝形を防ぐ |
| 4位 | 準即勝阻止 (blockPre) | 相手が置くと「次の1手勝ち」が生まれるマス | 3手先の必勝形を防ぐ |
| 5位 | 自分の二重脅威 (aiDouble) | ダブル4など複数の勝ち筋が同時に発生 | 攻撃の優位を確保 |
AIの優先度と強さについて
五目並べAIの強さは、「生き三」と「準即勝」を見抜けるか」に依存します。
優先度3:生き三(オープン3)急所ブロック
- 両端が空いた“3連”
- 次を打たれたらオープン4(1手勝ち)ができる
AIは listOpenThreeBlocks() 関数でこれを検出し、即ブロックします。
優先度4:準即勝ブロック
一見すると安全そうな場所でも、相手がそのマスに置く
→ 次の手で確定勝ちが発生
という「罠」のようなマスが存在します。
AIは countImmediateWinsAfter() 関数で
「相手がそのマスに置いたら、1手勝ちが発生するか?」を調べます。
1つでも勝ちが生まれるなら → 最優先でブロック します。
ミニマックス探索(強制手がない場合の思考)
強制手で着手が決まらなかった場合、AIはようやく探索フェーズに入ります。
評価関数(evaluateBoard)の役割
探索の終端や途中で以下のように盤面を数値化します:
- オープン4 → 超高得点(ほぼ勝ち)
- オープン3 → 高得点(2手勝ち)
- ダブル脅威 → ボーナス加算
- 中央寄りの手 → 少しだけ優遇
これにより、
- 中盤は中央寄りの布石
- 終盤は勝ち筋の読み
- 相手の強い形を回避
など、自然な動きを取るようになります。
カスタマイズ例(もっと強いAIにしたい人向け)
ここまでで基本の五目並べAIは完成しましたが、実はまだまだ強化できます。
「もっと頭のいいAIにしたい」「盤面やデザインを変えたい」など、発展の余地はたくさんあります。
ここでは、AIの強化とデザイン面のカスタマイズを、初心者でも取り組みやすい内容にしぼって紹介します。
AIをもっと強くするには?
今回のAIは、すでに次の2つの技術を使っています。
- ミニマックス法(Minimax)
→ 何手か先まで読み、最善手を選ぶ方法。 - α-β(アルファベータ)枝刈り
→「これ以上読む必要がない手」を早めにスキップして高速化する仕組み。
この仕組みのおかげで「上級(3手読み)」でもサクサク動きます。
もっと強くする方法①:探索深度を上げる
AIの強さは「どれくらい先まで読むか」に大きく左右されます。
例
MAX_DEPTH.hard = 3
↓MAX_DEPTH.hard = 4 に変更すると、AIは4手先まで読むようになり、より強くなります。
ただし、深く読むほど 処理が重くなる ため、ブラウザだと少し時間がかかることがあります。
もっと強くする方法②:局面の記憶(ハッシュテーブル)
「同じ局面を何度も計算しない」ようにする技術です。
一度評価した盤面をメモしておき、
もう一度同じ局面になったら、その評価を再利用する。
これだけでミニマックス法のスピードが劇的に改善します。
興味があれば「Transposition Table / Zobrist Hash」といったキーワードで調べてみてください。
ルールの拡張(連珠ルールに近づける)
現在のAIが対応している禁じ手は 黒の“長連”(6つ以上並ぶ) のみです。
五目並べの公式ルール(連珠)では、さらに以下の禁じ手があります。
| 禁じ手 | 内容 |
|---|---|
| 三々(さんさん) | 1手で「生き三」が2つできてしまう手 |
| 四々(しし) | 1手で「四連」が2つできてしまう手 |
どうやって判定するの?
三々や四々を判定するには、
- 着手後に盤面全方向の「連」を数える
- 「生き三」「四連」が同時に2つ以上できているかをチェック
といったかなり高度な数え上げが必要になります。
見た目やサイズを自由に変える(初心者向け)
デザインの変更はとても簡単です。
CSS変数を少し変えるだけで、盤の雰囲気をガラッと変更できます。
色の変更(CSS)
|
1 2 3 4 5 |
:root { --board-color: #d8b765; /* 盤の色 */ --black: #111; /* 黒石 */ --white: #f8f8f8; /* 白石 */ } |
好きな色に変更するだけで、オリジナル感のあるデザインになります。
初心者でもできる調整から、上級者向けの高度な技術まで幅広く挑戦できます。
ぜひあなた好みの五目並べに育ててみてください!
おすすめの学習リソース|もっとスキルを伸ばしたいあなたへ
「五目並べAI対戦」を完成させたことで、あなたはすでに HTML・CSS・JavaScriptの基礎 に触れました。
ここからさらにステップアップするために役立つ学習リソースを紹介します。
おすすめ書籍
『ゲームで学ぶJavaScript入門 増補改訂版 ~ブラウザゲームづくりでHTML&CSSも身につく!』
- ゲームを作りながら学べるので、退屈せずにJavaScriptの基礎が身につく
- HTML・CSS・JavaScriptの基本を一冊で網羅、Web制作の土台が自然に理解できる
- 13本のサンプルゲームを実際に動かせるから「作れる喜び」が味わえる
- 初心者・中高生にもやさしい解説で、初めてのゲームプログラミングに最適
『スラスラわかるHTML&CSSのきほん』(SBクリエイティブ)
- Web制作初心者に最適な入門書。HTMLとCSSの仕組みをやさしく解説。
- これからWebページを作ってみたい方にぴったり。
- レイアウトの基本やスタイルの調整方法など、実践的に学べます。
オンライン講座編
Udemy|世界最大級のオンライン学習プラットフォーム
世界中で利用されるオンライン学習サイト。
HTML、CSS、JavaScriptの入門から応用まで、高評価の講座が数百種類揃っています。
初心者でも動画を見ながら手を動かせるので、挫折しにくいのが魅力です。
まとめ
お疲れさまでした!
これであなたは、HTML・CSS・JavaScriptだけで動く本格的な五目並べAI対戦を作れるようになりました。
今回の開発を通して、
- 盤面を2次元配列で管理する方法
- 強制手(即勝・生き三など)を優先するAIの考え方
- ミニマックス法とα-β枝刈りによる「先を読む」仕組み
- 効率的な画面描画
といった、ゲームAIづくりの基礎をひと通り学べました。
この知識は、オセロや将棋などの他のボードゲームAIにも応用できます。
ぜひ今回の経験を活かして、次のゲーム作りにも挑戦してみてください!
関連記事
【実際に遊べる 五目並べはこちら。AIと対戦してみましょう!】▼
五目並べAIづくりを楽しめた方は、以下の記事もおすすめです。
どれもブラウザだけで動く本格的なゲームを題材にしており、AIロジックやJavaScriptの理解がさらに深まります。
【作り方記事】まるばつゲーム(三目並べ)の作り方|AI対戦ロジックを学ぼう!▼
【作り方記事】オセロ(リバーシ)の作り方|AIロジックやJavaScriptを学ぼう!▼




