← Chess results

Chess

Qwen 3.6 27b MTP · composer

Initial view of Chess

Reviews & generation details

Reviews

No human review yet. Unreviewed does not mean unsuccessful.

Generation context

Imported historical artifact. Its exact original instructions and lineage are not known.

Generation prompt · exact text unavailable

See the current prompt for reference. It may differ from the instructions used to generate this artifact.

Inspect original source 60,327 bytes · SHA-256 96c981d5a237
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — Play Against AI</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body {
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  background: #2c2c34;
  color: #e8e6e3;
}
#app { width: 100%; height: 100%; }

#game-container {
  display: flex;
  justify-content: center;
  align-items: stretch;
  gap: 24px;
  padding: 16px;
  height: 100vh;
  max-width: 1200px;
  margin: 0 auto;
}

#board-area {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
}

#side-panel {
  width: 260px;
  min-width: 220px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  padding: 8px;
  overflow-y: auto;
}

#status-bar {
  background: #3a3a45;
  border-radius: 8px;
  padding: 10px 14px;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 14px;
}

#turn-indicator {
  font-weight: 600;
  color: #f0e6d3;
}

#game-status {
  color: #ff9800;
  font-weight: 500;
}

#controls {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

#controls button, #btn-new-game-overlay {
  background: #4a4a58;
  color: #e8e6e3;
  border: 1px solid #5a5a68;
  border-radius: 6px;
  padding: 8px 12px;
  cursor: pointer;
  font-size: 13px;
  transition: background 0.15s;
}

#controls button:hover, #btn-new-game-overlay:hover {
  background: #5a5a68;
}

#difficulty, #side-choice {
  display: flex;
  align-items: center;
  gap: 8px;
  font-size: 13px;
}

#difficulty select, #side-choice select {
  background: #4a4a58;
  color: #e8e6e3;
  border: 1px solid #5a5a68;
  border-radius: 4px;
  padding: 4px 8px;
  font-size: 12px;
}

#move-list-container {
  flex: 1;
  background: #3a3a45;
  border-radius: 8px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
  min-height: 0;
}

#move-list-container h3 {
  padding: 8px 12px;
  font-size: 13px;
  color: #b0a89a;
  border-bottom: 1px solid #4a4a58;
}

#move-list {
  flex: 1;
  overflow-y: auto;
  padding: 6px 10px;
  font-family: 'Courier New', monospace;
  font-size: 13px;
  line-height: 1.7;
}

.move-row {
  display: flex;
  gap: 4px;
}

.move-num {
  color: #808090;
  min-width: 28px;
  text-align: right;
}

.move-white, .move-black {
  min-width: 56px;
  cursor: default;
}

.move-white:hover, .move-black:hover {
  background: #4a4a58;
  border-radius: 2px;
}

.hidden { display: none !important; }
/* ── css: board ── */
#board {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(8, 1fr);
  width: min(calc((100vh - 60px) * 0.9), calc((1200px - 300px) * 0.55));
  height: min(calc((100vh - 60px) * 0.9), calc((1200px - 300px) * 0.55));
  max-width: 560px;
  max-height: 560px;
  border: 3px solid #5a4a3a;
  border-radius: 4px;
  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  position: relative;
}

.square {
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: min(calc((100vh - 60px) * 0.9 / 8 * 0.7), calc((1200px - 300px) * 0.55 / 8 * 0.7));
  cursor: pointer;
  user-select: none;
  position: relative;
  transition: background-color 0.1s;
}

.square.light { background: #f0d9b5; }
.square.dark { background: #b58863; }

.square.selected { background: #7fc97f !important; }
.square.last-move { background: rgba(255, 255, 100, 0.4) !important; }
.square.check-glow { 
  background: radial-gradient(circle, #ff0000 0%, #cc0000 30%, transparent 70%) !important;
}

.piece {
  line-height: 1;
  pointer-events: none;
  filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.3));
}

.piece.white-piece { color: #fff; text-shadow: 0 0 2px rgba(0,0,0,0.8); }
.piece.black-piece { color: #222; text-shadow: 0 0 1px rgba(255,255,255,0.3); }

/* Move indicators */
.move-dot::after {
  content: '';
  position: absolute;
  width: 28%;
  height: 28%;
  border-radius: 50%;
  background: rgba(0, 0, 0, 0.2);
  pointer-events: none;
}

.capture-ring::after {
  content: '';
  position: absolute;
  width: 90%;
  height: 90%;
  border-radius: 50%;
  border: 4px solid rgba(0, 0, 0, 0.2);
  pointer-events: none;
}

/* Coordinate labels */
.coord-label {
  position: absolute;
  font-size: 10px;
  font-weight: 700;
  pointer-events: none;
  opacity: 0.6;
}
.file-label { bottom: 1px; right: 3px; }
.rank-label { top: 1px; left: 3px; }
.square.light .coord-label { color: #b58863; }
.square.dark .coord-label { color: #f0d9b5; }

/* Captured pieces */
#captured-top, #captured-bottom {
  min-height: 28px;
  display: flex;
  align-items: center;
  font-size: 18px;
  gap: 1px;
  padding: 0 4px;
  width: 100%;
}

/* Promotion overlay */
#promotion-overlay {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 100;
}

#promotion-picker {
  background: #3a3a45;
  border-radius: 12px;
  padding: 16px;
  display: flex;
  gap: 8px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}

.promo-piece {
  width: 64px;
  height: 64px;
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 42px;
  background: #f0d9b5;
  border-radius: 8px;
  cursor: pointer;
  transition: transform 0.1s, box-shadow 0.1s;
}

.promo-piece:hover {
  transform: scale(1.1);
  box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}

/* Game over banner */
#game-over-banner {
  position: fixed;
  top: 0; left: 0; right: 0; bottom: 0;
  background: rgba(0,0,0,0.7);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 200;
}

#game-over-content {
  background: #3a3a45;
  border-radius: 16px;
  padding: 32px 48px;
  text-align: center;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}

#game-over-title { font-size: 28px; margin-bottom: 8px; }
#game-over-detail { color: #b0a89a; margin-bottom: 20px; }

/* Dragging */
.square.drag-over { background: rgba(100, 200, 100, 0.5) !important; }
</style>
</head>
<body>
<div id="game-container">
  <div id="board-area">
    <div id="captured-top"></div>
    <div id="board"></div>
    <div id="captured-bottom"></div>
  </div>
  <div id="side-panel">
    <div id="status-bar"><span id="turn-indicator"></span><span id="game-status"></span></div>
    <div id="controls">
      <button id="btn-new-game" title="New Game">⟳ New</button>
      <button id="btn-undo" title="Undo (both moves)">↩ Undo</button>
      <button id="btn-flip" title="Flip Board">⇅ Flip</button>
      <button id="btn-mute" title="Toggle Sound">🔊</button>
    </div>
    <div id="difficulty">
      <label for="depth-select">AI Depth:</label>
      <select id="depth-select">
        <option value="1">1 — Easy</option>
        <option value="2" selected="">2 — Medium</option>
        <option value="3">3 — Hard</option>
      </select>
    </div>
    <div id="side-choice">
      <label>Play as:</label>
      <select id="color-select">
        <option value="white" selected="">White</option>
        <option value="black">Black</option>
        <option value="both">Both (Analysis)</option>
      </select>
    </div>
    <div id="move-list-container">
      <h3>Moves</h3>
      <div id="move-list"></div>
    </div>
  </div>
</div>
<div id="promotion-overlay" class="hidden">
  <div id="promotion-picker"></div>
</div>
<div id="game-over-banner" class="hidden">
  <div id="game-over-content">
    <h2 id="game-over-title"></h2>
    <p id="game-over-detail"></p>
    <button id="btn-new-game-overlay">New Game</button>
  </div>
</div>
<script>
'use strict';

// ── const: CFG ──
const CFG = {
  lightSquare: '#f0d9b5',
  darkSquare: '#b58863',
  selectedColor: '#7fc97f',
  lastMoveColor: 'rgba(255,255,100,0.4)',
  aiDepths: { 1: 1, 2: 2, 3: 3 },
  maxSearchTime: 800,
  soundVolume: 0.3,
};

// ── const: EMPTY ──
const EMPTY = 0;

// ── const: WP ──
const WP = 1;

// ── const: WN ──
const WN = 2;

// ── const: WB ──
const WB = 3;

// ── const: WR ──
const WR = 4;

// ── const: WQ ──
const WQ = 5;

// ── const: WK ──
const WK = 6;

// ── const: BP ──
const BP = -1;

// ── const: BN ──
const BN = -2;

// ── const: BB ──
const BB = -3;

// ── const: BR ──
const BR = -4;

// ── const: BQ ──
const BQ = -5;

// ── const: BK ──
const BK = -6;

// ── const: PAWN ──
const PAWN = 1;

// ── const: KNIGHT ──
const KNIGHT = 2;

// ── const: BISHOP ──
const BISHOP = 3;

// ── const: ROOK ──
const ROOK = 4;

// ── const: QUEEN ──
const QUEEN = 5;

// ── const: KING ──
const KING = 6;

// ── const: PIECE_UNICODE ──
const PIECE_UNICODE = {
  [WK]: '♔', [WN]: '♘', [WB]: '♗', [WR]: '♖', [WQ]: '♕', [WP]: '♙',
  [BK]: '♚', [BN]: '♞', [BB]: '♝', [BR]: '♜', [BQ]: '♛', [BP]: '♟',
};

// ── const: PIECE_LETTER ──
const PIECE_LETTER = {
  [KNIGHT]: 'N', [BISHOP]: 'B', [ROOK]: 'R', [QUEEN]: 'Q', [KING]: 'K',
};

// ── const: PIECE_VALUE ──
const PIECE_VALUE = {
  [PAWN]: 100, [KNIGHT]: 320, [BISHOP]: 330, [ROOK]: 500, [QUEEN]: 900, [KING]: 20000,
};

// ── const: FILES ──
const FILES = 'abcdefgh';

// ── const: RANKS ──
const RANKS = '12345678';

// ── const: PST ──
const PST = {
  [PAWN]: [0,0,0,0,0,0,0,0,50,50,50,50,50,50,50,50,10,10,20,30,30,20,10,10,5,5,10,25,25,10,5,5,0,0,0,20,20,0,0,0,5,-5,-10,0,0,-10,-5,5,5,10,10,-20,-20,10,10,5,0,0,0,0,0,0,0,0],
  [KNIGHT]: [-50,-40,-30,-30,-30,-30,-40,-50,-40,-20,0,0,0,0,-20,-40,-30,0,10,15,15,10,0,-30,-30,5,15,20,20,15,5,-30,-30,0,15,20,20,15,0,-30,-30,5,10,15,15,10,5,-30,-40,-20,0,5,5,0,-20,-40,-50,-40,-30,-30,-30,-30,-40,-50],
  [BISHOP]: [-20,-10,-10,-10,-10,-10,-10,-20,-10,0,0,0,0,0,0,-10,-10,0,10,10,10,10,0,-10,-10,5,5,10,10,5,5,-10,-10,0,10,10,10,10,0,-10,-10,10,10,10,10,10,10,-10,-10,5,0,0,0,0,5,-10,-20,-10,-10,-10,-10,-10,-10,-20],
  [ROOK]: [0,0,0,0,0,0,0,0,5,10,10,10,10,10,10,5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,0,0,0,5,5,0,0,0],
  [QUEEN]: [-20,-10,-10,-5,-5,-10,-10,-20,-10,0,0,0,0,0,0,-10,-10,0,5,5,5,5,0,-10,-5,0,5,5,5,5,0,-5,0,0,5,5,5,5,0,-5,-10,5,5,5,5,5,0,-10,-10,0,5,0,0,0,0,-10,-20,-10,-10,-5,-5,-10,-10,-20],
  [KING]: [-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-30,-40,-40,-50,-50,-40,-40,-30,-20,-30,-30,-40,-40,-30,-30,-20,-10,-20,-20,-20,-20,-20,-20,-10,20,20,0,0,0,0,20,20,20,30,10,0,0,10,30,20],
};

// ── const: INITIAL_CASTLING ──
const INITIAL_CASTLING = { K: true, Q: true, k: true, q: true };

// ── function: PIECE_LETTERInverse ──
function PIECE_LETTERInverse(letter) {
  const map = { 'Q': QUEEN, 'R': ROOK, 'B': BISHOP, 'N': KNIGHT };
  return map[letter] || QUEEN;
}

// ── class: ChessAI ──
// ============================================================
// AI ENGINE — Minimax with Alpha-Beta Pruning
// ============================================================
class ChessAI {
  constructor(game) {
    this.game = game;
    this.nodesSearched = 0;
    this.startTime = 0;
    this.maxTime = CFG.maxSearchTime;
  }

  // Evaluate the current position from White's perspective
  evaluate() {
    let score = 0;
    const board = this.game.board;

    for (let i = 0; i < 64; i++) {
      const p = board[i];
      if (!p) continue;
      const t = Math.abs(p);
      const isWhite = p > 0;
      const [r, c] = this.game.rc(i);

      // Material
      score += PIECE_VALUE[t];

      // Piece-square tables (mirrored for black)
      let pstIdx;
      if (isWhite) {
        pstIdx = r * 8 + c;
      } else {
        pstIdx = (7 - r) * 8 + c;
      }
      score += PST[t][pstIdx];
    }

    // Mobility bonus (simplified)
    const wMoves = this.game.generatePseudoMoves('w').length;
    const bMoves = this.game.generatePseudoMoves('b').length;
    score += (wMoves - bMoves) * 3;

    return score;
  }

  // Order moves for better alpha-beta pruning
  orderMoves(moves) {
    return [...moves].sort((a, b) => this.moveScore(b) - this.moveScore(a));
  }

  moveScore(move) {
    let score = 0;
    const captured = this.game.board[move.to];
    if (captured) {
      // MVV-LVA: value of victim minus value of attacker
      score += 10 * PIECE_VALUE[Math.abs(captured)] - PIECE_VALUE[Math.abs(this.game.board[move.from])];
    }
    if (move.enPassant) score += 10 * PIECE_VALUE[PAWN];
    if (move.promotion) score += PIECE_VALUE[Math.abs(move.promotion)];
    // Prefer center moves slightly
    const [fr, fc] = this.game.rc(move.from);
    const [tr, tc] = this.game.rc(move.to);
    const centerDist = Math.abs(tr - 3.5) + Math.abs(tc - 3.5);
    score += (7 - centerDist) * 2;
    return score;
  }

  // Minimax with alpha-beta pruning and quiescence search
  minimax(depth, alpha, beta, isMaximizing) {
    this.nodesSearched++;

    // Time check
    if (Date.now() - this.startTime > this.maxTime) {
      return null; // Signal timeout
    }

    if (depth === 0) {
      return this.quiescence(alpha, beta, isMaximizing);
    }

    const moves = this.game.generateLegalMoves(isMaximizing ? 'w' : 'b');

    if (moves.length === 0) {
      if (this.game.isInCheck(isMaximizing ? 'w' : 'b')) {
        return isMaximizing ? -99999 + (3 - depth) : 99999 - (3 - depth); // Checkmate, prefer quicker mates
      }
      return 0; // Stalemate
    }

    const ordered = this.orderMoves(moves);

    if (isMaximizing) {
      let maxEval = -Infinity;
      for (const move of ordered) {
        const notation = this.game.makeMove(move);
        const eval_ = this.minimax(depth - 1, alpha, beta, false);
        this.game.undoMove();

        if (eval_ === null) return null; // Timeout propagated

        maxEval = Math.max(maxEval, eval_);
        alpha = Math.max(alpha, eval_);
        if (beta <= alpha) break;
      }
      return maxEval;
    } else {
      let minEval = Infinity;
      for (const move of ordered) {
        const notation = this.game.makeMove(move);
        const eval_ = this.minimax(depth - 1, alpha, beta, true);
        this.game.undoMove();

        if (eval_ === null) return null; // Timeout propagated

        minEval = Math.min(minEval, eval_);
        beta = Math.min(beta, eval_);
        if (beta <= alpha) break;
      }
      return minEval;
    }
  }

  // Quiescence search to avoid horizon effect
  quiescence(alpha, beta, isMaximizing) {
    const standPat = this.evaluate();

    if (isMaximizing) {
      if (standPat >= beta) return beta;
      if (alpha < standPat) alpha = standPat;
    } else {
      if (standPat <= alpha) return alpha;
      if (beta > standPat) beta = standPat;
    }

    const color = isMaximizing ? 'w' : 'b';
    // Only consider captures and promotions for quiescence
    const moves = this.game.generateLegalMoves(color).filter(m => {
      return this.game.board[m.to] || m.enPassant || m.promotion;
    });

    const ordered = this.orderMoves(moves);

    if (isMaximizing) {
      for (const move of ordered) {
        this.game.makeMove(move);
        const eval_ = this.quiescence(alpha, beta, false);
        this.game.undoMove();

        if (eval_ === null) return standPat; // Timeout

        if (eval_ >= beta) return beta;
        if (eval_ > alpha) alpha = eval_;
      }
      return alpha;
    } else {
      for (const move of ordered) {
        this.game.makeMove(move);
        const eval_ = this.quiescence(alpha, beta, true);
        this.game.undoMove();

        if (eval_ === null) return standPat; // Timeout

        if (eval_ <= alpha) return alpha;
        if (eval_ < beta) beta = eval_;
      }
      return beta;
    }
  }

  // Find the best move at given depth, returns {move, score} or null on timeout
  findBestMove(depth) {
    this.nodesSearched = 0;
    this.startTime = Date.now();

    const color = this.game.turn;
    const isMaximizing = color === 'w';
    const moves = this.game.generateLegalMoves(color);

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

    let bestMove = null;
    let bestScore = isMaximizing ? -Infinity : Infinity;
    let alpha = -Infinity;
    let beta = Infinity;

    // Iterative deepening for time control
    const ordered = this.orderMoves(moves);

    for (const move of ordered) {
      this.game.makeMove(move);
      const score = this.minimax(depth - 1, alpha, beta, !isMaximizing);
      this.game.undoMove();

      if (score === null) break; // Timeout

      if (isMaximizing) {
        if (score > bestScore) {
          bestScore = score;
          bestMove = move;
        }
        alpha = Math.max(alpha, score);
      } else {
        if (score < bestScore) {
          bestScore = score;
          bestMove = move;
        }
        beta = Math.min(beta, score);
      }
    }

    return bestMove ? { move: bestMove, score: bestScore, nodes: this.nodesSearched } : null;
  }
}

// ── class: SoundEngine ──
// ============================================================
// SOUND ENGINE — Web Audio API
// ============================================================
class SoundEngine {
  constructor() {
    this.ctx = null;
    this.muted = false;
    this.initialized = false;
  }

  init() {
    if (this.initialized) return;
    try {
      this.ctx = new (window.AudioContext || window.webkitAudioContext)();
      this.initialized = true;
    } catch(e) {
      console.warn('Web Audio not available');
    }
  }

  play(type) {
    if (!this.ctx || this.muted) return;
    if (this.ctx.state === 'suspended') this.ctx.resume();

    const now = this.ctx.currentTime;
    const vol = CFG.soundVolume;

    switch(type) {
      case 'move': this._playMove(now, vol); break;
      case 'capture': this._playCapture(now, vol); break;
      case 'check': this._playCheck(now, vol); break;
      case 'castle': this._playCastle(now, vol); break;
    }
  }

  _playMove(t, v) {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.type = 'sine';
    osc.frequency.setValueAtTime(600, t);
    osc.frequency.exponentialRampToValueAtTime(400, t + 0.08);
    gain.gain.setValueAtTime(v * 0.5, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
    osc.start(t);
    osc.stop(t + 0.1);
  }

  _playCapture(t, v) {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.type = 'sawtooth';
    osc.frequency.setValueAtTime(300, t);
    osc.frequency.exponentialRampToValueAtTime(150, t + 0.12);
    gain.gain.setValueAtTime(v * 0.4, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
    osc.start(t);
    osc.stop(t + 0.15);
  }

  _playCheck(t, v) {
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.type = 'square';
    osc.frequency.setValueAtTime(800, t);
    osc.frequency.setValueAtTime(600, t + 0.1);
    gain.gain.setValueAtTime(v * 0.3, t);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
    osc.start(t);
    osc.stop(t + 0.25);
  }

  _playCastle(t, v) {
    this._playMove(t, v);
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.type = 'sine';
    osc.frequency.setValueAtTime(500, t + 0.06);
    osc.frequency.exponentialRampToValueAtTime(350, t + 0.14);
    gain.gain.setValueAtTime(v * 0.4, t + 0.06);
    gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
    osc.start(t + 0.06);
    osc.stop(t + 0.18);
  }

  toggleMute() {
    this.muted = !this.muted;
    return this.muted;
  }
}

// ── class: ChessUI ──
// ============================================================
// UI RENDERER — Board display, interaction, move list
// ============================================================
class ChessUI {
  constructor(game) {
    this.game = game;
    this.selectedSquare = null;
    this.legalMovesForSelected = [];
    this.flipped = false;
    this.dragging = null; // {from, piece}
    this.promotionPending = null; // {from, to, moves} waiting for user choice
  }

  render() {
    const boardEl = document.getElementById('board');
    boardEl.innerHTML = '';

    const inCheck = this.game.isInCheck(this.game.turn);
    const kingPos = inCheck ? this.game.findKing(this.game.turn) : -1;

    for (let displayRow = 0; displayRow < 8; displayRow++) {
      for (let displayCol = 0; displayCol < 8; displayCol++) {
        const r = this.flipped ? 7 - displayRow : displayRow;
        const c = this.flipped ? 7 - displayCol : displayCol;
        const idx = r * 8 + c;
        const piece = this.game.board[idx];

        const sq = document.createElement('div');
        sq.className = 'square';
        sq.dataset.idx = idx;
        sq.dataset.row = r;
        sq.dataset.col = c;

        // Square color
        if ((r + c) % 2 === 0) sq.classList.add('light');
        else sq.classList.add('dark');

        // Last move highlight
        if (this.game.lastMove) {
          if (idx === this.game.lastMove.from || idx === this.game.lastMove.to) {
            sq.classList.add('last-move');
          }
        }

        // Selected square
        if (this.selectedSquare === idx) {
          sq.classList.add('selected');
        }

        // Check glow on king
        if (idx === kingPos) {
          sq.classList.add('check-glow');
        }

        // Legal move indicators
        const legalMove = this.legalMovesForSelected.find(m => m.to === idx);
        if (legalMove) {
          if (piece || legalMove.enPassant) {
            sq.classList.add('capture-ring');
          } else {
            sq.classList.add('move-dot');
          }
        }

        // Piece rendering
        if (piece) {
          const pieceEl = document.createElement('span');
          pieceEl.className = 'piece';
          pieceEl.textContent = PIECE_UNICODE[piece];
          if (piece > 0) pieceEl.classList.add('white-piece');
          else pieceEl.classList.add('black-piece');
          sq.appendChild(pieceEl);
        }

        // Coordinate labels
        if (displayCol === 0) {
          const label = document.createElement('span');
          label.className = 'coord-label rank-label';
          label.textContent = RANKS[r];
          sq.appendChild(label);
        }
        if (displayRow === 7) {
          const label = document.createElement('span');
          label.className = 'coord-label file-label';
          label.textContent = FILES[c];
          sq.appendChild(label);
        }

        boardEl.appendChild(sq);
      }
    }

    this.renderCaptured();
    this.renderMoveList();
    this.renderStatus();
  }

  renderCaptured() {
    const topEl = document.getElementById('captured-top');
    const bottomEl = document.getElementById('captured-bottom');

    // Top shows pieces captured by the side at the bottom of the board
    // Bottom shows pieces captured by the side at the top of the board
    if (this.flipped) {
      this._renderCapturedRow(topEl, 'w', true);
      this._renderCapturedRow(bottomEl, 'b', false);
    } else {
      this._renderCapturedRow(topEl, 'b', true);
      this._renderCapturedRow(bottomEl, 'w', false);
    }
  }

  _renderCapturedRow(el, color, isTop) {
    const pieces = this.game.capturedPieces[color];
    // Sort by value: Q > R > B/N > P
    const order = [QUEEN, ROOK, BISHOP, KNIGHT, PAWN];
    const sorted = [...pieces].sort((a, b) => order.indexOf(a) - order.indexOf(b));

    el.innerHTML = '';
    for (const pt of sorted) {
      const span = document.createElement('span');
      span.className = 'piece';
      // Show the captured piece in its original color
      const pieceVal = isTop ? (-pt) : pt;
      span.textContent = PIECE_UNICODE[pieceVal];
      if (isTop) span.classList.add('black-piece');
      else span.classList.add('white-piece');
      el.appendChild(span);
    }
  }

  renderMoveList() {
    const listEl = document.getElementById('move-list');
    listEl.innerHTML = '';

    for (let i = 0; i < this.game.moveHistory.length; i += 2) {
      const moveNum = Math.floor(i / 2) + 1;
      const row = document.createElement('div');
      row.className = 'move-row';

      const numSpan = document.createElement('span');
      numSpan.className = 'move-num';
      numSpan.textContent = moveNum + '.';
      row.appendChild(numSpan);

      const whiteMove = this.game.moveHistory[i];
      if (whiteMove) {
        const wSpan = document.createElement('span');
        wSpan.className = 'move-white';
        wSpan.textContent = whiteMove.notation;
        row.appendChild(wSpan);
      }

      const blackMove = this.game.moveHistory[i + 1];
      if (blackMove) {
        const bSpan = document.createElement('span');
        bSpan.className = 'move-black';
        bSpan.textContent = blackMove.notation;
        row.appendChild(bSpan);
      }

      listEl.appendChild(row);
    }

    // Auto-scroll to bottom
    listEl.scrollTop = listEl.scrollHeight;
  }

  renderStatus() {
    const turnEl = document.getElementById('turn-indicator');
    const statusEl = document.getElementById('game-status');

    if (this.game.gameOver) {
      let resultText = '';
      switch(this.game.result) {
        case 'checkmate':
          const winner = this.game.turn === 'w' ? 'Black' : 'White';
          resultText = `Checkmate! ${winner} wins.`;
          break;
        case 'stalemate':
          resultText = 'Stalemate — Draw.';
          break;
        case 'draw-50move':
          resultText = 'Draw by 50-move rule.';
          break;
        case 'draw-material':
          resultText = 'Draw — Insufficient material.';
          break;
      }
      turnEl.textContent = '';
      statusEl.textContent = resultText;
    } else {
      const turnName = this.game.turn === 'w' ? 'White' : 'Black';
      turnEl.textContent = `${turnName}'s turn`;

      if (this.game.isInCheck(this.game.turn)) {
        statusEl.textContent = '⚠ Check!';
      } else {
        statusEl.textContent = '';
      }
    }
  }

  // ---- Interaction handlers ----
  handleSquareClick(idx) {
    if (this.game.gameOver || this.promotionPending) return;

    const piece = this.game.board[idx];

    // If we have a selected square and click on a legal move target
    if (this.selectedSquare !== null) {
      const move = this.legalMovesForSelected.find(m => m.to === idx);
      if (move) {
        this.executeMove(move);
        return;
      }
    }

    // Select a piece of the current side
    if (piece && this.game.colorOf(piece) === this.game.turn) {
      this.selectedSquare = idx;
      this.legalMovesForSelected = this.game.generateLegalMoves(this.game.turn).filter(m => m.from === idx);
      this.render();
    } else {
      // Deselect
      this.selectedSquare = null;
      this.legalMovesForSelected = [];
      this.render();
    }
  }

  handleDragStart(idx) {
    if (this.game.gameOver || this.promotionPending) return;
    const piece = this.game.board[idx];
    if (!piece || this.game.colorOf(piece) !== this.game.turn) return;

    this.dragging = { from: idx, piece };
    this.selectedSquare = idx;
    this.legalMovesForSelected = this.game.generateLegalMoves(this.game.turn).filter(m => m.from === idx);
    this.render();
  }

  handleDragOver(idx) {
    if (!this.dragging) return;
    const sqEl = document.querySelector(`.square[data-idx="${idx}"]`);
    if (sqEl) {
      const isLegal = this.legalMovesForSelected.some(m => m.to === idx);
      if (isLegal) sqEl.classList.add('drag-over');
      else sqEl.classList.remove('drag-over');
    }
  }

  handleDragEnd() {
    // Clear drag highlights
    document.querySelectorAll('.square.drag-over').forEach(el => el.classList.remove('drag-over'));
    this.dragging = null;
  }

  handleDrop(idx) {
    if (!this.dragging) return;
    const move = this.legalMovesForSelected.find(m => m.to === idx);
    if (move) {
      this.executeMove(move);
    }
    this.handleDragEnd();
  }

  executeMove(move) {
    // Check for promotion — need user input
    const piece = this.game.board[move.from];
    const [tr] = this.game.rc(move.to);
    const isPromotion = Math.abs(piece) === PAWN && (tr === 0 || tr === 7);

    if (isPromotion && move.promotion) {
      // If it's the player's turn and we need to pick promotion piece
      const playerColor = this.getPlayerColor();
      if (this.game.turn === playerColor || playerColor === 'both') {
        this.showPromotionPicker(move);
        return;
      }
    }

    this.selectedSquare = null;
    this.legalMovesForSelected = [];
    const notation = this.game.makeMove(move);
    this.playSoundForMove(move, notation);
    this.render();
    this.saveState();

    // Check game over
    if (this.game.gameOver) {
      this.showGameOver();
      return;
    }

    // Trigger AI move if needed
    const playerColor = this.getPlayerColor();
    if (this.game.turn !== playerColor && playerColor !== 'both') {
      setTimeout(() => this.triggerAIMove(), 100);
    }
  }

  showPromotionPicker(baseMove) {
    const overlay = document.getElementById('promotion-overlay');
    const picker = document.getElementById('promotion-picker');
    picker.innerHTML = '';

    const color = this.game.turn;
    const promoPieces = [QUEEN, ROOK, BISHOP, KNIGHT];

    for (const pt of promoPieces) {
      const pieceVal = color === 'w' ? pt : -pt;
      const btn = document.createElement('div');
      btn.className = 'promo-piece';
      btn.textContent = PIECE_UNICODE[pieceVal];
      if (color === 'w') btn.classList.add('white-piece');
      else btn.classList.add('black-piece');

      btn.addEventListener('click', () => {
        // Find the matching move with this promotion type
        const allMoves = this.game.generateLegalMoves(this.game.turn);
        const promoMove = allMoves.find(m => m.from === baseMove.from && m.to === baseMove.to && Math.abs(m.promotion) === pt);
        if (promoMove) {
          overlay.classList.add('hidden');
          this.selectedSquare = null;
          this.legalMovesForSelected = [];
          const notation = this.game.makeMove(promoMove);
          this.playSoundForMove(promoMove, notation);
          this.render();
          this.saveState();

          if (this.game.gameOver) {
            this.showGameOver();
            return;
          }

          const playerColor = this.getPlayerColor();
          if (this.game.turn !== playerColor && playerColor !== 'both') {
            setTimeout(() => this.triggerAIMove(), 100);
          }
        }
      });

      picker.appendChild(btn);
    }

    overlay.classList.remove('hidden');
  }

  playSoundForMove(move, notation) {
    const sound = window._soundEngine;
    if (!sound) return;

    if (move.castle) {
      sound.play('castle');
    } else if (this.game.board[move.to] || move.enPassant) {
      sound.play('capture');
    } else {
      sound.play('move');
    }

    // Check sound
    const enemy = this.game.turn === 'w' ? 'b' : 'w';
    if (this.game.isInCheck(enemy)) {
      setTimeout(() => sound.play('check'), 150);
    }
  }

  showGameOver() {
    const banner = document.getElementById('game-over-banner');
    const titleEl = document.getElementById('game-over-title');
    const detailEl = document.getElementById('game-over-detail');

    switch(this.game.result) {
      case 'checkmate':
        const winner = this.game.turn === 'w' ? 'Black' : 'White';
        titleEl.textContent = `${winner} Wins!`;
        detailEl.textContent = 'Checkmate';
        break;
      case 'stalemate':
        titleEl.textContent = 'Draw';
        detailEl.textContent = 'Stalemate — no legal moves, king not in check.';
        break;
      case 'draw-50move':
        titleEl.textContent = 'Draw';
        detailEl.textContent = '50-move rule — no captures or pawn moves for 100 half-moves.';
        break;
      case 'draw-material':
        titleEl.textContent = 'Draw';
        detailEl.textContent = 'Insufficient material to deliver checkmate.';
        break;
    }

    banner.classList.remove('hidden');
  }

  triggerAIMove() {
    const depthSelect = document.getElementById('depth-select');
    const depth = parseInt(depthSelect.value);
    const ai = new ChessAI(this.game);
    const result = ai.findBestMove(depth);

    if (result && result.move) {
      this.selectedSquare = null;
      this.legalMovesForSelected = [];
      const notation = this.game.makeMove(result.move);
      this.playSoundForMove(result.move, notation);
      this.render();
      this.saveState();

      if (this.game.gameOver) {
        this.showGameOver();
      }
    }
  }

  getPlayerColor() {
    const sel = document.getElementById('color-select');
    return sel.value;
  }

  // ---- Persistence ----
  saveState() {
    try {
      const state = {
        board: this.game.board,
        turn: this.game.turn,
        castling: this.game.castling,
        epSquare: this.game.epSquare,
        halfMoveClock: this.game.halfMoveClock,
        fullMoveNumber: this.game.fullMoveNumber,
        moveHistory: this.game.moveHistory.map(m => ({...m})),
        capturedPieces: { w: [...this.game.capturedPieces.w], b: [...this.game.capturedPieces.b] },
        gameOver: this.game.gameOver,
        result: this.game.result,
        lastMove: this.game.lastMove ? {...this.game.lastMove} : null,
      };
      localStorage.setItem('chessGameState', JSON.stringify(state));
    } catch(e) { /* ignore */ }
  }

  loadState() {
    try {
      const raw = localStorage.getItem('chessGameState');
      if (!raw) return false;
      const state = JSON.parse(raw);
      this.game.board = state.board;
      this.game.turn = state.turn;
      this.game.castling = state.castling;
      this.game.epSquare = state.epSquare;
      this.game.halfMoveClock = state.halfMoveClock;
      this.game.fullMoveNumber = state.fullMoveNumber;
      this.game.moveHistory = (state.moveHistory || []).map(m => ({...m}));
      this.game.capturedPieces = { w: [...(state.capturedPieces?.w || [])], b: [...(state.capturedPieces?.b || [])] };
      this.game.gameOver = state.gameOver;
      this.game.result = state.result;
      this.game.lastMove = state.lastMove ? {...state.lastMove} : null;
      return true;
    } catch(e) { return false; }
  }

  flipBoard() {
    this.flipped = !this.flipped;
    this.render();
  }

  undoBothMoves() {
    // Undo AI move then player move (or just one if it's the first move of a pair)
    const playerColor = this.getPlayerColor();
    if (playerColor === 'both') {
      // In analysis mode, undo just one move per click
      if (this.game.moveHistory.length > 0) {
        this.game.undoMove();
        this.render();
        this.saveState();
      }
    } else {
      // Undo up to two moves (AI + player), but only if it's the player's turn now
      if (this.game.turn === playerColor && this.game.moveHistory.length >= 2) {
        this.game.undoMove(); // undo AI move
        this.game.undoMove(); // undo player move
        this.render();
        this.saveState();
      } else if (this.game.turn !== playerColor && this.game.moveHistory.length >= 1) {
        // Player moved but AI hasn't responded yet — just undo one
        this.game.undoMove();
        this.render();
        this.saveState();
      }
    }
  }

  newGame() {
    document.getElementById('game-over-banner').classList.add('hidden');
    document.getElementById('promotion-overlay').classList.add('hidden');
    this.game.reset();
    this.selectedSquare = null;
    this.legalMovesForSelected = [];
    this.render();
    this.saveState();

    // If playing as black, AI (white) moves first
    const playerColor = this.getPlayerColor();
    if (playerColor === 'black') {
      setTimeout(() => this.triggerAIMove(), 200);
    }
  }
}

// ── const: INITIAL_BOARD ──
const INITIAL_BOARD = [
  BR, BN, BB, BQ, BK, BB, BN, BR, // rank 8: Black back row
  BP, BP, BP, BP, BP, BP, BP, BP, // rank 7: Black pawns
  EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 6: empty
  EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 5: empty
  EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 4: empty
  EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 3: empty
  WP, WP, WP, WP, WP, WP, WP, WP, // rank 2: White pawns
  WR, WN, WB, WQ, WK, WB, WN, WR, // rank 1: White back row
];

// ── class: ChessGame ──
class ChessGame {
  constructor() { this.reset(); }

  reset() {
    this.board = [...INITIAL_BOARD];
    this.turn = 'w';
    this.castling = { ...INITIAL_CASTLING };
    this.epSquare = null;
    this.halfMoveClock = 0;
    this.fullMoveNumber = 1;
    this.moveHistory = [];
    this.capturedPieces = { w: [], b: [] };
    this.gameOver = false;
    this.result = null;
    this.lastMove = null;
  }

  idx(r, c) { return r * 8 + c; }
  rc(i) { return [Math.floor(i / 8), i % 8]; }
  colorOf(p) { return p > 0 ? 'w' : p < 0 ? 'b' : null; }
  typeOf(p) { return Math.abs(p); }

  clone() {
    const g = new ChessGame();
    g.board = [...this.board];
    g.turn = this.turn;
    g.castling = { ...this.castling };
    g.epSquare = this.epSquare;
    g.halfMoveClock = this.halfMoveClock;
    g.fullMoveNumber = this.fullMoveNumber;
    g.lastMove = this.lastMove ? { ...this.lastMove } : null;
    return g;
  }

  generatePseudoMoves(color) {
    const moves = [];
    for (let i = 0; i < 64; i++) {
      const p = this.board[i];
      if (!p || this.colorOf(p) !== color) continue;
      const t = Math.abs(p);
      const [r, c] = this.rc(i);

      if (t === PAWN) {
        const dir = color === 'w' ? -1 : 1;
        const startRow = color === 'w' ? 6 : 1;
        const promoRow = color === 'w' ? 0 : 7;
        // Forward one
        const f1r = r + dir;
        if (f1r >= 0 && f1r < 8) {
          const f1 = this.idx(f1r, c);
          if (!this.board[f1]) {
            if (f1r === promoRow) {
              for (const pt of [QUEEN, ROOK, BISHOP, KNIGHT]) moves.push({ from: i, to: f1, promotion: color === 'w' ? pt : -pt });
            } else {
              moves.push({ from: i, to: f1 });
              // Forward two
              const f2r = r + 2 * dir;
              if (r === startRow && f2r >= 0 && f2r < 8) {
                const f2 = this.idx(f2r, c);
                if (!this.board[f2]) moves.push({ from: i, to: f2 });
              }
            }
          }
        }
        // Captures (regular + en passant)
        for (const dc of [-1, 1]) {
          const nc = c + dc;
          if (nc < 0 || nc > 7) continue;
          const capR = r + dir;
          if (capR < 0 || capR > 7) continue;
          const ti = this.idx(capR, nc);
          const target = this.board[ti];
          // Regular capture
          if (target && this.colorOf(target) !== color) {
            if (capR === promoRow) {
              for (const pt of [QUEEN, ROOK, BISHOP, KNIGHT]) moves.push({ from: i, to: ti, promotion: color === 'w' ? pt : -pt });
            } else {
              moves.push({ from: i, to: ti });
            }
          }
        }
        // En passant: check if epSquare is on a diagonal from this pawn
        // The captured pawn sits one step forward from the epSquare (in capturing direction)
        if (this.epSquare !== null) {
          const [epR, epC] = this.rc(this.epSquare);
          const capPawnRow = epR - dir; // where the captured pawn actually is
          if (capPawnRow >= 0 && capPawnRow < 8 && Math.abs(capPawnRow - r) === 1 && Math.abs(epC - c) === 1) {
            moves.push({ from: i, to: this.epSquare, enPassant: true });
          }
        }
      } else if (t === KNIGHT) {
        for (const [dr, dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
          const nr = r + dr, nc = c + dc;
          if (nr < 0 || nr > 7 || nc < 0 || nc > 7) continue;
          const ti = this.idx(nr, nc);
          if (!this.board[ti] || this.colorOf(this.board[ti]) !== color) moves.push({ from: i, to: ti });
        }
      } else if (t === BISHOP || t === ROOK || t === QUEEN) {
        const dirs = [];
        if (t === BISHOP || t === QUEEN) dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
        if (t === ROOK || t === QUEEN) dirs.push([-1,0],[1,0],[0,-1],[0,1]);
        for (const [dr, dc] of dirs) {
          let nr = r + dr, nc = c + dc;
          while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
            const ti = this.idx(nr, nc);
            if (!this.board[ti]) { moves.push({ from: i, to: ti }); }
            else {
              if (this.colorOf(this.board[ti]) !== color) moves.push({ from: i, to: ti });
              break;
            }
            nr += dr; nc += dc;
          }
        }
      } else if (t === KING) {
        for (const [dr, dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
          const nr = r + dr, nc = c + dc;
          if (nr < 0 || nr > 7 || nc < 0 || nc > 7) continue;
          const ti = this.idx(nr, nc);
          if (!this.board[ti] || this.colorOf(this.board[ti]) !== color) moves.push({ from: i, to: ti });
        }
        // Castling
        const row = color === 'w' ? 7 : 0;
        const kSide = color === 'w' ? 'K' : 'k';
        const qSide = color === 'w' ? 'Q' : 'q';
        if (this.castling[kSide] && !this.board[this.idx(row,5)] && !this.board[this.idx(row,6)]) {
          if (!this.isSquareAttacked(this.idx(row,4), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,5), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,6), color==='b'?'w':'b')) {
            moves.push({ from: this.idx(row,4), to: this.idx(row,6), castle: 'k' });
          }
        }
        if (this.castling[qSide] && !this.board[this.idx(row,3)] && !this.board[this.idx(row,2)] && !this.board[this.idx(row,1)]) {
          if (!this.isSquareAttacked(this.idx(row,4), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,3), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,2), color==='b'?'w':'b')) {
            moves.push({ from: this.idx(row,4), to: this.idx(row,2), castle: 'q' });
          }
        }
      }
    }
    return moves;
  }

  isSquareAttacked(sq, byColor) {
    const [sr, sc] = this.rc(sq);
    // Pawn attacks (pawns attack "down" from their perspective in array terms)
    const pawnDir = byColor === 'w' ? 1 : -1;
    for (const dc of [-1, 1]) {
      const pr = sr + pawnDir, pc = sc + dc;
      if (pr >= 0 && pr < 8 && pc >= 0 && pc < 8) {
        const p = this.board[this.idx(pr, pc)];
        if (p && Math.abs(p) === PAWN && this.colorOf(p) === byColor) return true;
      }
    }
    // Knight attacks
    for (const [dr, dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
      const nr = sr + dr, nc = sc + dc;
      if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
        const p = this.board[this.idx(nr, nc)];
        if (p && Math.abs(p) === KNIGHT && this.colorOf(p) === byColor) return true;
      }
    }
    // King attacks
    for (const [dr, dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
      const nr = sr + dr, nc = sc + dc;
      if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
        const p = this.board[this.idx(nr, nc)];
        if (p && Math.abs(p) === KING && this.colorOf(p) === byColor) return true;
      }
    }
    // Bishop/Queen diagonals
    for (const [dr, dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
      let nr = sr + dr, nc = sc + dc;
      while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
        const p = this.board[this.idx(nr, nc)];
        if (p) {
          if (this.colorOf(p) === byColor && (Math.abs(p) === BISHOP || Math.abs(p) === QUEEN)) return true;
          break;
        }
        nr += dr; nc += dc;
      }
    }
    // Rook/Queen straights
    for (const [dr, dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
      let nr = sr + dr, nc = sc + dc;
      while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
        const p = this.board[this.idx(nr, nc)];
        if (p) {
          if (this.colorOf(p) === byColor && (Math.abs(p) === ROOK || Math.abs(p) === QUEEN)) return true;
          break;
        }
        nr += dr; nc += dc;
      }
    }
    return false;
  }

  findKing(color) {
    const k = color === 'w' ? WK : BK;
    for (let i = 0; i < 64; i++) if (this.board[i] === k) return i;
    return -1;
  }

  makeMoveInternal(move) {
    const captured = this.board[move.to];
    let epCaptured = null;
    // En passant capture
    if (move.enPassant) {
      const [pr] = this.rc(move.from);
      const dir = this.turn === 'w' ? -1 : 1;
      const capRow = pr + dir;
      epCaptured = this.board[this.idx(capRow, move.to % 8)];
      this.board[this.idx(capRow, move.to % 8)] = EMPTY;
    }
    // Move piece
    this.board[move.to] = this.board[move.from];
    this.board[move.from] = EMPTY;
    if (move.promotion) this.board[move.to] = move.promotion;
    // Castling rook movement
    if (move.castle) {
      const row = Math.floor(move.from / 8);
      if (move.castle === 'k') { this.board[this.idx(row,5)] = this.board[this.idx(row,7)]; this.board[this.idx(row,7)] = EMPTY; }
      else { this.board[this.idx(row,3)] = this.board[this.idx(row,0)]; this.board[this.idx(row,0)] = EMPTY; }
    }
    return { captured, epCaptured };
  }

  unmakeMoveInternal(move, info) {
    this.board[move.from] = this.board[move.to];
    if (info.epCaptured !== null && move.enPassant) {
      const [pr] = this.rc(move.from);
      const dir = this.turn === 'w' ? -1 : 1; // turn already flipped back
      const capRow = pr + dir;
      this.board[this.idx(capRow, move.to % 8)] = info.epCaptured;
    } else {
      this.board[move.to] = info.captured || EMPTY;
    }
    if (move.castle) {
      const row = Math.floor(move.from / 8);
      if (move.castle === 'k') { this.board[this.idx(row,7)] = this.board[this.idx(row,5)]; this.board[this.idx(row,5)] = EMPTY; }
      else { this.board[this.idx(row,0)] = this.board[this.idx(row,3)]; this.board[this.idx(row,3)] = EMPTY; }
    }
  }

  generateLegalMoves(color) {
    const c = color || this.turn;
    const pseudo = this.generatePseudoMoves(c);
    const enemy = c === 'w' ? 'b' : 'w';
    return pseudo.filter(m => {
      const info = this.makeMoveInternal(m);
      const kingPos = this.findKing(c);
      const inCheck = this.isSquareAttacked(kingPos, enemy);
      this.unmakeMoveInternal(m, info);
      return !inCheck;
    });
  }

  isInCheck(color) {
    const c = color || this.turn;
    const kingPos = this.findKing(c);
    const enemy = c === 'w' ? 'b' : 'w';
    return this.isSquareAttacked(kingPos, enemy);
  }

  makeMove(move) {
    const piece = this.board[move.from];
    const pType = Math.abs(piece);
    const capturedPiece = move.enPassant ? null : this.board[move.to];
    let epCaptured = null;

    const record = {
      from: move.from, to: move.to, piece: piece,
      captured: capturedPiece, enPassant: !!move.enPassant,
      castle: move.castle || null, promotion: move.promotion || null,
      prevCastling: { ...this.castling }, prevEpSquare: this.epSquare,
      prevHalfMoveClock: this.halfMoveClock, prevFullMoveNumber: this.fullMoveNumber,
    };

    // En passant capture
    if (move.enPassant) {
      const [pr] = this.rc(move.from);
      const dir = this.turn === 'w' ? -1 : 1;
      const capRow = pr + dir;
      epCaptured = this.board[this.idx(capRow, move.to % 8)];
      record.epCapturedSquare = this.idx(capRow, move.to % 8);
      this.board[this.idx(capRow, move.to % 8)] = EMPTY;
    }

    // Move piece
    this.board[move.to] = this.board[move.from];
    this.board[move.from] = EMPTY;
    if (move.promotion) this.board[move.to] = move.promotion;

    // Castling rook movement
    if (move.castle) {
      const row = Math.floor(move.from / 8);
      if (move.castle === 'k') { this.board[this.idx(row,5)] = this.board[this.idx(row,7)]; this.board[this.idx(row,7)] = EMPTY; }
      else { this.board[this.idx(row,3)] = this.board[this.idx(row,0)]; this.board[this.idx(row,0)] = EMPTY; }
    }

    // Update castling rights
    if (pType === KING) {
      if (this.turn === 'w') { this.castling.K = false; this.castling.Q = false; }
      else { this.castling.k = false; this.castling.q = false; }
    }
    if (pType === ROOK) {
      if (move.from === 63) this.castling.K = false;
      if (move.from === 56) this.castling.Q = false;
      if (move.from === 7) this.castling.k = false;
      if (move.from === 0) this.castling.q = false;
    }
    if (move.to === 63) this.castling.K = false;
    if (move.to === 56) this.castling.Q = false;
    if (move.to === 7) this.castling.k = false;
    if (move.to === 0) this.castling.q = false;

    // Update en passant square
    this.epSquare = null;
    if (pType === PAWN && Math.abs(move.from - move.to) === 16) {
      const epRow = (Math.floor(move.from / 8) + Math.floor(move.to / 8)) / 2;
      this.epSquare = this.idx(epRow, move.to % 8);
    }

    // Half-move clock
    if (pType === PAWN || capturedPiece || epCaptured) this.halfMoveClock = 0;
    else this.halfMoveClock++;

    if (this.turn === 'b') this.fullMoveNumber++;

    const capPiece = epCaptured || capturedPiece;
    if (capPiece) {
      const capColor = this.colorOf(capPiece);
      this.capturedPieces[capColor].push(Math.abs(capPiece));
    }

    record.notation = this.toAlgebraic(record, move);

    // Switch turn
    this.turn = this.turn === 'w' ? 'b' : 'w';
    this.lastMove = { from: move.from, to: move.to };

    // Check game state
    const legalMoves = this.generateLegalMoves(this.turn);
    if (legalMoves.length === 0) {
      this.gameOver = true;
      if (this.isInCheck(this.turn)) { this.result = 'checkmate'; record.notation += '#'; }
      else this.result = 'stalemate';
    } else {
      if (this.halfMoveClock >= 100) { this.gameOver = true; this.result = 'draw-50move'; }
      else if (this.isInsufficientMaterial()) { this.gameOver = true; this.result = 'draw-material'; }
    }

    // Add check symbol
    if (!record.notation.includes('#') && !this.gameOver) {
      if (this.isInCheck(this.turn)) record.notation += '+';
    }

    this.moveHistory.push(record);
    return record.notation;
  }

  undoMove() {
    if (this.moveHistory.length === 0) return false;
    const rec = this.moveHistory.pop();
    this.turn = this.turn === 'w' ? 'b' : 'w';
    this.board[rec.from] = rec.piece;
    if (rec.enPassant) {
      this.board[rec.to] = EMPTY;
      this.board[rec.epCapturedSquare] = this.capturedPieces[this.turn === 'w' ? 'b' : 'w'].pop();
    } else {
      const cap = rec.captured;
      if (cap) { const arr = this.capturedPieces[this.colorOf(cap)]; arr.pop(); }
      this.board[rec.to] = cap || EMPTY;
    }
    if (rec.castle) {
      const row = Math.floor(rec.from / 8);
      if (rec.castle === 'k') { this.board[this.idx(row,7)] = this.board[this.idx(row,5)]; this.board[this.idx(row,5)] = EMPTY; }
      else { this.board[this.idx(row,0)] = this.board[this.idx(row,3)]; this.board[this.idx(row,3)] = EMPTY; }
    }
    this.castling = rec.prevCastling;
    this.epSquare = rec.prevEpSquare;
    this.halfMoveClock = rec.prevHalfMoveClock;
    this.fullMoveNumber = rec.prevFullMoveNumber;
    this.gameOver = false;
    this.result = null;
    if (this.moveHistory.length > 0) { const last = this.moveHistory[this.moveHistory.length - 1]; this.lastMove = { from: last.from, to: last.to }; }
    else this.lastMove = null;
    return true;
  }

  toAlgebraic(rec, move) {
    const pType = Math.abs(rec.piece);
    const [fr, fc] = this.rc(move.from);
    const [tr, tc] = this.rc(move.to);
    let s = '';
    if (rec.castle === 'k') return 'O-O';
    if (rec.castle === 'q') return 'O-O-O';
    if (pType !== PAWN) {
      s += PIECE_LETTER[pType];
      // Disambiguation: check for other pieces of same type that could move to the same square
      const color = this.colorOf(rec.piece);
      let needFile = false, needRank = false;
      for (let i = 0; i < 64; i++) {
        if (i === move.from) continue;
        const p = this.board[i];
        if (!p || Math.abs(p) !== pType || this.colorOf(p) !== color) continue;
        const [or, oc] = this.rc(i);
        // Check if this piece could also reach the destination (simplified check)
        if ((oc === tc && or !== fr) || (or === tr && oc !== fc)) needFile = true;
        if ((or === tr && oc !== fc) || (oc === tc && or !== fr)) needRank = true;
      }
      // More precise: check actual legal moves for disambiguation
      const sameMoves = this.generatePseudoMoves(color).filter(m => m.to === move.to && Math.abs(this.board[m.from]) === pType && m.from !== move.from);
      if (sameMoves.length > 0) {
        const sameFile = sameMoves.some(m => m.from % 8 === fc);
        const sameRank = sameMoves.some(m => Math.floor(m.from / 8) === fr);
        if (!sameFile) s += FILES[fc];
        else if (!sameRank) s += RANKS[7 - fr];
        else { s += FILES[fc] + RANKS[7 - fr]; }
      }
    }
    const isCapture = rec.captured || rec.enPassant;
    if (isCapture) { if (pType === PAWN) s += FILES[fc]; s += 'x'; }
    s += FILES[tc] + RANKS[7 - tr];
    if (rec.promotion) { const pt = Math.abs(rec.promotion); s += '=' + PIECE_LETTER[pt]; }
    if (rec.enPassant) s += ' e.p.';
    return s;
  }

  isInsufficientMaterial() {
    const pieces = [];
    for (let i = 0; i < 64; i++) { if (this.board[i]) pieces.push({ type: Math.abs(this.board[i]), color: this.colorOf(this.board[i]) }); }
    if (pieces.length === 2) return true;
    if (pieces.length === 3) { const nk = pieces.find(p => p.type !== KING); if (nk && (nk.type === BISHOP || nk.type === KNIGHT)) return true; }
    return false;
  }

  fen() {
    let fen = '';
    for (let r = 0; r < 8; r++) {
      let empty = 0;
      for (let c = 0; c < 8; c++) {
        const p = this.board[this.idx(r, c)];
        if (!p) { empty++; }
        else {
          if (empty > 0) { fen += empty; empty = 0; }
          const t = Math.abs(p);
          const letter = PIECE_LETTER[t] || 'P';
          fen += p > 0 ? letter : letter.toLowerCase();
        }
      }
      if (empty > 0) fen += empty;
      if (r < 7) fen += '/';
    }
    fen += ' ' + this.turn;
    let c = '';
    if (this.castling.K) c += 'K';
    if (this.castling.Q) c += 'Q';
    if (this.castling.k) c += 'k';
    if (this.castling.q) c += 'q';
    if (!c) c = '-';
    fen += ' ' + c;
    if (this.epSquare !== null) {
      const [er, ec] = this.rc(this.epSquare);
      fen += ' ' + FILES[ec] + RANKS[7 - er];
    } else { fen += ' -'; }
    fen += ' ' + this.halfMoveClock;
    fen += ' ' + this.fullMoveNumber;
    return fen;
  }

  legalMoves() {
    const moves = this.generateLegalMoves(this.turn);
    return moves.map(m => FILES[m.from % 8] + RANKS[7 - Math.floor(m.from / 8)] + FILES[m.to % 8] + RANKS[7 - Math.floor(m.to / 8)] + (m.promotion ? '=' + PIECE_LETTER[Math.abs(m.promotion)] : ''));
  }

  move(str) {
    const match = str.match(/^([a-h])(\d)([a-h])(\d)(?:=([QRBN]))?$/);
    if (!match) return null;
    const fc = match[1].charCodeAt(0) - 97;
    const fr = parseInt(match[2]) - 1;
    const tc = match[3].charCodeAt(0) - 97;
    const tr = parseInt(match[4]) - 1;
    // Convert chess notation (rank 1-8 from White's side) to array rows (row 7=rank1, row 0=rank8)
    const fromRow = 7 - fr;
    const toRow = 7 - tr;
    const from = this.idx(fromRow, fc);
    const to = this.idx(toRow, tc);
    const moves = this.generateLegalMoves(this.turn);
    let found = null;
    for (const m of moves) {
      if (m.from === from && m.to === to) {
        if (match[5]) {
          const promoType = PIECE_LETTERInverse(match[5]);
          if (m.promotion && Math.abs(m.promotion) === promoType) { found = m; break; }
        } else {
          if (!m.promotion || Math.abs(m.promotion) === QUEEN) { found = m; break; }
        }
      }
    }
    if (found) return this.makeMove(found);
    return null;
  }
}

// ── main ──
// ============================================================
// MAIN — Initialization and event wiring
// ============================================================
(function() {
  // Create game and UI instances
  const game = new ChessGame();
  const ui = new ChessUI(game);
  window._soundEngine = new SoundEngine();

  // Load saved state or start fresh
  if (!ui.loadState()) {
    ui.saveState();
  }

  // Initial render
  ui.render();

  // ---- Board interaction (click) ----
  document.getElementById('board').addEventListener('click', function(e) {
    window._soundEngine.init();
    const sq = e.target.closest('.square');
    if (!sq) return;
    const idx = parseInt(sq.dataset.idx);
    ui.handleSquareClick(idx);
  });

  // ---- Board interaction (drag and drop) ----
  let dragFromIdx = null;

  document.getElementById('board').addEventListener('mousedown', function(e) {
    window._soundEngine.init();
    const sq = e.target.closest('.square');
    if (!sq) return;
    const idx = parseInt(sq.dataset.idx);
    ui.handleDragStart(idx);
    dragFromIdx = idx;
  });

  document.getElementById('board').addEventListener('mousemove', function(e) {
    if (dragFromIdx === null) return;
    const sq = e.target.closest('.square');
    if (!sq) return;
    const idx = parseInt(sq.dataset.idx);
    ui.handleDragOver(idx);
  });

  document.addEventListener('mouseup', function(e) {
    if (dragFromIdx === null) return;
    const sq = e.target.closest('.square');
    if (sq) {
      const idx = parseInt(sq.dataset.idx);
      ui.handleDrop(idx);
    } else {
      ui.handleDragEnd();
    }
    dragFromIdx = null;
  });

  // Touch support for mobile
  let touchFromIdx = null;
  document.getElementById('board').addEventListener('touchstart', function(e) {
    window._soundEngine.init();
    const touch = e.touches[0];
    const sq = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.square');
    if (!sq) return;
    const idx = parseInt(sq.dataset.idx);
    ui.handleDragStart(idx);
    touchFromIdx = idx;
  }, { passive: true });

  document.getElementById('board').addEventListener('touchmove', function(e) {
    if (touchFromIdx === null) return;
    const touch = e.touches[0];
    const sq = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.square');
    if (!sq) return;
    const idx = parseInt(sq.dataset.idx);
    ui.handleDragOver(idx);
  }, { passive: true });

  document.getElementById('board').addEventListener('touchend', function(e) {
    if (touchFromIdx === null) return;
    // For touch, just treat as a click on the end position
    const touch = e.changedTouches[0];
    const sq = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.square');
    if (sq) {
      const idx = parseInt(sq.dataset.idx);
      ui.handleDrop(idx);
    } else {
      ui.handleDragEnd();
    }
    touchFromIdx = null;
  }, { passive: true });

  // ---- Button handlers ----
  document.getElementById('btn-new-game').addEventListener('click', function() {
    window._soundEngine.init();
    ui.newGame();
  });

  document.getElementById('btn-undo').addEventListener('click', function() {
    window._soundEngine.init();
    ui.undoBothMoves();
  });

  document.getElementById('btn-flip').addEventListener('click', function() {
    window._soundEngine.init();
    ui.flipBoard();
  });

  document.getElementById('btn-mute').addEventListener('click', function() {
    window._soundEngine.init();
    const muted = window._soundEngine.toggleMute();
    this.textContent = muted ? '🔇' : '🔊';
  });

  document.getElementById('btn-new-game-overlay').addEventListener('click', function() {
    ui.newGame();
  });

  // ---- Color selection change ----
  document.getElementById('color-select').addEventListener('change', function() {
    ui.newGame();
  });

  // ---- Debug interface on window ----
  window.game = game;
  window.ui = ui;
  window.ai = ChessAI;
})();
</script>
</body>
</html>