0
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?

暇なのでオセロ作ってみた。

Last updated at Posted at 2025-11-01

オセロ作ってみよう

以前にVBAでオセロしたが、JSでも作ってみようと思い早5年www 重すぎる腰を、うっ!腰痛だ。

index.htm
<!DOCTYPE html>
<html lang="ja">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>オセロ</title>
    <link rel="stylesheet" href="style.css">
</head>
<body>
    <div id="game-options">
        <label for="highlight-moves"><br>
            <input type="checkbox" id="highlight-moves" checked>
            有効な手をハイライト表示
        </label>
        <br><br>
        <label for="ai-level">AIレベル:</label>
        <select id="ai-level">
            <option value="none">none:人間と対戦</option>
            <option value="easy">easyAI:ランダムな場所に置くAI</option>
            <option value="smart">smartAI:最も多くの石をひっくり返せる場所に置くAI</option>
            <option value="hard">hardAI:強いAI</option> <!-- 強いAIの追加 -->
        </select>
    </div>
    <h1>JavaScriptオセロ</h1>
    <div id="game-info">
        <p>現在のターン: <span id="current-player"></span></p>
        <p>黒: <span id="black-score">2</span> / 白: <span id="white-score">2</span></p>
    </div>
    <div id="board"></div>
    <script src="main.js"></script>
</body>
</html>

style.css
#board {
    display: grid;
    grid-template-columns: repeat(8, 50px);
    grid-template-rows: repeat(8, 50px);
    width: 400px;
    height: 400px;
    border: 2px solid #333;
    margin: 20px auto;
}

.cell {
    width: 50px;
    height: 50px;
    background-color: green;
    border: 1px solid #333;
    display: flex;
    justify-content: center;
    align-items: center;
    cursor: pointer;
}

.cell:hover {
    background-color: #2e8b57;
}

.stone {
    width: 45px;
    height: 45px;
    border-radius: 50%;
}

.black {
    background-color: black;
}

.white {
    background-color: white;
}

.highlight {
    background-color: lightblue !important; /* !important で強制的に上書き */
}
main.js
document.addEventListener('DOMContentLoaded', () => {
    const boardElement = document.getElementById('board');
    const currentPlayerElement = document.getElementById('current-player');
    const blackScoreElement = document.getElementById('black-score');
    const whiteScoreElement = document.getElementById('white-score');
    const highlightMovesCheckbox = document.getElementById('highlight-moves');
    const aiLevelSelect = document.getElementById('ai-level');

    // 盤面の状態を管理する配列(0: 空, 1: 黒, -1: 白)
    let board = [];
    // 現在のプレイヤー(1: 黒, -1: 白)
    let currentPlayer = 1;
    // AIのレベル
    let aiLevel = 'none';
    // 有効な手をハイライト表示するか
    let highlightMoves = highlightMovesCheckbox.checked;

    // ゲームの初期化
    function initGame() {
        board = Array.from({ length: 8 }, () => Array(8).fill(0));
        board[3][3] = -1;
        board[3][4] = 1;
        board[4][3] = 1;
        board[4][4] = -1;
        currentPlayer = 1;
        renderBoard();
        updateGameInfo();
        aiTurn();
    }

    // イベントリスナーの設定
    highlightMovesCheckbox.addEventListener('change', () => {
        highlightMoves = highlightMovesCheckbox.checked;
        renderBoard();
    });

    aiLevelSelect.addEventListener('change', (e) => {
        aiLevel = e.target.value;
        initGame();
    });

    // 盤面を描画
    function renderBoard() {
        boardElement.innerHTML = '';
        const validMoves = getAllValidMoves(currentPlayer, board);

        for (let y = 0; y < 8; y++) {
            for (let x = 0; x < 8; x++) {
                const cell = document.createElement('div');
                cell.classList.add('cell');
                cell.dataset.x = x;
                cell.dataset.y = y;
                
                // 有効な手があるマスにのみイベントリスナーを追加
                if (validMoves.some(move => move.x === x && move.y === y)) {
                     cell.addEventListener('click', handleCellClick);
                }


                if (highlightMoves && validMoves.some(move => move.x === x && move.y === y)) {
                    cell.classList.add('highlight');
                }

                if (board[y][x] !== 0) {
                    const stone = document.createElement('div');
                    stone.classList.add('stone', board[y][x] === 1 ? 'black' : 'white');
                    cell.appendChild(stone);
                }
                boardElement.appendChild(cell);
            }
        }
    }

    // セルがクリックされた時の処理
    function handleCellClick(event) {
        if (aiLevel !== 'none' && currentPlayer === -1) return; // 人間vsAIの場合、AIのターンはクリック無効

        const x = parseInt(event.target.dataset.x);
        const y = parseInt(event.target.dataset.y);

        if (board[y][x] !== 0) return;

        const flippedStones = getFlippableStones(x, y, currentPlayer, board);

        if (flippedStones.length > 0) {
            placeStone(x, y, currentPlayer, board);
            flipStones(flippedStones, board);
            switchPlayer();
            renderBoard();
            updateGameInfo();
            aiTurn();
        }
    }

    // プレイヤーを交代
    function switchPlayer() {
        currentPlayer *= -1;
    }

    // AIのターン
    function aiTurn() {
        if (aiLevel !== 'none' && currentPlayer === -1) {
            setTimeout(() => {
                let move;
                switch (aiLevel) {
                    case 'easy':
                        move = getRandomMove(getAllValidMoves(currentPlayer, board));
                        break;
                    case 'smart':
                        move = getSmartMove(getAllValidMoves(currentPlayer, board));
                        break;
                    case 'hard':
                        move = getHardMove();
                        break;
                    default:
                        return;
                }

                if (move) {
                    // AIの行動を反映
                    const x = move.x;
                    const y = move.y;
                    const flippedStones = getFlippableStones(x, y, currentPlayer, board);
                    placeStone(x, y, currentPlayer, board);
                    flipStones(flippedStones, board);
                    switchPlayer();
                    renderBoard();
                    updateGameInfo();
                    aiTurn();
                } else {
                    passTurn();
                }
            }, 100);
        } else if (!canPlaceStone(currentPlayer, board)) {
            passTurn();
        }
    }

    // パス処理
    function passTurn() {
        if (canPlaceStone(-currentPlayer, board)) {
            alert(`${currentPlayer === 1 ? '' : ''}は石を置く場所がないのでパスします。`);
            switchPlayer();
            renderBoard();
            updateGameInfo();
            aiTurn();
        } else {
            endGame();
        }
    }

    // 石を置く処理
    function placeStone(x, y, player, targetBoard) {
        targetBoard[y][x] = player;
    }

    // 石をひっくり返す処理
    function flipStones(stones, targetBoard) {
        stones.forEach(({ x, y }) => {
            targetBoard[y][x] *= -1;
        });
    }

    // ひっくり返せる石を判定
    function getFlippableStones(x, y, player, targetBoard) {
        const flipped = [];
        if (targetBoard[y][x] !== 0) return [];
        const directions = [
            [-1, -1], [-1, 0], [-1, 1],
            [0, -1], [0, 1],
            [1, -1], [1, 0], [1, 1]
        ];

        directions.forEach(([dx, dy]) => {
            const currentFlippable = [];
            let [nx, ny] = [x + dx, y + dy];

            while (nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && targetBoard[ny][nx] === -player) {
                currentFlippable.push({ x: nx, y: ny });
                [nx, ny] = [nx + dx, ny + dy];
            }

            if (nx >= 0 && nx < 8 && ny >= 0 && ny < 8 && targetBoard[ny][nx] === player) {
                flipped.push(...currentFlippable);
            }
        });
        return flipped;
    }

    // 有効な手の座標をすべて取得
    function getAllValidMoves(player, targetBoard) {
        const validMoves = [];
        for (let y = 0; y < 8; y++) {
            for (let x = 0; x < 8; x++) {
                if (targetBoard[y][x] === 0 && getFlippableStones(x, y, player, targetBoard).length > 0) {
                    validMoves.push({ x, y });
                }
            }
        }
        return validMoves;
    }

    // 石を置ける場所があるか判定
    function canPlaceStone(player, targetBoard) {
        return getAllValidMoves(player, targetBoard).length > 0;
    }

    // ゲーム情報を更新
    function updateGameInfo() {
        const blackCount = board.flat().filter(s => s === 1).length;
        const whiteCount = board.flat().filter(s => s === -1).length;
        currentPlayerElement.textContent = currentPlayer === 1 ? '' : '';
        blackScoreElement.textContent = blackCount;
        whiteScoreElement.textContent = whiteCount;
    }

    // ゲーム終了処理
    function endGame() {
        const blackCount = board.flat().filter(s => s === 1).length;
        const whiteCount = board.flat().filter(s => s === -1).length;

        let message = 'ゲーム終了!';
        if (blackCount > whiteCount) {
            message += `黒の勝利です! (${blackCount} vs ${whiteCount})`;
        } else if (whiteCount > blackCount) {
            message += `白の勝利です! (${whiteCount} vs ${blackCount})`;
        } else {
            message += `引き分けです! (${blackCount} vs ${whiteCount})`;
        }
        alert(message);
    }

    // AIのランダムな有効手を取得
    function getRandomMove(validMoves) {
        if (validMoves.length === 0) return null;
        const randomIndex = Math.floor(Math.random() * validMoves.length);
        return validMoves[randomIndex];
    }

    // AIが最も多くの石をひっくり返せる手を取得
    function getSmartMove(validMoves) {
        if (validMoves.length === 0) return null;

        let bestMove = null;
        let maxFlips = -1;

        for (const move of validMoves) {
            const flips = getFlippableStones(move.x, move.y, currentPlayer, board).length;
            if (flips > maxFlips) {
                maxFlips = flips;
                bestMove = move;
            }
        }
        return bestMove;
    }

    // 最強AI(ミニマックス法 + α-β法)
    function getHardMove() {
        let bestMove = null;
        let bestScore = -Infinity;
        const validMoves = getAllValidMoves(currentPlayer, board);

        if (validMoves.length === 0) return null;

        for (const move of validMoves) {
            const tempBoard = deepCopyBoard(board);
            const flippedStones = getFlippableStones(move.x, move.y, currentPlayer, tempBoard);

            placeStone(move.x, move.y, currentPlayer, tempBoard);
            flipStones(flippedStones, tempBoard);
            //既存深度は3、あまり深くすると考察が長い
            const score = minimax(tempBoard, 5, -currentPlayer, -Infinity, Infinity);

            if (score > bestScore) {
                bestScore = score;
                bestMove = move;
            }
        }
        return bestMove;
    }

    // ミニマックス法(α-β法で枝刈り)
    function minimax(currentBoard, depth, player, alpha, beta) {
        if (depth === 0 || isGameOver(currentBoard)) {
            return evaluateBoard(currentBoard, player);
        }

        const validMoves = getAllValidMoves(player, currentBoard);

        if (validMoves.length === 0) {
            if (getAllValidMoves(-player, currentBoard).length === 0) {
                return evaluateBoard(currentBoard, player);
            }
            return -minimax(currentBoard, depth - 1, -player, -beta, -alpha);
        }

        let value = -Infinity;
        for (const move of validMoves) {
            const newBoard = deepCopyBoard(currentBoard);
            const flipped = getFlippableStones(move.x, move.y, player, newBoard);
            placeStone(move.x, move.y, player, newBoard);
            flipStones(flipped, newBoard);
            
            value = Math.max(value, -minimax(newBoard, depth - 1, -player, -beta, -alpha));
            alpha = Math.max(alpha, value);
            if (beta <= alpha) {
                break;
            }
        }
        return value;
    }

    // 盤面を評価
    function evaluateBoard(targetBoard, player) {
        const weights = [
            [100,-20, 10, 5, 5, 10, -20, 100],
            [-20,-50, -2, -2, -2, -2, -50, -20],
            [10, -2, -1, -1, -1, -1, -2, 10],
            [5, -2, -1, -1, -1, -1, -2, 5],
            [5, -2, -1, -1, -1, -1, -2, 5],
            [10, -2, -1, -1, -1,-1, -2, 10],
            [-20, -50, -2, -2, -2, -2, -50, -20],
            [100, -20, 10, 5, 5, 10, -20, 100]
        ];
        let score = 0;
        for (let y = 0; y < 8; y++) {
            for (let x = 0; x < 8; x++) {
                score += weights[y][x] * targetBoard[y][x];
            }
        }
        if (isGameOver(targetBoard)) {
            const myStones = targetBoard.flat().filter(s => s === player).length;
            const opponentStones = targetBoard.flat().filter(s => s === -player).length;
            return score + (myStones - opponentStones) * 1000;
        }
        return score * player;
    }

    // ボードのディープコピー
    function deepCopyBoard(original) {
        return original.map(arr => [...arr]);
    }

    // ゲームが終了したかを判定
    function isGameOver(targetBoard) {
        return getAllValidMoves(1, targetBoard).length === 0 && getAllValidMoves(-1, targetBoard).length === 0;
    }

    // 最初にゲームを初期化
    initGame();
});

ミニマックス法 + α-β法で最強を目指した。

法は良いとして、設定はまだ詰めが甘い。

あまり強くないんだがwww

疲れたからこのへんで!

0
0
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
0
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?