「オセロ(リバーシ)のAI対戦ゲームを自分で作ってみたい!」
でも、プログラミングって難しそう…と思っていませんか?
実は、HTML・CSS・JavaScriptの3つだけで、
ブラウザで動く本格的なAI対戦オセロを作ることができます。
しかも今回紹介する方法なら、
ソースコードをコピペするだけですぐに完成!
複雑な環境構築も不要で、ブラウザでそのまま実行できます。
この記事では、まず動くオセロ(リバーシ)を完成させてから、
「どうして動くのか?」という仕組みを一つずつ解説していきます。
オセロ(リバーシ)の基本仕様を理解しよう
まずは、今回作るオセロ(リバーシ)がどんな仕組みで動いているのかをざっくり見ていきましょう。
全体像を先に理解しておくことで、後のコード解説がぐっとわかりやすくなります。
完成版を体験してみよう
ゲームの基本ルール
オセロ(リバーシ)は、黒と白の石を交互に置いていくボードゲームです。
相手の石を自分の石ではさむと、その間の石がすべて自分の色に裏返ります。
最終的に石の数が多い方が勝ちです。
ゲームの主な機能
今回のプログラムには、次のような機能が含まれています。
- 盤面の自動生成:HTMLとCSSで8×8のマスを作成。
- 合法手の判定:「石を置ける場所」を計算。
- 石の反転処理:相手の石をはさんだとき、自動で裏返す。
- AI対戦機能:コンピューターが次の一手を選んで自動でプレイ。
- 難易度選択:簡単/普通/難しいの3段階に対応。
- レスポンシブデザイン:スマホでも快適にプレイ可能。
ファイルを準備して完成コードを動かそう
それでは、いよいよ実際にオセロ(リバーシ)のAI対戦ゲームを動かしてみましょう。
この章では、ファイルを作ってコードを貼り付け、ブラウザで実行するところまでを一気に行います。
ステップ1:ファイルを作成しよう
まずは、あなたのパソコンでHTMLファイルを作りましょう。
メモ帳・VSCodeなど、どんなエディタでもOKです。
- 新しいファイルを作成
- ファイル名を
reversi.htmlにして保存
ステップ2:コードをコピペしよう
コードはHTML・CSS・JavaScriptが一体化した単一ファイル構成になっています。
下の「完成コード」をすべてコピーして、 に貼り付け、上書き保存します。reversi.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 |
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title>オセロ(リバーシ)AI対戦</title> <style> :root{ --board:#0b6b3a; --grid:#0a5c32; --cell:#0e7a44; --black:#111; --white:#f8f8f8; --accent:#ffd54d; } *{box-sizing:border-box} body{ margin:0; font-family:system-ui,-apple-system,Segoe UI,Roboto,Helvetica,Arial,"Hiragino Kaku Gothic ProN",Meiryo,sans-serif; display:flex; align-items:center; justify-content:center; background:#f4f7f6; color:#222; padding:16px; min-height: 100vh; } .wrap{ background-color: #fff; padding: 2px; border-radius: 10px; box-shadow: 0 5px 5px rgba(0, 0, 0, 0.1); text-align: center; width: 95%; max-width: 600px; } h1{ margin:1.2em 0 .6em; font-size:clamp(1.2rem,2.2vw,1.6rem); } .topbar{ display:flex; flex-wrap:wrap; gap:12px; align-items:center; justify-content:space-between; } .controls{ display:flex; flex-wrap:wrap; gap:12px; align-items:center } select,button{ font-size:1rem; padding:8px 12px; border-radius:8px; border:1px solid #cdd3cf; background:#fff } button.primary{ background:#198754; color:#fff; border-color:#198754 } button:disabled{ opacity:.6; cursor:not-allowed } .status{ display:flex; gap:14px; align-items:center; flex-wrap:wrap } .badge{ display:inline-flex; align-items:center; gap:6px; padding:6px 10px; border-radius:999px; background:#eef3f1; } .dot{ width:14px; height:14px; border-radius:50% } .dot.black{background:var(--black)} .dot.white{background:var(--white); box-shadow:inset 0 0 0 2px #ccc} .board{ position:relative; margin:14px auto; width:min(92vw,520px); aspect-ratio:1/1; background:var(--board); border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,.12); } .grid{ display:grid; grid-template-columns:repeat(8,1fr); grid-template-rows:repeat(8,1fr); width:100%; height:100%; gap:3px; padding:8px; } .cell{ position:relative; background:var(--cell); border-radius:6px; display:flex; align-items:center; justify-content:center; cursor:pointer; outline:none; } .cell:focus-visible{ box-shadow:0 0 0 3px rgba(255,213,77,.9) inset, 0 0 0 3px rgba(255,213,77,.9) } .cell.nohover{cursor:not-allowed} .stone{ width:72%; height:72%; border-radius:50%; box-shadow:inset 0 8px 10px rgba(255,255,255,.15), inset 0 -10px 14px rgba(0,0,0,.25), 0 2px 3px rgba(0,0,0,.25); } .stone.black{ background:var(--black) } .stone.white{ background:var(--white); box-shadow:inset 0 8px 10px rgba(255,255,255,.55), inset 0 -10px 14px rgba(0,0,0,.15), 0 2px 3px rgba(0,0,0,.25) } /* ヒント(合法手)中央に白っぽいドット */ .hint::after{ content:""; position:absolute; width:14px; height:14px; border-radius:50%; background-color:rgba(255,255,255,.75); top:50%; left:50%; transform:translate(-50%,-50%); box-shadow:0 0 0 2px rgba(0,0,0,.15) inset; } /* 直近の一手を破線で囲う */ .last-move::after{ content:""; position:absolute; inset:6px; border:2px dashed rgba(255,255,255,.65); border-radius:8px; pointer-events:none; } /* AI手番中の明示 */ .no-click{ cursor:not-allowed !important; } .no-click .cell{ filter:brightness(.96) } .msg{ min-height:1.6em; font-weight:600 } .sr-only{ position:absolute !important; height:1px; width:1px; overflow:hidden; clip:rect(1px,1px,1px,1px); white-space:nowrap } .footer{ display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap } /* ===== Responsive ===== */ @media (max-width: 768px){ .board{ width:min(94vw, 460px); } .grid{ gap:3px; padding:6px; } .stone{ width:74%; height:74%; } .controls select, .controls button{ font-size:.95rem; } .msg{ font-size:1rem; } } @media (max-width: 480px){ .wrap{ width:98vw; } .topbar{ gap:8px; } .board{ width:96vw; } .grid{ gap:2px; padding:6px; } .cell{ border-radius:5px; } .stone{ width:78%; height:78%; } h1{ font-size:clamp(1rem,4.8vw,1.25rem); } .msg{ font-size:.95rem; } } </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="btnReset" class="primary">開始(スタート)</button> </div> <div class="status"> <span class="badge"><span class="dot black"></span>黒:<span id="scoreBlack">2</span></span> <span class="badge"><span class="dot white"></span>白:<span id="scoreWhite">2</span></span> </div> </div> <div class="msg" id="message" aria-live="polite">先手と難易度を選び、「開始」を押してください。</div> <div class="board" id="boardWrap"> <div class="grid" id="grid" aria-label="リバーシ盤面" role="grid"></div> </div> <div class="footer"> <div>操作:タップ/クリック。キーボードは <b>Tab/矢印</b> で移動、<b>Enter</b> で着手。</div> </div> </div> <script> // ===== 定数・初期設定 ===== const SIZE = 8; const EMPTY = 0, BLACK = 1, WHITE = 2; // UI参照 const gridEl = document.getElementById('grid'); const msgEl = document.getElementById('message'); const scoreBlackEl = document.getElementById('scoreBlack'); const scoreWhiteEl = document.getElementById('scoreWhite'); const levelEl = document.getElementById('level'); const firstEl = document.getElementById('first'); const btnReset = document.getElementById('btnReset'); // 方向(8近傍) const DIRECTIONS = [ [-1,-1],[-1,0],[-1,1], [0,-1],[0,1], [1,-1],[1,0],[1,1] ]; // 盤面と状態 let board = createEmptyBoard(); let currentPlayer = BLACK; // 石の色で表す(常に黒から開始) let humanColor = BLACK; // あなたの色 let aiColor = WHITE; // AIの色 let lastMove = null; // 直前の手 {y,x} let inputLocked = false; // AI手番の操作ロック let gameStarted = false; // ゲーム開始フラグ // 評価重み(位置) Positive=良、Negative=悪(X/Cマスリスク) const WEIGHTS = [ [120,-25, 15, 5, 5, 15,-25,120], [-25,-40, 2, 2, 2, 2,-40,-25], [ 15, 2, 3, 3, 3, 3, 2, 15], [ 5, 2, 3, 1, 1, 3, 2, 5], [ 5, 2, 3, 1, 1, 3, 2, 5], [ 15, 2, 3, 3, 3, 3, 2, 15], [-25,-40, 2, 2, 2, 2,-40,-25], [120,-25, 15, 5, 5, 15,-25,120] ]; // ===== 初期化 ===== function createEmptyBoard(){ return Array.from({length:SIZE},()=>Array(SIZE).fill(EMPTY)); } function initBoard(){ board = createEmptyBoard(); const mid = SIZE/2; board[mid-1][mid-1] = WHITE; board[mid][mid] = WHITE; board[mid-1][mid] = BLACK; board[mid][mid-1] = BLACK; currentPlayer = BLACK; lastMove = null; renderBoard(); updateStatus(true); // 初期メッセージ } // ===== 描画・UI ===== function renderBoard(){ gridEl.innerHTML = ''; gridEl.classList.toggle('no-click', inputLocked); for(let y=0;y<SIZE;y++){ for(let x=0;x<SIZE;x++){ const cell = document.createElement('div'); cell.className = 'cell'; cell.setAttribute('role','gridcell'); cell.setAttribute('tabindex','0'); cell.dataset.y = y; cell.dataset.x = x; const v = board[y][x]; if(v!==EMPTY){ const st = document.createElement('div'); st.className = 'stone ' + (v===BLACK?'black':'white'); cell.appendChild(st); } if(lastMove && lastMove.y===y && lastMove.x===x){ cell.classList.add('last-move'); } // 合法手のヒント(人間の手番の時だけ) if(gameStarted && currentPlayer===humanColor && v===EMPTY){ const flips = getFlippableStones(y,x,currentPlayer); if(flips.length) cell.classList.add('hint'); } cell.addEventListener('click', onCellClick); cell.addEventListener('keydown', onCellKey); gridEl.appendChild(cell); } } } function onCellKey(e){ const y = +e.currentTarget.dataset.y; const x = +e.currentTarget.dataset.x; if(['Enter',' '].includes(e.key)){ e.preventDefault(); tryPlace(y,x); } else if(['ArrowUp','ArrowDown','ArrowLeft','ArrowRight'].includes(e.key)){ e.preventDefault(); let ny=y, nx=x; if(e.key==='ArrowUp') ny=Math.max(0,y-1); if(e.key==='ArrowDown') ny=Math.min(SIZE-1,y+1); if(e.key==='ArrowLeft') nx=Math.max(0,x-1); if(e.key==='ArrowRight') nx=Math.min(SIZE-1,x+1); const next = gridEl.querySelector(`.cell[data-y="${ny}"][data-x="${nx}"]`); next && next.focus(); } } function onCellClick(e){ const y = +e.currentTarget.dataset.y; const x = +e.currentTarget.dataset.x; tryPlace(y,x); } function tryPlace(y,x){ if(!gameStarted) return; if(inputLocked) return; if(currentPlayer!==humanColor) return; // 人間の番以外は弾く const flips = getFlippableStones(y,x,currentPlayer); if(!flips.length) return; placeStoneAndFlip(y,x,flips,currentPlayer); currentPlayer = opponentOf(currentPlayer); updateStatus(); maybeAILater(); } function placeStoneAndFlip(y,x,flips,color){ board[y][x] = color; for(const [fy,fx] of flips){ board[fy][fx] = color; } lastMove = {y,x}; renderBoard(); } function countStones(){ let black=0, white=0, empty=0; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if(board[y][x]===BLACK) black++; else if(board[y][x]===WHITE) white++; else empty++; } return {black,white,empty}; } function legalMoves(color){ const moves=[]; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if(board[y][x]!==EMPTY) continue; const flips = getFlippableStones(y,x,color); if(flips.length) moves.push({y,x,flippable:flips}); } return moves; } function getFlippableStones(y,x,player){ if(board[y][x]!==EMPTY) return []; const opponent = opponentOf(player); let flippable=[]; for(const [dy,dx] of DIRECTIONS){ let line=[], ny=y+dy, nx=x+dx; while(ny>=0&&ny<SIZE&&nx>=0&&nx<SIZE && board[ny][nx]===opponent){ line.push([ny,nx]); ny+=dy; nx+=dx; } if(ny>=0&&ny<SIZE&&nx>=0&&nx<SIZE && board[ny][nx]===player && line.length){ flippable.push(...line); } } return flippable; } function opponentOf(c){ return c===BLACK?WHITE:BLACK; } // ===== ステータス更新・ゲーム進行 ===== function updateStatus(initPhase=false){ const scores = countStones(); scoreBlackEl.textContent = scores.black; scoreWhiteEl.textContent = scores.white; // 盤面埋まりで即終局 if(scores.empty===0){ gameOver(); return; } // 現手番の合法手 const moves = legalMoves(currentPlayer); if(moves.length===0){ // 相手にも無ければ終局、あればパス const oppMoves = legalMoves(opponentOf(currentPlayer)); if(oppMoves.length===0){ gameOver(); return; } if(currentPlayer===humanColor){ msgEl.textContent = `あなた(${colorName(currentPlayer)})はパスです。AIの番へ。`; } else { msgEl.textContent = `AI(${colorName(currentPlayer)})はパスです。あなたの番です。`; } currentPlayer = opponentOf(currentPlayer); renderBoard(); if(currentPlayer===aiColor) maybeAILater(400); return; } // メッセージ if(initPhase && !gameStarted){ msgEl.textContent = '先手と難易度を選び、「開始」を押してください。'; } else if(currentPlayer===humanColor){ msgEl.textContent = `${colorName(currentPlayer)}(あなた)の番です。合法手は◯表示。`; } else { msgEl.textContent = `${colorName(currentPlayer)}(AI)の番です…考えています。`; } renderBoard(); } function colorName(c){ return c===BLACK? '黒' : '白'; } function gameOver(){ const {black,white} = countStones(); let result = (black===white)? '引き分け' : (black>white? '黒の勝ち' : '白の勝ち'); msgEl.textContent = `ゲーム終了:黒 ${black} - 白 ${white}(${result})`; inputLocked = false; gridEl.classList.remove('no-click'); gameStarted = false; // 終了 setControlsLocked(false); // 設定を変更可能に btnReset.textContent = '開始(スタート)'; } // ===== AI手番 ===== function maybeAIStart(){ inputLocked = (humanColor!==currentPlayer); gridEl.classList.toggle('no-click', inputLocked); if(currentPlayer===aiColor) maybeAILater(400); } function startGame(){ gameStarted = true; setControlsLocked(true); maybeAIStart(); } function setControlsLocked(lock){ firstEl.disabled = lock; levelEl.disabled = lock; } function maybeAILater(delay=650){ inputLocked = true; gridEl.classList.add('no-click'); setTimeout(aiTurn, delay); } function aiTurn(){ if(currentPlayer!==aiColor){ inputLocked=false; gridEl.classList.remove('no-click'); return; } const moves = legalMoves(aiColor); if(moves.length===0){ // パス currentPlayer = humanColor; inputLocked=false; gridEl.classList.remove('no-click'); updateStatus(); return; } const level = levelEl.value; let chosen; // 角があれば最優先 const corners = moves.filter(m => isCorner(m.y,m.x)); if(corners.length){ chosen = corners[Math.floor(Math.random()*corners.length)]; return doAIMove(chosen); } if(level==='easy'){ // ランダム + 軽い事故回避 const safe = moves.filter(m => !isNextToEmptyCorner(m.y,m.x)); chosen = (safe.length? safe : moves)[Math.floor(Math.random()* (safe.length?safe.length:moves.length))]; return doAIMove(chosen); } // NORMAL/HARD 共通:スコアリング let bestScore=-1e9, bestMove=moves[0]; const empties = countStones().empty; for(const m of moves){ let score = 0; // 位置の重み score += WEIGHTS[m.y][m.x]; // 角隣リスク(角が空いているのに隣接へ打つ) if(isNextToEmptyCorner(m.y,m.x)) score -= 25; // ひっくり返す枚数(終盤は重く) const weightFlip = (level==='hard' && empties<=10) ? 12 : 5; score += m.flippable.length * weightFlip; if(level==='hard'){ // 可動性:相手の合法手を減らすほど加点(ワンプレイ先読み) const tmp = cloneBoard(board); tmp[m.y][m.x] = aiColor; for(const [fy,fx] of m.flippable) tmp[fy][fx] = aiColor; const oppMob = countLegalWithBoard(tmp, humanColor); score += (8 - oppMob) * 2; // 相手の選択肢を減らす } if(score>bestScore){ bestScore=score; bestMove=m; } } chosen = bestMove; doAIMove(chosen); } function doAIMove(move){ placeStoneAndFlip(move.y, move.x, move.flippable, aiColor); currentPlayer = humanColor; inputLocked = false; gridEl.classList.remove('no-click'); updateStatus(); } // ----- AI補助関数 ----- function isCorner(y,x){ return (y===0&&x===0)||(y===0&&x===SIZE-1)||(y===SIZE-1&&x===0)||(y===SIZE-1&&x===SIZE-1); } function isNextToEmptyCorner(y,x){ const corners = [ {c:[0,0], n:[[0,1],[1,0],[1,1]]}, {c:[0,SIZE-1], n:[[0,SIZE-2],[1,SIZE-1],[1,SIZE-2]]}, {c:[SIZE-1,0], n:[[SIZE-2,0],[SIZE-1,1],[SIZE-2,1]]}, {c:[SIZE-1,SIZE-1], n:[[SIZE-2,SIZE-1],[SIZE-1,SIZE-2],[SIZE-2,SIZE-2]]} ]; for(const k of corners){ const [cy,cx] = k.c; if(board[cy][cx]!==EMPTY) continue; for(const [ny,nx] of k.n){ if(ny===y && nx===x) return true; } } return false; } function cloneBoard(bd){ return bd.map(row=>row.slice()); } function getFlippableStonesWithBoard(bd,y,x,player){ if(bd[y][x]!==EMPTY) return []; const opponent = player===BLACK?WHITE:BLACK; let flips=[]; for(const [dy,dx] of DIRECTIONS){ let line=[], ny=y+dy, nx=x+dx; while(ny>=0&&ny<SIZE&&nx>=0&&nx<SIZE && bd[ny][nx]===opponent){ line.push([ny,nx]); ny+=dy; nx+=dx; } if(ny>=0&&ny<SIZE&&nx>=0&&nx<SIZE && bd[ny][nx]===player && line.length) flips.push(...line); } return flips; } function countLegalWithBoard(bd, player){ let c=0; for(let y=0;y<SIZE;y++) for(let x=0;x<SIZE;x++){ if(bd[y][x]!==EMPTY) continue; if(getFlippableStonesWithBoard(bd,y,x,player).length) c++; } return c; } // ===== リセット・設定変更 ===== btnReset.addEventListener('click', () => { if(!gameStarted){ // 開始前:選択値を読み込んでゲーム開始 const first = firstEl.value; // black or white humanColor = (first==='black'? BLACK : WHITE); aiColor = (first==='black'? WHITE : BLACK); currentPlayer = BLACK; // 黒が初手 initBoard(); startGame(); btnReset.textContent = 'リセット(再開)'; if(humanColor!==BLACK) msgEl.textContent = `黒(AI)が先手です…`; }else{ // プレイ中のリセット:新規ゲームを即開始(設定は固定のまま) initBoard(); startGame(); btnReset.textContent = 'リセット(再開)'; } }); // 初期セットアップ(開始待ちの状態で表示) setControlsLocked(false); initBoard(); </script> </body> </html> |
ステップ3:ブラウザで開いてみよう
保存した reversi.html ファイルをダブルクリックすると、
お使いのブラウザで自動的に開きます。
もし自動で開かない場合は、
ブラウザのメニューから「ファイル → 開く」を選んで、
ファイルを指定して開いてください。
ステップ4:実際に動かしてみよう
起動すると、次のような流れでプレイできます。
- 「黒(先手)」または「白(後手)」を選ぶ
- 難易度(簡単/普通/難しい)を選択
- 「開始」ボタンを押してゲームスタート!
AIが自動で思考し、盤面に石を置いてきます。
すぐにあなた vs コンピューターの頭脳バトルが始まります!
HTML構造を理解しよう(ゲームの骨組み)
オセロ(リバーシ)の土台を支えるのが、HTML(エイチ・ティー・エム・エル)です。
HTMLは、いわばゲーム画面の「設計図」。
ボード(盤面)やスコア表示、操作ボタンなど、プレイヤーが見るすべての要素を配置しています。
HTML全体の役割
HTMLファイルには大きく3つの役割があります。
- 見出し(タイトル)を表示する部分
- 盤面や情報を配置する部分
- ゲーム操作(ボタンなど)を置く部分
ブラウザがこのHTMLを読み取り、CSSとJavaScriptの力で見た目と動きを付け加えます。
各要素の説明
| 要素 | 役割 |
|---|---|
<div class="wrap"> | ゲーム全体を中央にまとめる外枠。レイアウトの中心軸。 |
<h1> | ゲームタイトル(画面上部に表示される大見出し)。 |
<div id="message"> | 「あなたの番です」「AIの番です」などのメッセージを表示。 |
<div id="status"> | 黒石・白石のスコアをリアルタイム表示。 |
<select id="level"> | 難易度選択のプルダウンメニュー。 |
<div id="board"> | 8×8の盤面が生成される領域。JavaScriptがここにマスを作る。 |
JavaScriptで動きをつけるためには、
それぞれの要素にID属性を付けておくことが大事なんです。
これで、JSから『この部分を操作したい!』と指定できるようになります。
CSSでデザインとレスポンシブ対応を作る
オセロ(リバーシ)の見た目を整えているのが CSS(カスケーディング・スタイル・シート) です。
CSSは、ゲームに「色」「形」「配置」といったデザイン要素を与え、
さらにスマートフォンでも快適に遊べるレスポンシブデザインを実現します。
CSSの主な役割
- 盤面(ボード)をきれいに並べる
- 石(黒・白)を円形に表示する
- メッセージやボタンを整える
- スマホでも操作しやすいレイアウトに調整する
ボードレイアウトを整える
盤面は、CSSの強力な機能 Gridレイアウト を使って作られています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
.board{ position:relative; margin:14px auto; width:min(92vw,520px); aspect-ratio:1/1; background:var(--board); border-radius:12px; box-shadow:0 10px 30px rgba(0,0,0,.12); } .grid{ display:grid; grid-template-columns:repeat(8,1fr); grid-template-rows:repeat(8,1fr); width:100%; height:100%; gap:3px; padding:8px; } |
grid-template-columns:8列のマスを等間隔で配置する命令。
マスと石のデザイン
各マス(.cell)は緑系の背景で、クリック可能なエリアです。
中には .stone クラスの要素が入っており、黒石・白石をCSSで描画しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.cell { background: var(--cell); border-radius: 6px; display: flex; align-items: center; justify-content: center; cursor: pointer; } .stone { width: 72%; height: 72%; border-radius: 50%; } .stone.black { background: var(--black); } .stone.white { background: var(--white); } |
プレイ補助の演出
プレイヤーが置ける位置(合法手)には .hint が付き、中央に白ドットが表示されます。
また、直前に置かれたマスには .last-move の破線枠がつきます。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
.hint::after { content: ""; position: absolute; width: 14px; height: 14px; border-radius: 50%; background-color: rgba(255,255,255,.75); } .last-move::after { content: ""; position: absolute; inset: 6px; border: 2px dashed rgba(255,255,255,.65); border-radius: 8px; } |
スマホ対応(@mediaクエリ)
このソースでは、2段階のレスポンシブ設計を導入しています。
|
1 2 3 4 5 6 7 8 9 10 11 12 |
@media (max-width: 768px){ .board{ width:min(94vw, 460px); } .stone{ width:74%; height:74%; } .msg{ font-size:1rem; } } @media (max-width: 480px){ .wrap{ width:98vw; } .board{ width:96vw; } .stone{ width:78%; height:78%; } h1{ font-size:clamp(1rem,4.8vw,1.25rem); } .msg{ font-size:.95rem; } } |
- 画面が狭くなるにつれて、盤面の幅と石のサイズを自動調整。
clamp()でタイトル文字を画面幅に応じて縮小。
JavaScriptでオセロ(リバーシ)AI対戦を作る仕組み
ここからは、このゲームの頭脳である JavaScript(ジャバスクリプト) の役割を見ていきましょう。
今回のソースは、HTML・CSSと同じファイル内に記述されており、
「盤面の制御」「プレイヤーとAIのターン処理」「思考ロジック」 までをすべて担っています。
1. ゲームの基本となるデータ
まず、JavaScriptの中では次のように数字で石を管理しています。
|
1 2 |
// ===== 定数・初期設定 ===== const EMPTY = 0, BLACK = 1, WHITE = 2; |
EMPTY(空きマス)= 0BLACK(黒石)= 1WHITE(白石)= 2
このように数値で管理すると、プログラムで比較や判定がしやすくなります。
2. 盤面を作るしくみ
盤面は「8×8マスの表(2次元配列)」で作ります。
1行に8マス、これを8行分用意する形です。
|
1 2 3 4 5 6 |
const SIZE = 8; let board = createEmptyBoard(); function createEmptyBoard(){ return Array.from({length:SIZE},()=>Array(SIZE).fill(EMPTY)); } |
最初に中央にだけ石を置いてスタートします。
このときの並びはオセロの基本形です。
|
1 2 3 4 |
board[3][3] = WHITE; board[3][4] = BLACK; board[4][3] = BLACK; board[4][4] = WHITE; |
そして、この「配列に入っている数字の状態」をrenderBoard() という関数で実際の画面(HTML)に表示します。
3. 石を置いたときの動き
- クリックしたマスに石を置けるか調べる
- 置けるなら、その方向にある相手の石を「はさみ返す」
はさみ返せるかを調べるのが という関数です。getFlippableStones()
getFlippableStones(y, x, player)
この関数がオセロのルール処理の核心です。
- 指定されたマス
(y, x)が空きマスであるかチェック。 DIRECTIONS(8方向)を順にループします。- 各方向に対し、隣接するマスから順に、相手の駒が連続しているかチェックします。
- 連続した相手の駒の先に、自分の駒が見つかった場合、その間に挟まれた全ての相手の駒の座標を返します。
- 合法手でない(ひっくり返せる駒がない)場合は空の配列を返します。
4. コンピューターの考え方(AI)
AIのターンになると、次のような手順で打つ場所を決めます。
- 盤面をすべてチェックして「置ける場所」を探す
- その中から「一番良い場所」を選ぶ
この“良い場所”を見つけるために、AIは盤面をスコア化して考えます。
これを「評価関数」といいます。
5. 評価関数(どのマスが有利かを点数化)
AIは「角が強い」「端はまあまあ」「中央は普通」というルールで重みをつけています。
|
1 2 3 4 5 6 7 8 9 10 11 |
// 評価重み(位置) const WEIGHTS = [ [120,-25, 15, 5, 5, 15,-25,120], [-25,-40, 2, 2, 2, 2,-40,-25], [ 15, 2, 3, 3, 3, 3, 2, 15], [ 5, 2, 3, 1, 1, 3, 2, 5], [ 5, 2, 3, 1, 1, 3, 2, 5], [ 15, 2, 3, 3, 3, 3, 2, 15], [-25,-40, 2, 2, 2, 2,-40,-25], [120,-25, 15, 5, 5, 15,-25,120] ]; |
盤面には、オセロのセオリーに基づいた重みが設定されています。
- 角マス: 非常に高い正の値(例:
120)を設定し、最優先します。 - 角隣のマス(Xマス、Cマス): 非常に高い負の値(例:
-25,-40)を設定し、避けます。
6. 難易度の違いは「考える深さ」
難易度は、AIが「どこまで先を読むか」で変わります。
- 簡単:ランダムで置く(すぐ終わる)
- 普通:重み表で判断(1手先を見る)
- 難しい:1手先+相手の返しを少し読む
つまり、思考の深さを変えることで強さを調整しています。
さらに発展させるアイデア
今回のオセロ(リバーシ)AI対戦をベースに、いろいろな改良を試すことができます。
| 発展テーマ | 内容 | 難易度 |
|---|---|---|
| アニメーション追加 | 石が回転して裏返る演出を追加 | ★☆☆ |
| AIの強化 | 先読みの深さや評価関数を改良 | ★★☆ |
盤面アニメーションの追加
見た目の面白さを出したい場合は、
石がひっくり返るときにアニメーションをつけることもできます。
CSSだけでOKです。
|
1 2 3 4 5 6 |
.stone { transition: transform 0.3s ease; } .stone.flipping { transform: rotateY(180deg); } |
AIカスタマイズ 「評価表(重みマップ)」を変える
前の章で紹介した「WEIGHTS(重み表)」は、AIの価値観を決める重要な部分です。
この値を少し変えるだけで、AIの“打ち方”がまったく変わります。
たとえば:
| 改造例 | AIの性格 | 変更ポイント |
|---|---|---|
| 初心者用 | 角を軽視、中央を好む | 角(120)を50に下げる、中央(3)を10に上げる |
| 強化版 | 角を最優先、辺の隣を嫌う | 角を150に上げ、-40(角隣)を-60にする |
| 守り型AI | 辺を重視して角を避ける | 辺(5)を10に上げ、角(120)を80にする |
AIの強さは“正確さ”だけでなく“性格”でも感じ方が変わります。
人間のプレイスタイルのように、あなた好みに調整してみましょう。
AIカスタマイズ “合法手の少なさ”を評価に加える
強いAIは単純に“石の数”や“位置の強さ”だけでなく、
「次のターンで相手の選択肢を減らす」ことも考えます。
たとえば、AIがある場所に打ったあと、
相手が打てる場所が3つしかなくなるなら、それは“良い手”です。
逆に、相手が10手も選べるようになるなら“悪い手”です。
この考え方を可動性(mobility)と呼びます。
|
1 2 3 4 5 6 7 |
if(level==='hard'){ // 可動性:相手の合法手を減らすほど加点(ワンプレイ先読み) const tmp = cloneBoard(board); tmp[m.y][m.x] = aiColor; for(const [fy,fx] of m.flippable) tmp[fy][fx] = aiColor; const oppMob = countLegalWithBoard(tmp, humanColor); score += (8 - oppMob) * 2; // 相手の選択肢を減らす } |
score += (8 - oppMob) * 2; の部分で、評価の強さを調整できます。
8:定数。オセロの盤面サイズは 8×8 なので、これは評価の基準点として使用されています。oppMob:相手に残された合法手の数。2:評価係数(ウェイト)。この値を大きくするほど、「合法手の少なさ」(相手の可動性制限)が他の評価項目(位置の重み、フリップ枚数など)よりも優先されます。
例えば、合法手の少なさを最優先するAIにしたい場合は、この係数2を5や10といった大きな値に設定し直します。
おすすめの学習リソース|もっとスキルを伸ばしたいあなたへ
「オセロ(リバーシ)」を完成させたことで、あなたはすでに 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対戦ゲームを作ることができました。
AIが「石を裏返す」「有利な位置を選ぶ」という仕組みを理解すれば、
プログラミングの考え方も自然と身につきます。
少しずつ改良したり、難易度を変えたりすることで、
あなただけのオリジナルオセロを作ることもできます。
ゲーム開発の第一歩として、ぜひ楽しみながら学んでみてください。
関連記事
【実際に遊べる オセロ(リバーシ)AI対戦 はこちら。AIと対戦してみましょう!】▼
オセロ(リバーシ)AI対戦ゲームを作れたあなたは、
他のゲームも作れる実力があります。
次のような作品にも挑戦してみましょう。
【作り方記事】まるばつゲーム(三目並べ)の作り方|AI対戦ロジックを学ぼう!▼
【作り方記事】スライドパズル(15パズル)の作り方|配列操作とアルゴリズムを学ぼう!▼




