← Chess results

Chess

Deepseek v4 Flash Vision Exp · 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 46,441 bytes · SHA-256 9af68ef24ce5
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — Vanilla JS Rules Engine + AI</title>
<style>
  /* ============================================================
     TUNABLE CONSTANTS (colors, board look)
     ============================================================ */
  :root{
    --wood-light:#f0d9b5;
    --wood-dark:#b58863;
    --wood-light-sel:#f7e8b5;
    --wood-dark-sel:#d9a86c;
    --accent:#2e86ab;
    --glow:#e63946;
    --panel:#2b2620;
    --panel-text:#f1e9d8;
    --panel-muted:#bfae90;
    --board-size:min(80vmin, 600px);
  }
  *{ box-sizing:border-box; margin:0; padding:0; }
  html,body{ height:100%; }
  body{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    background:radial-gradient(circle at 50% 20%, #3a3228, #1c1712 70%);
    color:var(--panel-text);
    min-height:100vh;
    -webkit-user-select:none; user-select:none;
    display:flex; justify-content:center;
  }
  #app{
    width:100%; max-width:1080px;
    padding:14px 18px 30px;
    display:flex; flex-direction:column; align-items:center; gap:18px;
  }
  header{ text-align:center; }
  header h1{ font-size:30px; letter-spacing:2px; font-weight:600;
    background:linear-gradient(180deg,#f7ead0,#c8a878); -webkit-background-clip:text; background-clip:text; -webkit-text-fill-color:transparent; }
  header p{ color:var(--panel-muted); font-size:12px; letter-spacing:1px; margin-top:2px; }

  main{ display:flex; gap:22px; align-items:flex-start; justify-content:center; flex-wrap:wrap; width:100%; }

  /* ---------- Board ---------- */
  #board-wrap{ position:relative; }
  #board{
    width:var(--board-size); height:var(--board-size);
    display:grid; grid-template-columns:repeat(8,1fr); grid-template-rows:repeat(8,1fr);
    border:8px solid #4a3b2c; border-radius:6px; overflow:hidden;
    box-shadow:0 18px 50px rgba(0,0,0,.55), inset 0 0 0 1px rgba(0,0,0,.3);
    background:#000;
    cursor:pointer;
  }
  .square{
    position:relative; display:flex; align-items:center; justify-content:center;
  }
  .square.light{ background:var(--wood-light); }
  .square.dark{ background:var(--wood-dark); }
  .square.light.selected{ background:var(--wood-light-sel); }
  .square.dark.selected{ background:var(--wood-dark-sel); }
  .square .piece{
    font-size:calc(var(--board-size)/8*0.78);
    line-height:1; z-index:2; pointer-events:none;
    filter:drop-shadow(0 2px 2px rgba(0,0,0,.35));
  }
  .square .piece.white{ color:#fdf6e3; text-shadow:0 1px 2px rgba(0,0,0,.55); }
  .square .piece.black{ color:#2a221a; text-shadow:0 1px 1px rgba(255,255,255,.18); }

  .square.last-move::after{
    content:""; position:absolute; inset:0;
    background:rgba(255,214,120,.45); mix-blend-mode:multiply; pointer-events:none;
  }
  .square.selected::before{
    content:""; position:absolute; inset:0;
    background:rgba(255,215,130,.55); mix-blend-mode:multiply; pointer-events:none;
  }
  .square.check{ box-shadow:inset 0 0 0 4px var(--glow), inset 0 0 24px var(--glow); }
  .square.check .piece{ animation:checkPulse 1.1s ease-in-out infinite; }
  @keyframes checkPulse{ 0%,100%{ text-shadow:0 2px 4px rgba(230,57,70,.5);} 50%{ text-shadow:0 2px 16px rgba(230,57,70,.95);} }

  /* legal move dots / capture rings */
  .dot{
    width:calc(var(--board-size)/8*0.30); height:calc(var(--board-size)/8*0.30);
    border-radius:50%; background:rgba(35,27,20,.35);
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    z-index:3; pointer-events:none; box-shadow:0 0 0 1px rgba(0,0,0,.12);
  }
  .ring{
    width:calc(var(--board-size)/8*0.86); height:calc(var(--board-size)/8*0.86);
    border-radius:50%; border:5px solid rgba(35,27,20,.4);
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    z-index:3; pointer-events:none; box-shadow:0 0 0 1px rgba(0,0,0,.15);
  }
  .castle-marker{
    width:calc(var(--board-size)/8*0.72); height:calc(var(--board-size)/8*0.72);
    border-radius:6px; border:4px solid rgba(255,215,130,.75);
    position:absolute; top:50%; left:50%; transform:translate(-50%,-50%);
    z-index:3; pointer-events:none;
  }

  /* drag ghost */
  #drag-ghost{
    position:fixed; z-index:99; pointer-events:none; display:none;
    font-size:calc(var(--board-size)/8*0.78); line-height:1;
  }
  #drag-ghost.white{ color:#fdf6e3; text-shadow:0 2px 3px rgba(0,0,0,.6); }
  #drag-ghost.black{ color:#2a221a; text-shadow:0 1px 2px rgba(255,255,255,.25); }

  /* ---------- Promotion overlay ---------- */
  #promo{
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:rgba(20,15,10,.55); z-index:8; backdrop-filter:blur(2px);
  }
  #promo.hidden{ display:none; }
  #promo .box{
    background:var(--panel); border:1px solid #5a4a36; border-radius:10px;
    padding:16px 18px; box-shadow:0 12px 40px rgba(0,0,0,.6); text-align:center;
  }
  #promo .box h3{ font-size:14px; color:var(--panel-muted); margin-bottom:12px; letter-spacing:1px; }
  #promo .opts{ display:flex; gap:10px; }
  #promo button{
    font-size:40px; width:64px; height:64px; background:#3a3228; color:var(--panel-text);
    border:1px solid #5a4a36; border-radius:8px; cursor:pointer; transition:.12s;
  }
  #promo button:hover{ background:var(--accent); transform:translateY(-2px); }
  #promo .cancel{ margin-top:12px; font-size:12px; color:var(--panel-muted); cursor:pointer; }

  /* ---------- Banner ---------- */
  #banner{
    position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
    background:rgba(18,14,10,.62); z-index:9;
    animation:fadeIn .3s ease;
  }
  #banner.hidden{ display:none; }
  #banner .card{
    background:var(--panel); border:1px solid #5a4a36; border-radius:12px; padding:26px 34px;
    text-align:center; box-shadow:0 16px 50px rgba(0,0,0,.65);
  }
  #banner .title{ font-size:30px; letter-spacing:1px; margin-bottom:6px; }
  #banner .sub{ color:var(--panel-muted); margin-bottom:18px; }
  #banner button{ padding:10px 22px; }
  @keyframes fadeIn{ from{opacity:0} to{opacity:1} }

  /* ---------- Side panel ---------- */
  #panel{
    width:300px; background:var(--panel); border:1px solid #4a3b2c; border-radius:12px;
    padding:18px; display:flex; flex-direction:column; gap:16px;
    box-shadow:0 12px 30px rgba(0,0,0,.4);
  }
  #turn{
    display:flex; align-items:center; gap:10px; font-size:15px; font-weight:600; letter-spacing:.5px;
  }
  #turn .dot{ width:16px; height:16px; border-radius:50%; display:inline-block; position:static; transform:none; }
  #turn .dot.w{ background:#fdf6e3; box-shadow:0 0 0 2px #5a4a36; }
  #turn .dot.b{ background:#2a221a; box-shadow:0 0 0 2px #5a4a36; }
  #turn .think{ font-size:11px; color:var(--accent); margin-left:auto; }

  .captured-tray{ display:flex; align-items:center; gap:8px; min-height:26px; }
  .captured-tray .label{ font-size:11px; color:var(--panel-muted); width:52px; }
  .captured-tray .pieces{ display:flex; flex-wrap:wrap; }
  .captured-tray .p{ font-size:22px; line-height:1; }
  .captured-tray .p.b{ color:#d9cebb; text-shadow:0 1px 1px #000; }
  .captured-tray .p.w{ color:#fdf6e3; text-shadow:0 1px 1px #000; }
  .captured-tray .adv{ font-size:11px; color:var(--panel-muted); margin-left:6px; }

  #move-list{
    flex:1; min-height:150px; max-height:280px; overflow-y:auto;
    background:#221d17; border:1px solid #3c3227; border-radius:8px; padding:10px;
    font-family:ui-monospace,Menlo,Consolas,monospace; font-size:14px; line-height:1.7;
  }
  #move-list .ply{ display:inline-block; width:34px; color:var(--panel-muted); }
  #move-list .san{ display:inline-block; margin-right:12px; cursor:pointer; }
  #move-list .san:hover{ color:var(--accent); }
  #move-list .muted{ color:#5c4f3f; }

  .controls{ display:grid; grid-template-columns:1fr 1fr; gap:8px; }
  .controls .full{ grid-column:1 / -1; }
  button{
    background:#3a3228; color:var(--panel-text); border:1px solid #5a4a36; border-radius:8px;
    padding:9px; font-size:13px; cursor:pointer; transition:.12s; letter-spacing:.3px;
  }
  button:hover{ background:var(--accent); border-color:var(--accent); }
  button:active{ transform:translateY(1px); }
  button.primary{ background:var(--accent); border-color:var(--accent); }
  button.primary:hover{ background:#3b9bc4; }
  .field{ display:flex; flex-direction:column; gap:4px; }
  .field label{ font-size:11px; color:var(--panel-muted); letter-spacing:.5px; }
  select{
    background:#3a3228; color:var(--panel-text); border:1px solid #5a4a36; border-radius:8px;
    padding:7px; font-size:13px; cursor:pointer; width:100%;
  }
  .status-line{ font-size:12px; color:var(--panel-muted); }

  @media (max-width:760px){
    #app{ padding:10px; }
    #panel{ width:min(94vw, 420px); }
    #move-list{ max-height:160px; }
  }
</style>
</head>
<body>
  <div id="app">
    <header>
      <h1>♞ CHESS</h1>
      <p>rule-complete engine · minimax AI</p>
    </header>
    <main>
      <div id="board-wrap">
        <div id="board"></div>
        <div id="promo" class="hidden"></div>
        <div id="banner" class="hidden"></div>
      </div>
      <aside id="panel">
        <div id="turn"><span class="dot w" id="turn-dot"></span><span id="turn-text">White to move</span><span class="think" id="thinking" style="display:none">thinking…</span></div>
        <div class="captured-tray"><span class="label">Captured</span><span class="pieces" id="cap-w"></span><span class="adv" id="adv-w"></span></div>
        <div class="captured-tray"><span class="label">Captured</span><span class="pieces" id="cap-b"></span><span class="adv" id="adv-b"></span></div>
        <div id="move-list"></div>
        <div class="controls">
          <div class="field">
            <label>Play as</label>
            <select id="side-select">
              <option value="w">White</option>
              <option value="b">Black</option>
              <option value="both">Both (analysis)</option>
            </select>
          </div>
          <div class="field">
            <label>Difficulty</label>
            <select id="diff-select">
              <option value="1">Depth 1</option>
              <option value="2" selected>Depth 2</option>
              <option value="3">Depth 3</option>
            </select>
          </div>
          <button class="full primary" id="btn-new">♟ New Game</button>
          <button id="btn-undo">⟲ Undo</button>
          <button id="btn-flip">⇄ Flip</button>
          <button class="full" id="btn-mute">🔊 Sound On</button>
        </div>
        <div class="status-line" id="status-line"></div>
      </aside>
    </main>
  </div>

<script>
/* =====================================================================
   SECTION 1 — TUNABLES + CONSTANTS
   ===================================================================== */
const TUNE = {
  // AI search depth (0 = use difficulty selector value at runtime)
  defaultDepth: 2,
  maxDepth: 3,
  // evaluation weights (centipawns)
  material: { P:100, N:320, B:330, R:500, Q:900, K:0 },
  checkmateScore: 100000,
};

const FILES = 'abcdefgh';
const GLYPHS = { K:'♔', Q:'♕', R:'♖', B:'♗', N:'♘', P:'♙',
                 k:'♚', q:'♛', r:'♜', b:'♝', n:'♞', p:'♟' };
const MATE = TUNE.checkmateScore;

// piece-square tables — index 0..63, rank 0 = the piece's home rank (white orientation)
const 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,  5, 10, 10,  5,  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,
       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],
};

/* =====================================================================
   SECTION 2 — BOARD HELPERS
   ===================================================================== */
function pieceType(p){ return p.toUpperCase(); }
function pieceColor(p){ return p === p.toUpperCase() ? 'w' : 'b'; }
function other(c){ return c === 'w' ? 'b' : 'w'; }
function sqName(i){ return FILES[i % 8] + (Math.floor(i / 8) + 1); }
function inBoard(f, r){ return f >= 0 && f < 8 && r >= 0 && r < 8; }
function mirror(i){ const f = i % 8, r = Math.floor(i / 8); return (7 - r) * 8 + f; }

function makeEmptyBoard(){ return new Array(64).fill(null); }
function initialBoard(){
  const b = makeEmptyBoard();
  const back = ['R','N','B','Q','K','B','N','R'];
  for (let f = 0; f < 8; f++){
    b[f] = back[f];               // white back rank (rank index 0)
    b[8 + f] = 'P';               // white pawns
    b[56 + f] = back[f].toLowerCase(); // black back rank
    b[48 + f] = 'p';              // black pawns
  }
  return b;
}
function newState(){
  return {
    board: initialBoard(),
    turn: 'w',
    castling: { K:true, Q:true, k:true, q:true },
    ep: -1,          // en-passant target square, -1 if none
    halfmove: 0,
    fullmove: 1,
  };
}
function cloneState(s){
  return {
    board: s.board.slice(),
    turn: s.turn,
    castling: { ...s.castling },
    ep: s.ep,
    halfmove: s.halfmove,
    fullmove: s.fullmove,
  };
}

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

/* Attack detection: is square `sq` attacked by any piece of `byColor`? */
function isSquareAttacked(board, sq, byColor){
  const f = sq % 8, r = Math.floor(sq / 8);
  const test = (i, type) => { const p = board[i]; return p && p.toUpperCase() === type && pieceColor(p) === byColor; };

  // pawns — a pawn attacks diagonally forward; check the two squares it could come from
  const prow = byColor === 'w' ? r - 1 : r + 1;
  if (prow >= 0 && prow < 8){
    for (const df of [-1, 1]){
      const nf = f + df;
      if (nf >= 0 && nf < 8 && test(prow * 8 + nf, 'P')) return true;
    }
  }
  // knights
  const kDirs = [[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
  for (const [df, dr] of kDirs){
    const nf = f + df, nr = r + dr;
    if (inBoard(nf, nr) && test(nr * 8 + nf, 'N')) return true;
  }
  // king
  const kDirs2 = [[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]];
  for (const [df, dr] of kDirs2){
    const nf = f + df, nr = r + dr;
    if (inBoard(nf, nr) && test(nr * 8 + nf, 'K')) return true;
  }
  // diagonal sliders (bishop / queen)
  for (const [df, dr] of [[1,1],[1,-1],[-1,1],[-1,-1]]){
    let nf = f + df, nr = r + dr;
    while (inBoard(nf, nr)){
      const p = board[nr * 8 + nf];
      if (p){ const t = p.toUpperCase(); if (pieceColor(p) === byColor && (t === 'B' || t === 'Q')) return true; break; }
      nf += df; nr += dr;
    }
  }
  // orthogonal sliders (rook / queen)
  for (const [df, dr] of [[1,0],[-1,0],[0,1],[0,-1]]){
    let nf = f + df, nr = r + dr;
    while (inBoard(nf, nr)){
      const p = board[nr * 8 + nf];
      if (p){ const t = p.toUpperCase(); if (pieceColor(p) === byColor && (t === 'R' || t === 'Q')) return true; break; }
      nf += df; nr += dr;
    }
  }
  return false;
}

function kingInCheck(board, color){ const k = findKing(board, color); return k >= 0 && isSquareAttacked(board, k, other(color)); }

/* =====================================================================
   SECTION 3 — MOVE GENERATION (generator + legality filter)
   ===================================================================== */
function mkMove(from, to, piece, captured, opts = {}){
  return { from, to, piece, captured, promo: opts.promo || null, castle: opts.castle || null,
           double: !!opts.double, ep: !!opts.ep };
}

function genPawnMoves(state, out, i, color){
  const b = state.board;
  const f = i % 8, r = Math.floor(i / 8);
  const dir = color === 'w' ? 1 : -1;
  const startRank = color === 'w' ? 1 : 6;
  const promoRank = color === 'w' ? 6 : 1;
  const nr = r + dir;
  if (nr >= 0 && nr < 8){
    const ni = nr * 8 + f;
    if (!b[ni]){
      if (nr === promoRank){ for (const promo of ['Q','R','B','N']) out.push(mkMove(i, ni, b[i], null, { promo })); }
      else {
        out.push(mkMove(i, ni, b[i], null));
        if (r === startRank){
          const ni2 = (r + 2 * dir) * 8 + f;
          if (!b[ni2]) out.push(mkMove(i, ni2, b[i], null, { double: true }));
        }
      }
    }
  }
  // captures + en passant
  for (const df of [-1, 1]){
    const nf = f + df;
    if (nf < 0 || nf > 7) continue;
    const nr2 = r + dir;
    if (nr2 < 0 || nr2 > 7) continue;
    const ni = nr2 * 8 + nf;
    const target = b[ni];
    if (target && pieceColor(target) !== color){
      if (nr2 === promoRank){ for (const promo of ['Q','R','B','N']) out.push(mkMove(i, ni, b[i], target, { promo })); }
      else out.push(mkMove(i, ni, b[i], target));
    } else if (state.ep === ni){
      // en passant — only on the square a pawn just double-pushed through (guaranteed by how ep is set)
      out.push(mkMove(i, ni, b[i], target || (color === 'w' ? 'p' : 'P'), { ep: true }));
    }
  }
}

const KNIGHT_OFFS = [[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
const KING_OFFS    = [[1,0],[1,1],[0,1],[-1,1],[-1,0],[-1,-1],[0,-1],[1,-1]];
const BISHOP_OFFS  = [[1,1],[1,-1],[-1,1],[-1,-1]];
const ROOK_OFFS    = [[1,0],[-1,0],[0,1],[0,-1]];
const QUEEN_OFFS   = [...BISHOP_OFFS, ...ROOK_OFFS];

function genLeaper(state, out, i, color, offsets, type){
  const b = state.board, f = i % 8, r = Math.floor(i / 8);
  for (const [df, dr] of offsets){
    const nf = f + df, nr = r + dr;
    if (!inBoard(nf, nr)) continue;
    const ni = nr * 8 + nf, t = b[ni];
    if (!t) out.push(mkMove(i, ni, b[i], null));
    else if (pieceColor(t) !== color) out.push(mkMove(i, ni, b[i], t));
  }
}
function genSlider(state, out, i, color, offsets, type){
  const b = state.board, f = i % 8, r = Math.floor(i / 8);
  for (const [df, dr] of offsets){
    let nf = f + df, nr = r + dr;
    while (inBoard(nf, nr)){
      const ni = nr * 8 + nf, t = b[ni];
      if (!t) out.push(mkMove(i, ni, b[i], null));
      else { if (pieceColor(t) !== color) out.push(mkMove(i, ni, b[i], t)); break; }
      nf += df; nr += dr;
    }
  }
}

function genCastling(state, out, color){
  // castling is only ever generated for the side to move
  if (color !== state.turn) return;
  const b = state.board, rank = color === 'w' ? 0 : 7, ksq = rank * 8 + 4;
  if (findKing(b, color) !== ksq) return;
  const enemy = other(color);
  const kr = color === 'w' ? state.castling.K : state.castling.k;
  const qr = color === 'w' ? state.castling.Q : state.castling.q;
  // king-side: e->g (f,g empty; e,f,g not attacked)
  if (kr){
    if (!b[rank*8+5] && !b[rank*8+6] &&
        !isSquareAttacked(b, ksq, enemy) && !isSquareAttacked(b, rank*8+5, enemy) && !isSquareAttacked(b, rank*8+6, enemy)){
      out.push(mkMove(ksq, rank*8+6, b[ksq], null, { castle:'K' }));
    }
  }
  // queen-side: e->c (b,c,d empty; e,d,c not attacked)
  if (qr){
    if (!b[rank*8+1] && !b[rank*8+2] && !b[rank*8+3] &&
        !isSquareAttacked(b, ksq, enemy) && !isSquareAttacked(b, rank*8+3, enemy) && !isSquareAttacked(b, rank*8+2, enemy)){
      out.push(mkMove(ksq, rank*8+2, b[ksq], null, { castle:'Q' }));
    }
  }
}

function pseudoMoves(state, color){
  const out = [];
  const b = state.board;
  for (let i = 0; i < 64; i++){
    const p = b[i];
    if (!p || pieceColor(p) !== color) continue;
    const t = pieceType(p);
    if (t === 'P') genPawnMoves(state, out, i, color);
    else if (t === 'N') genLeaper(state, out, i, color, KNIGHT_OFFS, 'N');
    else if (t === 'B') genSlider(state, out, i, color, BISHOP_OFFS, 'B');
    else if (t === 'R') genSlider(state, out, i, color, ROOK_OFFS, 'R');
    else if (t === 'Q') genSlider(state, out, i, color, QUEEN_OFFS, 'Q');
    else if (t === 'K') genLeaper(state, out, i, color, KING_OFFS, 'K');
  }
  genCastling(state, out, color);
  return out;
}

/* apply a move to a bare board (no state bookkeeping) */
function applyMoveToArray(b, m){
  const from = m.from, to = m.to;
  b[to] = m.promo || b[from];
  b[from] = null;
  if (m.ep){
    const cap = pieceColor(b[to]) === 'w' ? to - 8 : to + 8;
    b[cap] = null;
  }
  if (m.castle){
    const rank = from < 32 ? 0 : 7;
    const rf = m.castle === 'K' ? rank*8+7 : rank*8+0;
    const rt = m.castle === 'K' ? rank*8+5 : rank*8+3;
    b[rt] = b[rf]; b[rf] = null;
  }
}

/* legality — reject any move that leaves/puts own king in check */
function filterLegal(state, color){
  const candidates = pseudoMoves(state, color);
  const legal = [];
  for (const m of candidates){
    const b = state.board.slice();
    applyMoveToArray(b, m);
    if (!kingInCheck(b, color)) legal.push(m);
  }
  return legal;
}
function legalMoves(state, color){
  return filterLegal(state, color || state.turn);
}
/* =====================================================================
   SECTION 4 — STATE BOOKKEEPING (applyMove) + NOTATION + STATUS
   ===================================================================== */
function castlingAfter(m, before){
  const c = { ...before };
  const col = pieceColor(m.piece);
  if (m.piece.toUpperCase() === 'K'){
    if (col === 'w'){ c.K = false; c.Q = false; } else { c.k = false; c.q = false; }
  }
  // rook left its home square
  if (m.from === 0) c.Q = false;
  if (m.from === 7) c.K = false;
  if (m.from === 56) c.q = false;
  if (m.from === 63) c.k = false;
  // rook captured on its home square
  if (m.to === 0) c.Q = false;
  if (m.to === 7) c.K = false;
  if (m.to === 56) c.q = false;
  if (m.to === 63) c.k = false;
  return c;
}

function applyMove(state, m){
  const b = state.board;
  const col = pieceColor(m.piece);
  state.castling = castlingAfter(m, state.castling);
  state.ep = m.double ? (m.from + m.to) / 2 : -1;
  state.halfmove = (m.piece.toUpperCase() === 'P' || m.captured) ? 0 : state.halfmove + 1;
  applyMoveToArray(b, m);
  state.turn = other(col);
  if (state.turn === 'w') state.fullmove++;
}

/* Build the SAN prefix (no check/mate suffix) using the pre-move legal set. */
function baseSAN(state, m, preLegal){
  const type = m.piece.toUpperCase();
  let san = '';
  if (m.castle) return m.castle === 'K' ? 'O-O' : 'O-O-O';
  if (type === 'P'){
    if (m.captured) san += FILES[m.from % 8] + 'x';
    san += FILES[m.to % 8] + (Math.floor(m.to / 8) + 1);
    if (m.promo) san += '=' + m.promo;
    if (m.ep) san += ' e.p.';
    return san;
  }
  san = type;
  const others = preLegal.filter(x => x !== m && x.to === m.to && x.from !== m.from && x.piece.toUpperCase() === type);
  if (others.length){
    const sameFile = others.some(o => o.from % 8 === m.from % 8);
    const sameRank = others.some(o => Math.floor(o.from / 8) === Math.floor(m.from / 8));
    if (!sameFile) san += FILES[m.from % 8];
    else if (!sameRank) san += String(Math.floor(m.from / 8) + 1);
    else san += FILES[m.from % 8] + String(Math.floor(m.from / 8) + 1);
  }
  if (m.captured) san += 'x';
  san += FILES[m.to % 8] + (Math.floor(m.to / 8) + 1);
  return san;
}

/* Full SAN (with + / #) simulated from an arbitrary state. */
function sanFor(state, m){
  const pre = legalMoves(state, state.turn);
  const base = baseSAN(state, m, pre);
  const ns = cloneState(state); applyMove(ns, m);
  const opp = ns.turn;
  const oppMoves = legalMoves(ns, opp);
  const chk = kingInCheck(ns.board, opp);
  if (oppMoves.length === 0) return base + (chk ? '#' : '');
  return base + (chk ? '+' : '');
}

/* ---- game-ending conditions ---- */
function insufficientMaterial(board){
  const pieces = [];
  for (let i = 0; i < 64; i++){
    const p = board[i];
    if (p && p.toUpperCase() !== 'K') pieces.push({ p, sq: i });
  }
  if (pieces.length === 0) return true;                       // K vs K
  if (pieces.length === 1){
    const t = pieces[0].p.toUpperCase();
    if (t === 'B' || t === 'N') return true;                  // K+B vs K, K+N vs K
    return false;
  }
  // K+bishops all on the same color square -> draw
  if (pieces.every(x => x.p.toUpperCase() === 'B')){
    const cols = pieces.map(x => (x.sq % 8 + Math.floor(x.sq / 8)) % 2);
    if (new Set(cols).size === 1) return true;
  }
  return false;
}

/* =====================================================================
   SECTION 5 — EVALUATION + MINIMAX (alpha-beta) + QUIESCENCE-LITE
   ===================================================================== */
function evaluateSide(board, color){
  let score = 0;
  for (let i = 0; i < 64; i++){
    const p = board[i];
    if (!p || pieceColor(p) !== color) continue;
    const t = p.toUpperCase();
    score += TUNE.material[t];
    score += PST[t][color === 'w' ? i : mirror(i)];
  }
  return score;
}
function evaluate(state, color){
  return evaluateSide(state.board, color) - evaluateSide(state.board, other(color));
}

function moveScore(m){
  const val = TUNE.material;
  let s = 0;
  if (m.captured) s += val[m.captured.toUpperCase()] * 10 - val[m.piece.toUpperCase()];
  if (m.promo) s += val[m.promo];
  if (m.castle) s += 30;
  return s;
}
function orderMoves(moves){ moves.sort((a, b) => moveScore(b) - moveScore(a)); }

function quiescence(state, alpha, beta){
  const stand = evaluate(state, state.turn);
  if (stand >= beta) return beta;
  if (stand > alpha) alpha = stand;
  const moves = legalMoves(state, state.turn).filter(m => m.captured || m.promo);
  orderMoves(moves);
  const opp = other(state.turn);
  for (const m of moves){
    const ns = cloneState(state); applyMove(ns, m);
    const score = -quiescence(ns, -beta, -alpha);
    if (score >= beta) return beta;
    if (score > alpha) alpha = score;
  }
  return alpha;
}

function negamax(state, depth, alpha, beta, ply){
  const moves = legalMoves(state, state.turn);
  if (moves.length === 0){
    return kingInCheck(state.board, state.turn) ? -(MATE - ply) : 0; // mated / stalemate
  }
  if (depth <= 0) return quiescence(state, alpha, beta);
  orderMoves(moves);
  let best = -Infinity, a = alpha;
  const opp = other(state.turn);
  for (const m of moves){
    const ns = cloneState(state); applyMove(ns, m);
    const sc = -negamax(ns, depth - 1, -beta, -a, ply + 1);
    if (sc > best) best = sc;
    if (best > a) a = best;
    if (a >= beta) break;
  }
  return best;
}

function search(state, depth){
  const moves = legalMoves(state, state.turn);
  if (moves.length === 0) return null;
  orderMoves(moves);
  let best = null, bestScore = -Infinity, alpha = -Infinity, beta = Infinity;
  const opp = other(state.turn);
  for (const m of moves){
    const ns = cloneState(state); applyMove(ns, m);
    const sc = -negamax(ns, depth - 1, -beta, -alpha, 1);
    if (sc > bestScore){ bestScore = sc; best = m; }
    if (sc > alpha) alpha = sc;
  }
  return best;
}

/* =====================================================================
   SECTION 6 — GAME STATE (UI-facing) + PERSISTENCE
   ===================================================================== */
let stateStack = [];   // snapshots for undo (in-memory; rebuilt from log on reload)

const game = {
  state: newState(),
  playerColor: 'w',
  mode: 'normal',      // 'normal' | 'analysis'
  depth: 2,
  flipped: false,
  muted: false,
  log: [],             // move records {from,to,promo,piece,castle,double,ep,captured,san,byHuman}
  selected: null,
  legalCache: [],
  status: 'playing',
  lastMove: null,
  aiThinking: false,
};
const aiColor = () => (game.mode === 'analysis' ? null : other(game.playerColor));

function snapshot(s){
  return { board: s.board.slice(), turn: s.turn, castling: { ...s.castling },
           ep: s.ep, halfmove: s.halfmove, fullmove: s.fullmove };
}
function restoreSnapshot(s){
  game.state = { board: s.board.slice(), turn: s.turn, castling: { ...s.castling },
                 ep: s.ep, halfmove: s.halfmove, fullmove: s.fullmove };
}
function toFEN(s){
  const rows = [];
  for (let r = 7; r >= 0; r--){
    let row = '', empty = 0;
    for (let f = 0; f < 8; f++){
      const p = s.board[r * 8 + f];
      if (!p){ empty++; }
      else { if (empty){ row += empty; empty = 0; } row += p; }
    }
    if (empty) row += empty;
    rows.push(row);
  }
  let castling = '';
  if (s.castling.K) castling += 'K';
  if (s.castling.Q) castling += 'Q';
  if (s.castling.k) castling += 'k';
  if (s.castling.q) castling += 'q';
  if (!castling) castling = '-';
  const ep = s.ep >= 0 ? sqName(s.ep) : '-';
  return rows.join('/') + ' ' + s.turn + ' ' + castling + ' ' + ep + ' ' + s.halfmove + ' ' + s.fullmove;
}
function serialize(){
  return {
    state: game.state,
    playerColor: game.playerColor,
    mode: game.mode,
    depth: game.depth,
    flipped: game.flipped,
    muted: game.muted,
    log: game.log,
    status: game.status,
    lastMove: game.lastMove,
  };
}
function persist(){
  try { localStorage.setItem('chess-game', JSON.stringify(serialize())); } catch (e) {}
}
function loadPersisted(){
  try {
    const raw = localStorage.getItem('chess-game');
    if (!raw) return false;
    const d = JSON.parse(raw);
    game.state = { board: d.state.board, turn: d.state.turn, castling: d.state.castling,
                   ep: d.state.ep, halfmove: d.state.halfmove, fullmove: d.state.fullmove };
    game.playerColor = d.playerColor || 'w';
    game.mode = d.mode || 'normal';
    game.depth = d.depth || 2;
    game.flipped = !!d.flipped;
    game.muted = !!d.muted;
    game.log = d.log || [];
    game.status = d.status || 'playing';
    game.lastMove = d.lastMove || null;
    // rebuild undo history by replaying the log
    stateStack = [];
    const s = newState();
    for (const m of game.log){ s.board = s.board; applyMove(s, m); }
    return true;
  } catch (e){ return false; }
}
/* =====================================================================
   SECTION 7 — SOUND (Web Audio)
   ===================================================================== */
let audioCtx = null;
function ensureAudio(){ if (!audioCtx){ const AC = window.AudioContext || window.webkitAudioContext; if (AC) audioCtx = new AC(); } }
function playSound(kind){
  if (game.muted) return;
  try {
    ensureAudio();
    if (!audioCtx) return;
    const t = audioCtx.currentTime;
    const o = audioCtx.createOscillator(), g = audioCtx.createGain();
    let freq = 500, dur = 0.07, type = 'triangle';
    if (kind === 'move'){ freq = 520; dur = 0.06; type = 'triangle'; }
    else if (kind === 'capture'){ freq = 240; dur = 0.12; type = 'sawtooth'; }
    else if (kind === 'check'){ freq = 760; dur = 0.20; type = 'square'; }
    o.type = type; o.frequency.value = freq;
    g.gain.setValueAtTime(0.0001, t);
    g.gain.exponentialRampToValueAtTime(0.22, t + 0.012);
    g.gain.exponentialRampToValueAtTime(0.0001, t + dur);
    o.connect(g); g.connect(audioCtx.destination);
    o.start(t); o.stop(t + dur + 0.03);
  } catch (e) {}
}

/* =====================================================================
   SECTION 8 — RENDERING
   ===================================================================== */
function glyphFor(color, type){ return color === 'w' ? GLYPHS[type] : GLYPHS[type.toLowerCase()]; }
function castleRook(m){ const rank = m.from < 32 ? 0 : 7; return m.castle === 'K' ? rank * 8 + 5 : rank * 8 + 3; }

function renderBoard(){
  const board = game.state.board;
  const wrap = document.getElementById('board');
  wrap.innerHTML = '';
  for (let dr = 0; dr < 8; dr++){
    for (let col = 0; col < 8; col++){
      const actualRow = game.flipped ? dr : 7 - dr;
      const idx = actualRow * 8 + col;
      const sq = document.createElement('div');
      sq.className = 'square ' + (((idx % 8) + Math.floor(idx / 8)) % 2 === 0 ? 'light' : 'dark');
      sq.dataset.index = idx;
      if (game.lastMove && (idx === game.lastMove.from || idx === game.lastMove.to)) sq.classList.add('last-move');
      if (game.selected === idx) sq.classList.add('selected');
      const p = board[idx];
      if (p){
        const sp = document.createElement('span');
        sp.className = 'piece ' + (pieceColor(p) === 'w' ? 'white' : 'black');
        sp.textContent = GLYPHS[p];
        sq.appendChild(sp);
        if (p.toUpperCase() === 'K' && pieceColor(p) === game.state.turn && kingInCheck(board, pieceColor(p))) sq.classList.add('check');
      }
      if (game.selected != null && game.status === 'playing'){
        for (const m of game.legalCache){
          if (m.from === game.selected && (m.to === idx || (m.castle && (idx === m.to || idx === castleRook(m))))){
            if (m.castle){ const mk = document.createElement('span'); mk.className = 'castle-marker'; sq.appendChild(mk); }
            else if (m.captured || m.ep){ const mk = document.createElement('span'); mk.className = 'ring'; sq.appendChild(mk); }
            else { const mk = document.createElement('span'); mk.className = 'dot'; sq.appendChild(mk); }
            break;
          }
        }
      }
      wrap.appendChild(sq);
    }
  }
}

function renderCaptured(){
  const capW = [], capB = [];
  for (const m of game.log){ if (m.captured){ (pieceColor(m.piece) === 'w' ? capW : capB).push(m.captured); } }
  const val = TUNE.material;
  capW.sort((a, b) => val[b.toUpperCase()] - val[a.toUpperCase()]);
  capB.sort((a, b) => val[b.toUpperCase()] - val[a.toUpperCase()]);
  document.getElementById('cap-w').innerHTML = capW.map(p => `<span class="p ${pieceColor(p)}">${GLYPHS[p]}</span>`).join('');
  document.getElementById('cap-b').innerHTML = capB.map(p => `<span class="p ${pieceColor(p)}">${GLYPHS[p]}</span>`).join('');
  const advW = capW.reduce((a, p) => a + val[p.toUpperCase()], 0) - capB.reduce((a, p) => a + val[p.toUpperCase()], 0);
  document.getElementById('adv-w').textContent = advW > 0 ? '+' + Math.round(advW / 100) : '';
  document.getElementById('adv-b').textContent = advW < 0 ? '+' + Math.round(-advW / 100) : '';
}

function renderTurn(){
  const t = game.state.turn;
  const dot = document.getElementById('turn-dot');
  dot.className = 'dot ' + t;
  document.getElementById('turn-text').textContent = (t === 'w' ? 'White' : 'Black') + ' to move';
  document.getElementById('thinking').style.display = game.aiThinking ? 'inline' : 'none';
}

function renderMoveList(){
  const el = document.getElementById('move-list');
  if (game.log.length === 0){ el.innerHTML = '<div class="muted">No moves yet</div>'; return; }
  let html = '';
  for (let i = 0; i < game.log.length; i++){
    if (i % 2 === 0) html += `<div><span class="ply">${Math.floor(i / 2) + 1}.</span>`;
    html += `<span class="san">${game.log[i].san}</span>`;
    if (i % 2 === 1) html += '</div>';
  }
  if (game.log.length % 2 === 1) html += '</div>';
  el.innerHTML = html;
  el.scrollTop = el.scrollHeight;
}

function renderStatusLine(){
  const el = document.getElementById('status-line');
  el.textContent = game.mode === 'analysis' ? 'Analysis — you control both sides' : 'You play ' + (game.playerColor === 'w' ? 'White' : 'Black');
}

function renderBanner(){
  const b = document.getElementById('banner');
  if (game.status === 'playing'){ b.classList.add('hidden'); return; }
  b.classList.remove('hidden');
  let title, sub;
  if (game.status === 'checkmate'){ title = 'Checkmate!'; sub = (game.state.turn === 'w' ? 'Black' : 'White') + ' wins'; }
  else if (game.status === 'stalemate'){ title = 'Stalemate'; sub = 'Draw — no legal moves'; }
  else { title = 'Draw'; sub = '50-move rule or insufficient material'; }
  b.innerHTML = `<div class="card"><div class="title">${title}</div><div class="sub">${sub}</div><button id="banner-new">New Game</button></div>`;
  b.querySelector('#banner-new').onclick = newGame;
}

function renderAll(){ renderBoard(); renderCaptured(); renderTurn(); renderMoveList(); renderStatusLine(); renderBanner(); }

/* =====================================================================
   SECTION 9 — MOVE COMMIT + STATUS + AI SCHEDULING + UNDO
   ===================================================================== */
function doMove(m){
  if (!m) return;
  const s = game.state;
  const preLegal = legalMoves(s, s.turn);
  const base = baseSAN(s, m, preLegal);
  const movingColor = pieceColor(m.piece);
  const byHuman = (game.mode === 'analysis' || movingColor === game.playerColor);
  applyMove(s, m);
  const opp = s.turn;
  const oppMoves = legalMoves(s, opp);
  const inCheck = kingInCheck(s.board, opp);
  let suffix = '';
  game.status = 'playing';
  if (oppMoves.length === 0){
    if (inCheck){ suffix = '#'; game.status = 'checkmate'; }
    else { game.status = 'stalemate'; }
  } else {
    if (inCheck) suffix = '+';
    if (s.halfmove >= 100) game.status = 'draw';
    else if (insufficientMaterial(s.board)) game.status = 'draw';
  }
  m.san = base + suffix;
  game.log.push({ from: m.from, to: m.to, promo: m.promo, piece: m.piece, castle: m.castle,
                  double: m.double, ep: m.ep, captured: m.captured, san: m.san, byHuman });
  game.lastMove = m;
  game.selected = null;
  game.legalCache = legalMoves(s, s.turn);
  playSound(m.captured ? 'capture' : (inCheck ? 'check' : 'move'));
  renderAll();
  persist();
  if (game.status === 'playing' && game.mode === 'normal' && s.turn === aiColor()) scheduleAI();
}

function scheduleAI(){
  if (game.status !== 'playing') return;
  const ai = aiColor();
  if (ai === null || game.state.turn !== ai) return;
  game.aiThinking = true;
  renderTurn();
  setTimeout(() => {
    const mv = search(game.state, game.depth);
    game.aiThinking = false;
    if (mv && game.status === 'playing') doMove(mv);
    else renderAll();
  }, 60);
}

function recomputeStatus(){
  const s = game.state, opp = s.turn;
  const moves = legalMoves(s, opp), inCheck = kingInCheck(s.board, opp);
  game.status = 'playing';
  if (moves.length === 0){ if (inCheck) game.status = 'checkmate'; else game.status = 'stalemate'; }
  else if (s.halfmove >= 100) game.status = 'draw';
  else if (insufficientMaterial(s.board)) game.status = 'draw';
}

function rebuildTo(n){
  game.state = newState();
  for (let i = 0; i < n; i++) applyMove(game.state, game.log[i]);
  game.log = game.log.slice(0, n);
  game.selected = null;
  game.lastMove = n > 0 ? game.log[n - 1] : null;
  recomputeStatus();
  game.legalCache = legalMoves(game.state, game.state.turn);
  game.aiThinking = false;
  renderAll();
  persist();
}

function undo(){
  let h = -1;
  for (let i = game.log.length - 1; i >= 0; i--) if (game.log[i].byHuman){ h = i; break; }
  if (h < 0) return;
  rebuildTo(h); // keep moves [0 .. h-1], i.e. back to before the player's last move
}

function newGame(){
  game.state = newState();
  game.log = [];
  game.selected = null;
  game.status = 'playing';
  game.lastMove = null;
  game.aiThinking = false;
  game.legalCache = legalMoves(game.state, game.state.turn);
  renderAll();
  persist();
  scheduleAI(); // fires if the AI is White (player chose Black)
}

/* =====================================================================
   SECTION 10 — INTERACTION (click-to-move + drag & drop)
   ===================================================================== */
function humanCanMove(){ return game.status === 'playing' && (game.mode === 'analysis' || game.state.turn === game.playerColor); }
function isTarget(from, to){ return game.legalCache.some(m => m.from === from && m.to === to); }
function select(sq){ game.selected = sq; renderBoard(); }
function deselect(){ if (game.selected != null){ game.selected = null; renderBoard(); } }

function findMoves(from, to){ return game.legalCache.filter(m => m.from === from && m.to === to); }

function requestMove(from, to){
  const cands = findMoves(from, to);
  if (cands.length === 0) return false;
  if (cands.some(m => m.promo)){ showPromo(from, to, cands); return true; }
  doMove({ ...cands[0] });
  return true;
}

function showPromo(from, to, cands){
  const promo = document.getElementById('promo');
  promo.classList.remove('hidden');
  const color = pieceColor(cands[0].piece);
  promo.innerHTML = `<div class="box"><h3>Choose promotion</h3><div class="opts">` +
    ['Q','R','B','N'].map(t => `<button data-t="${t}">${glyphFor(color, t)}</button>`).join('') +
    `</div><div class="cancel">Cancel</div></div>`;
  promo.querySelectorAll('button[data-t]').forEach(btn => {
    btn.onclick = () => { promo.classList.add('hidden'); doMove({ ...cands[0], promo: btn.dataset.t }); };
  });
  promo.querySelector('.cancel').onclick = () => { promo.classList.add('hidden'); deselect(); };
}

let pressSquare = null, dragging = false, dragMoved = false, dragStartX = 0, dragStartY = 0;

function startDrag(e, piece){
  dragging = true;
  const gh = document.getElementById('drag-ghost');
  gh.style.display = 'block';
  gh.className = pieceColor(piece) === 'w' ? 'white' : 'black';
  gh.textContent = GLYPHS[piece];
  dragStartX = e.clientX; dragStartY = e.clientY;
  moveGhost(e);
}
function moveGhost(e){
  const gh = document.getElementById('drag-ghost');
  gh.style.left = (e.clientX - 26) + 'px';
  gh.style.top = (e.clientY - 34) + 'px';
}
function endDrag(){ const gh = document.getElementById('drag-ghost'); if (gh) gh.style.display = 'none'; }

function boardEvents(){
  const board = document.getElementById('board');
  board.addEventListener('pointerdown', (e) => {
    const sq = e.target.closest('.square'); if (!sq) return;
    const idx = +sq.dataset.index;
    const p = game.state.board[idx];
    pressSquare = idx; dragMoved = false; dragging = false;
    if (humanCanMove() && p && pieceColor(p) === game.state.turn){
      select(idx);
      startDrag(e, p);
    }
  });
  document.addEventListener('pointermove', (e) => {
    if (pressSquare == null || !dragging) return;
    if (!dragMoved && Math.hypot(e.clientX - dragStartX, e.clientY - dragStartY) > 6) dragMoved = true;
    if (dragMoved){ moveGhost(e); renderBoard(); }
  });
  document.addEventListener('pointerup', (e) => {
    if (pressSquare == null) return;
    const sq = e.target.closest('.square');
    const idx = sq ? +sq.dataset.index : -1;
    let moved = false;
    if (game.selected != null && idx >= 0 && idx !== game.selected && isTarget(game.selected, idx)){
      moved = true;
      requestMove(game.selected, idx);
    } else if (idx >= 0 && game.selected != null && idx !== game.selected){
      const p = game.state.board[idx];
      if (!(p && pieceColor(p) === game.state.turn)) deselect();
    }
    endDrag();
    pressSquare = null; dragging = false; dragMoved = false;
  });
}

/* =====================================================================
   SECTION 11 — DEBUG INTERFACE + INIT
   ===================================================================== */
function parseCoord(str){
  if (typeof str !== 'string' || str.length < 4) return null;
  const f1 = FILES.indexOf(str[0]), r1 = +str[1] - 1;
  const f2 = FILES.indexOf(str[2]), r2 = +str[3] - 1;
  if (f1 < 0 || r1 < 0 || f1 > 7 || r1 > 7 || f2 < 0 || r2 < 0 || f2 > 7 || r2 > 7) return null;
  return { from: r1 * 8 + f1, to: r2 * 8 + f2 };
}
function dbgMove(str){
  const c = parseCoord(str); if (!c) return null;
  const m = findMoves(c.from, c.to)[0];
  if (!m) return null;
  const cloned = { ...m };
  doMove(cloned);
  return cloned.san;
}

window.game = {
  fen: () => toFEN(game.state),
  move: dbgMove,
  legalMoves: () => legalMoves(game.state, game.state.turn).map(m => sanFor(game.state, m)),
  setDepth: (d) => { game.depth = Math.max(1, Math.min(3, d)); },
};

function bindControls(){
  document.getElementById('btn-new').onclick = newGame;
  document.getElementById('btn-undo').onclick = undo;
  document.getElementById('btn-flip').onclick = () => { game.flipped = !game.flipped; renderBoard(); persist(); };
  document.getElementById('btn-mute').onclick = () => {
    game.muted = !game.muted;
    document.getElementById('btn-mute').textContent = game.muted ? '🔇 Sound Off' : '🔊 Sound On';
    persist();
  };
  document.getElementById('side-select').onchange = (e) => {
    const v = e.target.value;
    if (v === 'both'){ game.mode = 'analysis'; game.playerColor = 'w'; }
    else { game.mode = 'normal'; game.playerColor = v; }
    newGame();
  };
  document.getElementById('diff-select').onchange = (e) => { game.depth = +e.target.value; persist(); };
}

function syncControls(){
  const side = document.getElementById('side-select');
  side.value = game.mode === 'analysis' ? 'both' : game.playerColor;
  document.getElementById('diff-select').value = String(game.depth);
  document.getElementById('btn-mute').textContent = game.muted ? '🔇 Sound Off' : '🔊 Sound On';
}

function init(){
  loadPersisted();
  game.legalCache = legalMoves(game.state, game.state.turn);
  bindControls();
  syncControls();
  boardEvents();
  renderAll();
  if (game.status === 'playing' && game.mode === 'normal' && game.state.turn === aiColor()) scheduleAI();
}

// a drag-ghost element lives at <body> level (position:fixed)
const ghost = document.createElement('div');
ghost.id = 'drag-ghost';
document.body.appendChild(ghost);

init();
</script>
</body>
</html>