初心者でも簡単！HTML・CSS・JavaScriptで作るオセロゲーム

## はじめに
この記事では、HTML、CSS、JavaScriptのみを使ったオセロゲームの作成方法を解説します。完成形のゲームは以下のデモリンクから確認できます。

操作方法
- パス: 石が置けない場合、パスします。
- リセット: ゲームをリセットします。
- 自動モード: コンピュータ同士で戦います。

[デモを見る](http://reversi.wealthy-design.com/)

プレイ画面：
![スクリーンショット 2024-12-22 20.39.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/682035/deee8bb2-1065-1ff1-b076-5a4a3a10642b.png)

---

## 作成するゲームの概要
以下の機能を持つオセロゲームを作成します：
- 8×8マスのオセロボードを実装。
- 現在のプレイヤー（黒・白）を表示。
- 現在のスコアを表示。
- 「パス」「リセット」「自動モード」のボタンを搭載。
- 自動モードをオンにすると、プレイヤーが黒の場合はコンピュータが自動で手を打ちます。

---

## 使用する技術
このプロジェクトでは以下の技術を使用します：
- **HTML5**: ゲームボードやコントロールの構造を記述。
- **CSS3**: デザインやレスポンシブ対応のスタイリング。
- **JavaScript（ES6）**: ゲームロジックの実装。

---

## コード全体

こちらがオセロゲームを実装するHTML、CSS、JavaScriptのコードです。

<details>
<summary>完全なHTMLコードを見る</summary>

```html
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>オセロゲーム</title>
    <style>
        /* 基本スタイル */
        body {
            font-family: 'Arial', sans-serif;
            text-align: center;
            margin: 20px;
            background-color: #f4f4f9;
        }

        /* タイトルのスタイル */
        h1 {
            color: #333;
            margin-bottom: 20px;
        }

        /* ゲームボード全体のスタイル */
        #game-board {
            display: grid;
            grid-template-columns: repeat(8, 1fr); /* セルの幅を均等にする */
            gap: 5px;
            width: 100%;
            max-width: 480px; /* 最大幅を480pxに制限 */
            background-color: #2c7a7b;
            padding: 10px;
            border-radius: 10px;
            box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
            justify-content: center; /* フレックスボックスで水平中央揃え */
            align-items: center; /* 垂直方向の中央揃え */
        }

        /* ボードの外側を中央揃えにするための親要素（スマホ対応用） */
        #game-container {
            display: flex; /* フレックスボックスを使用 */
            justify-content: center; /* 水平方向に中央揃え */
            align-items: center; /* 垂直方向に中央揃え */
            flex-direction: column; /* 縦方向に要素を並べる */
            min-height: 100vh; /* 画面全体をカバー */
            width: 100%; /* 親要素の幅を100%に */
            box-sizing: border-box; /* パディングを含めた幅計算 */
        }

        /* 各セルのスタイル */
        .cell {
            aspect-ratio: 1 / 1; /* セルを正方形にする */
            background-color: #3aafa9;
            display: flex;
            justify-content: center;
            align-items: center;
            cursor: pointer;
            position: relative;
            border-radius: 5px;
        }

        /* セルのホバー時の効果 */
        .cell:hover {
            background-color: #76e1e3;
        }

        /* 駒のスタイル */
        .piece {
            width: 80%; /* 駒のサイズをセルに対して相対的に */
            height: 80%; /* 駒のサイズをセルに対して相対的に */
            border-radius: 50%;
            position: absolute;
        }

        /* 黒駒のスタイル */
        .black {
            background-color: #2f2f2f;
        }

        /* 白駒のスタイル */
        .white {
            background-color: #ffffff;
        }

        /* コントロールボタンのスタイル */
        .controls {
            margin: 20px;
        }

        .button {
            font-size: 1rem; /* レスポンシブなフォントサイズ */
            padding: 10px 20px;
            margin: 5px;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            transition: all 0.3s;
        }

        /* ボタンのホバー時のエフェクト */
        .button:hover {
            transform: scale(1.05);
            opacity: 0.9;
        }

        /* 無効化されたボタンのスタイル */
        .button:disabled {
            background-color: #ccc;
            color: #666;
            cursor: not-allowed;
            opacity: 0.6;
        }

        /* 各ボタンの固有スタイル */
        .pass-button {
            background-color: #ffcc00;
            color: #333;
        }

        .reset-button {
            background-color: #e74c3c;
            color: #fff;
        }

        .auto-button {
            background-color: #3498db;
            color: #fff;
        }

        /* パスメッセージのスタイル */
        #pass-message {
            font-size: 1.2rem; /* フォントサイズをレスポンシブに */
            font-weight: bold;
            background: linear-gradient(90deg, #ff7e5f, #feb47b);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
            text-shadow: 1px 1px 2px rgba(0, 0, 0, 0.3);
            padding: 5px 10px;
            display: inline-block;
        }

        /* 自動モードの状態表示 */
        #auto-mode-status {
            font-size: 1rem;
            font-weight: bold;
            margin: 10px 0;
            color: #3498db;
        }

        /* メディアクエリで小さい画面用に調整 */
        @media (max-width: 480px) {
            h1 {
                font-size: 1.5rem;
            }

            #game-board {
                gap: 3px;
            }

            .cell {
                border-radius: 3px;
            }

            .piece {
                width: 70%;
                height: 70%;
            }

            .button {
                font-size: 0.9rem;
                padding: 8px 16px;
            }

            #pass-message {
                font-size: 1rem;
            }
        }
    </style>
</head>
<body>
    <div id="game-container">
        <h1>オセロゲーム</h1>
        <div id="game-board"></div>
        <div id="turn-indicator">現在のターン: 黒</div>
        <div class="controls">
            <!-- 各操作ボタン -->
            <button class="button pass-button" id="pass-button">パス</button>
            <button class="button reset-button" id="reset-button">リセット</button>
            <button class="button auto-button" id="auto-button">自動モード</button>
        </div>
        <div>
            <p>スコア - 黒: <span id="black-score">0</span> 白: <span id="white-score">0</span></p>
        </div>
        <h2 id="result"></h2>
        <p id="pass-message"></p>
        <p id="auto-mode-status">自動モード: オフ</p>
    </div>
    <script>
        document.addEventListener('DOMContentLoaded', () => {
            // 変数の初期化
            const boardSize = 8;
            let board = Array(boardSize).fill(null).map(() => Array(boardSize).fill(null));
            let currentPlayer = 'black';
            let autoMode = false;

            // ボードの初期化
            function initializeBoard() {
                board = Array(boardSize).fill(null).map(() => Array(boardSize).fill(null));
                board[3][3] = 'white';
                board[3][4] = 'black';
                board[4][3] = 'black';
                board[4][4] = 'white';
                currentPlayer = 'black';
                autoMode = false;
                updateAutoModeStatus();
                updateScores();
                updateTurnIndicator();
                togglePassButton();
                document.getElementById('pass-message').textContent = '';
                document.getElementById('result').textContent = '';
                drawBoard();
            }

            const gameBoard = document.getElementById('game-board');

            // ボードを描画する関数
            function drawBoard() {
                gameBoard.innerHTML = ''; // ボードを一旦クリア
                for (let row = 0; row < boardSize; row++) { // 各行をループ
                    for (let col = 0; col < boardSize; col++) { // 各列をループ
                        const cell = document.createElement('div'); // 新しいセル要素を作成
                        cell.className = 'cell'; // セルにクラスを設定
                        cell.dataset.row = row; // セルに行番号を設定
                        cell.dataset.col = col; // セルに列番号を設定

                        if (board[row][col]) { // セルに駒が置かれている場合
                            const piece = document.createElement('div'); // 駒を表す要素を作成
                            piece.className = `piece ${board[row][col]}`; // 駒のクラスを設定（黒または白）
                            cell.appendChild(piece); // セルに駒を追加
                        }

                        // セルにクリックイベントを追加（駒を置く処理）
                        cell.addEventListener('click', () => handleMove(row, col));

                        gameBoard.appendChild(cell); // セルをボードに追加
                    }
                }
                checkGameOver(); // ゲーム終了判定を実行
            }

            // 駒を置いた際の処理を行う関数
            function handleMove(row, col) {
                if (board[row][col] !== null) return; // 既に駒が置かれている場合は処理を終了

                const flips = getFlips(row, col, currentPlayer); // 指定した位置でひっくり返せる駒を取得
                if (flips.length > 0) { // ひっくり返せる駒がある場合
                    board[row][col] = currentPlayer; // 現在のプレイヤーの駒を指定した位置に置く
                    flips.forEach(([r, c]) => (board[r][c] = currentPlayer)); // 取得した駒を全てひっくり返す
                    passCount = 0; // パスカウントをリセット
                    currentPlayer = currentPlayer === 'black' ? 'white' : 'black'; // プレイヤーを交代
                    document.getElementById('pass-message').textContent = ''; // パスメッセージをクリア
                    updateScores();
                    updateTurnIndicator();
                    drawBoard(); // ボードを再描画

                    if (currentPlayer === 'white' && !autoMode) { // 白プレイヤーかつ自動モードがオフの場合
                        setTimeout(computerMove, 500); // 500ミリ秒後にコンピューターの動きを実行
                    }
                }
            }

            // 指定した位置に駒を置いたときにひっくり返す駒を取得する関数
            function getFlips(row, col, player) {
                // 8方向の移動を表す配列
                const directions = [
                    [-1, 0], [1, 0], [0, -1], [0, 1], // 縦と横の方向
                    [-1, -1], [-1, 1], [1, -1], [1, 1] // 斜めの方向
                ];
                const flips = []; // ひっくり返す駒の位置を格納する配列

                // 各方向に対して駒を調べる
                directions.forEach(([dr, dc]) => {
                    const line = []; // 一方向に並ぶ相手の駒を一時的に保存する配列
                    let r = row + dr; // 次に調べる行
                    let c = col + dc; // 次に調べる列

                    // ボードの範囲内で駒をチェック
                    while (r >= 0 && r < boardSize && c >= 0 && c < boardSize) {
                        if (board[r][c] === null) break; // 空白のマスなら終了
                        if (board[r][c] === player) {
                            // 自分の駒に到達した場合、これまでの駒をひっくり返すリストに追加
                            flips.push(...line);
                            break;
                        }
                        // 相手の駒を一時保存
                        line.push([r, c]);
                        r += dr; // 次の行に進む
                        c += dc; // 次の列に進む
                    }
                });

                return flips; // ひっくり返す駒のリストを返す
            }

            // コンピューターのターンを実行する関数
            function computerMove() {
                let moveMade = false; // 駒を置けたかどうかのフラグ

                // 全てのマスをチェック
                for (let row = 0; row < boardSize; row++) {
                    for (let col = 0; col < boardSize; col++) {
                        if (board[row][col] === null) { // 空白のマスが対象
                            const flips = getFlips(row, col, currentPlayer); // ひっくり返せる駒を取得
                            if (flips.length > 0) { // ひっくり返せる場合
                                board[row][col] = currentPlayer; // 現在の駒を配置
                                flips.forEach(([r, c]) => (board[r][c] = currentPlayer)); // 駒をひっくり返す
                                currentPlayer = currentPlayer === 'black' ? 'white' : 'black'; // プレイヤーを交代
                                moveMade = true; // 駒を置けたフラグを更新
                                document.getElementById('pass-message').textContent = ''; // パスメッセージをクリア
                                updateScores();
                                updateTurnIndicator();
                                drawBoard(); // ボードを再描画

                                if (autoMode) {
                                    setTimeout(computerMove, 500); // 自動モードの場合、再帰的に次のターンを実行
                                }
                                return; // 処理を終了
                            }
                        }
                    }
                }

                // 駒を置けなかった場合（パス）
                if (!moveMade) {
                    const playerName = currentPlayer === 'black' ? '黒' : '白'; // 現在のプレイヤー名を取得
                    document.getElementById('pass-message').textContent = `${playerName}がパスしました`; // パスメッセージを表示
                    currentPlayer = currentPlayer === 'black' ? 'white' : 'black'; // プレイヤーを交代

                    if (autoMode) {
                        setTimeout(computerMove, 500); // 自動モードの場合、再帰的に次のターンを実行
                    }
                }
            }

            let passCount = 0; // 連続パスのカウント

            // パスの処理を行う関数
            function handlePass() {
                const playerName = currentPlayer === 'black' ? '黒' : '白';
                document.getElementById('pass-message').textContent = `${playerName}がパスしました`;
                passCount++; // パスカウントを増加

                if (passCount >= 2) { // 連続で両者がパスした場合
                    endGame(); // ゲームを終了
                    return;
                }

                currentPlayer = currentPlayer === 'black' ? 'white' : 'black'; // プレイヤーを交代
                if (currentPlayer === 'white' && autoMode) { // 自動モードで白プレイヤーの場合
                    setTimeout(computerMove, 500);
                }
            }

            // ゲームが終了したかどうかを判定する関数
            function checkGameOver() {
                const flatBoard = board.flat(); // ボードを1次元の配列に変換
                if (!flatBoard.includes(null)) { // 空白のマスがない場合、ゲーム終了
                    autoMode = false; // 自動モードをオフにする
                    updateAutoModeStatus(); // 自動モードのステータスを更新
                    document.getElementById('pass-button').disabled = true; // パスボタンを無効化する

                    // 黒と白の駒の数をカウント
                    const blackCount = flatBoard.filter(cell => cell === 'black').length;
                    const whiteCount = flatBoard.filter(cell => cell === 'white').length;

                    // 結果を判定してテキストを作成
                    const resultText = blackCount > whiteCount
                        ? `黒の勝ち！ ${blackCount} 対 ${whiteCount}` // 黒の勝利
                        : whiteCount > blackCount
                        ? `白の勝ち！ ${whiteCount} 対 ${blackCount}` // 白の勝利
                        : `引き分け！ ${blackCount} 対 ${whiteCount}`; // 引き分け

                    // 結果を画面に表示
                    document.getElementById('result').textContent = resultText;
                }
            }

            // パスボタンのクリックイベントを設定
            document.getElementById('pass-button').addEventListener('click', () => {
                handlePass();
                const playerName = currentPlayer === 'black' ? '黒' : '白'; // 現在のプレイヤー名を取得
                document.getElementById('pass-message').textContent = `${playerName}がパスしました`; // パスメッセージを表示
                currentPlayer = currentPlayer === 'black' ? 'white' : 'black'; // プレイヤーを交代
                if (currentPlayer === 'white') { // 白プレイヤーの場合
                    setTimeout(computerMove, 500); // 500ミリ秒後にコンピューターの動きを実行
                }
            });

            // パスボタンの有効/無効を切り替える関数
            function togglePassButton() {
                document.getElementById('pass-button').disabled = autoMode; // 自動モードがオンの場合はボタンを無効化
            }

            // スコアを更新する関数
            function updateScores() {
                // ボードを1次元配列に変換
                const flatBoard = board.flat();
                // 黒の駒の数を数え、対応する要素に表示
                document.getElementById('black-score').textContent = flatBoard.filter(cell => cell === 'black').length;
                // 白の駒の数を数え、対応する要素に表示
                document.getElementById('white-score').textContent = flatBoard.filter(cell => cell === 'white').length;
            }

            // 現在のターンを画面に表示する関数
            function updateTurnIndicator() {
                // 現在のプレイヤーに応じて「黒」または「白」を取得
                const playerName = currentPlayer === 'black' ? '黒' : '白';
                // 現在のターンを表示エリアに更新
                document.getElementById('turn-indicator').textContent = `現在のターン: ${playerName}`;
            }

            // リセットボタンのクリックイベントを設定
            document.getElementById('reset-button').addEventListener('click', () => {
                initializeBoard(); // ボードを初期化
                document.getElementById('pass-button').disabled = false; // パスボタンを有効化
            });

            // 自動モードボタンのクリックイベントを設定
            document.getElementById('auto-button').addEventListener('click', () => {
                autoMode = !autoMode; // 自動モードのオン/オフを切り替え
                updateAutoModeStatus(); // 自動モードのステータスを更新
                togglePassButton(); // パスボタンの状態を更新
                if (autoMode) { // 自動モードがオンの場合
                    computerMove(); // コンピューターの動きを開始
                }
            });

            // 自動モードのステータスを更新する関数
            function updateAutoModeStatus() {
                const status = autoMode ? 'オン' : 'オフ'; // 自動モードの状態をテキストで取得
                document.getElementById('auto-mode-status').textContent = `自動モード: ${status}`; // 画面に状態を表示
            }

            initializeBoard();
        });
    </script>
</body>
</html>
```
</details> 

## 改善案
- **AIの強化**  
  現在の自動モードは簡易的なロジックです。ミニマックス法を導入すると、より強力なAIが作れます。
---

## まとめ
この記事では、HTML、CSS、JavaScriptを使用してオセロゲームを作成する方法を解説しました。このプロジェクトを通じて、基本的なDOM操作やグリッドレイアウトの知識が深まります。ぜひ、次のステップとしてAIの強化やUIの改善に挑戦してください！