「ブラウザで動くゲームを自分で作ってみたい」
「AIが相手をしてくれる本格的なゲームを作りたい」
そんな方に向けて、この記事では 4目並べ(Connect4)AI対戦ゲームを“ゼロから自作する方法” を丁寧に解説します。
難しそうに見えるAIゲームですが、必要なのは
HTML・CSS・JavaScript の3つだけ。
しかも、1つの HTML ファイルをコピペするだけで動かせるので、
プログラミング初心者でも安心して取り組めます。
今回作るゲームは、
- 落下アニメーションつきの本格ボード
- 先手/後手の切り替え
- AIの強さは3段階(初級・中級・上級)
- 勝利ラインが光る演出
- スマホ対応
といった、完成度の高い仕上がりになります。
記事内では
「まずは動かす」→「仕組みを理解する」→「カスタマイズする」
という流れで、初心者でも迷わず進められる構成にしています。
4目並べ(Connect4)AI対戦はどんなゲーム?
まずは、この記事で作れる 完成版の4目並べ(Connect4)AI対戦ゲーム が
どんな動きをするのかイメージしておきましょう。
以下のようなゲームが、HTML・CSS・JavaScript だけであなた自身の手で作れるようになります。
完成版を体験してみよう
【4目並べの基本ルール】
- 縦6 × 横7 のボード を使う
- プレイヤーは交互にコマを置く
- コマは上から落ちて、その列の一番下に積み重なる
- 縦・横・斜めのどこかで 4つ連続で並べたら勝ち!
- 全てのマスが埋まっても勝者がいない場合はドローになります。
今回実装したゲームの仕様(実装ポイント)
先手・後手を自由に選択
ゲーム開始前に プレイヤー先手/AI先手 を切り替え可能。
AIの強さは 3 段階(初級・中級・上級)
ゲーム開始前に AI の強さを選択できます。
- 初級:3手先まで読む
- 中級:6手先を読む
- 上級:8手先まで読む本格AI
初めての人でも簡単に勝てる設定から、
“本気でやらないと勝てない” レベルまで幅広く対応。
コマがストンと落ちるアニメーション
コマの落下演出。
視覚的に気持ちよく、完成度の高い見た目になります。
勝利ラインは光ってハイライト
4つが揃ったら、勝利したラインを 緑色の枠線 で強調表示。
初心者でも「どこで揃ったのか」がすぐわかります。
スコアを自動で記録
- プレイヤー勝利数
- AI勝利数
が自動でカウントされます。
スマホでもプレイ可能(レスポンシブ対応)
画面サイズに合わせて自動調整。
PC・スマホどちらでも遊べるようになっています。
まずは動かしてみよう(完成コードをコピペ)
4目並べ(Connect4)AI対戦ゲームは、
たった1つの HTML ファイルだけで動きます。
ゲーム作りが初めての方でも、以下の手順どおりに進めれば
必ず動く状態まで完成します。
必要なもの(最低限)
- パソコン(Windows / Mac どちらでもOK)
- ブラウザ(Chrome 推奨)
- エディタ(VSCode・メモ帳・メモ帳アプリでも可)
最初は「メモ帳」でも問題ありません。
新規ファイルを用意する
- デスクトップなど分かりやすい場所に、任意のフォルダを作成(例:
game-make)。 - メモ帳(Windows)やテキストエディット(Mac)などのテキストエディタを開く。
- ファイル名を
connect4.htmlとして保存。
完成コードをまるごと貼り付ける
下の「完成コード」をすべてコピーして、 に貼り付け、上書き保存します。connect4.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 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>4目並べ AI対戦(コネクト4) </title> <style> /* ===== CSS変数と基本リセット ===== */ :root { --rows: 6; --cols: 7; --cell-size: 60px; /* PC用 */ --board-color: #0000ff; /* 青 */ --player1-color: #ff4500; /* 赤 */ --player2-color: #ffcc00; /* 黄 */ --accent-color: #00ff00; /* 緑 */ } * { box-sizing: border-box; } body { margin: 0; font-family: sans-serif; display: flex; align-items: flex-start; justify-content: center; background: #f0f0f0; min-height: 100vh; padding: 20px 2px; } .wrap { max-width: 500px; width: 100%; text-align: center; background: #fff; padding: 20px; border-radius: 10px; box-shadow: 0 5px 15px rgba(0,0,0,0.1); } /* ===== コントロールパネル ===== */ .controls { display: flex; justify-content: space-around; gap: 10px; margin-bottom: 10px; flex-wrap: wrap; } .controls > * { flex-grow: 1; padding: 8px; border-radius: 5px; border: 1px solid #ccc; } button { cursor: pointer; background-color: var(--accent-color); color: #111; font-weight: bold; border: none; padding: 8px 15px; border-radius: 5px; transition: background 0.2s; } button:hover:not(:disabled) { background-color: #00cc00; } /* ===== ステータスとメッセージ ===== */ .status { margin: 15px 0 10px; display: flex; justify-content: space-around; } .score { font-weight: bold; } .msg { padding: 10px; min-height: 40px; font-weight: bold; } /* ===== 盤面とグリッド ===== */ .board-container { display: inline-block; border: 8px solid var(--board-color); border-radius: 10px; box-shadow: 0 0 15px rgba(0,0,0,0.3); background-color: var(--board-color); overflow: hidden; user-select: none; } .grid { display: grid; grid-template-columns: repeat(var(--cols), var(--cell-size)); grid-template-rows: repeat(var(--rows), var(--cell-size)); background-color: var(--board-color); } .cell { position: relative; width: var(--cell-size); height: var(--cell-size); cursor: pointer; display: flex; align-items: center; justify-content: center; } .cell::after { content: ''; width: 80%; height: 80%; border-radius: 50%; background-color: #fff; z-index: 1; } /* ===== 石の表示 (穴の中に配置) ===== */ .stone { position: absolute; width: 80%; height: 80%; border-radius: 50%; z-index: 2; transition: background-color 0.2s; } .stone.player1 { background-color: var(--player1-color); } .stone.player2 { background-color: var(--player2-color); } .stone.win-highlight { box-shadow: 0 0 10px 3px var(--accent-color); border: 3px solid #fff; } /* ===== アニメーションキーフレーム ===== */ @keyframes drop { 0% { transform: translateY(calc(var(--drop-start-row) * var(--cell-size))); } 100% { transform: translateY(0); } } /* 石の落下アニメーションクラス */ .stone.dropping { animation-name: drop; animation-timing-function: cubic-bezier(0.5, 0, 1, 1); } /* ===== UX: ホバーとハイライト ===== */ .column-highlight { background-color: rgba(0, 220, 255, 0.7); transition: background-color 0.2s; } .cell:hover.column-highlight { background-color: rgba(0, 220, 255, 0.9); } /* ===== スマホ対応 (レスポンシブ) ===== */ @media (max-width: 600px) { :root { --cell-size: 13vw; } .grid { width: calc(var(--cols) * var(--cell-size)); height: calc(var(--rows) * var(--cell-size)); } .wrap { padding: 10px; } .controls { flex-direction: column; } } </style> </head> <body> <div class="wrap"> <h1>4目並べ AI対戦</h1> <div class="controls"> <select id="first" title="先手"> <option value="1">プレイヤーが先手(赤)</option> <option value="2">AIが先手(黄)</option> </select> <select id="level" title="AIの強さ"> <option value="3">初級</option> <option value="6">中級</option> <option value="8">上級</option> </select> <button id="btnStart" class="primary">ゲーム開始</button> </div> <div class="status"> <span class="score">プレイヤー:<span id="scorePlayer">0</span>勝</span> <span class="score">AI:<span id="scoreAI">0</span>勝</span> </div> <div class="msg" id="message">設定を選んで「ゲーム開始」を押してください。</div> <div class="board-container"> <div class="grid" id="grid"> </div> </div> </div> <script> document.addEventListener('DOMContentLoaded', () => { 'use strict'; // =============================================== // 1. 定数定義 (Magic Numbers の定数化を含む) // =============================================== const ROWS = 6; const COLS = 7; const CONNECT_COUNT = 4; const EMPTY = 0, PLAYER1 = 1, PLAYER2 = 2; // PLAYER1=赤, PLAYER2=黄(AI) const WIN_SCORE = 100000000; const ANIMATION_DURATION = 200; // AI評価関数の重み const WEIGHTS = { CENTER: 10, TWO_IN_A_ROW: 10, THREE_IN_A_ROW: 1000, }; const DIRECTIONS = [[0, 1], [1, 0], [1, 1], [1, -1]]; // 横, 縦, 右下, 左下 const createEmptyBoard = () => Array.from({ length: ROWS }, () => Array(COLS).fill(EMPTY)); // =============================================== // 2. 状態管理 (State Object) // =============================================== const gameState = { board: createEmptyBoard(), gameEnded: true, currentPlayer: EMPTY, // 現在のターン humanColor: PLAYER1, // プレイヤーの担当色 aiColor: PLAYER2, // AIの担当色 playerScore: 0, aiScore: 0, isThinking: false, }; // =============================================== // 3. DOM要素とDOM参照の保持 // =============================================== const gridEl = document.getElementById('grid'); const msgEl = document.getElementById('message'); const btnStart = document.getElementById('btnStart'); const selectFirst = document.getElementById('first'); const selectLevel = document.getElementById('level'); const scorePlayerEl = document.getElementById('scorePlayer'); const scoreAIEl = document.getElementById('scoreAI'); // DOM要素の2次元配列参照(DOMクエリ削減のため) const stoneEls = Array.from({ length: ROWS }, () => Array(COLS)); const cellEls = Array.from({ length: ROWS }, () => Array(COLS)); // =============================================== // 4. ユーティリティ // =============================================== const opponentOf = (color) => color === PLAYER1 ? PLAYER2 : PLAYER1; const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); /** ボードの特定列における次に着手可能な行インデックスを返す */ const getNextOpenRow = (boardState, colIndex) => { for (let rowIndex = ROWS - 1; rowIndex >= 0; rowIndex--) { if (boardState[rowIndex][colIndex] === EMPTY) return rowIndex; } return -1; }; /** 現在の盤面における有効な着手可能列のリストを返す */ const getLegalMoves = (boardState) => { const moves = []; for (let colIndex = 0; colIndex < COLS; colIndex++) { if (boardState[0][colIndex] === EMPTY) { moves.push(colIndex); } } return moves; }; // =============================================== // 5. UI/アニメーションと描画 // =============================================== /** 盤面のDOMを一度だけ生成し、参照を保持する */ const createBoardDOM = () => { gridEl.innerHTML = ''; gridEl.style.setProperty('--rows', ROWS); gridEl.style.setProperty('--cols', COLS); for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const cell = document.createElement('div'); cell.classList.add('cell'); cell.dataset.r = r; cell.dataset.c = c; const stone = document.createElement('div'); stone.classList.add('stone'); stone.dataset.r = r; stone.dataset.c = c; cell.appendChild(stone); gridEl.appendChild(cell); cellEls[r][c] = cell; stoneEls[r][c] = stone; } } // イベントリスナーを gridEl に委譲 gridEl.addEventListener('click', handleBoardClick); gridEl.addEventListener('mouseover', handleBoardMouseOver); gridEl.addEventListener('mouseout', handleBoardMouseOut); }; /** 盤面状態をDOMに反映する */ const redrawStones = (winLine = []) => { for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { const stoneEl = stoneEls[r][c]; const val = gameState.board[r][c]; stoneEl.className = 'stone'; // クラスをリセット if (val === PLAYER1) { stoneEl.classList.add('player1'); } else if (val === PLAYER2) { stoneEl.classList.add('player2'); } // 勝利ハイライト if (winLine.some(p => p.r === r && p.c === c)) { stoneEl.classList.add('win-highlight'); } // ハイライトの解除(redraw後に確実にクリア) cellEls[r][c].classList.remove('column-highlight'); } } }; /** イベント委譲によるハイライト処理 */ const highlightColumn = (colIndex, highlight) => { if (gameState.gameEnded || gameState.isThinking) return; const isLegal = gameState.board[0][colIndex] === EMPTY; if (isLegal) { for(let r = 0; r < ROWS; r++){ const cell = cellEls[r][colIndex]; cell.classList.toggle('column-highlight', highlight); } } }; // =============================================== // 6. ゲームロジック // =============================================== /** 連続した石の数を数える */ const countLine = (boardState, r, c, dr, dc, color) => { let count = 0; let nr = r + dr; let nc = c + dc; while (nr >= 0 && nr < ROWS && nc >= 0 && nc < COLS && boardState[nr][nc] === color) { count++; nr += dr; nc += dc; } return count; }; /** 勝利判定と勝利ラインの取得 */ const checkWin = (boardState, r, c, color) => { for (const [dr, dc] of DIRECTIONS) { // countLineの結果をキャッシュして再利用 const forward = countLine(boardState, r, c, dr, dc, color); const backward = countLine(boardState, r, c, -dr, -dc, color); const count = 1 + forward + backward; if (count >= CONNECT_COUNT) { const winLine = []; for (let i = -backward; i <= forward; i++) { winLine.push({ r: r + i * dr, c: c + i * dc }); } return winLine; } } return null; }; /** 盤面に石を置き、勝敗をチェックする */ const applyMoveAndCheck = (colIndex, color, boardState = gameState.board) => { const rowIndex = getNextOpenRow(boardState, colIndex); if (rowIndex === -1) { return { success: false }; } boardState[rowIndex][colIndex] = color; const winLine = checkWin(boardState, rowIndex, colIndex, color); const isDraw = getLegalMoves(boardState).length === 0 && !winLine; return { success: true, rowIndex, winLine, isDraw }; }; /** 実際の着手とターン進行を制御する */ const handleMove = async (colIndex, color, isAI = false) => { if (gameState.gameEnded || gameState.isThinking && !isAI) return false; const result = applyMoveAndCheck(colIndex, color); if (!result.success) { if (!isAI) { msgEl.textContent = 'その列はすでにいっぱいです!'; await sleep(1000); updateStatus(); } return false; } gameState.isThinking = isAI; // アニメーション await handleDropAnimation(colIndex, result.rowIndex, color); if (result.winLine) { gameOver(color, result.winLine); return true; } if (result.isDraw) { gameOver(EMPTY); return true; } // ターン切り替え gameState.currentPlayer = opponentOf(color); updateStatus(); if (isAI) { gameState.isThinking = false; } // AIのターンであれば呼び出す if (gameState.currentPlayer === gameState.aiColor) { await sleep(500); aiTurn(); } return true; }; const handlePlayerClick = (colIndex) => { if (gameState.currentPlayer !== gameState.humanColor) return; handleMove(colIndex, gameState.humanColor, false); }; const handleBoardClick = (e) => { if (gameState.gameEnded || gameState.isThinking) return; const cell = e.target.closest('.cell'); if (!cell) return; const colIndex = Number(cell.dataset.c); handlePlayerClick(colIndex); }; const handleBoardMouseOver = (e) => { if (gameState.gameEnded || gameState.isThinking) return; const cell = e.target.closest('.cell'); if (!cell) return; const colIndex = Number(cell.dataset.c); highlightColumn(colIndex, true); }; const handleBoardMouseOut = (e) => { if (gameState.gameEnded || gameState.isThinking) return; const cell = e.target.closest('.cell'); if (!cell) return; const colIndex = Number(cell.dataset.c); highlightColumn(colIndex, false); }; const gameOver = (winner, winLine = []) => { gameState.gameEnded = true; btnStart.textContent = 'もう一度プレイ'; if (winner === EMPTY) { msgEl.textContent = '引き分けです!'; } else { let winnerName; if (winner === gameState.humanColor) { winnerName = 'プレイヤー'; gameState.playerScore++; scorePlayerEl.textContent = gameState.playerScore; } else { winnerName = 'AI'; gameState.aiScore++; scoreAIEl.textContent = gameState.aiScore; } const winnerColor = winner === PLAYER1 ? '(赤)' : '(黄)'; msgEl.textContent = `${winnerName}${winnerColor}の勝ちです!`; redrawStones(winLine); } gameState.isThinking = false; }; const updateStatus = () => { if (gameState.gameEnded) return; const turnColor = gameState.currentPlayer; const turnName = turnColor === gameState.humanColor ? 'プレイヤー' : 'AI'; const turnColorName = turnColor === PLAYER1 ? '(赤)' : '(黄)'; msgEl.textContent = `${turnName}${turnColorName}の番です。`; }; const handleDropAnimation = async (colIndex, targetRowIndex, color) => { const duration = ANIMATION_DURATION; if (duration === 0) return; const startRow = targetRowIndex - ROWS; const dropEl = document.createElement('div'); dropEl.classList.add('stone', color === PLAYER1 ? 'player1' : 'player2', 'dropping'); dropEl.style.setProperty('--drop-start-row', startRow); dropEl.style.animationDuration = `${duration}ms`; // 盤面の一番上のセル(r=0)に着手アニメーション用の石を一時的に追加 const targetCell = cellEls[0][colIndex]; if (!targetCell) return; targetCell.appendChild(dropEl); await sleep(duration); targetCell.removeChild(dropEl); redrawStones(); }; // =============================================== // 7. AIロジック (MinimaxにApply/Undoを導入) // =============================================== /** 盤面に石を打ち、その行を返す (Minimax探索用) */ const applyMove = (boardState, colIndex, color) => { const rowIndex = getNextOpenRow(boardState, colIndex); if (rowIndex === -1) throw new Error("Invalid move applied in minimax."); boardState[rowIndex][colIndex] = color; return rowIndex; }; /** 打った石を取り消す (Minimax探索用) */ const undoMove = (boardState, colIndex, rowIndex) => { boardState[rowIndex][colIndex] = EMPTY; }; const evaluateBoard = (boardState, maximizingColor) => { let score = 0; const minimizingColor = opponentOf(maximizingColor); for (let r = 0; r < ROWS; r++) { for (let c = 0; c < COLS; c++) { if (boardState[r][c] === EMPTY) continue; const currentStone = boardState[r][c]; const multiplier = currentStone === maximizingColor ? 1 : -1; // 中央重視 (重み定数を使用) score += multiplier * (3 - Math.abs(c - (COLS - 1) / 2)) * WEIGHTS.CENTER; // 連続チェック for (const [dr, dc] of DIRECTIONS) { const forward = countLine(boardState, r, c, dr, dc, currentStone); const backward = countLine(boardState, r, c, -dr, -dc, currentStone); const count = 1 + forward + backward; if (count === CONNECT_COUNT - 1) { score += multiplier * WEIGHTS.THREE_IN_A_ROW; // 3つ並び } if (count === CONNECT_COUNT - 2) { score += multiplier * WEIGHTS.TWO_IN_A_ROW; // 2つ並び } } } } return score; }; const minimax = (boardState, depth, alpha, beta, isMaximizingPlayer, playerColor) => { const legalMoves = getLegalMoves(boardState); // 勝利判定 (早期終了) for (const colIndex of legalMoves) { const r = getNextOpenRow(boardState, colIndex); if (r === -1) continue; // 盤面をコピーせず、一時的に着手してチェック boardState[r][colIndex] = playerColor; if (checkWin(boardState, r, colIndex, playerColor)) { boardState[r][colIndex] = EMPTY; // 盤面を元に戻す const winScore = playerColor === gameState.aiColor ? WIN_SCORE + depth : -WIN_SCORE - depth; return { score: winScore, move: colIndex }; } boardState[r][colIndex] = EMPTY; // 盤面を元に戻す } if (depth === 0 || legalMoves.length === 0) { return { score: evaluateBoard(boardState, gameState.aiColor), move: null }; } // 中央の列を優先的に探索 const centerColumn = Math.floor((COLS - 1) / 2); legalMoves.sort((a, b) => Math.abs(a - centerColumn) - Math.abs(b - centerColumn)); if (isMaximizingPlayer) { let maxScore = -Infinity; let bestMove = legalMoves[0]; for (const colIndex of legalMoves) { const rowIndex = applyMove(boardState, colIndex, playerColor); // 着手 const result = minimax(boardState, depth - 1, alpha, beta, false, opponentOf(playerColor)); undoMove(boardState, colIndex, rowIndex); // 取り消し if (result.score > maxScore) { maxScore = result.score; bestMove = colIndex; } alpha = Math.max(alpha, maxScore); if (beta <= alpha) break; } return { score: maxScore, move: bestMove }; } else { let minScore = Infinity; let bestMove = legalMoves[0]; for (const colIndex of legalMoves) { const rowIndex = applyMove(boardState, colIndex, playerColor); // 着手 const result = minimax(boardState, depth - 1, alpha, beta, true, opponentOf(playerColor)); undoMove(boardState, colIndex, rowIndex); // 取り消し if (result.score < minScore) { minScore = result.score; bestMove = colIndex; } beta = Math.min(beta, minScore); if (beta <= alpha) break; } return { score: minScore, move: bestMove }; } }; const findForcedMove = (boardState, color) => { const opponent = opponentOf(color); const legalMoves = getLegalMoves(boardState); // 1. 勝利手を探す for (const c of legalMoves) { const r = getNextOpenRow(boardState, c); if (r === -1) continue; boardState[r][c] = color; if (checkWin(boardState, r, c, color)) { boardState[r][c] = EMPTY; // 元に戻す return c; } boardState[r][c] = EMPTY; // 元に戻す } // 2. 相手の勝利手(ブロック手)を探す for (const c of legalMoves) { const r = getNextOpenRow(boardState, c); if (r === -1) continue; boardState[r][c] = opponent; if (checkWin(boardState, r, c, opponent)) { boardState[r][c] = EMPTY; // 元に戻す return c; } boardState[r][c] = EMPTY; // 元に戻す } return null; }; const aiTurn = async () => { if (gameState.gameEnded || gameState.currentPlayer !== gameState.aiColor || gameState.isThinking) return; gameState.isThinking = true; try { msgEl.textContent = 'AIが思考中です...'; // 修正: 入力値のチェック let depth = Number(selectLevel.value); if (!Number.isInteger(depth) || depth <= 0) { depth = 4; } let bestCol = findForcedMove(gameState.board, gameState.aiColor); if (bestCol === null) { const result = minimax(gameState.board, depth, -Infinity, Infinity, true, gameState.aiColor); bestCol = result.move; } if (bestCol !== null) { await handleMove(bestCol, gameState.aiColor, true); } else if (getLegalMoves(gameState.board).length > 0) { // 最悪のケース(評価関数がバグったときなど)のフォールバック const centerColumn = Math.floor((COLS - 1) / 2); const moves = getLegalMoves(gameState.board); moves.sort((a, b) => Math.abs(a - centerColumn) - Math.abs(b - centerColumn)); await handleMove(moves[0], gameState.aiColor, true); } else { // ドロー } } catch(e) { console.error("AI思考中にエラーが発生:", e); msgEl.textContent = 'AIの思考中にエラーが発生しました。'; gameState.gameEnded = true; } finally { gameState.isThinking = false; updateStatus(); } }; // =============================================== // 8. 初期化とゲーム制御 // =============================================== const startGame = () => { if (gameState.isThinking) return; // 1. 状態リセット gameState.gameEnded = false; gameState.board = createEmptyBoard(); // 2. 色の割り当て const firstPlayerSelection = parseInt(selectFirst.value); gameState.humanColor = firstPlayerSelection === PLAYER1 ? PLAYER1 : PLAYER2; gameState.aiColor = opponentOf(gameState.humanColor); // 3. 最初のターンは常に赤(PLAYER1)から始める gameState.currentPlayer = PLAYER1; btnStart.textContent = 'ゲーム再開'; redrawStones(); // 盤面をリセット状態に描画 // 4. 状態更新 updateStatus(); // 5. AIが先手の場合、AIのターンを実行 if (gameState.currentPlayer === gameState.aiColor) { aiTurn(); } }; const init = () => { createBoardDOM(); // DOM要素は一度だけ作成 btnStart.addEventListener('click', startGame); msgEl.textContent = '設定を選んで「ゲーム開始」を押してください。'; }; init(); }); </script> </body> </html> |
ブラウザで開いて動作確認する
-
connect4.htmlをダブルクリックして開く(既定のブラウザで起動)。 - すぐにプレイできます。
- スマホ表示の確認は、PCのブラウザでデベロッパーツールのデバイス表示を使うと便利です。
HTMLでゲームの土台を作成する
ここから、4目並べ(Connect4)AI対戦ゲームを“実際に形にしていく”工程に入ります。
まずは HTML でゲーム画面の土台(枠組み) を作ります。
HTMLは、ゲームの
- ボード
- スコア表示
- ゲーム開始ボタン
- 選択メニュー(先手・難易度)
などの「見える部分」を構成する大事なパートです。
HTMLで作る主なパーツ
HTML部分に貼り付けるコードには、次のような構成が含まれます。
1. ゲーム全体を包む .wrap
画面中央にボードや設定UIをまとめて配置するための大枠です。
操作UI(先手・難易度・スタートボタン)
- 先手/後手(プレイヤー or AI)を選ぶセレクトボックス
<select id="first"> - AIの強さ(初級・中級・上級)を選ぶセレクトボックス
<select id="level"> - ゲーム開始ボタン
<button id="btnStart">
HTML側では、これらを <select> や <button> で定義しています。
現在の手番やスコアを表示する
ゲームが始まると、
- 「プレイヤー(赤)の番です」
- 「AIが思考中です…」
- 「あなたの勝ち!」
などのメッセージが表示されます。
また、スコア(プレイヤー勝利数・AI勝利数)もここに表示されます。
HTML側では、これらを <div class="msg" id="message"> や <div class="status"> で定義しています。
ボード(6行 × 7列)を表示する仕組み
4目並べの「6行 × 7列」のボードは、
HTMLに直接書くのではなく、
JavaScript が <div class="grid" id="grid"> の中に自動生成します。
HTML側では「ボードを置くための空の箱」だけを用意しておき、
実際のマス(セル)はすべてスクリプトで生成される仕組みです。
▼ HTMLにあるのは“枠”だけ
|
1 2 3 |
<div class="board-container"> <div class="grid" id="grid"></div> </div> |
▼ JavaScript側で動的にマスを追加
- 6×7 のセルをループで生成
- DOMに追加してボードを構築
- Clickイベントや石の描画はここで行う
マスは JavaScript が自動で作るので、HTMLは“置き場”だけ用意します。
CSSでボードとコマの見た目・アニメーションを作る
HTMLでゲームの「土台(枠)」ができたので、
この章では CSS を使って見た目を整え、4目並べらしい盤面とコマ を作っていきます。
CSS変数でサイズや色を一括管理(:root)
CSSの先頭では、:root に以下のような変数が定義されています。
--rows… 行数(6)--cols… 列数(7)--cell-size… 1マスの大きさ--board-color… 盤面の色(青)--player1-color… プレイヤー(赤)のコマ色--player2-color… AI(黄)のコマ色--accent-color… 勝利ラインの光り方に使う色(緑)
これらの変数は、のちほど出てくる .grid や .stone で使われます。
サイズや色をあとからまとめて調整できるのがポイントです。
全体レイアウト:.wrap と .controls
画面全体の見た目を整えているのがこちらです。
body… 背景色や中央寄せ、余白など.wrap… ゲーム全体のカード風コンテナ- 白背景に丸みのある角
- 影をつけて「ひとつのツール」に見えるように
.controls… 先手・レベル・「ゲーム開始」ボタンの並び- フレックスレイアウトで横並び
- スマホでも崩れにくいように
gapやflex-wrapを設定
ここまでで、ツールらしい見た目 が整います。
盤面を描画する .board-container と .grid
4目並べのボードは、以下の2つで構成されています。
.board-container- 枠線(
border)と角丸(border-radius) - 影(
box-shadow)で立体感 background-color: var(--board-color);でボード全体を青く
- 枠線(
.griddisplay: grid;grid-template-columns: repeat(var(--cols), var(--cell-size));grid-template-rows: repeat(var(--rows), var(--cell-size));
で 6行 × 7列のマス目レイアウト を作っています。
実際の「マス」は、JavaScriptが #grid の中に .cell 要素をどんどん append して生成していく仕組みです。
穴の開いたボードを再現する .cell と .cell::after
盤面の1マスは .cell で表現されます。
.cell- 各マスの大きさ(
width/height) - 中央寄せ(
display: flex; align-items: center; justify-content: center;) - マウスカーソル用の
cursor: pointer;
- 各マスの大きさ(
さらに、.cell::after で 白い円形の「穴」 を重ねることで、
4目並べのボードらしい見た目を実現しています。
コマ(石)の見た目:.stone, .stone.player1, .stone.player2
コマは .stone クラスで表示されます。
.stone… コマ共通の形(円・サイズ・位置).stone.player1… プレイヤー(赤)のコマ色.stone.player2… AI(黄)のコマ色
円のサイズを .cell::after と揃えることで、
穴の中にぴったり収まるような見た目になっています。
勝利ラインのハイライト:.stone.win-highlight
4つそろったときに、そのラインを光らせているのが.stone.win-highlight です。
- 緑色の光(
box-shadow) - 白い枠線で「ここが勝ち!」と分かりやすく強調
JavaScriptから勝利したコマにこのクラスを追加することで、
自動的にエフェクトが適用されます。
コマが落ちる演出:@keyframes drop と .stone.dropping
CSSアニメーションで「上からストンと落ちる」動きを付けています。
|
1 2 3 4 5 6 7 8 9 |
@keyframes drop { 0% { transform: translateY(calc(var(--drop-start-row) * var(--cell-size))); } 100% { transform: translateY(0); } } .stone.dropping { animation-name: drop; animation-timing-function: cubic-bezier(0.5, 0, 1, 1); } |
--drop-start-row… どの高さから落ちてくるかをJS側で設定.stone.droppingを付けたコマだけ、上から下へスッと落ちる
視覚的に「置いた感」が出るので、
ゲームがぐっと気持ちよくなります。
列をわかりやすくするハイライト:.column-highlight
マウスを重ねた列をわかりやすくするためのクラスです。
JavaScript側で、同じ列の .cell に.column-highlight を付けたり外したりしています。
スマホ対応:@media (max-width: 600px)
画面幅が狭いとき(スマホなど)は、@media (max-width: 600px) の中で
--cell-sizeを13vwに変更.gridの幅・高さをそれに合わせて再計算.wrapの余白を少し減らす.controlsの並びが詰まりすぎないよう調整
といったレスポンシブ対応を行っています。
JavaScriptでゲームのロジックを作る
この章では、4目並べ(Connect4)AI対戦ゲームの
JavaScript ロジックの流れを、できるだけ噛み砕いて解説します。
定数と状態管理:ゲームの「ルール」と「今の状況」を持つ
まず最初に、ゲームの基本ルールと状態を定義しています。
ROWS/COLS… 盤面サイズ(6行×7列)CONNECT_COUNT… 何個そろえたら勝ちか(4個)EMPTY/PLAYER1/PLAYER2… 盤面上の状態を数字で表現ANIMATION_DURATION… 落下アニメーションの時間(ミリ秒)
さらに、ゲーム全体の状態は gameState オブジェクトでまとめています。
board…board[row][col]でコマの状態を保存currentPlayer… 今打つ側(PLAYER1 か PLAYER2)humanColor/aiColor… プレイヤーとAIの担当色playerScore/aiScore… 勝利数isThinking… AIが思考中かどうか(多重入力防止)
gameState に状態をまとめると、“今ゲームがどんな状況か” が追いやすくなります。
DOM要素の取得と 2次元配列参照
HTML側の要素は、次のように getElementById で取得しています。
#grid… 盤面(マス)を並べるコンテナ#message… 「プレイヤーの番です」「AIが思考中です…」などのメッセージ表示#btnStart… ゲーム開始/再開ボタン#first… 先手(プレイヤー or AI)選択#level… AIの強さ(3 / 6 / 8)選択#scorePlayer/#scoreAI… スコア表示
また、盤面に対応するセルと石の DOM を
2次元配列で参照できるようにしています。
|
1 2 |
const stoneEls = Array.from({ length: ROWS }, () => Array(COLS)); const cellEls = Array.from({ length: ROWS }, () => Array(COLS)); |
これにより、
cellEls[r][c]… r 行 c 列の.cell要素stoneEls[r][c]… r 行 c 列に対応する.stone要素
という形で、あとから高速にアクセスできます。
盤面DOMを作る:createBoardDOM()
盤面の 6×7 のマスは、HTMLに直接書かれておらず、
JavaScript の createBoardDOM() で動的に生成されています。
ポイント:
.cell要素にdata-r/data-cを持たせて、どの行・列か分かるようにしている.stoneは常に存在し、クラス(.player1/.player2)で色を切り替える方式- クリック・マウスオーバーは
#gridに対する イベント委譲 で処理
基本ユーティリティ関数
ゲームロジックを読みやすくするために、
いくつかの「小さな便利関数」が用意されています。
代表的なもの:
|
1 2 |
const getNextOpenRow = (boardState, colIndex) => { ... }; const getLegalMoves = (boardState) => { ... }; |
getNextOpenRow()… 指定列で「一番下から見て空いている行」を返すgetLegalMoves()… まだ石を置ける列の一覧を返す
勝敗判定:checkWin() と評価用ロジック
勝敗の判定は、DIRECTIONS と countLine() を使って行います。
|
1 |
const DIRECTIONS = [[0, 1], [1, 0], [1, 1], [1, -1]]; // 横, 縦, 右下, 左下 |
checkWin(boardState, r, c, color) は、
最後に置いた位置 (r, c) から各方向へ同じ色が
4つ連続しているかどうか を調べます。
評価関数 evaluateBoard() では、
4マスの「窓」をスライドしながら、
- 自分の石 3つ+空き1つ → 高得点
- 自分の石 2つ+空き2つ → そこそこ得点
- 中央の列 → 少しボーナス
といったスコアを加算しています(WEIGHTS.CENTER / WEIGHTS.THREE_IN_A_ROW など)。
石を置く流れ:handleBoardClick() → handleMove()
プレイヤーが盤面をクリックしたときの流れはこうです。
gridElに登録されたhandleBoardClick()が呼ばれるe.target.closest('.cell')でクリックされた.cellを取得cell.dataset.cから列番号colIndexを取得handlePlayerClick(colIndex)→handleMove(colIndex, gameState.humanColor, false)を実行
handleMove(colIndex, color, isAI) の中では、
applyMoveAndCheck()でboardに石を置きつつ、勝敗をチェック- 落下アニメーションを
handleDropAnimation()で再生 - 勝ちなら
gameOver(color)、引き分けならgameOver(EMPTY) - どちらでもないなら
currentPlayerを交代しupdateStatus()を実行 - AIのターンになったら
aiTurn()を呼び出す
という一連の流れを管理しています。
画面更新:redrawStones() とハイライト
redrawStones() は、gameState.board の内容を
画面の .stone に反映させる関数です。
stoneEls[r][c]のclassListをクリア- 値が
PLAYER1なら.player1を、PLAYER2なら.player2を付ける - 勝利ラインがある場合は
.win-highlightを付与して光らせる
列のハイライトは highlightColumn(colIndex, highlight) が担当し、cellEls[r][colIndex].classList.toggle('column-highlight', highlight);
で列全体を分かりやすくしています。
AIのロジック:aiTurn() と minimax()
AIのターンでは、aiTurn() が中心になります。
ポイント:
gameState.isThinkingで重複操作を防止findForcedMove()で- 自分がすぐ勝てる手
- 相手がすぐ勝ちそうな手
を先にチェック(「即勝ち・即ブロック」)
- それ以外は
minimax()で
与えられた深さ(レベルごとに 3 / 6 / 8)まで探索 - αβ枝刈り(アルファ・ベータ)で不要な探索を早めにカット
- 中央列に近い着手ほど優先して探索し、賢く振る舞うように工夫
【カスタマイズ案】自分だけの4目並べに進化させよう
4目並べ(Connect4)AI対戦ゲームは、
そのままでも十分遊べますが、
ちょっとしたカスタマイズで“オリジナルゲーム”に進化します。
ここでは、初心者でも簡単にできるものから、
AIロジックを深めたい人向けの高度な改造まで紹介します。
コマの色を変える(初心者向け)
プレイヤーやAIの色は、CSSの変数で変えられます。
|
1 2 3 4 |
:root { --player1-color: #ff4500; /* 赤 → 変えたい色へ */ --player2-color: #ffcc00; /* 黄 → 変えたい色へ */ } |
色を変えるだけで、ゲームの雰囲気がガラッと変わります。
AIの評価関数を調整する(中〜上級)
AIの強さを本格的に変えたいなら、evaluateBoard() のスコアリングを調整します。
たとえば:
- 中央列の優先度を強める
- 2連・3連のスコアを別の値にする
- 相手の脅威(危険な形)に高い評価をつける
ほんの少し変えるだけで“性格の違うAI”が生まれます。
おすすめの学習リソース|もっとスキルを伸ばしたいあなたへ
「4目並べ(Connect4)」を完成させたことで、あなたはすでに HTML・CSS・JavaScriptの基礎 に触れました。
ここからさらにステップアップするために役立つ学習リソースを紹介します。
おすすめ書籍
『ゲームで学ぶJavaScript入門 増補改訂版 ~ブラウザゲームづくりでHTML&CSSも身につく!』
- ゲームを作りながら学べるので、退屈せずにJavaScriptの基礎が身につく
- HTML・CSS・JavaScriptの基本を一冊で網羅、Web制作の土台が自然に理解できる
- 13本のサンプルゲームを実際に動かせるから「作れる喜び」が味わえる
- 初心者・中高生にもやさしい解説で、初めてのゲームプログラミングに最適
『スラスラわかるHTML&CSSのきほん』(SBクリエイティブ)
- Web制作初心者に最適な入門書。HTMLとCSSの仕組みをやさしく解説。
- これからWebページを作ってみたい方にぴったり。
- レイアウトの基本やスタイルの調整方法など、実践的に学べます。
オンライン講座編
Udemy|世界最大級のオンライン学習プラットフォーム
世界中で利用されるオンライン学習サイト。
HTML、CSS、JavaScriptの入門から応用まで、高評価の講座が数百種類揃っています。
初心者でも動画を見ながら手を動かせるので、挫折しにくいのが魅力です。
【まとめ】4目並べAIを作ってゲーム開発の基礎を身につけよう
ここまでの手順で、4目並べ(Connect4)AI対戦ゲームが完成しました。
HTML・CSS・JavaScript だけで、
- 盤面の生成
- 石の落下アニメーション
- 勝利判定
- AIとの対戦
といった本格的なゲームが作れたのは、大きな経験になります。
「動くゲームを作れた」という経験は、
プログラミングを続ける上で大きな自信になります。
ぜひこの経験を活かして、
さらに楽しい作品を作り続けてください!
関連記事
【実際に遊べる 4目並べ(Connect4)はこちら。AIと対戦してみましょう!】▼
4目並べ(Connect4)づくりを楽しめた方は、以下の記事もおすすめです。
どれもブラウザだけで動く本格的なゲームを題材にしており、AIロジックやJavaScriptの理解がさらに深まります。
【作り方記事】まるばつゲーム(三目並べ)の作り方|AI対戦ロジックを学ぼう!▼
【作り方記事】五目並べAI対戦の作り方|本格的なAI構築の応用編!▼




