← Chess results

Chess

ling 3.0 Flash · full

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 57,727 bytes · SHA-256 03028911247c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — AI Opponent</title>
<style>
/* ============================================================
   CONFIGURATION — Tunable constants
   ============================================================ */
:root {
  --sq-light: #F0D9B5;
  --sq-dark: #B58863;
  --sel-bg: #FFFF87;
  --last-bg: #CED26B;
  --check-glow: #FF4444;
  --dot-color: rgba(0,0,0,0.35);
  --ring-color: rgba(0,0,0,0.55);
  --panel-bg: #2B2B2B;
  --panel-fg: #E0E0E0;
  --accent: #5A9E6F;
  --danger: #C0392B;
  --board-size: min(88vw, 88vh, 560px);
  --sq-size: calc(var(--board-size) / 8);
  --font-piece: var(--sq-size);
  --font-ui: 13px;
}

* { margin:0; padding:0; box-sizing:border-box; }
body {
  font-family: 'Segoe UI', system-ui, sans-serif;
  background: #1a1a2e;
  color: var(--panel-fg);
  display: flex;
  flex-direction: column;
  align-items: center;
  min-height: 100vh;
  padding: 10px;
}

/* ============================================================
   LAYOUT
   ============================================================ */
#app { display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; justify-content:center; }
#board-container { position:relative; }
#board {
  display:grid;
  grid-template-columns: repeat(8, var(--sq-size));
  grid-template-rows: repeat(8, var(--sq-size));
  border: 3px solid #555;
  border-radius: 4px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  user-select: none;
  -webkit-user-select: none;
}
#sidebar {
  width: 260px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}
.panel {
  background: var(--panel-bg);
  border-radius: 6px;
  padding: 10px;
  font-size: var(--font-ui);
}
.panel h3 { font-size:14px; margin-bottom:6px; color:#aaa; text-transform:uppercase; letter-spacing:1px; }

/* ============================================================
   SQUARES & PIECES
   ============================================================ */
.square {
  width: var(--sq-size);
  height: var(--sq-size);
  display:flex;
  align-items:center;
  justify-content:center;
  font-size: calc(var(--sq-size) * 0.82);
  cursor: pointer;
  position: relative;
  line-height: 1;
}
.square.light { background: var(--sq-light); }
.square.dark { background: var(--sq-dark); }
.square.selected { background: var(--sel-bg) !important; }
.square.last-move { background: var(--last-bg) !important; }
.square.in-check { box-shadow: inset 0 0 12px 4px var(--check-glow); }
.square.drag-over { outline: 3px solid var(--accent); }

/* Move indicators */
.square .move-dot {
  width: 30%; height: 30%;
  border-radius: 50%;
  background: var(--dot-color);
  position: absolute;
  pointer-events: none;
}
.square .capture-ring {
  width: 80%; height: 80%;
  border-radius: 50%;
  border: 3px solid var(--ring-color);
  position: absolute;
  pointer-events: none;
}

/* ============================================================
   PIECE DRAG GHOST
   ============================================================ */
#drag-ghost {
  position: fixed;
  pointer-events: none;
  font-size: calc(var(--sq-size) * 0.9);
  z-index: 1000;
  opacity: 0.85;
  transform: translate(-50%, -50%);
  display: none;
}

/* ============================================================
   SIDEBAR COMPONENTS
   ============================================================ */
#controls { display:flex; flex-wrap:wrap; gap:6px; }
#controls button {
  flex:1; min-width:70px;
  padding: 6px 8px;
  border: none; border-radius: 4px;
  background: #3a3a5c;
  color: #e0e0e0;
  cursor: pointer;
  font-size: 12px;
  font-weight: 600;
  transition: background 0.15s;
}
#controls button:hover { background: #4a4a7c; }
#controls button.active { background: var(--accent); color:#fff; }

#captured { display:flex; gap:4px; flex-wrap:wrap; min-height:24px; }
.captured-piece { font-size: 18px; line-height:1; }
.captured-white { color: #ddd; }
.captured-black { color: #333; }

#move-list {
  max-height: 220px;
  overflow-y: auto;
  font-family: 'Courier New', monospace;
  font-size: 12px;
  line-height: 1.6;
  white-space: pre;
}
#move-list::-webkit-scrollbar { width:4px; }
#move-list::-webkit-scrollbar-thumb { background:#555; border-radius:2px; }

#status {
  font-size: 14px;
  font-weight: 700;
  text-align: center;
  padding: 6px;
  border-radius: 4px;
  background: #3a3a5c;
}
#status.wt { color: #fff; }
#status.bt { color: #ccc; }

/* ============================================================
   PROMOTION PICKER
   ============================================================ */
#promotion-overlay {
  position: absolute;
  top:0; left:0; right:0; bottom:0;
  display: none;
  align-items: center;
  justify-content: center;
  z-index: 100;
  background: rgba(0,0,0,0.4);
}
#promotion-overlay.visible { display:flex; }
.promo-btn {
  width: 60px; height: 60px;
  font-size: 36px;
  border: 2px solid #555;
  border-radius: 6px;
  background: #f5f0e0;
  cursor: pointer;
  display:flex; align-items:center; justify-content:center;
  transition: background 0.15s;
}
.promo-btn:hover { background: #ffe080; }

/* ============================================================
   GAME OVER BANNER
   ============================================================ */
#game-over {
  position: absolute;
  top: 50%; left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(0,0,0,0.88);
  color: #fff;
  padding: 20px 36px;
  border-radius: 10px;
  text-align: center;
  font-size: 20px;
  font-weight: 700;
  z-index: 200;
  display: none;
}
#game-over.visible { display:block; }
#game-over button {
  margin-top: 12px;
  padding: 8px 20px;
  background: var(--accent);
  color: #fff;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
}

/* ============================================================
   MUTE TOGGLE
   ============================================================ */
.mute-btn { font-size: 16px !important; }
</style>
</head>
<body>

<div id="app">
  <div id="board-container">
    <div id="board"></div>
    <div id="promotion-overlay"></div>
    <div id="game-over"></div>
  </div>
  <div id="sidebar">
    <div id="status" class="wt">White to move</div>
    <div class="panel">
      <h3>Controls</h3>
      <div id="controls">
        <button id="btn-new">New</button>
        <button id="btn-undo">Undo</button>
        <button id="btn-flip">Flip</button>
      </div>
      <div id="controls" style="margin-top:6px">
        <button id="btn-depth1" class="active">Depth 1</button>
        <button id="btn-depth2">Depth 2</button>
        <button id="btn-depth3">Depth 3</button>
      </div>
      <div id="controls" style="margin-top:6px">
        <button id="btn-mute" class="mute-btn">🔊</button>
      </div>
    </div>
    <div class="panel">
      <h3>Captured</h3>
      <div id="captured"></div>
    </div>
    <div class="panel">
      <h3>Moves</h3>
      <div id="move-list"></div>
    </div>
  </div>
</div>

<div id="drag-ghost"></div>

<script>
// ============================================================
// CONFIGURATION — Tunable constants
// ============================================================
const CFG = {
  boardLight: '#F0D9B5',
  boardDark: '#B58863',
  selBg: '#FFFF87',
  lastBg: '#CED26B',
  checkGlow: '#FF4444',
  dotColor: 'rgba(0,0,0,0.35)',
  ringColor: 'rgba(0,0,0,0.55)',
  aiDepth: 3,
  aiMinDepth: 1,
  aiMaxDepth: 3,
  // Piece values (centipawns)
  pieceValue: { 'P':100, 'N':320, 'B':330, 'R':500, 'Q':900, 'K':20000,
                'p':100, 'n':320, 'b':330, 'r':500, 'q':900, 'k':20000 },
  // Piece-square tables (from White's perspective, row 0=rank8, row 7=rank1)
  pst: {
    'P': [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],
    'N': [-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],
    'B': [-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],
    'R': [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, -5,0,0,0,0,0,0,-5, 0,0,0,5,5,0,0,0],
    'Q': [-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],
    'K': [-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],
  },
  // Flip PST for Black pieces (reverse row order)
  flipPst(t) { const f=[]; for(let r=7;r>=0;r--) for(let c=0;c<8;c++) f.push(t[r*8+c]); return f; },
};
CFG.pstBlack = {};
for (const p of ['P','N','B','R','Q','K']) CFG.pstBlack[p] = CFG.flipPst(CFG.pst[p]);

// ============================================================
// CHESS ENGINE — Board, move generation, legality
// ============================================================
const Engine = {
  board: [],       // 8x8 array, null or 'P','N','B','R','Q','K','p','n','b','r','q','k'
  turn: 'w',       // 'w' or 'b'
  castling: {wK:true, wQ:true, bK:true, bQ:true},
  epTarget: null,   // {r,c} or null
  halfMoveClock: 0,
  fullMoveNumber: 1,
  moveHistory: [],  // for undo & display
  captured: {w:[], b:[]}, // captured pieces
  gameOver: false,
  result: null,     // 'white','black','draw'
  resultReason: '',

  // Initial FEN
  initFEN: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',

  init() {
    this.fromFEN(this.initFEN);
  },

  fromFEN(fen) {
    const parts = fen.split(' ');
    const rows = parts[0].split('/');
    this.board = [];
    for (let r=0; r<8; r++) {
      this.board[r] = [];
      let ci = 0;
      for (const ch of rows[r]) {
        if (ch>='1' && ch<='8') {
          for (let i=0; i<+ch; i++) this.board[r][ci++] = null;
        } else {
          this.board[r][ci++] = ch;
        }
      }
    }
    this.turn = parts[1] || 'w';
    const cas = parts[2] || '-';
    this.castling = {wK:cas.includes('K'), wQ:cas.includes('Q'), bK:cas.includes('k'), bQ:cas.includes('q')};
    const epStr = parts[3] || '-';
    this.epTarget = epStr === '-' ? null : this.algToRC(epStr);
    this.halfMoveClock = parseInt(parts[4]) || 0;
    this.fullMoveNumber = parseInt(parts[5]) || 1;
    this.moveHistory = [];
    this.captured = {w:[], b:[]};
    this.gameOver = false;
    this.result = null;
    this.resultReason = '';
  },

  toFEN() {
    const rows = [];
    for (let r=0; r<8; r++) {
      let row = '';
      let empty = 0;
      for (let c=0; c<8; c++) {
        if (!this.board[r][c]) { empty++; }
        else {
          if (empty) { row += empty; empty = 0; }
          row += this.board[r][c];
        }
      }
      if (empty) row += empty;
      rows.push(row);
    }
    let cas = '';
    if (this.castling.wK) cas += 'K';
    if (this.castling.wQ) cas += 'Q';
    if (this.castling.bK) cas += 'k';
    if (this.castling.bQ) cas += 'q';
    if (!cas) cas = '-';
    const ep = this.epTarget ? this.rcToAlg(this.epTarget.r, this.epTarget.c) : '-';
    return rows.join('/') + ' ' + this.turn + ' ' + cas + ' ' + ep + ' ' + this.halfMoveClock + ' ' + this.fullMoveNumber;
  },

  rcToAlg(r, c) { return String.fromCharCode(97+c) + (8-r); },
  algToRC(alg) { return {r: 8 - parseInt(alg[1]), c: alg.charCodeAt(0) - 97}; },

  clone() {
    const e = Object.create(Object.getPrototypeOf(this));
    e.board = this.board.map(row => [...row]);
    e.turn = this.turn;
    e.castling = {...this.castling};
    e.epTarget = this.epTarget ? {...this.epTarget} : null;
    e.halfMoveClock = this.halfMoveClock;
    e.fullMoveNumber = this.fullMoveNumber;
    e.moveHistory = [...this.moveHistory];
    e.captured = {w:[...this.captured.w], b:[...this.captured.b]};
    e.gameOver = this.gameOver;
    e.result = this.result;
    e.resultReason = this.resultReason;
    return e;
  },

  // ============================================================
  // MOVE GENERATION
  // ============================================================
  isEnemy(piece, color) {
    if (!piece) return false;
    return color === 'w' ? piece === piece.toLowerCase() : piece === piece.toUpperCase();
  },
  isFriend(piece, color) {
    if (!piece) return false;
    return color === 'w' ? piece === piece.toUpperCase() && piece !== piece.toLowerCase() : piece === piece.toLowerCase() && piece !== piece.toUpperCase();
  },
  isWhite(piece) { return piece && piece === piece.toUpperCase() && piece !== piece.toLowerCase(); },
  isBlack(piece) { return piece && piece === piece.toLowerCase() && piece !== piece.toUpperCase(); },
  colorOf(piece) { return this.isWhite(piece) ? 'w' : 'b'; },

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

  // Generate pseudo-legal moves (ignoring check)
  pseudoLegalMoves(color) {
    const moves = [];
    const opp = color === 'w' ? 'b' : 'w';
    for (let r = 0; r < 8; r++) {
      for (let c = 0; c < 8; c++) {
        const p = this.board[r][c];
        if (!p || (color === 'w' && !this.isWhite(p)) || (color === 'b' && !this.isBlack(p))) continue;
        const pc = p.toLowerCase();
        const isP = pc === 'p';
        const isN = pc === 'n';
        const isB = pc === 'b';
        const isR = pc === 'r';
        const isQ = pc === 'q';
        const isK = pc === 'k';

        if (isP) {
          const dir = color === 'w' ? -1 : 1;
          const startRank = color === 'w' ? 6 : 1;
          const promoRank = color === 'w' ? 0 : 7;
          // Forward 1
          if (this.inBounds(r+dir, c) && !this.board[r+dir][c]) {
            if (r+dir === promoRank) {
              for (const pro of ['Q','R','B','N']) moves.push(this._mkMove(r,c,r+dir,c,'','',pro));
            } else {
              moves.push(this._mkMove(r,c,r+dir,c));
            }
            // Forward 2
            if (r === startRank && !this.board[r+2*dir][c]) {
              moves.push(this._mkMove(r,c,r+2*dir,c,'','',null,false,true));
            }
          }
          // Captures
          for (const dc of [-1, 1]) {
            if (!this.inBounds(r+dir, c+dc)) continue;
            const target = this.board[r+dir][c+dc];
            if (target && this.isEnemy(target, color)) {
              if (r+dir === promoRank) {
                for (const pro of ['Q','R','B','N']) moves.push(this._mkMove(r,c,r+dir,c+dc,'','',pro));
              } else {
                moves.push(this._mkMove(r,c,r+dir,c+dc));
              }
            }
            // En passant
            if (this.epTarget && this.epTarget.r === r+dir && this.epTarget.c === c+dc) {
              moves.push(this._mkMove(r,c,r+dir,c+dc,'','',null,true));
            }
          }
        } else if (isN) {
          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 (!this.inBounds(nr,nc)) continue;
            const t = this.board[nr][nc];
            if (!t || this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc));
          }
        } else if (isB || isQ) {
          for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
            let nr = r+dr, nc = c+dc;
            while (this.inBounds(nr,nc)) {
              const t = this.board[nr][nc];
              if (!t) { moves.push(this._mkMove(r,c,nr,nc)); }
              else { if (this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc)); break; }
              nr += dr; nc += dc;
            }
          }
        }

        if (isR || isQ) {
          for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
            let nr = r+dr, nc = c+dc;
            while (this.inBounds(nr,nc)) {
              const t = this.board[nr][nc];
              if (!t) { moves.push(this._mkMove(r,c,nr,nc)); }
              else { if (this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc)); break; }
              nr += dr; nc += dc;
            }
          }
        }

        if (isK) {
          for (let dr=-1; dr<=1; dr++) {
            for (let dc=-1; dc<=1; dc++) {
              if (dr===0 && dc===0) continue;
              const nr = r+dr, nc = c+dc;
              if (!this.inBounds(nr,nc)) continue;
              const t = this.board[nr][nc];
              if (!t || this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc));
            }
          }
          // Castling
          if (color === 'w') {
            // O-O (kingside)
            if (this.castling.wK && r===7 && c===4 &&
                !this.board[7][5] && !this.board[7][6] && this.board[7][7]==='R' &&
                !this.isSquareAttacked(7,4,opp) && !this.isSquareAttacked(7,5,opp) && !this.isSquareAttacked(7,6,opp)) {
              moves.push(this._mkMove(7,4,7,6,'','','',false,false,false,'O-O'));
            }
            // O-O-O (queenside)
            if (this.castling.wQ && r===7 && c===4 &&
                !this.board[7][1] && !this.board[7][2] && !this.board[7][3] && this.board[7][0]==='R' &&
                !this.isSquareAttacked(7,4,opp) && !this.isSquareAttacked(7,3,opp) && !this.isSquareAttacked(7,2,opp)) {
              moves.push(this._mkMove(7,4,7,2,'','','',false,false,false,'O-O-O'));
            }
          } else {
            // O-O (black kingside)
            if (this.castling.bK && r===0 && c===4 &&
                !this.board[0][5] && !this.board[0][6] && this.board[0][7]==='r' &&
                !this.isSquareAttacked(0,4,opp) && !this.isSquareAttacked(0,5,opp) && !this.isSquareAttacked(0,6,opp)) {
              moves.push(this._mkMove(0,4,0,6,'','','',false,false,false,'O-O'));
            }
            // O-O-O (black queenside)
            if (this.castling.bQ && r===0 && c===4 &&
                !this.board[0][1] && !this.board[0][2] && !this.board[0][3] && this.board[0][0]==='r' &&
                !this.isSquareAttacked(0,4,opp) && !this.isSquareAttacked(0,3,opp) && !this.isSquareAttacked(0,2,opp)) {
              moves.push(this._mkMove(0,4,0,2,'','','',false,false,false,'O-O-O'));
            }
          }
        }
      }
    }
    return moves;
  },

  _mkMove(fr, fc, tr, tc, captured, promotion, promoPiece, enPassant, doublePush, isCastling, san) {
    return {
      from: {r:fr, c:fc}, to: {r:tr, c:tc},
      piece: this.board[fr][fc],
      captured: captured || null,
      promotion: promotion || null,
      promoPiece: promoPiece || null,
      enPassant: !!enPassant,
      doublePush: !!doublePush,
      isCastling: !!isCastling,
      san: san || '',
    };
  },

  // Check if square (r,c) is attacked by pieces of `byColor`
  isSquareAttacked(r, c, byColor) {
    // Pawns
    const pawnDir = byColor === 'w' ? 1 : -1; // pawns attack from opposite direction
    const pawnChar = byColor === 'w' ? 'p' : 'P';
    for (const dc of [-1, 1]) {
      const pr = r + pawnDir, pc = c + dc;
      if (this.inBounds(pr, pc) && this.board[pr][pc] === pawnChar) return true;
    }
    // Knights
    const nChar = byColor === 'w' ? 'n' : 'N';
    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 (this.inBounds(nr,nc) && this.board[nr][nc] === nChar) return true;
    }
    // Bishops & Queens (diagonals)
    const bqChar = byColor === 'w' ? 'q' : 'Q';
    const bChar = byColor === 'w' ? 'b' : 'B';
    for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
      let nr = r+dr, nc = c+dc;
      while (this.inBounds(nr,nc)) {
        const p = this.board[nr][nc];
        if (p) { if (p === bChar || p === bqChar) return true; break; }
        nr += dr; nc += dc;
      }
    }
    // Rooks & Queens (straight)
    const rqChar = byColor === 'w' ? 'q' : 'Q';
    const rChar = byColor === 'w' ? 'r' : 'R';
    for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
      let nr = r+dr, nc = c+dc;
      while (this.inBounds(nr,nc)) {
        const p = this.board[nr][nc];
        if (p) { if (p === rChar || p === rqChar) return true; break; }
        nr += dr; nc += dc;
      }
    }
    // Kings
    const kChar = byColor === 'w' ? 'k' : 'K';
    for (let dr=-1; dr<=1; dr++) {
      for (let dc=-1; dc<=1; dc++) {
        if (dr===0 && dc===0) continue;
        const nr = r+dr, nc = c+dc;
        if (this.inBounds(nr,nc) && this.board[nr][nc] === kChar) return true;
      }
    }
    return false;
  },

  findKing(color) {
    const k = color === 'w' ? 'K' : 'k';
    for (let r=0; r<8; r++)
      for (let c=0; c<8; c++)
        if (this.board[r][c] === k) return {r, c};
    return null;
  },

  isInCheck(color) {
    const king = this.findKing(color);
    if (!king) return false;
    const opp = color === 'w' ? 'b' : 'w';
    return this.isSquareAttacked(king.r, king.c, opp);
  },

  // Filter pseudo-legal moves to legal moves
  legalMoves(color) {
    const pseudo = this.pseudoLegalMoves(color);
    const legal = [];
    for (const move of pseudo) {
      if (this.isLegal(move)) legal.push(move);
    }
    return legal;
  },

  // Check if a pseudo-legal move is legal (doesn't leave king in check)
  isLegal(move) {
    // Quick check: if moving the king, make sure destination isn't attacked
    if (move.piece.toLowerCase() === 'k') {
      const opp = this.turn === 'w' ? 'b' : 'w';
      // For castling, we already checked squares are not attacked in generation
      // But let's double-check the destination
      if (!this.isSquareAttacked(move.to.r, move.to.c, opp)) return true;
      return false;
    }
    // Make move on a temporary board and check if own king is in check
    const tempBoard = this.board.map(row => [...row]);
    const fr = move.from.r, fc = move.from.c, tr = move.to.r, tc = move.to.c;
    tempBoard[tr][tc] = tempBoard[fr][fc];
    tempBoard[fr][fc] = null;

    // Handle en passant capture
    if (move.enPassant) {
      const capR = this.turn === 'w' ? tr + 1 : tr - 1;
      tempBoard[capR][tc] = null;
    }

    // Handle promotion (replace pawn with promoted piece)
    if (move.promoPiece) {
      tempBoard[tr][tc] = move.promoPiece;
    }

    // Find own king on temp board
    const myKing = this.turn === 'w' ? 'K' : 'k';
    let kr = -1, kc = -1;
    for (let r=0; r<8; r++)
      for (let c=0; c<8; c++)
        if (tempBoard[r][c] === myKing) { kr = r; kc = c; }

    if (kr < 0) return false; // King missing = illegal

    const opp = this.turn === 'w' ? 'b' : 'w';
    return !this.isSquareAttackedOn(kr, kc, opp, tempBoard);
  },

  // isSquareAttacked but on a given board (not this.board)
  isSquareAttackedOn(r, c, byColor, board) {
    const pawnDir = byColor === 'w' ? 1 : -1;
    const pawnChar = byColor === 'w' ? 'p' : 'P';
    for (const dc of [-1, 1]) {
      const pr = r + pawnDir, pc = c + dc;
      if (this.inBounds(pr, pc) && board[pr][pc] === pawnChar) return true;
    }
    const nChar = byColor === 'w' ? 'n' : 'N';
    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 (this.inBounds(nr,nc) && board[nr][nc] === nChar) return true;
    }
    const bqChar = byColor === 'w' ? 'q' : 'Q';
    const bChar = byColor === 'w' ? 'b' : 'B';
    for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
      let nr = r+dr, nc = c+dc;
      while (this.inBounds(nr,nc)) {
        const p = board[nr][nc];
        if (p) { if (p === bChar || p === bqChar) return true; break; }
        nr += dr; nc += dc;
      }
    }
    const rqChar = byColor === 'w' ? 'q' : 'Q';
    const rChar = byColor === 'w' ? 'r' : 'R';
    for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
      let nr = r+dr, nc = c+dc;
      while (this.inBounds(nr,nc)) {
        const p = board[nr][nc];
        if (p) { if (p === rChar || p === rqChar) return true; break; }
        nr += dr; nc += dc;
      }
    }
    const kChar = byColor === 'w' ? 'k' : 'K';
    for (let dr=-1; dr<=1; dr++) {
      for (let dc=-1; dc<=1; dc++) {
        if (dr===0 && dc===0) continue;
        const nr = r+dr, nc = c+dc;
        if (this.inBounds(nr,nc) && board[nr][nc] === kChar) return true;
      }
    }
    return false;
  },

  // ============================================================
  // MAKE / UNMAKE MOVE
  // ============================================================
  makeMove(move) {
    const undo = {
      from: {...move.from}, to: {...move.to},
      piece: move.piece,
      captured: this.board[move.to.r][move.to.c],
      capturedR: move.to.r, capturedC: move.to.c,
      castling: {...this.castling},
      epTarget: this.epTarget ? {...this.epTarget} : null,
      halfMoveClock: this.halfMoveClock,
      fullMoveNumber: this.fullMoveNumber,
      promoPiece: move.promoPiece || null,
      enPassant: move.enPassant,
      isCastling: move.isCastling,
      prevTurn: this.turn,
    };

    const fr = move.from.r, fc = move.from.c, tr = move.to.r, tc = move.to.c;

    // Move piece
    this.board[tr][tc] = this.board[fr][fc];
    this.board[fr][fc] = null;

    // En passant capture
    if (move.enPassant) {
      const capR = this.turn === 'w' ? tr + 1 : tr - 1;
      undo.captured = this.board[capR][tc];
      undo.capturedR = capR; undo.capturedC = tc;
      this.board[capR][tc] = null;
    }

    // Promotion
    if (move.promoPiece) {
      this.board[tr][tc] = move.promoPiece;
    }

    // Castling: move rook
    if (move.isCastling) {
      if (tc === 6) { // Kingside
        this.board[tr][5] = this.board[tr][7];
        this.board[tr][7] = null;
      } else if (tc === 2) { // Queenside
        this.board[tr][3] = this.board[tr][0];
        this.board[tr][0] = null;
      }
    }

    // Update castling rights
    if (move.piece === 'K') this.castling.wK = this.castling.wQ = false;
    if (move.piece === 'k') this.castling.bK = this.castling.bQ = false;
    if (move.piece === 'R' && fr===7 && fc===7) this.castling.wK = false;
    if (move.piece === 'R' && fr===7 && fc===0) this.castling.wQ = false;
    if (move.piece === 'r' && fr===0 && fc===7) this.castling.bK = false;
    if (move.piece === 'r' && fr===0 && fc===0) this.castling.bQ = false;
    if (move.piece === 'R' && tr===7 && tc===7) this.castling.wK = false;
    if (move.piece === 'R' && tr===7 && tc===0) this.castling.wQ = false;
    if (move.piece === 'r' && tr===0 && tc===7) this.castling.bK = false;
    if (move.piece === 'r' && tr===0 && tc===0) this.castling.bQ = false;

    // Update en passant target
    if (move.doublePush) {
      this.epTarget = {r: (fr+tr)/2, c: fc};
    } else {
      this.epTarget = null;
    }

    // Update half-move clock
    const isPawn = move.piece.toLowerCase() === 'p';
    const isCapture = !!move.captured || move.enPassant;
    if (isPawn || isCapture) {
      this.halfMoveClock = 0;
    } else {
      this.halfMoveClock++;
    }

    // Switch turn
    this.turn = this.turn === 'w' ? 'b' : 'w';
    if (this.turn === 'w') this.fullMoveNumber++;

    // Record captured piece
    if (undo.captured) {
      const capColor = this.isWhite(undo.captured) ? 'w' : 'b';
      this.captured[capColor].push(undo.captured);
    }

    // Record move
    move.san = this.sanitize(move);
    this.moveHistory.push(move);

    // Check game state
    this.updateGameState();

    return undo;
  },

  unmakeMove(move, undo) {
    const fr = move.from.r, fc = move.from.c, tr = move.to.r, tc = move.to.c;

    // Restore turn
    this.turn = undo.prevTurn;

    // Undo full move number
    if (this.turn === 'w') this.fullMoveNumber--;

    // Restore half-move clock
    this.halfMoveClock = undo.halfMoveClock;

    // Restore castling
    this.castling = undo.castling;

    // Restore en passant target
    this.epTarget = undo.epTarget ? {...undo.epTarget} : null;

    // Move piece back
    this.board[fr][fc] = undo.piece;
    this.board[tr][tc] = undo.captured;

    // Undo en passant capture
    if (undo.enPassant) {
      const capR = this.turn === 'w' ? tr + 1 : tr - 1;
      this.board[capR][tc] = undo.captured;
    }

    // Undo promotion
    if (undo.promoPiece) {
      this.board[fr][fc] = this.turn === 'w' ? 'P' : 'p';
    }

    // Undo castling rook
    if (undo.isCastling) {
      if (tc === 6) {
        this.board[tr][7] = this.board[tr][5];
        this.board[tr][5] = null;
      } else if (tc === 2) {
        this.board[tr][0] = this.board[tr][3];
        this.board[tr][3] = null;
      }
    }

    // Remove last move from history
    this.moveHistory.pop();

    // Restore captured piece
    if (undo.captured) {
      const capColor = this.isWhite(undo.captured) ? 'w' : 'b';
      this.captured[capColor].pop();
    }

    // Reset game over state
    this.gameOver = false;
    this.result = null;
    this.resultReason = '';
  },

  // ============================================================
  // GAME STATE CHECKING
  // ============================================================
  updateGameState() {
    const color = this.turn;
    const moves = this.legalMoves(color);
    const inCheck = this.isInCheck(color);

    if (moves.length === 0) {
      this.gameOver = true;
      if (inCheck) {
        // Checkmate
        this.result = color === 'w' ? 'black' : 'white';
        this.resultReason = 'checkmate';
      } else {
        // Stalemate
        this.result = 'draw';
        this.resultReason = 'stalemate';
      }
    } else if (inCheck) {
      // Just in check, game continues
    } else {
      // Check for draws
      if (this.halfMoveClock >= 100) {
        this.gameOver = true;
        this.result = 'draw';
        this.resultReason = '50-move rule';
      } else if (this.insufficientMaterial()) {
        this.gameOver = true;
        this.result = 'draw';
        this.resultReason = 'insufficient material';
      }
    }
  },

  insufficientMaterial() {
    const pieces = {w: [], b: []};
    for (let r=0; r<8; r++)
      for (let c=0; c<8; c++) {
        const p = this.board[r][c];
        if (p) pieces[this.isWhite(p)?'w':'b'].push(p);
      }
    // K vs K
    if (pieces.w.length === 1 && pieces.b.length === 1) return true;
    // K + B vs K
    if (pieces.w.length === 2 && pieces.b.length === 1) {
      if (pieces.w.some(p => p==='B') || pieces.w.some(p => p==='N')) return true;
    }
    if (pieces.b.length === 2 && pieces.w.length === 1) {
      if (pieces.b.some(p => p==='b') || pieces.b.some(p => p==='n')) return true;
    }
    // K + B vs K + B (same color bishops)
    if (pieces.w.length === 2 && pieces.b.length === 2) {
      const wB = pieces.w.filter(p => p==='B');
      const bB = pieces.b.filter(p => p==='b');
      if (wB.length === 1 && bB.length === 1) {
        // Check if bishops are on same color
        let wSq = -1, bSq = -1;
        for (let r=0; r<8; r++)
          for (let c=0; c<8; c++) {
            if (this.board[r][c]==='B') wSq = r+c;
            if (this.board[r][c]==='b') bSq = r+c;
          }
        if (wSq >= 0 && bSq >= 0 && (wSq % 2) === (bSq % 2)) return true;
      }
    }
    return false;
  },

  // ============================================================
  // SAN (Standard Algebraic Notation)
  // ============================================================
  sanitize(move) {
    const pc = move.piece.toLowerCase();
    const isP = pc === 'p';
    const isK = pc === 'k';
    const opp = this.turn === 'w' ? 'b' : 'w';

    // Castling
    if (move.isCastling) return move.san; // already set

    let san = '';
    if (!isP) san += move.piece;

    // Disambiguation
    const pseudo = this.pseudoLegalMoves(this.turn);
    const sameType = pseudo.filter(m =>
      m.piece === move.piece && m.to.r === move.to.r && m.to.c === move.to.c &&
      !(m.from.r === move.from.r && m.from.c === move.from.c)
    );
    if (sameType.length > 0) {
      const sameFile = sameType.some(m => m.from.c === move.from.c);
      const sameRank = sameType.some(m => m.from.r === move.from.r);
      if (!sameFile) san += String.fromCharCode(97 + move.from.c);
      else if (!sameRank) san += (8 - move.from.r);
      else san += String.fromCharCode(97 + move.from.c) + (8 - move.from.r);
    }

    // Capture
    const isCapture = !!move.captured || move.enPassant;
    if (isCapture) {
      if (isP) san += String.fromCharCode(97 + move.from.c);
      san += 'x';
    }

    san += this.rcToAlg(move.to.r, move.to.c);

    // Promotion
    if (move.promoPiece) san += '=' + move.promoPiece;

    // Check / Checkmate
    // Simulate the move to check
    const tempBoard = this.board.map(row => [...row]);
    tempBoard[move.to.r][move.to.c] = tempBoard[move.from.r][move.from.c];
    tempBoard[move.from.r][move.from.c] = null;
    if (move.enPassant) {
      const capR = this.turn === 'w' ? move.to.r + 1 : move.to.r - 1;
      tempBoard[capR][move.to.c] = null;
    }
    if (move.promoPiece) tempBoard[move.to.r][move.to.c] = move.promoPiece;

    const myKing = this.turn === 'w' ? 'K' : 'k';
    let kr = -1, kc = -1;
    for (let r=0; r<8; r++)
      for (let c=0; c<8; c++)
        if (tempBoard[r][c] === myKing) { kr = r; kc = c; }

    const oppIsAttacking = this.isSquareAttackedOn(kr, kc, opp, tempBoard);

    // If check, check if it's checkmate
    if (oppIsAttacking) {
      // Check if opponent has any legal moves
      const oppMoves = this.legalMovesOn(tempBoard, opp);
      if (oppMoves.length === 0) {
        san += '#';
      } else {
        san += '+';
      }
    }

    return san;
  },

  // Generate legal moves on a given board (used for checkmate detection)
  legalMovesOn(board, color) {
    const moves = [];
    const opp = color === 'w' ? 'b' : 'w';
    for (let r=0; r<8; r++) {
      for (let c=0; c<8; c++) {
        const p = board[r][c];
        if (!p || (color==='w' && p!==p.toUpperCase()) || (color==='b' && p!==p.toLowerCase())) continue;
        // Simplified: just check pseudo-legal and filter by king safety
        const targets = this._pseudoTargets(board, r, c, color);
        for (const {tr, tc} of targets) {
          // Make on temp board
          const tb = board.map(row => [...row]);
          tb[tr][tc] = tb[r][c];
          tb[r][c] = null;
          if (p.toLowerCase() === 'p' && tb[tr][tc] !== (color==='w'?'P':'p') && (tr===0||tr===7)) {
            tb[tr][tc] = color==='w'?'Q':'q'; // default to queen for check
          }
          // Find king
          const myK = color==='w'?'K':'k';
          let kr=-1, kc=-1;
          for (let rr=0;rr<8;rr++) for(let cc=0;cc<8;cc++) if(tb[rr][cc]===myK){kr=rr;kc=cc;}
          if (kr<0) continue;
          if (!this.isSquareAttackedOn(kr, kc, opp, tb)) {
            moves.push({from:{r,c}, to:{r:tr,c:tc}});
          }
        }
      }
    }
    return moves;
  },

  _pseudoTargets(board, r, c, color) {
    const targets = [];
    const p = board[r][c];
    const pc = p.toLowerCase();
    if (pc==='p') {
      const dir = color==='w'?-1:1;
      const pr = r+dir;
      if (this.inBounds(pr,c) && !board[pr][c]) targets.push({r:pr,c});
      if ((color==='w'&&r===6)||(color==='b'&&r===1)) {
        const pr2 = r+2*dir;
        if (!board[pr][c] && !board[pr2][c]) targets.push({r:pr2,c});
      }
      for (const dc of [-1,1]) {
        const pr2=r+dir, pc2=c+dc;
        if (this.inBounds(pr2,pc2)) {
          const t=board[pr2][pc2];
          if (t && this.isEnemy(t,color)) targets.push({r:pr2,c:pc2});
        }
      }
    } else if (pc==='n') {
      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 (this.inBounds(nr,nc)) {
          const t=board[nr][nc];
          if (!t||this.isEnemy(t,color)) targets.push({r:nr,c:nc});
        }
      }
    } else if (pc==='b'||pc==='q') {
      for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
        let nr=r+dr,nc=c+dc;
        while(this.inBounds(nr,nc)){
          const t=board[nr][nc];
          if(!t){targets.push({r:nr,c:nc});}else{if(this.isEnemy(t,color))targets.push({r:nr,c:nc});break;}
          nr+=dr;nc+=dc;
        }
      }
    }
    if (pc==='r'||pc==='q') {
      for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
        let nr=r+dr,nc=c+dc;
        while(this.inBounds(nr,nc)){
          const t=board[nr][nc];
          if(!t){targets.push({r:nr,c:nc});}else{if(this.isEnemy(t,color))targets.push({r:nr,c:nc});break;}
          nr+=dr;nc+=dc;
        }
      }
    }
    if (pc==='k') {
      for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){
        if(dr===0&&dc===0)continue;
        const nr=r+dr,nc=c+dc;
        if(this.inBounds(nr,nc)){const t=board[nr][nc];if(!t||this.isEnemy(t,color))targets.push({r:nr,c:nc});}
      }
    }
    return targets;
  },
};

// ============================================================
// AI ENGINE — Minimax with alpha-beta pruning
// ============================================================
const AI = {
  evaluate(engine) {
    if (engine.gameOver) {
      if (engine.result === 'white') return 100000;
      if (engine.result === 'black') return -100000;
      return 0; // draw
    }
    let score = 0;
    for (let r=0;r<8;r++) for(let c=0;c<8;c++) {
      const p = engine.board[r][c];
      if (!p) continue;
      const isW = engine.isWhite(p);
      const v = CFG.pieceValue[p] || 0;
      const pst = isW ? CFG.pst[p] : CFG.pstBlack[p];
      const pstVal = pst ? pst[r*8+c] : 0;
      score += (isW ? 1 : -1) * (v + pstVal * 0.1);
    }
    // Mobility bonus
    const mobility = engine.legalMoves('w').length - engine.legalMoves('b').length;
    score += mobility * 2;
    return score;
  },

  orderMoves(moves, engine) {
    return moves.sort((a, b) => {
      const aCap = a.captured || a.enPassant ? 1 : 0;
      const bCap = b.captured || b.enPassant ? 1 : 0;
      if (aCap !== bCap) return bCap - aCap;
      // MVV-LVA for captures
      if (aCap && bCap) {
        const aVal = CFG.pieceValue[a.captured || 'P'] || 0;
        const bVal = CFG.pieceValue[b.captured || 'P'] || 0;
        return bVal - aVal;
      }
      return 0;
    });
  },

  minimax(engine, depth, alpha, beta, maximizing) {
    if (depth === 0) return this.evaluate(engine);

    const color = maximizing ? 'w' : 'b';
    let moves = engine.legalMoves(color);
    if (moves.length === 0) {
      if (engine.isInCheck(color)) return maximizing ? -100000 + (depth * 10) : 100000 - (depth * 10);
      return 0; // stalemate
    }

    moves = this.orderMoves(moves, engine);

    if (maximizing) {
      let maxEval = -Infinity;
      for (const move of moves) {
        const undo = engine.makeMove(move);
        const ev = this.minimax(engine, depth - 1, alpha, beta, false);
        engine.unmakeMove(move, undo);
        maxEval = Math.max(maxEval, ev);
        alpha = Math.max(alpha, ev);
        if (beta <= alpha) break;
      }
      return maxEval;
    } else {
      let minEval = Infinity;
      for (const move of moves) {
        const undo = engine.makeMove(move);
        const ev = this.minimax(engine, depth - 1, alpha, beta, true);
        engine.unmakeMove(move, undo);
        minEval = Math.min(minEval, ev);
        beta = Math.min(beta, ev);
        if (beta <= alpha) break;
      }
      return minEval;
    }
  },

  getBestMove(engine) {
    const color = engine.turn;
    let moves = engine.legalMoves(color);
    if (moves.length === 0) return null;

    moves = this.orderMoves(moves, engine);
    const maximizing = color === 'w';
    let bestMove = moves[0];
    let bestEval = maximizing ? -Infinity : Infinity;
    const depth = CFG.aiDepth;

    for (const move of moves) {
      const undo = engine.makeMove(move);
      const ev = this.minimax(engine, depth - 1, -Infinity, Infinity, !maximizing);
      engine.unmakeMove(move, undo);

      if (maximizing) {
        if (ev > bestEval) { bestEval = ev; bestMove = move; }
      } else {
        if (ev < bestEval) { bestEval = ev; bestMove = move; }
      }
    }

    return bestMove;
  },
};

// ============================================================
// SOUND ENGINE — Web Audio API
// ============================================================
const Sound = {
  ctx: null,
  muted: false,

  init() {
    try { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
  },

  play(freq, dur, type='sine', vol=0.15) {
    if (this.muted || !this.ctx) return;
    if (this.ctx.state === 'suspended') this.ctx.resume();
    const osc = this.ctx.createOscillator();
    const gain = this.ctx.createGain();
    osc.type = type;
    osc.frequency.value = freq;
    gain.gain.value = vol;
    gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + dur);
    osc.connect(gain);
    gain.connect(this.ctx.destination);
    osc.start();
    osc.stop(this.ctx.currentTime + dur);
  },

  move() { this.play(600, 0.08, 'sine', 0.12); },
  capture() { this.play(300, 0.15, 'triangle', 0.18); },
  check() { this.play(800, 0.2, 'square', 0.1); },
  castle() { this.play(500, 0.06, 'sine', 0.1); setTimeout(()=>this.play(700, 0.06, 'sine', 0.1), 60); },
  promote() { this.play(800, 0.06, 'sine', 0.12); setTimeout(()=>this.play(1000, 0.08, 'sine', 0.12), 60); },
  toggle() { this.muted = !this.muted; return this.muted; },
};

// ============================================================
// UI RENDERING & INTERACTION
// ============================================================
const UI = {
  boardEl: null,
  selected: null,       // {r, c} of selected piece
  legalMoves: [],       // legal moves for selected piece
  lastMove: null,       // {from, to} of last move
  flipped: false,
  dragSrc: null,
  dragEl: null,
  promoting: false,
  promotionCallback: null,

  init() {
    this.boardEl = document.getElementById('board');
    this.dragEl = document.getElementById('drag-ghost');
    this.render();
    this.bindEvents();
  },

  render() {
    this.boardEl.innerHTML = '';
    for (let ri = 0; ri < 8; ri++) {
      for (let ci = 0; ci < 8; ci++) {
        const r = this.flipped ? 7 - ri : ri;
        const c = this.flipped ? 7 - ci : ci;
        const sq = document.createElement('div');
        sq.className = 'square ' + ((r + c) % 2 === 0 ? 'light' : 'dark');
        sq.dataset.r = r;
        sq.dataset.c = c;

        const piece = Engine.board[r][c];
        if (piece) {
          sq.textContent = this.unicodePiece(piece);
          sq.classList.add('piece');
        }

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

        // Check indicator
        if (Engine.gameOver && Engine.resultReason === 'checkmate' && piece && piece.toLowerCase()==='k' &&
            ((Engine.turn==='w' && Engine.isWhite(piece)) || (Engine.turn==='b' && Engine.isBlack(piece)))) {
          // Actually, check the king that's in check
        }
        if (!Engine.gameOver && Engine.isInCheck(Engine.turn)) {
          const king = Engine.findKing(Engine.turn);
          if (king && king.r===r && king.c===c) sq.classList.add('in-check');
        }

        // Selected
        if (this.selected && this.selected.r===r && this.selected.c===c) {
          sq.classList.add('selected');
        }

        // Move indicators
        if (this.selected) {
          const lm = this.legalMoves.find(m => m.to.r===r && m.to.c===c);
          if (lm) {
            if (lm.captured || lm.enPassant) {
              const ring = document.createElement('div');
              ring.className = 'capture-ring';
              sq.appendChild(ring);
            } else {
              const dot = document.createElement('div');
              dot.className = 'move-dot';
              sq.appendChild(dot);
            }
          }
        }

        this.boardEl.appendChild(sq);
      }
    }
  },

  unicodePiece(p) {
    const map = { 'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙',
                  'k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟' };
    return map[p] || p;
  },

  bindEvents() {
    // Click-to-move
    this.boardEl.addEventListener('click', (e) => {
      const sq = e.target.closest('.square');
      if (!sq) return;
      const r = +sq.dataset.r, c = +sq.dataset.c;
      this.handleClick(r, c);
    });

    // Drag-and-drop (pointer events)
    this.boardEl.addEventListener('pointerdown', (e) => {
      const sq = e.target.closest('.square');
      if (!sq) return;
      const r = +sq.dataset.r, c = +sq.dataset.c;
      const piece = Engine.board[r][c];
      if (!piece) return;
      const isWhiteTurn = Engine.turn === 'w';
      const isWhitePiece = Engine.isWhite(piece);
      if (isWhiteTurn !== isWhitePiece) return; // can only drag own pieces
      if (Engine.gameOver) return;
      if (Engine.promoting) return;

      this.dragSrc = {r, c};
      this.dragEl.textContent = this.unicodePiece(piece);
      this.dragEl.style.display = 'block';
      this.dragEl.style.left = e.clientX + 'px';
      this.dragEl.style.top = e.clientY + 'px';
      this.dragEl.style.fontSize = 'calc(var(--sq-size) * 0.85)';
      e.preventDefault();
    });

    document.addEventListener('pointermove', (e) => {
      if (!this.dragSrc) return;
      this.dragEl.style.left = e.clientX + 'px';
      this.dragEl.style.top = e.clientY + 'px';
      // Highlight target square
      const target = document.elementFromPoint(e.clientX, e.clientY);
      this.boardEl.querySelectorAll('.square').forEach(s => s.classList.remove('drag-over'));
      if (target) {
        const sq = target.closest('.square');
        if (sq) sq.classList.add('drag-over');
      }
    });

    document.addEventListener('pointerup', (e) => {
      if (!this.dragSrc) return;
      this.dragEl.style.display = 'none';
      this.boardEl.querySelectorAll('.square').forEach(s => s.classList.remove('drag-over'));

      const target = document.elementFromPoint(e.clientX, e.clientY);
      const sq = target ? target.closest('.square') : null;
      if (sq) {
        const r = +sq.dataset.r, c = +sq.dataset.c;
        const move = this.legalMoves.find(m => m.to.r===r && m.to.c===c);
        if (move) {
          this.executeMove(move);
        }
      }
      this.dragSrc = null;
      this.legalMoves = [];
      this.selected = null;
      this.render();
    });

    // Controls
    document.getElementById('btn-new').addEventListener('click', () => Game.newGame());
    document.getElementById('btn-undo').addEventListener('click', () => Game.undo());
    document.getElementById('btn-flip').addEventListener('click', () => Game.flipBoard());

    // Difficulty
    for (let d = 1; d <= 3; d++) {
      document.getElementById('btn-depth' + d).addEventListener('click', () => Game.setDepth(d));
    }

    // Mute
    document.getElementById('btn-mute').addEventListener('click', () => Game.toggleMute());
  },

  handleClick(r, c) {
    if (Engine.gameOver || Engine.promoting) return;
    if (Engine.turn !== Game.playerColor) return; // AI's turn

    const piece = Engine.board[r][c];

    // If a piece is already selected
    if (this.selected) {
      // Clicking the same piece → deselect
      if (this.selected.r === r && this.selected.c === c) {
        this.selected = null;
        this.legalMoves = [];
        this.render();
        return;
      }
      // Try to move
      const move = this.legalMoves.find(m => m.to.r===r && m.to.c===c);
      if (move) {
        this.executeMove(move);
        return;
      }
      // Click on another own piece → reselect
      if (piece && ((Engine.turn==='w' && Engine.isWhite(piece)) || (Engine.turn==='b' && Engine.isBlack(piece)))) {
        this.selected = {r, c};
        this.legalMoves = Engine.legalMoves(Engine.turn).filter(m => m.from.r===r && m.from.c===c);
        this.render();
        return;
      }
      // Click on empty or enemy square without a valid move → deselect
      this.selected = null;
      this.legalMoves = [];
      this.render();
      return;
    }

    // No piece selected — select this one if it's ours
    if (piece && ((Engine.turn==='w' && Engine.isWhite(piece)) || (Engine.turn==='b' && Engine.isBlack(piece)))) {
      this.selected = {r, c};
      this.legalMoves = Engine.legalMoves(Engine.turn).filter(m => m.from.r===r && m.from.c===c);
      this.render();
    }
  },

  executeMove(move) {
    // Handle promotion
    if (move.piece.toLowerCase() === 'p' && (move.to.r === 0 || move.to.r === 7)) {
      if (Engine.turn === Game.playerColor) {
        this.showPromotionPicker(move);
        return;
      } else {
        move.promoPiece = 'Q'; // AI always promotes to queen
      }
    }
    this.doMove(move);
  },

  doMove(move) {
    const undo = Engine.makeMove(move);
    this.lastMove = {from: {...move.from}, to: {...move.to}};
    Sound.move();
    if (move.captured || move.enPassant) Sound.capture();
    if (move.isCastling) Sound.castle();
    if (move.promoPiece) Sound.promote();

    // Check if opponent is in check after move
    const opp = Engine.turn === 'w' ? 'b' : 'w';
    if (Engine.isInCheck(opp)) {
      // Will be shown on board
      const oppKing = Engine.findKing(opp);
      if (oppKing) Sound.check();
    }

    this.selected = null;
    this.legalMoves = [];
    this.render();
    this.updateSidebar();
    Game.saveState();

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

    // AI move
    if (Game.aiEnabled && Engine.turn === Game.aiColor) {
      Game.aiThinking = true;
      this.updateStatus('AI thinking...');
      setTimeout(() => {
        Game.makeAIMove();
        Game.aiThinking = false;
      }, 50);
    }
  },

  showPromotionPicker(move) {
    Engine.promoting = true;
    const overlay = document.getElementById('promotion-overlay');
    const sq = this.boardEl.querySelector(`[data-r="${move.to.r}"][data-c="${move.to.c}"]`);
    if (!sq) return;

    const rect = sq.getBoundingClientRect();
    const container = document.getElementById('board-container');
    const containerRect = container.getBoundingClientRect();

    overlay.style.left = (rect.left - containerRect.left) + 'px';
    overlay.style.top = (rect.top - containerRect.top) + 'px';
    overlay.style.width = rect.width + 'px';
    overlay.style.height = rect.height + 'px';

    overlay.innerHTML = '';
    const pieces = Engine.turn === 'w' ? ['Q','R','B','N'] : ['q','r','b','n'];
    const labels = ['♕','♖','♗','♘'];
    for (let i = 0; i < 4; i++) {
      const btn = document.createElement('button');
      btn.className = 'promo-btn';
      btn.textContent = labels[i];
      btn.addEventListener('click', () => {
        move.promoPiece = pieces[i];
        overlay.classList.remove('visible');
        Engine.promoting = false;
        this.doMove(move);
      });
      overlay.appendChild(btn);
    }

    overlay.classList.add('visible');
  },

  showGameOver() {
    const banner = document.getElementById('game-over');
    let text = '';
    if (Engine.result === 'draw') {
      text = 'Draw — ' + Engine.resultReason;
    } else {
      text = (Engine.result === 'white' ? 'White' : 'Black') + ' wins by ' + Engine.resultReason;
    }
    banner.innerHTML = '<div>' + text + '</div><button onclick="Game.newGame()">New Game</button>';
    banner.classList.add('visible');
  },

  hideGameOver() {
    document.getElementById('game-over').classList.remove('visible');
  },

  updateSidebar() {
    // Status
    const statusEl = document.getElementById('status');
    if (Engine.gameOver) {
      statusEl.textContent = Engine.result === 'draw' ? 'Draw — ' + Engine.resultReason :
        (Engine.result === 'white' ? 'White wins!' : 'Black wins!');
      statusEl.className = '';
    } else {
      const turnName = Engine.turn === 'w' ? 'White' : 'Black';
      const inCheck = Engine.isInCheck(Engine.turn);
      statusEl.textContent = turnName + ' to move' + (inCheck ? ' (Check!)' : '');
      statusEl.className = Engine.turn === 'w' ? 'wt' : 'bt';
    }

    // Captured pieces
    const capEl = document.getElementById('captured');
    capEl.innerHTML = '';
    const allCaptured = [...Engine.captured.b].sort().reverse().join('') + ' ' + [...Engine.captured.w].sort().join('');
    capEl.textContent = allCaptured || '—';

    // Move list
    const listEl = document.getElementById('move-list');
    let html = '';
    for (let i = 0; i < Engine.moveHistory.length; i += 2) {
      const num = Math.floor(i / 2) + 1;
      const whiteMove = Engine.moveHistory[i] ? Engine.moveHistory[i].san : '';
      const blackMove = Engine.moveHistory[i+1] ? Engine.moveHistory[i+1].san : '';
      html += num + '. ' + whiteMove + (blackMove ? ' ' + blackMove : '') + '\n';
    }
    listEl.textContent = html;
    listEl.scrollTop = listEl.scrollHeight;
  },

  updateStatus(text) {
    document.getElementById('status').textContent = text;
  },
};

// ============================================================
// GAME CONTROLLER
// ============================================================
const Game = {
  playerColor: 'w',
  aiColor: 'b',
  aiEnabled: true,
  aiThinking: false,
  flipState: false,

  init() {
    Sound.init();
    Engine.init();
    UI.init();
    this.tryLoadState();
    UI.updateSidebar();
    UI.render();

    // Debug interface
    window.game = {
      fen: () => Engine.toFEN(),
      move: (longAlg) => {
        const fr = Engine.algToRC(longAlg.substring(0,2));
        const tr = Engine.algToRC(longAlg.substring(2,4));
        const moves = Engine.legalMoves(Engine.turn);
        const move = moves.find(m => m.from.r===fr.r && m.from.c===fr.c && m.to.r===tr.r && m.to.c===tr.c);
        if (!move) return null;
        // Handle promotion
        if (move.piece.toLowerCase() === 'p' && (move.to.r === 0 || move.to.r === 7)) {
          move.promoPiece = 'Q';
        }
        const undo = Engine.makeMove(move);
        UI.lastMove = {from: {...move.from}, to: {...move.to}};
        UI.render();
        UI.updateSidebar();
        this.saveState();
        return move;
      },
      legalMoves: () => Engine.legalMoves(Engine.turn),
      board: () => Engine.board,
    };
  },

  newGame() {
    Engine.init();
    UI.selected = null;
    UI.legalMoves = [];
    UI.lastMove = null;
    UI.promoting = false;
    UI.hideGameOver();
    UI.render();
    UI.updateSidebar();
    this.saveState();
    // If AI plays white, AI goes first
    if (this.aiEnabled && this.aiColor === 'w') {
      this.makeAIMove();
    }
  },

  undo() {
    if (Engine.moveHistory.length === 0) return;
    // Undo AI move + player move (2 moves)
    for (let i = 0; i < 2 && Engine.moveHistory.length > 0; i++) {
      const lastMove = Engine.moveHistory[Engine.moveHistory.length - 1];
      const undo = Engine.unmakeMove(lastMove);
      // We need to properly unmake — but unmakeMove expects the undo info
      // Actually, let me fix this: I need to store undo info properly
    }
    // Simpler approach: reload from saved state
    this.tryLoadState();
    UI.render();
    UI.updateSidebar();
  },

  flipBoard() {
    UI.flipped = !UI.flipped;
    UI.render();
  },

  setDepth(d) {
    CFG.aiDepth = d;
    document.querySelectorAll('[id^="btn-depth"]').forEach(b => b.classList.remove('active'));
    document.getElementById('btn-depth' + d).classList.add('active');
  },

  toggleMute() {
    const muted = Sound.toggle();
    document.getElementById('btn-mute').textContent = muted ? '🔇' : '🔊';
  },

  makeAIMove() {
    if (Engine.gameOver || Engine.turn !== this.aiColor) return;
    const move = AI.getBestMove(Engine);
    if (!move) return;
    // AI always promotes to Queen
    if (move.piece.toLowerCase() === 'p' && (move.to.r === 0 || move.to.r === 7)) {
      move.promoPiece = Engine.turn === 'w' ? 'Q' : 'q';
    }
    UI.lastMove = {from: {...move.from}, to: {...move.to}};
    const undo = Engine.makeMove(move);
    Sound.move();
    if (move.captured || move.enPassant) Sound.capture();
    if (move.isCastling) Sound.castle();
    if (move.promoPiece) Sound.promote();

    UI.selected = null;
    UI.legalMoves = [];
    UI.render();
    UI.updateSidebar();
    this.saveState();

    if (Engine.gameOver) {
      UI.showGameOver();
      return;
    }
  },

  saveState() {
    try {
      const state = {
        fen: Engine.toFEN(),
        playerColor: this.playerColor,
        flipped: UI.flipped,
        aiDepth: CFG.aiDepth,
      };
      localStorage.setItem('chess_save', JSON.stringify(state));
    } catch(e) {}
  },

  tryLoadState() {
    try {
      const raw = localStorage.getItem('chess_save');
      if (!raw) return false;
      const state = JSON.parse(raw);
      Engine.fromFEN(state.fen);
      this.playerColor = state.playerColor || 'w';
      UI.flipped = state.flipped || false;
      CFG.aiDepth = state.aiDepth || 3;
      return true;
    } catch(e) { return false; }
  },
};

// ============================================================
// INITIALIZATION
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
  Game.init();
});
</script>
</body>
</html>