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.

0
·OpenAI·43.6s·7:16 AM·#1
<!doctype html>
<html lang="en">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width,initial-scale=1" />
  <title>Othello (Reversi) — 2 Player</title>
  <style>
    :root{
      --bg:#0f172a;
      --panel:#111827;
      --text:#e5e7eb;
      --muted:#9ca3af;
      --board:#065f46;
      --cell:#047857;
      --cellBorder: rgba(255,255,255,.15);
      --hint: rgba(255,255,255,.18);
      --black:#111827;
      --white:#f9fafb;
      --accent:#60a5fa;
      --danger:#f87171;
    }

    *{box-sizing:border-box}
    body{
      margin:0;
      font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
      background: radial-gradient(1200px 600px at 20% 0%, #1f2937, var(--bg));
      color:var(--text);
      display:flex;
      min-height:100vh;
      align-items:center;
      justify-content:center;
      padding:24px;
    }

    .app{
      width:min(980px, 100%);
      display:grid;
      grid-template-columns: 1fr 280px;
      gap:18px;
      align-items:start;
    }

    @media (max-width: 860px){
      .app{ grid-template-columns: 1fr; }
    }

    .boardWrap{
      background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
      border:1px solid rgba(255,255,255,.12);
      border-radius:14px;
      padding:14px;
      box-shadow: 0 20px 60px rgba(0,0,0,.25);
    }

    .board{
      aspect-ratio: 1 / 1;
      width:100%;
      max-width: 640px;
      margin:0 auto;
      background: var(--board);
      border-radius:12px;
      padding:10px;
      display:grid;
      grid-template-columns: repeat(8, 1fr);
      gap:6px;
    }

    .cell{
      border-radius:10px;
      background: linear-gradient(180deg, rgba(255,255,255,.08), rgba(0,0,0,.06)), var(--cell);
      border:1px solid var(--cellBorder);
      position:relative;
      cursor:pointer;
      user-select:none;
      display:flex;
      align-items:center;
      justify-content:center;
      transition: transform .06s ease, filter .12s ease;
    }
    .cell:hover{ filter: brightness(1.06); }
    .cell:active{ transform: scale(.98); }

    .disc{
      width:78%;
      height:78%;
      border-radius:50%;
      box-shadow:
        inset 0 3px 10px rgba(255,255,255,.25),
        inset 0 -10px 18px rgba(0,0,0,.35),
        0 10px 22px rgba(0,0,0,.28);
    }
    .disc.black{
      background: radial-gradient(circle at 30% 25%, #374151, var(--black) 60%);
    }
    .disc.white{
      background: radial-gradient(circle at 30% 25%, #ffffff, var(--white) 60%);
    }

    .hint::after{
      content:"";
      width:28%;
      height:28%;
      border-radius:50%;
      background: var(--hint);
      outline: 2px solid rgba(255,255,255,.18);
      outline-offset: 2px;
      box-shadow: 0 0 0 6px rgba(0,0,0,.08);
    }

    .side{
      background: linear-gradient(180deg, rgba(255,255,255,.06), rgba(255,255,255,.02));
      border:1px solid rgba(255,255,255,.12);
      border-radius:14px;
      padding:14px;
      box-shadow: 0 20px 60px rgba(0,0,0,.25);
    }

    h1{
      margin: 2px 0 10px;
      font-size: 18px;
      letter-spacing:.2px;
    }

    .row{
      display:flex;
      align-items:center;
      justify-content:space-between;
      gap:10px;
      padding:10px 10px;
      border-radius:12px;
      background: rgba(0,0,0,.18);
      border:1px solid rgba(255,255,255,.08);
      margin-bottom:10px;
    }

    .pill{
      display:inline-flex;
      align-items:center;
      gap:8px;
      padding:6px 10px;
      border-radius:999px;
      border:1px solid rgba(255,255,255,.12);
      background: rgba(255,255,255,.04);
      color:var(--text);
      font-size: 13px;
      white-space:nowrap;
    }
    .dot{
      width:12px; height:12px; border-radius:50%;
      box-shadow: inset 0 1px 2px rgba(255,255,255,.35), 0 2px 8px rgba(0,0,0,.25);
    }
    .dot.black{ background: var(--black); }
    .dot.white{ background: var(--white); border:1px solid rgba(0,0,0,.15); }

    .status{
      padding:10px;
      border-radius:12px;
      background: rgba(96,165,250,.12);
      border:1px solid rgba(96,165,250,.25);
      color: var(--text);
      font-size: 14px;
      line-height: 1.35;
      margin-bottom:10px;
    }

    .status.gameover{
      background: rgba(248,113,113,.12);
      border-color: rgba(248,113,113,.25);
    }

    .controls{
      display:flex;
      gap:10px;
      flex-wrap:wrap;
      margin-top:10px;
    }
    button{
      appearance:none;
      border:1px solid rgba(255,255,255,.14);
      background: rgba(255,255,255,.06);
      color:var(--text);
      padding:10px 12px;
      border-radius:12px;
      cursor:pointer;
      font-weight:600;
      letter-spacing:.2px;
    }
    button:hover{ filter: brightness(1.08); }
    button.primary{
      background: rgba(96,165,250,.18);
      border-color: rgba(96,165,250,.35);
    }

    .small{
      color: var(--muted);
      font-size: 12px;
      line-height: 1.35;
      margin-top:10px;
    }
    .small code{ color: #d1d5db; }
  </style>
</head>
<body>
  <div class="app">
    <div class="boardWrap">
      <div id="board" class="board" aria-label="Othello board"></div>
    </div>

    <aside class="side">
      <h1>Othello (Reversi) — 2 Players</h1>

      <div class="row">
        <span class="pill"><span class="dot black"></span> Black: <strong id="scoreBlack">2</strong></span>
        <span class="pill"><span class="dot white"></span> White: <strong id="scoreWhite">2</strong></span>
      </div>

      <div id="status" class="status"></div>

      <div class="controls">
        <button id="newGame" class="primary">New Game</button>
        <button id="toggleHints">Hide Hints</button>
      </div>

      <div class="small">
        Rules reminder: You must place a disc that <em>outflanks</em> one or more opponent discs in any of the 8 directions.
        If a player has no legal moves, they pass automatically. Game ends when neither player can move.
      </div>
    </aside>
  </div>

  <script>
    // 0 = empty, 1 = black, -1 = white
    const SIZE = 8;
    const BLACK = 1;
    const WHITE = -1;

    const DIRS = [
      [-1,-1], [-1,0], [-1,1],
      [ 0,-1],         [ 0,1],
      [ 1,-1], [ 1,0], [ 1,1],
    ];

    const boardEl = document.getElementById('board');
    const statusEl = document.getElementById('status');
    const scoreBlackEl = document.getElementById('scoreBlack');
    const scoreWhiteEl = document.getElementById('scoreWhite');
    const newGameBtn = document.getElementById('newGame');
    const toggleHintsBtn = document.getElementById('toggleHints');

    let board = [];
    let current = BLACK;
    let gameOver = false;
    let showHints = true;

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

    function initBoard(){
      board = Array.from({length: SIZE}, () => Array(SIZE).fill(0));
      // Standard start:
      // (3,3)=W (3,4)=B (4,3)=B (4,4)=W
      board[3][3] = WHITE;
      board[3][4] = BLACK;
      board[4][3] = BLACK;
      board[4][4] = WHITE;

      current = BLACK;
      gameOver = false;
      render();
    }

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

    // Returns array of positions to flip if move is made at (r,c) by player.
    // Empty array => not a legal move.
    function flipsForMove(r, c, player){
      if (!inBounds(r,c) || board[r][c] !== 0) return [];
      const opponent = -player;
      const flips = [];

      for (const [dr,dc] of DIRS){
        let rr = r + dr, cc = c + dc;
        const line = [];

        // first must be opponent
        while (inBounds(rr,cc) && board[rr][cc] === opponent){
          line.push([rr,cc]);
          rr += dr; cc += dc;
        }

        // then must end with player's disc
        if (line.length > 0 && inBounds(rr,cc) && board[rr][cc] === player){
          flips.push(...line);
        }
      }
      return flips;
    }

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

    function setStatus(text, isGameOver=false){
      statusEl.textContent = text;
      statusEl.classList.toggle('gameover', isGameOver);
    }

    function maybeAutoPassOrEnd(){
      const movesCurrent = legalMoves(current);
      if (movesCurrent.length > 0) return;

      const other = -current;
      const movesOther = legalMoves(other);

      if (movesOther.length === 0){
        gameOver = true;
        const {b,w} = countDiscs();
        let result = 'Draw.';
        if (b > w) result = 'Black wins!';
        else if (w > b) result = 'White wins!';
        setStatus(`Game over. ${result} Final score — Black: ${b}, White: ${w}`, true);
      } else {
        // pass
        current = other;
        setStatus(`${discName(-current)} has no legal moves. Turn passes to ${discName(current)}.`);
      }
    }

    function discName(player){ return player === BLACK ? 'Black' : 'White'; }

    function placeMove(r,c){
      if (gameOver) return;

      const flips = flipsForMove(r,c,current);
      if (!flips.length) return; // invalid move

      // apply
      board[r][c] = current;
      for (const [rr,cc] of flips) board[rr][cc] = current;

      // switch turn
      current = -current;

      // check pass/end
      maybeAutoPassOrEnd();

      render();
    }

    function render(){
      // update scores
      const {b,w} = countDiscs();
      scoreBlackEl.textContent = b;
      scoreWhiteEl.textContent = w;

      // status (if not game over, show turn + move availability)
      if (!gameOver){
        const moves = legalMoves(current);
        if (moves.length){
          setStatus(`${discName(current)} to move. Legal moves: ${moves.length}.`);
        } else {
          // If no moves, maybeAutoPassOrEnd will handle; but keep safe:
          setStatus(`${discName(current)} has no legal moves.`);
        }
      }

      // render board
      boardEl.innerHTML = '';

      const hints = showHints && !gameOver ? new Set(legalMoves(current).map(m => `${m.r},${m.c}`)) : new Set();

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

          const v = board[r][c];
          if (v === BLACK || v === WHITE){
            const disc = document.createElement('div');
            disc.className = `disc ${v === BLACK ? 'black' : 'white'}`;
            cell.appendChild(disc);
          } else if (hints.has(`${r},${c}`)){
            cell.classList.add('hint');
          }

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

    newGameBtn.addEventListener('click', initBoard);

    toggleHintsBtn.addEventListener('click', () => {
      showHints = !showHints;
      toggleHintsBtn.textContent = showHints ? 'Hide Hints' : 'Show Hints';
      render();
    });

    // Start
    initBoard();
  </script>
</body>
</html>

Comments