2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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

Last updated at Posted at 2024-12-22

はじめに

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

操作方法

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

デモを見る

プレイ画面:
スクリーンショット 2024-12-22 20.39.39.png


作成するゲームの概要

以下の機能を持つオセロゲームを作成します:

  • 8×8マスのオセロボードを実装。
  • 現在のプレイヤー(黒・白)を表示。
  • 現在のスコアを表示。
  • 「パス」「リセット」「自動モード」のボタンを搭載。
  • 自動モードをオンにすると、プレイヤーが黒の場合はコンピュータが自動で手を打ちます。

使用する技術

このプロジェクトでは以下の技術を使用します:

  • HTML5: ゲームボードやコントロールの構造を記述。
  • CSS3: デザインやレスポンシブ対応のスタイリング。
  • JavaScript(ES6): ゲームロジックの実装。

コード全体

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

完全な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>

改善案

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

まとめ

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

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?