3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【JavaScript/HTML】4x4ミニオセロゲームを作ってみた

Last updated at Posted at 2025-05-17

こんにちは!今回は4x4のミニオセロゲームをHTML、CSS、JavaScriptのみで実装する方法をご紹介します。盤面が小さいミニオセロは通常の8x8オセロよりも戦略が単純でわかりやすく、JavaScriptの学習にもぴったりです。

この記事では以下の内容について説明します:

  • ゲームの要件と仕様
  • 設計方針(オブジェクト指向アプローチ)
  • 実装の詳細
  • ユーザーインターフェースの構築
  • AI(コンピュータ) の実装方法

完成イメージ

image.png

完成したゲームでは以下の機能が実現できます:

  • 4x4のオセロ盤面
  • 黒と白の石を交互に置ける
  • 有効な手の表示
  • 石を裏返すアニメーション
  • シンプルなコンピュータ対戦
  • ゲームログの表示
  • デバッグモード

ゲームの要件と仕様

image.png

基本要件

  1. 4x4のゲーム盤を作成
  2. プレイヤーが交互に石を置ける
  3. オセロのルールに従って石を裏返す
  4. 有効な手がない場合はパス
  5. ゲーム終了時に勝者を表示

追加機能

  1. 有効な手を視覚的に表示
  2. コンピュータプレイヤーの実装
  3. ゲームログの表示
  4. デバッグモード
  5. ゲーム状態の表示(現在のプレイヤー、石の数など)

技術仕様

  • フレームワークなしの純粋なHTML/CSS/JavaScript
  • オブジェクト指向設計(クラスベース)
  • レスポンシブデザイン
  • モダンなCSSアニメーション

設計方針

mini-othello-design.png

image.png

ゲームは主に2つのクラスで構成されています:

  1. GameState - ゲームのロジックと状態を管理
  2. GameUI - ユーザーインターフェースと表示を管理

この分離によって、ゲームのロジックとUIを独立して扱うことができ、コードの保守性が向上します。

GameStateクラス

GameStateクラスはゲームの状態を管理し、以下の責任を持ちます:

  • ゲーム盤の状態管理
  • 有効な手の計算
  • 石を置いた時の処理
  • プレイヤーの切り替え
  • ゲーム終了条件のチェック
  • コンピュータプレイヤーの戦略実装

GameUIクラス

GameUIクラスはユーザーインターフェースを管理し、以下の責任を持ちます:

  • DOM要素の取得と操作
  • ユーザー入力のハンドリング
  • 盤面の視覚的な更新
  • ゲーム情報の表示更新
  • ゲームログの管理

実装の詳細

1. ゲーム盤の表現

ゲーム盤は4x4の二次元配列で表現しています:

this.board = [
    [".", ".", ".", "."],
    [".", "W", "B", "."],
    [".", "B", "W", "."],
    [".", ".", ".", "."]
];
  • "." - 空のセル
  • "B" - 黒石
  • "W" - 白石

初期状態では、中央の4マスに黒と白の石が交互に配置されています。

2. 有効な手の計算

有効な手を計算するアルゴリズムは以下の通りです:

  1. 空のセルごとに8方向(上、下、左、右、斜め)をチェック
  2. 各方向で、相手の石が連続しているかを確認
  3. 相手の石の先に自分の石があるかを確認
  4. 条件を満たす場合、そのセルは有効な手となる
getValidMoves() {
    const validMoves = [];
    const opponent = this.currentPlayer === "B" ? "W" : "B";

    // 各方向のオフセット(左上、上、右上、左、右、左下、下、右下)
    const directions = [
        [-1, -1], [-1, 0], [-1, 1],
        [0, -1],           [0, 1],
        [1, -1],  [1, 0],  [1, 1]
    ];

    for (let row = 0; row < 4; row++) {
        for (let col = 0; col < 4; col++) {
            // 空のセルのみチェック
            if (this.board[row][col] !== ".") continue;

            let isValidMove = false;

            // 各方向をチェック
            for (const [dr, dc] of directions) {
                let r = row + dr;
                let c = col + dc;
                let hasOpponentDisk = false;

                // 相手のディスクが連続しているか確認
                while (this.isValidPosition(r, c) && this.board[r][c] === opponent) {
                    hasOpponentDisk = true;
                    r += dr;
                    c += dc;
                }

                // 自分のディスクで挟んでいるか確認
                if (hasOpponentDisk && this.isValidPosition(r, c) && this.board[r][c] === this.currentPlayer) {
                    isValidMove = true;
                    break;
                }
            }

            if (isValidMove) {
                validMoves.push([row, col]);
            }
        }
    }

    return validMoves;
}

3. 石を置いた時の処理

石を置いた時の処理は以下の手順で行います:

  1. 指定された位置に石を置く
  2. 8方向それぞれで、挟まれる相手の石を特定
  3. 挟まれた石を裏返す
  4. 裏返された石の位置を記録(アニメーション用)
applyMove(row, col) {
    if (this.board[row][col] !== ".") {
        return { valid: false, flipped: [] };
    }

    const opponent = this.currentPlayer === "B" ? "W" : "B";
    const directions = [
        [-1, -1], [-1, 0], [-1, 1],
        [0, -1],           [0, 1],
        [1, -1],  [1, 0],  [1, 1]
    ];

    let isValidMove = false;
    const flipped = [];

    // 各方向をチェック
    for (const [dr, dc] of directions) {
        const toFlip = [];
        let r = row + dr;
        let c = col + dc;

        // 相手のディスクが連続しているか確認
        while (this.isValidPosition(r, c) && this.board[r][c] === opponent) {
            toFlip.push([r, c]);
            r += dr;
            c += dc;
        }

        // 自分のディスクで挟んでいるか確認
        if (toFlip.length > 0 && this.isValidPosition(r, c) && this.board[r][c] === this.currentPlayer) {
            isValidMove = true;
            flipped.push(...toFlip);
        }
    }

    // 有効な手の場合、盤面を更新
    if (isValidMove) {
        this.board[row][col] = this.currentPlayer;

        for (const [r, c] of flipped) {
            this.board[r][c] = this.currentPlayer;
        }

        this.lastMove = [row, col];
        this.flippedDisks = flipped;
        return { valid: true, flipped };
    }

    return { valid: false, flipped: [] };
}

4. コンピュータプレイヤーの実装

mini-othello-ai-strategy-svg.png

コンピュータプレイヤーは簡単な戦略を持っています:

  1. 角の位置があれば最優先で選択(角は戦略的に有利な位置)
  2. それ以外は、最も多くの石を裏返せる手を選択
getAIMove() {
    const validMoves = this.getValidMoves();
    if (validMoves.length === 0) return null;

    // 簡易的な戦略:角を優先、次に多くの石を裏返せる手を選択
    const corners = [[0, 0], [0, 3], [3, 0], [3, 3]];

    // コーナーがあれば最優先
    for (const corner of corners) {
        for (const move of validMoves) {
            if (move[0] === corner[0] && move[1] === corner[1]) {
                return move;
            }
        }
    }

    // 最も多くの石を裏返せる手を選択
    let bestMove = validMoves[0];
    let maxFlips = 0;

    for (const [row, col] of validMoves) {
        // 一時的に手を適用して裏返る石の数を確認
        const boardCopy = this.board.map(row => [...row]);
        const { flipped } = this.applyMove(row, col);

        // 盤面を元に戻す
        this.board = boardCopy;

        if (flipped.length > maxFlips) {
            maxFlips = flipped.length;
            bestMove = [row, col];
        }
    }

    return bestMove;
}

5. UI実装

UIの実装では、以下の主要な機能を提供しています:

  1. 盤面の表示と更新
  2. 有効な手の視覚的表示
  3. ゲーム情報の更新(現在のプレイヤー、石の数など)
  4. ユーザー入力の処理
  5. AIの手の実行
  6. ゲームログの表示

例えば、盤面の更新処理は次のように実装されています:

updateBoard() {
    const cells = document.querySelectorAll('.cell');
    cells.forEach(cell => {
        // 既存のディスクをクリア
        cell.innerHTML = '';

        const row = parseInt(cell.dataset.row);
        const col = parseInt(cell.dataset.col);

        if (this.gameState.board[row][col] === "B") {
            const disk = document.createElement('div');
            disk.className = 'disk black';

            // 最後に裏返されたディスクにはハイライトを適用
            if (this.gameState.flippedDisks.some(([r, c]) => r === row && c === col)) {
                disk.classList.add('highlight');
            }

            cell.appendChild(disk);
        } else if (this.gameState.board[row][col] === "W") {
            const disk = document.createElement('div');
            disk.className = 'disk white';

            // 最後に裏返されたディスクにはハイライトを適用
            if (this.gameState.flippedDisks.some(([r, c]) => r === row && c === col)) {
                disk.classList.add('highlight');
            }

            cell.appendChild(disk);
        }
    });
}

CSSスタイリング

ゲームのスタイリングにはモダンなCSSテクニックを使用しています:

  1. Flexboxとグリッドレイアウト
  2. CSSアニメーション(石の裏返し効果)
  3. ボックスシャドウと境界線効果
  4. レスポンシブデザイン

特に石の裏返しアニメーションは、以下のCSSで実装されています:

.highlight {
    animation: highlight 1s ease-in-out;
}

@keyframes highlight {
    0% { transform: scale(1); }
    50% { transform: scale(1.2); box-shadow: 0 0 10px #ffcc00; }
    100% { transform: scale(1); }
}

実装の工夫点

パフォーマンスの最適化
処理の無駄を減らすため、不要なDOM操作を抑え、必要な箇所のみを効率的に更新しています。また、アルゴリズムも高速なものを採用し、快適な動作を実現しています。

ユーザー体験の向上
視覚的に有効な手を示したり、石が裏返るアニメーションを取り入れたりすることで、操作しやすく分かりやすいインターフェースを心がけています。ゲームの進行状況もログで確認できるようにしています。

コードの可読性と保守性
ゲームロジックとUIの責務を分け、意味のある名前やコメントを使ってコードを整理しています。コーディングスタイルも統一し、読みやすく変更しやすい構造にしています。

今後の発展の可能性
将来的にはAIの難易度設定、盤面サイズの変更、ゲーム履歴の保存やオンライン対戦など、多様な機能追加が可能です。統計情報の表示など、分析機能も検討できます。

まとめ

今回は4x4のミニオセロゲームをHTML、CSS、JavaScriptで実装する方法を紹介しました。オブジェクト指向設計を採用することで、コードの保守性と拡張性を高めることができました。

特にゲームロジックとUIを分離することで、ゲームルールの変更やUIの改善を独立して行うことができます。また、シンプルながらも戦略的なゲームプレイを実現するAIの実装方法も学びました。

ぜひこのコードを基にして、自分だけのオリジナル機能を追加してみてください。JavaScript学習の良い題材になると思います!

参考リンク

実装例


<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>ミニオセロ(4x4)</title>
    <style>
        body {
            font-family: 'Meiryo', 'Hiragino Kaku Gothic Pro', sans-serif;
            background-color: #f0f0f0;
            display: flex;
            flex-direction: column;
            align-items: center;
            padding: 20px;
        }

        .game-container {
            display: flex;
            flex-direction: column;
            align-items: center;
            background-color: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 4px 8px rgba(0,0,0,0.1);
            margin-bottom: 20px;
        }

        h1 {
            color: #333;
            text-align: center;
            margin-bottom: 20px;
        }

        .game-info {
            margin-bottom: 15px;
            font-size: 18px;
            text-align: center;
        }

        .current-player {
            font-weight: bold;
            padding: 5px 10px;
            border-radius: 5px;
            display: inline-block;
            margin: 10px 0;
        }

        .black-player {
            background-color: #333;
            color: white;
        }

        .white-player {
            background-color: #eee;
            color: black;
            border: 1px solid #ccc;
        }

        .turn-info {
            font-size: 16px;
            margin-bottom: 15px;
        }

        .board {
            display: grid;
            grid-template-columns: 30px repeat(4, 60px);
            grid-template-rows: 30px repeat(4, 60px);
            gap: 2px;
            background-color: #888;
            padding: 2px;
            border-radius: 5px;
            margin-bottom: 20px;
        }

        .header-cell {
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #666;
            color: white;
            font-weight: bold;
        }

        .cell {
            display: flex;
            justify-content: center;
            align-items: center;
            background-color: #1e8449;
            position: relative;
        }

        .disk {
            width: 45px;
            height: 45px;
            border-radius: 50%;
            transition: transform 0.3s, background-color 0.3s;
        }

        .black {
            background-color: #000;
            border: 2px solid #000;
        }

        .white {
            background-color: #fff;
            border: 2px solid #ccc;
        }

        .valid-move {
            width: 20px;
            height: 20px;
            border-radius: 50%;
            background-color: rgba(255, 255, 255, 0.3);
            position: absolute;
            cursor: pointer;
        }

        .highlight {
            animation: highlight 1s ease-in-out;
        }

        @keyframes highlight {
            0% { transform: scale(1); }
            50% { transform: scale(1.2); box-shadow: 0 0 10px #ffcc00; }
            100% { transform: scale(1); }
        }

        .controls {
            display: flex;
            flex-direction: column;
            gap: 10px;
            margin-top: 20px;
            width: 100%;
            max-width: 400px;
        }

        button {
            padding: 10px 15px;
            background-color: #2c3e50;
            color: white;
            border: none;
            border-radius: 5px;
            cursor: pointer;
            font-size: 16px;
            transition: background-color 0.3s;
        }

        button:hover {
            background-color: #34495e;
        }

        .game-log {
            width: 100%;
            max-width: 400px;
            height: 200px;
            overflow-y: auto;
            border: 1px solid #ccc;
            padding: 10px;
            margin-top: 15px;
            background-color: white;
            border-radius: 5px;
            font-family: monospace;
        }

        .score-display {
            display: flex;
            justify-content: space-around;
            width: 100%;
            max-width: 300px;
            margin: 15px 0;
        }

        .score-item {
            display: flex;
            align-items: center;
            gap: 10px;
        }

        .score-disk {
            width: 20px;
            height: 20px;
            border-radius: 50%;
        }

        .game-over-panel {
            margin-top: 20px;
            padding: 15px;
            background-color: rgba(255, 255, 255, 0.9);
            border-radius: 10px;
            text-align: center;
            box-shadow: 0 4px 8px rgba(0,0,0,0.2);
            display: none;
        }

        .winner-text {
            font-size: 24px;
            font-weight: bold;
            margin-bottom: 10px;
        }
    </style>
</head>
<body>
    <div class="game-container">
        <h1>ミニオセロ(4x4)</h1>

        <div class="game-info">
            <div class="turn-info">ターン: <span id="turn-count">1</span></div>
            <div>現在のプレイヤー: <span id="current-player" class="current-player black-player"></span></div>
        </div>

        <div class="score-display">
            <div class="score-item">
                <div class="score-disk black"></div>
                <span id="black-count">2</span>
            </div>
            <div class="score-item">
                <div class="score-disk white"></div>
                <span id="white-count">2</span>
            </div>
        </div>

        <div class="board" id="game-board">
            <!-- ヘッダー行 -->
            <div class="header-cell"></div>
            <div class="header-cell">0</div>
            <div class="header-cell">1</div>
            <div class="header-cell">2</div>
            <div class="header-cell">3</div>

            <!-- ボード行 -->
            <!-- 1行目 -->
            <div class="header-cell">0</div>
            <div class="cell" data-row="0" data-col="0"></div>
            <div class="cell" data-row="0" data-col="1"></div>
            <div class="cell" data-row="0" data-col="2"></div>
            <div class="cell" data-row="0" data-col="3"></div>

            <!-- 2行目 -->
            <div class="header-cell">1</div>
            <div class="cell" data-row="1" data-col="0"></div>
            <div class="cell" data-row="1" data-col="1"></div>
            <div class="cell" data-row="1" data-col="2"></div>
            <div class="cell" data-row="1" data-col="3"></div>

            <!-- 3行目 -->
            <div class="header-cell">2</div>
            <div class="cell" data-row="2" data-col="0"></div>
            <div class="cell" data-row="2" data-col="1"></div>
            <div class="cell" data-row="2" data-col="2"></div>
            <div class="cell" data-row="2" data-col="3"></div>

            <!-- 4行目 -->
            <div class="header-cell">3</div>
            <div class="cell" data-row="3" data-col="0"></div>
            <div class="cell" data-row="3" data-col="1"></div>
            <div class="cell" data-row="3" data-col="2"></div>
            <div class="cell" data-row="3" data-col="3"></div>
        </div>

        <div class="controls">
            <button id="btn-ai-move">AIの手を実行</button>
            <button id="btn-restart">ゲームをリスタート</button>
            <label>
                <input type="checkbox" id="debug-mode"> デバッグモードを有効にする
            </label>
        </div>

        <div class="game-log" id="game-log"></div>

        <div class="game-over-panel" id="game-over-panel">
            <div class="winner-text" id="winner-text"></div>
            <div>黒: <span id="final-black-count"></span></div>
            <div>白: <span id="final-white-count"></span></div>
            <button id="btn-new-game">新しいゲームを開始</button>
        </div>
    </div>

    <script>
        // ゲームの状態を管理するクラス
        class GameState {
            constructor() {
                this.board = [
                    [".", ".", ".", "."],
                    [".", "W", "B", "."],
                    [".", "B", "W", "."],
                    [".", ".", ".", "."]
                ];
                this.currentPlayer = "B"; // B: 黒, W: 白
                this.turn = 1;
                this.gameOver = false;
                this.passCount = 0;
                this.lastMove = null;
                this.flippedDisks = [];
                this.debugMode = false;
            }

            // 座標が盤面内にあるか確認
            isValidPosition(row, col) {
                return row >= 0 && row < 4 && col >= 0 && col < 4;
            }

            // ディスクの数を数える
            countDisks() {
                let blackCount = 0;
                let whiteCount = 0;

                for (let row = 0; row < 4; row++) {
                    for (let col = 0; col < 4; col++) {
                        if (this.board[row][col] === "B") {
                            blackCount++;
                        } else if (this.board[row][col] === "W") {
                            whiteCount++;
                        }
                    }
                }

                return { blackCount, whiteCount };
            }

            // 有効な手を取得
            getValidMoves() {
                const validMoves = [];
                const opponent = this.currentPlayer === "B" ? "W" : "B";

                // 各方向のオフセット(左上、上、右上、左、右、左下、下、右下)
                const directions = [
                    [-1, -1], [-1, 0], [-1, 1],
                    [0, -1],           [0, 1],
                    [1, -1],  [1, 0],  [1, 1]
                ];

                for (let row = 0; row < 4; row++) {
                    for (let col = 0; col < 4; col++) {
                        // 空のセルのみチェック
                        if (this.board[row][col] !== ".") continue;

                        let isValidMove = false;

                        // 各方向をチェック
                        for (const [dr, dc] of directions) {
                            let r = row + dr;
                            let c = col + dc;
                            let hasOpponentDisk = false;

                            // 相手のディスクが連続しているか確認
                            while (this.isValidPosition(r, c) && this.board[r][c] === opponent) {
                                hasOpponentDisk = true;
                                r += dr;
                                c += dc;
                            }

                            // 自分のディスクで挟んでいるか確認
                            if (hasOpponentDisk && this.isValidPosition(r, c) && this.board[r][c] === this.currentPlayer) {
                                isValidMove = true;
                                break;
                            }
                        }

                        if (isValidMove) {
                            validMoves.push([row, col]);
                        }
                    }
                }

                return validMoves;
            }

            // 手を実行し、裏返されるディスクを返す
            applyMove(row, col) {
                if (this.board[row][col] !== ".") {
                    return { valid: false, flipped: [] };
                }

                const opponent = this.currentPlayer === "B" ? "W" : "B";
                const directions = [
                    [-1, -1], [-1, 0], [-1, 1],
                    [0, -1],           [0, 1],
                    [1, -1],  [1, 0],  [1, 1]
                ];

                let isValidMove = false;
                const flipped = [];

                // 各方向をチェック
                for (const [dr, dc] of directions) {
                    const toFlip = [];
                    let r = row + dr;
                    let c = col + dc;

                    // 相手のディスクが連続しているか確認
                    while (this.isValidPosition(r, c) && this.board[r][c] === opponent) {
                        toFlip.push([r, c]);
                        r += dr;
                        c += dc;
                    }

                    // 自分のディスクで挟んでいるか確認
                    if (toFlip.length > 0 && this.isValidPosition(r, c) && this.board[r][c] === this.currentPlayer) {
                        isValidMove = true;
                        flipped.push(...toFlip);
                    }
                }

                // 有効な手の場合、盤面を更新
                if (isValidMove) {
                    this.board[row][col] = this.currentPlayer;

                    for (const [r, c] of flipped) {
                        this.board[r][c] = this.currentPlayer;
                    }

                    this.lastMove = [row, col];
                    this.flippedDisks = flipped;
                    return { valid: true, flipped };
                }

                return { valid: false, flipped: [] };
            }

            // 次のプレイヤーに交代
            switchPlayer() {
                this.currentPlayer = this.currentPlayer === "B" ? "W" : "B";
            }

            // ゲーム終了判定
            checkGameOver() {
                // 盤面が全て埋まっている場合
                let isBoardFull = true;
                for (let row = 0; row < 4; row++) {
                    for (let col = 0; col < 4; col++) {
                        if (this.board[row][col] === ".") {
                            isBoardFull = false;
                            break;
                        }
                    }
                    if (!isBoardFull) break;
                }

                // 両プレイヤーが打てる手があるか確認
                const blackMoves = this.getValidMovesForPlayer("B");
                const whiteMoves = this.getValidMovesForPlayer("W");

                if (isBoardFull || (blackMoves.length === 0 && whiteMoves.length === 0)) {
                    this.gameOver = true;
                }

                return this.gameOver;
            }

            // 特定のプレイヤーの有効な手を取得
            getValidMovesForPlayer(player) {
                const originalPlayer = this.currentPlayer;
                this.currentPlayer = player;
                const moves = this.getValidMoves();
                this.currentPlayer = originalPlayer;
                return moves;
            }

            // テスト用のAI戦略
            getAIMove() {
                const validMoves = this.getValidMoves();
                if (validMoves.length === 0) return null;

                // 簡易的な戦略:角を優先、次に多くの石を裏返せる手を選択
                const corners = [[0, 0], [0, 3], [3, 0], [3, 3]];

                // コーナーがあれば最優先
                for (const corner of corners) {
                    for (const move of validMoves) {
                        if (move[0] === corner[0] && move[1] === corner[1]) {
                            return move;
                        }
                    }
                }

                // 最も多くの石を裏返せる手を選択
                let bestMove = validMoves[0];
                let maxFlips = 0;

                for (const [row, col] of validMoves) {
                    // 一時的に手を適用して裏返る石の数を確認
                    const boardCopy = this.board.map(row => [...row]);
                    const { flipped } = this.applyMove(row, col);

                    // 盤面を元に戻す
                    this.board = boardCopy;

                    if (flipped.length > maxFlips) {
                        maxFlips = flipped.length;
                        bestMove = [row, col];
                    }
                }

                return bestMove;
            }
        }

        // UI関連の機能を管理するクラス
        class GameUI {
            constructor() {
                this.gameState = new GameState();
                this.boardElement = document.getElementById('game-board');
                this.currentPlayerElement = document.getElementById('current-player');
                this.turnCountElement = document.getElementById('turn-count');
                this.blackCountElement = document.getElementById('black-count');
                this.whiteCountElement = document.getElementById('white-count');
                this.gameLogElement = document.getElementById('game-log');
                this.gameOverPanel = document.getElementById('game-over-panel');
                this.winnerTextElement = document.getElementById('winner-text');
                this.finalBlackCountElement = document.getElementById('final-black-count');
                this.finalWhiteCountElement = document.getElementById('final-white-count');
                this.debugModeCheckbox = document.getElementById('debug-mode');

                // ボタンイベントの設定
                document.getElementById('btn-ai-move').addEventListener('click', () => this.executeAIMove());
                document.getElementById('btn-restart').addEventListener('click', () => this.restartGame());
                document.getElementById('btn-new-game').addEventListener('click', () => this.restartGame());
                this.debugModeCheckbox.addEventListener('change', () => {
                    this.gameState.debugMode = this.debugModeCheckbox.checked;
                    this.log(`デバッグモード: ${this.gameState.debugMode ? '有効' : '無効'}`);
                });

                // セルクリックイベントの設定
                this.setupCellClickEvents();

                // 初期盤面の表示
                this.updateBoard();
                this.updateGameInfo();
                this.showValidMoves();

                // ログの初期化
                this.log('ミニオセロ(4x4)ゲームを開始します...');
            }

            // セルクリックイベントを設定
            setupCellClickEvents() {
                const cells = document.querySelectorAll('.cell');
                cells.forEach(cell => {
                    cell.addEventListener('click', () => {
                        if (this.gameState.gameOver) return;

                        const row = parseInt(cell.dataset.row);
                        const col = parseInt(cell.dataset.col);

                        // 有効な手かチェック
                        const validMoves = this.gameState.getValidMoves();
                        const isValidMove = validMoves.some(move => move[0] === row && move[1] === col);

                        if (isValidMove) {
                            this.executeMove(row, col);
                        }
                    });
                });
            }

            // 盤面を更新
            updateBoard() {
                const cells = document.querySelectorAll('.cell');
                cells.forEach(cell => {
                    // 既存のディスクをクリア
                    cell.innerHTML = '';

                    const row = parseInt(cell.dataset.row);
                    const col = parseInt(cell.dataset.col);

                    if (this.gameState.board[row][col] === "B") {
                        const disk = document.createElement('div');
                        disk.className = 'disk black';

                        // 最後に裏返されたディスクにはハイライトを適用
                        if (this.gameState.flippedDisks.some(([r, c]) => r === row && c === col)) {
                            disk.classList.add('highlight');
                        }

                        cell.appendChild(disk);
                    } else if (this.gameState.board[row][col] === "W") {
                        const disk = document.createElement('div');
                        disk.className = 'disk white';

                        // 最後に裏返されたディスクにはハイライトを適用
                        if (this.gameState.flippedDisks.some(([r, c]) => r === row && c === col)) {
                            disk.classList.add('highlight');
                        }

                        cell.appendChild(disk);
                    }
                });
            }

            // 有効な手を表示
            showValidMoves() {
                // 既存の有効な手のマークをクリア
                const validMoveMarkers = document.querySelectorAll('.valid-move');
                validMoveMarkers.forEach(marker => marker.remove());

                const validMoves = this.gameState.getValidMoves();

                validMoves.forEach(([row, col]) => {
                    const cell = document.querySelector(`.cell[data-row="${row}"][data-col="${col}"]`);
                    const marker = document.createElement('div');
                    marker.className = 'valid-move';
                    cell.appendChild(marker);
                });
            }

            // ゲーム情報の更新
            updateGameInfo() {
                // ターン数
                this.turnCountElement.textContent = this.gameState.turn;

                // 現在のプレイヤー
                this.currentPlayerElement.textContent = this.gameState.currentPlayer === "B" ? "" : "";
                this.currentPlayerElement.className = `current-player ${this.gameState.currentPlayer === "B" ? "black-player" : "white-player"}`;

                // ディスク数
                const { blackCount, whiteCount } = this.gameState.countDisks();
                this.blackCountElement.textContent = blackCount;
                this.whiteCountElement.textContent = whiteCount;
            }

            // 手を実行
            executeMove(row, col) {
                const playerStr = this.gameState.currentPlayer === "B" ? "" : "";
                this.log(`${playerStr}の手: (${row}, ${col})`);

                // デバッグモードの場合、詳細を表示
                if (this.gameState.debugMode) {
                    this.log(`検証: プレイヤー ${this.gameState.currentPlayer} が (${row}, ${col}) に打つ場合`);
                }

                const { valid, flipped } = this.gameState.applyMove(row, col);

                if (valid) {
                    this.log(`有効な手です。${flipped.length}個のディスクが裏返りました。`);

                    if (this.gameState.debugMode) {
                        this.log(`裏返された石の位置: ${JSON.stringify(flipped)}`);
                    }

                    // 盤面を更新
                    this.updateBoard();

                    // プレイヤーを交代
                    this.gameState.switchPlayer();
                    this.gameState.turn++;

                    // ゲーム情報を更新
                    this.updateGameInfo();

                    // パスの確認
                    const validMoves = this.gameState.getValidMoves();
                    if (validMoves.length === 0) {
                        const currentPlayerStr = this.gameState.currentPlayer === "B" ? "" : "";
                        this.log(`${currentPlayerStr}は打てる手がないためパスします`);

                        this.gameState.passCount++;

                        // 両プレイヤーが連続してパスした場合
                        if (this.gameState.passCount >= 2) {
                            this.handleGameOver();
                        } else {
                            this.gameState.switchPlayer();
                            this.updateGameInfo();
                        }
                    } else {
                        this.gameState.passCount = 0;
                    }

                    // ゲーム終了チェック
                    if (this.gameState.checkGameOver()) {
                        this.handleGameOver();
                    } else {
                        // 有効な手を更新
                        this.showValidMoves();
                    }
                } else {
                    this.log(`無効な手です。`);
                }
            }

            // AIの手を実行
            executeAIMove() {
                if (this.gameState.gameOver) return;

                const move = this.gameState.getAIMove();
                if (move) {
                    const [row, col] = move;
                    this.executeMove(row, col);
                } else {
                    const currentPlayerStr = this.gameState.currentPlayer === "B" ? "" : "";
                    this.log(`${currentPlayerStr}は打てる手がないためパスします`);

                    this.gameState.passCount++;

                    // 両プレイヤーが連続してパスした場合
                    if (this.gameState.passCount >= 2) {
                        this.handleGameOver();
                    } else {
                        this.gameState.switchPlayer();
                        this.updateGameInfo();
                        this.showValidMoves();
                    }
                }
            }

            // ゲーム終了処理
            handleGameOver() {
                const { blackCount, whiteCount } = this.gameState.countDisks();

                this.log('\n===== ゲーム終了 =====');
                this.log(`黒: ${blackCount} 石`);
                this.log(`白: ${whiteCount} 石`);

                let winnerText = '';
                if (blackCount > whiteCount) {
                    winnerText = '黒の勝ち!';
                } else if (whiteCount > blackCount) {
                    winnerText = '白の勝ち!';
                } else {
                    winnerText = '引き分け!';
                }

                this.log(winnerText);

                // ゲーム終了パネルを表示
                this.finalBlackCountElement.textContent = blackCount;
                this.finalWhiteCountElement.textContent = whiteCount;
                this.winnerTextElement.textContent = winnerText;
                this.gameOverPanel.style.display = 'block';

                this.gameState.gameOver = true;
            }

            // ゲームをリスタート
            restartGame() {
                this.gameState = new GameState();
                this.gameState.debugMode = this.debugModeCheckbox.checked;

                // UIを更新
                this.updateBoard();
                this.updateGameInfo();
                this.showValidMoves();

                // ゲーム終了パネルを非表示
                this.gameOverPanel.style.display = 'none';

                // ログをクリア
                this.gameLogElement.innerHTML = '';
                this.log('ミニオセロ(4x4)ゲームを開始します...');
            }

            // ログにメッセージを追加
            log(message) {
                const logEntry = document.createElement('div');
                logEntry.textContent = message;
                this.gameLogElement.appendChild(logEntry);

                // 自動スクロール
                this.gameLogElement.scrollTop = this.gameLogElement.scrollHeight;
            }
        }

        // ゲーム開始
        document.addEventListener('DOMContentLoaded', () => {
            new GameUI();
        });
    </script>
</body>
</html>
3
2
1

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
3
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?