How do I build a 2-player Othello game with HTML, CSS, and JavaScript?

I want to create a browser-based Othello (Reversi) game for two human players using plain HTML, CSS, and JavaScript (no frameworks). The game should include: - An 8x8 game board with the standard starting position (2 white and 2 black pieces in the center) - Click-to-place mechanics for each player's turn - Valid move detection (pieces must outflank opponent pieces) - Automatic flipping of captured pieces - Turn switching between black and white players - Display of current player's turn - Game-over detection (when neither player can move) and score display Please provide complete, working code I can use directly in a single HTML file.

1
·Anthropic·58.6s·7:16 AM·#1

2-Player Othello Game

Here's a complete, working Othello game in a single HTML file:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Othello (Reversi)</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        body {
            display: flex;
            flex-direction: column;
            align-items: center;
            justify-content: center;
            min-height: 100vh;
            background: #1a1a2e;
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            color: #eee;
        }

        h1 {
            margin-bottom: 10px;
            font-size: 2rem;
            letter-spacing: 2px;
            text-transform: uppercase;
            background: linear-gradient(135deg, #e2e2e2, #a0d468);
            -webkit-background-clip: text;
            -webkit-text-fill-color: transparent;
        }

        #info-bar {
            display: flex;
            align-items: center;
            justify-content: center;
            gap: 30px;
            margin: 12px 0 16px;
            font-size: 1.1rem;
        }

        .score-box {
            display: flex;
            align-items: center;
            gap: 8px;
            background: #16213e;
            padding: 8px 18px;
            border-radius: 10px;
            border: 1px solid #0f3460;
        }

        .score-disc {
            width: 22px;
            height: 22px;
            border-radius: 50%;
            display: inline-block;
            border: 2px solid #555;
        }

        .score-disc.black { background: radial-gradient(circle at 35% 35%, #555, #111); }
        .score-disc.white { background: radial-gradient(circle at 35% 35%, #fff, #bbb); }

        #turn-display {
            padding: 8px 20px;
            border-radius: 10px;
            font-weight: bold;
            font-size: 1.05rem;
            transition: background 0.3s;
        }

        #turn-display.black-turn {
            background: #222;
            border: 2px solid #666;
        }

        #turn-display.white-turn {
            background: #ddd;
            color: #222;
            border: 2px solid #aaa;
        }

        #board {
            display: grid;
            grid-template-columns: repeat(8, 1fr);
            gap: 2px;
            background: #111;
            padding: 2px;
            border-radius: 8px;
            box-shadow: 0 0 30px rgba(0, 100, 0, 0.3), 0 8px 32px rgba(0,0,0,0.5);
            width: min(85vw, 520px);
            height: min(85vw, 520px);
        }

        .cell {
            background: #2d6a30;
            display: flex;
            align-items: center;
            justify-content: center;
            cursor: pointer;
            position: relative;
            transition: background 0.15s;
        }

        .cell:hover {
            background: #3a8a3e;
        }

        .cell.valid-move::after {
            content: '';
            width: 30%;
            height: 30%;
            background: rgba(255, 255, 255, 0.25);
            border-radius: 50%;
            position: absolute;
            pointer-events: none;
            animation: pulse 1.5s ease-in-out infinite;
        }

        @keyframes pulse {
            0%, 100% { transform: scale(1); opacity: 0.5; }
            50% { transform: scale(1.3); opacity: 1; }
        }

        .disc {
            width: 80%;
            height: 80%;
            border-radius: 50%;
            transition: transform 0.5s ease;
            position: absolute;
        }

        .disc.black {
            background: radial-gradient(circle at 35% 35%, #555, #111);
            box-shadow: 0 2px 6px rgba(0,0,0,0.6);
        }

        .disc.white {
            background: radial-gradient(circle at 35% 35%, #fff, #bbb);
            box-shadow: 0 2px 6px rgba(0,0,0,0.3);
        }

        .disc.flipping {
            animation: flip 0.5s ease-in-out;
        }

        @keyframes flip {
            0%   { transform: rotateY(0deg)   scaleX(1); }
            50%  { transform: rotateY(90deg)  scaleX(0.1); }
            100% { transform: rotateY(180deg) scaleX(1); }
        }

        .disc.placed {
            animation: pop-in 0.3s ease-out;
        }

        @keyframes pop-in {
            0%   { transform: scale(0); opacity: 0; }
            70%  { transform: scale(1.15); }
            100% { transform: scale(1); opacity: 1; }
        }

        #game-over-overlay {
            display: none;
            position: fixed;
            inset: 0;
            background: rgba(0,0,0,0.75);
            align-items: center;
            justify-content: center;
            z-index: 100;
        }

        #game-over-overlay.show {
            display: flex;
        }

        #game-over-box {
            background: #16213e;
            padding: 40px 50px;
            border-radius: 16px;
            text-align: center;
            box-shadow: 0 0 40px rgba(0,0,0,0.6);
            border: 1px solid #0f3460;
        }

        #game-over-box h2 {
            font-size: 1.8rem;
            margin-bottom: 12px;
        }

        #game-over-box p {
            font-size: 1.15rem;
            margin-bottom: 20px;
            color: #aaa;
        }

        #restart-btn, #restart-btn-top {
            padding: 10px 30px;
            font-size: 1rem;
            border: none;
            border-radius: 8px;
            cursor: pointer;
            background: linear-gradient(135deg, #0f3460, #533483);
            color: white;
            font-weight: bold;
            transition: transform 0.15s, box-shadow 0.15s;
        }

        #restart-btn:hover, #restart-btn-top:hover {
            transform: scale(1.05);
            box-shadow: 0 4px 15px rgba(83, 52, 131, 0.5);
        }

        #restart-btn-top {
            margin-top: 10px;
            font-size: 0.9rem;
            padding: 7px 20px;
        }

        #message {
            height: 24px;
            margin-top: 8px;
            font-size: 0.95rem;
            color: #f0c040;
            text-align: center;
        }
    </style>
</head>
<body>

    <h1>Othello</h1>

    <div id="info-bar">
        <div class="score-box">
            <span class="score-disc black"></span>
            <span>Black: <strong id="black-score">2</strong></span>
        </div>
        <div id="turn-display" class="black-turn">⬤ Black's Turn</div>
        <div class="score-box">
            <span class="score-disc white"></span>
            <span>White: <strong id="white-score">2</strong></span>
        </div>
    </div>

    <div id="board"></div>

    <div id="message"></div>

    <button id="restart-btn-top" onclick="initGame()">Restart Game</button>

    <div id="game-over-overlay">
        <div id="game-over-box">
            <h2 id="winner-text">Game Over!</h2>
            <p id="final-score"></p>
            <button id="restart-btn" onclick="initGame()">Play Again</button>
        </div>
    </div>

    <script>
        // --- Constants ---
        const SIZE = 8;
        const EMPTY = 0;
        const BLACK = 1;
        const WHITE = 2;
        const DIRECTIONS = [
            [-1, -1], [-1, 0], [-1, 1],
            [ 0, -1],          [ 0, 1],
            [ 1, -1], [ 1, 0], [ 1, 1]
        ];

        // --- State ---
        let board = [];
        let currentPlayer = BLACK;
        let gameActive = true;
        let consecutivePasses = 0;

        // --- DOM refs ---
        const boardEl = document.getElementById('board');
        const blackScoreEl = document.getElementById('black-score');
        const whiteScoreEl = document.getElementById('white-score');
        const turnDisplayEl = document.getElementById('turn-display');
        const messageEl = document.getElementById('message');
        const overlayEl = document.getElementById('game-over-overlay');
        const winnerTextEl = document.getElementById('winner-text');
        const finalScoreEl = document.getElementById('final-score');

        // --- Initialize ---
        function initGame() {
            board = [];
            for (let r = 0; r < SIZE; r++) {
                board[r] = [];
                for (let c = 0; c < SIZE; c++) {
                    board[r][c] = EMPTY;
                }
            }
            // Standard starting position
            board[3][3] = WHITE;
            board[3][4] = BLACK;
            board[4][3] = BLACK;
            board[4][4] = WHITE;

            currentPlayer = BLACK;
            gameActive = true;
            consecutivePasses = 0;
            messageEl.textContent = '';
            overlayEl.classList.remove('show');

            renderBoard();
            updateInfo();
        }

        // --- Render ---
        function renderBoard() {
            boardEl.innerHTML = '';
            const validMoves = getValidMoves(currentPlayer);

            for (let r = 0; r < SIZE; r++) {
                for (let c = 0; c < SIZE; c++) {
                    const cell = document.createElement('div');
                    cell.classList.add('cell');
                    cell.dataset.row = r;
                    cell.dataset.col = c;

                    if (board[r][c] !== EMPTY) {
                        const disc = document.createElement('div');
                        disc.classList.add('disc', board[r][c] === BLACK ? 'black' : 'white');
                        cell.appendChild(disc);
                    }

                    if (gameActive && validMoves.some(m => m[0] === r && m[1] === c)) {
                        cell.classList.add('valid-move');
                    }

                    cell.addEventListener('click', () => handleCellClick(r, c));
                    boardEl.appendChild(cell);
                }
            }
        }

        function updateInfo() {
            const scores = getScores();
            blackScoreEl.textContent = scores.black;
            whiteScoreEl.textContent = scores.white;

            if (gameActive) {
                if (currentPlayer === BLACK) {
                    turnDisplayEl.textContent = '⬤ Black\'s Turn';
                    turnDisplayEl.className = 'black-turn';
                } else {
                    turnDisplayEl.textContent = '◯ White\'s Turn';
                    turnDisplayEl.className = 'white-turn';
                }
            }
        }

        function getScores() {
            let black = 0, white = 0;
            for (let r = 0; r < SIZE; r++) {
                for (let c = 0; c < SIZE; c++) {
                    if (board[r][c] === BLACK) black++;
                    else if (board[r][c] === WHITE) white++;
                }
            }
            return { black, white };
        }

        // --- Game Logic ---
        function opponent(player) {
            return player === BLACK ? WHITE : BLACK;
        }

        function inBounds(r, c) {
            return r >= 0 && r < SIZE && c >= 0 && c < SIZE;
        }

        // Returns array of pieces that would be flipped in a given direction
        function getFlipsInDirection(r, c, dr, dc, player) {
            const opp = opponent(player);
            const flips = [];
            let cr = r + dr;
            let cc = c + dc;

            while (inBounds(cr, cc) && board[cr][cc] === opp) {
                flips.push([cr, cc]);
                cr += dr;
                cc += dc;
            }

            // Must end with the current player's piece to be valid
            if (flips.length > 0 && inBounds(cr, cc) && board[cr][cc] === player) {
                return flips;
            }
            return [];
        }

        // All flips for placing a piece at (r, c)
        function getAllFlips(r, c, player) {
            if (board[r][c] !== EMPTY) return [];
            let allFlips = [];
            for (const [dr, dc] of DIRECTIONS) {
                allFlips = allFlips.concat(getFlipsInDirection(r, c, dr, dc, player));
            }
            return allFlips;
        }

        function isValidMove(r, c, player) {
            return board[r][c] === EMPTY && getAllFlips(r, c, player).length > 0;
        }

        function getValidMoves(player) {
            const moves = [];
            for (let r = 0; r < SIZE; r++) {
                for (let c = 0; c < SIZE; c++) {
                    if (isValidMove(r, c, player)) {
                        moves.push([r, c]);
                    }
                }
            }
            return moves;
        }

        function makeMove(r, c, player) {
            const flips = getAllFlips(r, c, player);
            if (flips.length === 0) return false;

            board[r][c] = player;
            for (const [fr, fc] of flips) {
                board[fr][fc] = player;
            }
            return flips;
        }

        // --- Click Handler ---
        function handleCellClick(r, c) {
            if (!gameActive) return;
            if (!isValidMove(r, c, currentPlayer)) return;

            const flips = makeMove(r, c, currentPlayer);
            if (!flips) return;

            consecutivePasses = 0;
            messageEl.textContent = '';

            // Render and animate
            renderBoard();
            animatePlacement(r, c);
            animateFlips(flips);

            // Switch turns
            currentPlayer = opponent(currentPlayer);

            // Check if next player has valid moves
            if (getValidMoves(currentPlayer).length === 0) {
                // Pass turn
                consecutivePasses++;
                if (consecutivePasses >= 2 || isBoardFull()) {
                    endGame();
                    return;
                }
                const skippedColor = currentPlayer === BLACK ? 'Black' : 'White';
                messageEl.textContent = `${skippedColor} has no valid moves — turn skipped!`;
                currentPlayer = opponent(currentPlayer);

                // Check again — both stuck means game over
                if (getValidMoves(currentPlayer).length === 0) {
                    endGame();
                    return;
                }
            } else {
                consecutivePasses = 0;
            }

            renderBoard();
            updateInfo();
        }

        function animatePlacement(r, c) {
            const index = r * SIZE + c;
            const cell = boardEl.children[index];
            const disc = cell.querySelector('.disc');
            if (disc) disc.classList.add('placed');
        }

        function animateFlips(flips) {
            flips.forEach(([fr, fc]) => {
                const index = fr * SIZE + fc;
                const cell = boardEl.children[index];
                const disc = cell.querySelector('.disc');
                if (disc) disc.classList.add('flipping');
            });
        }

        function isBoardFull() {
            for (let r = 0; r < SIZE; r++) {
                for (let c = 0; c < SIZE; c++) {
                    if (board[r][c] === EMPTY) return false;
                }
            }
            return true;
        }

        function endGame() {
            gameActive = false;
            const scores = getScores();

            let winner;
            if (scores.black > scores.white) {
                winner = '⬤ Black Wins!';
            } else if (scores.white > scores.black) {
                winner = '◯ White Wins!';
            } else {
                winner = "It's a Tie!";
            }

            turnDisplayEl.textContent = 'Game Over';
            turnDisplayEl.className = '';

            winnerTextEl.textContent = winner;
            finalScoreEl.textContent = `Black ${scores.black}${scores.white} White`;

            // Small delay so the last move animates before overlay
            setTimeout(() => {
                overlayEl.classList.add('show');
            }, 600);
        }

        // --- Start ---
        initGame();
    </script>

</body>
</html>

How It Works

Board Representation

  • An 8×8 2D array where 0 = empty, 1 = black, 2 = white
  • Standard opening: two black and two white discs placed diagonally in the center

Valid Move Detection

For each empty cell, the algorithm checks all 8 directions (horizontal, vertical, diagonal). A move is valid if placing a disc there outflanks at least one line of opponent discs — meaning there's a contiguous line of opponent pieces bookended by your own piece.

Core Game Loop

  1. Compute valid moves for the current player and highlight them with pulsing dots
  2. On click, place the disc and flip all outflanked opponent pieces
  3. Switch turns — if the next player has no valid moves, their turn is automatically skipped with a message
  4. Game ends when neither player can move (or the board is full), and the overlay shows the winner

Key Features

FeatureImplementation
Valid move hintsPulsing translucent dots via CSS animation
Disc placementpop-in scale animation
Piece flippingrotateY CSS flip animation
Turn indicatorDynamic styling changes between black/white themes
Score trackingReal-time count of all discs on the board
Game overModal overlay with final score and replay button

Simply save the code as an .html file and open it in any modern browser to play!

Comments