← Chess results

Chess

Deepseek v4.1 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 55,118 bytes · SHA-256 566b5d5a6f69
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — Play vs Computer</title>
<style>
/* ============================================================
   THEME / LAYOUT
   ============================================================ */
:root{
  --light:#f0d9b5;
  --dark:#b58863;
  --frame:#3a2a1c;
  --frame2:#5a422c;
  --bg:#15161a;
  --bg2:#1d1f25;
  --panel:#22242b;
  --panel2:#2a2d35;
  --line:#383b45;
  --text:#eae4d8;
  --dim:#9a958a;
  --accent:#c9a15c;
  --sel:#f6e27a;
  --last:#e4cf4d;
  --ok:#7fbf6a;
  --bad:#e06a5f;
}
*{box-sizing:border-box}
html,body{height:100%}
body{
  margin:0;background:
    radial-gradient(1200px 700px at 50% -10%, #2b2f38 0%, transparent 60%),
    linear-gradient(180deg,#101116,#171922 70%,#101116);
  color:var(--text);
  font-family:"Segoe UI",Inter,system-ui,-apple-system,Roboto,Helvetica,Arial,sans-serif;
  min-height:100vh;
}
.wrap{max-width:1180px;margin:0 auto;padding:18px 18px 40px}
.topbar{display:flex;align-items:baseline;gap:16px;flex-wrap:wrap;margin:2px 0 14px}
.topbar h1{
  font-size:20px;margin:0;letter-spacing:.14em;text-transform:uppercase;
  font-weight:600;color:var(--text)
}
.topbar h1 span{color:var(--accent)}
#turnInd{
  margin-left:auto;font-size:13px;letter-spacing:.06em;text-transform:uppercase;
  color:var(--dim);display:flex;align-items:center;gap:9px;
}
#turnInd b{color:var(--text);font-weight:600}
.dot{width:11px;height:11px;border-radius:50%;background:#f5f0e6;box-shadow:0 0 0 1px #000 inset,0 0 10px rgba(255,255,255,.25)}
.dot.b{background:#22201d;box-shadow:0 0 0 1px #6b6459 inset,0 0 10px rgba(0,0,0,.5)}
.dot.think{animation:pulse 1s infinite}
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.25}}

main{display:flex;gap:22px;align-items:flex-start}
.boardCol{flex:0 0 auto;display:flex;flex-direction:column;align-items:center;gap:10px}
.boardFrame{
  position:relative;padding:12px;border-radius:10px;
  background:linear-gradient(160deg,var(--frame2),var(--frame) 60%,#241a11);
  box-shadow:0 18px 40px rgba(0,0,0,.55), 0 0 0 1px rgba(255,255,255,.05) inset,
             0 2px 0 rgba(255,255,255,.06) inset;
}
.board{
  display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);
  width:min(72vmin,600px);aspect-ratio:1;border-radius:3px;overflow:hidden;
  user-select:none;-webkit-user-select:none;touch-action:none;position:relative;
  box-shadow:0 0 0 1px rgba(0,0,0,.5) inset;
}
.sq{position:relative;display:flex;align-items:center;justify-content:center;line-height:1}
.sq.light{background:var(--light)}
.sq.dark{background:var(--dark)}
.sq.last::after{content:"";position:absolute;inset:0;background:var(--last);opacity:.42}
.sq.sel::after{content:"";position:absolute;inset:0;background:var(--sel);opacity:.55}
.sq.over::after{content:"";position:absolute;inset:0;box-shadow:0 0 0 3px var(--sel) inset;opacity:.9}
.sq.check::after{
  content:"";position:absolute;inset:0;
  background:radial-gradient(circle at 50% 50%, rgba(255,60,50,.85) 0%, rgba(220,30,20,.45) 45%, rgba(200,0,0,0) 72%)
}
.sq .hint{
  position:absolute;width:30%;height:30%;border-radius:50%;
  background:rgba(24,20,14,.34);pointer-events:none
}
.sq .hint.cap{
  width:88%;height:88%;border-radius:50%;background:transparent;
  box-shadow:0 0 0 .34em rgba(24,20,14,.30) inset;opacity:.95
}
.sq .coord{
  position:absolute;font-size:.2em;font-weight:700;letter-spacing:.02em;
  pointer-events:none;user-select:none
}
.sq .coord.f{right:.22em;bottom:.12em}
.sq .coord.r{left:.22em;top:.06em}
.sq.light .coord{color:rgba(120,85,50,.75)}
.sq.dark .coord{color:rgba(250,235,215,.8)}
.pc{
  font-size:1em;line-height:1;pointer-events:none;position:relative;z-index:2;
  font-family:"Segoe UI Symbol","Noto Sans Symbols 2","Apple Symbols","DejaVu Sans",serif;
  transition:transform .07s ease;
}
.pc.w{color:#fdfbf5;-webkit-text-stroke:1.6px #2c2419;text-shadow:0 2px 2px rgba(0,0,0,.35)}
.pc.b{color:#292523;-webkit-text-stroke:1.4px #0e0c0b;text-shadow:0 2px 2px rgba(0,0,0,.28)}
.sq.sel .pc{transform:translateY(-2px)}

/* dragged ghost */
#ghost{
  position:fixed;z-index:60;pointer-events:none;transform:translate(-50%,-50%);
  filter:drop-shadow(0 8px 10px rgba(0,0,0,.5));opacity:.95
}

/* promotion overlay */
.promo{
  position:absolute;z-index:40;display:grid;grid-template-columns:repeat(2,1fr);
  gap:4px;padding:6px;border-radius:8px;
  background:linear-gradient(180deg,#fdf8ee,#e8dcc6);
  box-shadow:0 12px 28px rgba(0,0,0,.55),0 0 0 1px rgba(0,0,0,.35);
}
.promo button{
  width:2.55rem;height:2.55rem;border:0;border-radius:6px;cursor:pointer;
  background:#f4ecdc;display:flex;align-items:center;justify-content:center;
  font-size:1.7rem;line-height:1;padding:0;
  font-family:"Segoe UI Symbol","Noto Sans Symbols 2","DejaVu Sans",serif;
}
.promo button:hover{background:var(--sel)}
.promo button .pc{font-size:1.6rem}

/* banner */
.banner{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(1);
  z-index:30;min-width:60%;text-align:center;padding:16px 22px;border-radius:12px;
  background:rgba(18,16,14,.9);backdrop-filter:blur(3px);
  box-shadow:0 16px 40px rgba(0,0,0,.6),0 0 0 1px rgba(255,255,255,.1) inset;
  animation:pop .25s ease-out
}
@keyframes pop{from{transform:translate(-50%,-50%) scale(.85);opacity:0}to{transform:translate(-50%,-50%) scale(1);opacity:1}}
.banner .big{font-size:19px;font-weight:700;letter-spacing:.05em;margin-bottom:4px}
.banner .sub{font-size:12.5px;color:var(--dim);letter-spacing:.05em}
.banner button{margin-top:12px}
.hidden{display:none!important}

/* panel */
.panel{
  flex:1 1 300px;min-width:270px;max-width:420px;display:flex;flex-direction:column;gap:12px
}
.group{
  background:linear-gradient(180deg,var(--panel),var(--panel2));
  border:1px solid var(--line);border-radius:10px;padding:11px 12px;
  box-shadow:0 8px 20px rgba(0,0,0,.3)
}
.group h2{
  margin:0 0 8px;font-size:10.5px;letter-spacing:.16em;text-transform:uppercase;
  color:var(--dim);font-weight:700
}
.row{display:flex;align-items:center;gap:10px;margin:6px 0}
.row .lbl{font-size:12px;color:var(--dim);flex:0 0 78px}
select{
  flex:1;background:#15171c;color:var(--text);border:1px solid var(--line);
  border-radius:7px;padding:6px 8px;font-size:13px;font-family:inherit;cursor:pointer
}
button{
  background:linear-gradient(180deg,#33363f,#282b33);color:var(--text);
  border:1px solid var(--line);border-radius:8px;padding:8px 12px;font-size:12.5px;
  font-family:inherit;font-weight:600;letter-spacing:.03em;cursor:pointer;
  transition:background .12s,transform .06s
}
button:hover{background:linear-gradient(180deg,#3d414c,#313540)}
button:active{transform:translateY(1px)}
button:disabled{opacity:.42;cursor:default}
.controls{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.controls button{width:100%}

.trays{display:flex;flex-direction:column;gap:7px}
.trayRow{display:flex;align-items:center;gap:8px}
.trayRow .side{font-size:10.5px;color:var(--dim);letter-spacing:.08em;text-transform:uppercase;flex:0 0 52px}
.tray{
  flex:1;min-height:30px;display:flex;flex-wrap:wrap;align-items:center;gap:0;
  font-size:20px;line-height:1;letter-spacing:-2px;
  background:rgba(0,0,0,.22);border-radius:6px;padding:3px 6px;border:1px solid rgba(255,255,255,.04)
}
.tray .pc{font-size:19px}
.tray .adv{margin-left:auto;font-size:11px;letter-spacing:0;color:var(--ok);font-weight:700;align-self:center}

.movesList{
  margin:0;padding:0;list-style:none;max-height:230px;overflow-y:auto;
  font-variant-numeric:tabular-nums;font-size:13.5px;scrollbar-width:thin
}
.movesList::-webkit-scrollbar{width:8px}
.movesList::-webkit-scrollbar-thumb{background:#3b3f4a;border-radius:8px}
.mvrow{display:grid;grid-template-columns:34px 1fr 1fr;gap:4px;padding:2.5px 4px;border-radius:5px}
.mvrow:nth-child(odd){background:rgba(255,255,255,.028)}
.mvrow .num{color:var(--dim);font-size:12px}
.mvrow .mv{font-weight:600}
.mvrow .mv.cur{color:var(--accent)}
.movesEmpty{color:var(--dim);font-size:12.5px;font-style:italic}

.statusline{font-size:12.5px;color:var(--dim);letter-spacing:.04em;min-height:17px;text-align:center}
.statusline b{color:var(--text)}
.footnote{font-size:11px;color:#6f6b63;text-align:center;margin-top:6px;letter-spacing:.03em}

@media (max-width:900px){
  main{flex-direction:column;align-items:stretch}
  .boardCol{align-items:center}
  .panel{max-width:none}
  .board{width:min(92vmin,600px)}
}
</style>
</head>
<body>
<div class="wrap">
  <div class="topbar">
    <h1>Chess<span>·</span>Engine</h1>
    <div id="turnInd"><span class="dot" id="turnDot"></span><span id="turnTxt">White to move</span></div>
  </div>
  <main>
    <section class="boardCol">
      <div class="boardFrame" id="boardFrame">
        <div class="board" id="board"></div>
        <div class="promo hidden" id="promo"></div>
        <div class="banner hidden" id="banner"></div>
      </div>
      <div class="statusline" id="statusLine"></div>
    </section>
    <aside class="panel">
      <div class="group">
        <h2>Game</h2>
        <div class="row"><span class="lbl">You play</span>
          <select id="sideSel">
            <option value="w">White</option>
            <option value="b">Black</option>
            <option value="both">Both (analysis)</option>
          </select>
        </div>
        <div class="row"><span class="lbl">Strength</span>
          <select id="depthSel">
            <option value="1">Easy — depth 1</option>
            <option value="2" selected>Medium — depth 2</option>
            <option value="3">Hard — depth 3</option>
          </select>
        </div>
      </div>
      <div class="group">
        <h2>Captured</h2>
        <div class="trays">
          <div class="trayRow"><span class="side">White</span><div class="tray" id="trayW"></div></div>
          <div class="trayRow"><span class="side">Black</span><div class="tray" id="trayB"></div></div>
        </div>
      </div>
      <div class="group">
        <h2>Moves</h2>
        <ol class="movesList" id="movesList"></ol>
      </div>
      <div class="group">
        <h2>Controls</h2>
        <div class="controls">
          <button id="newBtn">New Game</button>
          <button id="undoBtn">Undo</button>
          <button id="flipBtn">Flip Board</button>
          <button id="muteBtn">🔊 Sound</button>
        </div>
      </div>
    </aside>
  </main>
  <div class="footnote">Click or drag to move · Debug: <code>game.fen()</code>, <code>game.move('e2e4')</code>, <code>game.legalMoves()</code></div>
</div>
<script>
/* ============================================================================
   CHESS ENGINE — vanilla JS, no dependencies
   Sections:
     1. Tunables / constants
     2. Board representation + utilities
     3. Move generation (pseudo-legal) + attack detection
     4. Making moves (immutable clone)
     5. Legal move generator (king-safety filtered)
     6. Game status (check / mate / stalemate / draws)
     7. Algebraic notation (SAN)
     8. Evaluation
     9. Search (negamax + alpha-beta + quiescence, time-sliced)
    10. Audio
    11. Rendering / UI
    12. Persistence (localStorage)
    13. Debug interface
    14. Boot
   Board layout: index 0 = a8, 7 = h8, 56 = a1, 63 = h1.
   Pieces: single chars, uppercase = white, lowercase = black.
   ============================================================================ */

/* ============================ 1. TUNABLES ================================= */
const CFG = {
  // colours
  COLOR_LIGHT: '#f0d9b5',
  COLOR_DARK:  '#b58863',

  // AI
  AI_TIME_MS: 1400,        // soft time cap per move (iterative deepening aborts past this)
  AI_MAX_QUIESCE: 4,       // quiescence-lite depth
  DEPTHS: { 1:1, 2:2, 3:3 },

  // evaluation weights
  W_MATERIAL: { P:100, N:320, B:330, R:500, Q:900, K:0 },
  W_BISHOP_PAIR: 28,
  W_TEMPO: 8,
  W_PAWN_STRUCT: { DOUBLED:-12, ISOLATED:-14, PASSED:0 },
  W_MOBILITY: 0            // reserved (kept 0 for speed)
};
const MATE = 100000, INF = 1e9;
const FILES = 'abcdefgh';

/* ==================== 2. BOARD REP + UTILITIES ============================ */
const P_VAL = CFG.W_MATERIAL;

function colorOf(p){ return p ? (p === p.toUpperCase() ? 'w' : 'b') : null; }
function typeOf(p){ return p ? p.toUpperCase() : null; }
function opp(c){ return c === 'w' ? 'b' : 'w'; }
function sqName(sq){ return FILES[sq & 7] + (8 - (sq >> 3)); }
function nameSq(s){
  const f = FILES.indexOf(s[0]), r = 8 - parseInt(s[1], 10);
  if (f < 0 || r < 0 || r > 7) return -1;
  return r * 8 + f;
}
const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';

function parseFen(fen){
  const parts = fen.trim().split(/\s+/);
  const board = new Array(64).fill(null);
  let sq = 0;
  for (const ch of parts[0]) {
    if (ch === '/') continue;
    if (ch >= '1' && ch <= '8') sq += +ch;
    else board[sq++] = ch;
  }
  return {
    board,
    turn: parts[1] || 'w',
    castling: parts[2] === '-' ? '' : (parts[2] || ''),
    ep: (!parts[3] || parts[3] === '-') ? -1 : nameSq(parts[3]),
    half: parts[4] ? +parts[4] : 0,
    full: parts[5] ? +parts[5] : 1
  };
}
function toFen(st){
  let out = '', empty = 0;
  for (let sq = 0; sq < 64; sq++){
    const p = st.board[sq];
    if (!p) { empty++; }
    else {
      if (empty) { out += empty; empty = 0; }
      out += p;
    }
    if ((sq & 7) === 7){ if (empty){ out += empty; empty = 0; } if (sq !== 63) out += '/'; }
  }
  return out + ' ' + st.turn + ' ' + (st.castling || '-') + ' ' +
         (st.ep >= 0 ? sqName(st.ep) : '-') + ' ' + st.half + ' ' + st.full;
}
function cloneState(st){
  return { board: st.board.slice(), turn: st.turn, castling: st.castling,
           ep: st.ep, half: st.half, full: st.full };
}
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;
}

/* ================= 3. MOVE GENERATION + ATTACKS =========================== */
const N_DIRS = [[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
const B_DIRS = [[1,1],[1,-1],[-1,1],[-1,-1]];
const R_DIRS = [[1,0],[-1,0],[0,1],[0,-1]];
const K_DIRS = B_DIRS.concat(R_DIRS);

function mk(from, to, piece, capture, extra){
  return Object.assign({ from, to, piece, capture: capture || null, promo: null, flag: null }, extra || {});
}

/** Pseudo-legal moves for side to move. Captures are flagged. */
function genPseudo(st, capturesOnly){
  const b = st.board, us = st.turn, them = opp(us);
  const out = [];
  for (let sq = 0; sq < 64; sq++){
    const p = b[sq];
    if (!p || colorOf(p) !== us) continue;
    const t = typeOf(p), f = sq & 7, r = sq >> 3;

    if (t === 'P'){
      const dir = us === 'w' ? -1 : 1;
      const startR = us === 'w' ? 6 : 1;
      const promoR = us === 'w' ? 0 : 7;
      const one = sq + dir * 8;
      // pushes
      if (!capturesOnly && one >= 0 && one < 64 && !b[one]){
        if ((one >> 3) === promoR){
          for (const q of 'QRBN') out.push(mk(sq, one, p, null, { promo: q }));
        } else {
          out.push(mk(sq, one, p));
          const two = sq + dir * 16;
          if (r === startR && !b[two]) out.push(mk(sq, two, p, null, { flag:'double' }));
        }
      }
      // captures
      for (const df of [-1, 1]){
        const nf = f + df, nr = r + dir;
        if (nf < 0 || nf > 7 || nr < 0 || nr > 7) continue;
        const to = nr * 8 + nf;
        const tp = b[to];
        if (tp && colorOf(tp) === them){
          if (nr === promoR){ for (const q of 'QRBN') out.push(mk(sq, to, p, tp, { promo:q })); }
          else out.push(mk(sq, to, p, tp));
        } else if (!tp && to === st.ep && st.ep >= 0){
          const victim = to - dir * 8;
          if (b[victim] === (us === 'w' ? 'p' : 'P'))
            out.push(mk(sq, to, p, b[victim], { flag:'ep' }));
        }
      }
    }
    else if (t === 'N' || t === 'K'){
      const dirs = t === 'N' ? N_DIRS : K_DIRS;
      for (const [df, dr] of dirs){
        const nf = f + df, nr = r + dr;
        if (nf < 0 || nf > 7 || nr < 0 || nr > 7) continue;
        const to = nr * 8 + nf, tp = b[to];
        if (tp && colorOf(tp) === us) continue;
        if (capturesOnly && !tp) continue;
        out.push(mk(sq, to, p, tp));
      }
      if (t === 'K') genCastles(st, sq, us, out);
    }
    else { // sliders
      const dirs = t === 'B' ? B_DIRS : t === 'R' ? R_DIRS : K_DIRS;
      for (const [df, dr] of dirs){
        let nf = f + df, nr = r + dr;
        while (nf >= 0 && nf < 8 && nr >= 0 && nr < 8){
          const to = nr * 8 + nf, tp = b[to];
          if (!tp){ if (!capturesOnly) out.push(mk(sq, to, p)); }
          else {
            if (colorOf(tp) === them) out.push(mk(sq, to, p, tp));
            break;
          }
          nf += df; nr += dr;
        }
      }
    }
  }
  return out;
}

function genCastles(st, ksq, us, out){
  const b = st.board;
  const enemy = opp(us);
  if (isAttacked(st, ksq, enemy)) return;
  if (us === 'w'){
    if (ksq !== 60) return;
    if (st.castling.includes('K') && b[63] === 'R' && !b[61] && !b[62] &&
        !isAttacked(st, 61, enemy) && !isAttacked(st, 62, enemy))
      out.push(mk(60, 62, 'K', null, { flag:'castle-k' }));
    if (st.castling.includes('Q') && b[56] === 'R' && !b[59] && !b[58] && !b[57] &&
        !isAttacked(st, 59, enemy) && !isAttacked(st, 58, enemy))
      out.push(mk(60, 58, 'K', null, { flag:'castle-q' }));
  } else {
    if (ksq !== 4) return;
    if (st.castling.includes('k') && b[7] === 'r' && !b[5] && !b[6] &&
        !isAttacked(st, 5, enemy) && !isAttacked(st, 6, enemy))
      out.push(mk(4, 6, 'k', null, { flag:'castle-k' }));
    if (st.castling.includes('q') && b[0] === 'r' && !b[3] && !b[2] && !b[1] &&
        !isAttacked(st, 3, enemy) && !isAttacked(st, 2, enemy))
      out.push(mk(4, 2, 'k', null, { flag:'castle-q' }));
  }
}

/** Is square `sq` attacked by side `by`? */
function isAttacked(st, sq, by){
  const b = st.board;
  const f = sq & 7, r = sq >> 3;

  // pawns
  if (by === 'w'){
    if (r + 1 < 8){
      if (f > 0 && b[(r+1)*8 + f-1] === 'P') return true;
      if (f < 7 && b[(r+1)*8 + f+1] === 'P') return true;
    }
  } else {
    if (r - 1 >= 0){
      if (f > 0 && b[(r-1)*8 + f-1] === 'p') return true;
      if (f < 7 && b[(r-1)*8 + f+1] === 'p') return true;
    }
  }
  // knights
  const kn = by === 'w' ? 'N' : 'n';
  for (const [df, dr] of N_DIRS){
    const nf = f + df, nr = r + dr;
    if (nf < 0 || nf > 7 || nr < 0 || nr > 7) continue;
    if (b[nr*8+nf] === kn) return true;
  }
  // king
  const kg = by === 'w' ? 'K' : 'k';
  for (const [df, dr] of K_DIRS){
    const nf = f + df, nr = r + dr;
    if (nf < 0 || nf > 7 || nr < 0 || nr > 7) continue;
    if (b[nr*8+nf] === kg) return true;
  }
  // sliders
  for (const [df, dr] of B_DIRS){
    let nf = f + df, nr = r + dr;
    while (nf >= 0 && nf < 8 && nr >= 0 && nr < 8){
      const p = b[nr*8+nf];
      if (p){
        const t = typeOf(p);
        if (colorOf(p) === by && (t === 'B' || t === 'Q')) return true;
        break;
      }
      nf += df; nr += dr;
    }
  }
  for (const [df, dr] of R_DIRS){
    let nf = f + df, nr = r + dr;
    while (nf >= 0 && nf < 8 && nr >= 0 && nr < 8){
      const p = b[nr*8+nf];
      if (p){
        const t = typeOf(p);
        if (colorOf(p) === by && (t === 'R' || t === 'Q')) return true;
        break;
      }
      nf += df; nr += dr;
    }
  }
  return false;
}

/* ======================= 4. MAKING MOVES ================================= */
function makeMove(st, m){
  const ns = {
    board: st.board.slice(), turn: opp(st.turn), castling: st.castling,
    ep: -1, half: st.half + 1, full: st.full + (st.turn === 'b' ? 1 : 0)
  };
  const b = ns.board, us = st.turn;
  const p = b[m.from];
  b[m.from] = null;

  if (m.flag === 'ep'){
    b[m.to + (us === 'w' ? 8 : -8)] = null;
  }
  if (m.promo){
    b[m.to] = us === 'w' ? m.promo : m.promo.toLowerCase();
  } else {
    b[m.to] = p;
  }
  if (m.flag === 'castle-k'){
    if (us === 'w'){ b[61] = b[63]; b[63] = null; } else { b[5] = b[7]; b[7] = null; }
  } else if (m.flag === 'castle-q'){
    if (us === 'w'){ b[59] = b[56]; b[56] = null; } else { b[3] = b[0]; b[0] = null; }
  }
  if (m.flag === 'double') ns.ep = (m.from + m.to) / 2;

  // halfmove clock
  if (typeOf(p) === 'P' || m.capture) ns.half = 0;

  // castling rights
  let rights = ns.castling;
  if (typeOf(p) === 'K') rights = rights.replace(/[KQ]/g, '');
  if (typeOf(p) === 'k') rights = rights.replace(/[kq]/g, '');
  const clear = sq => {
    if (sq === 63) rights = rights.replace('K','');
    if (sq === 56) rights = rights.replace('Q','');
    if (sq === 7)  rights = rights.replace('k','');
    if (sq === 0)  rights = rights.replace('q','');
  };
  clear(m.from); clear(m.to);
  ns.castling = rights;

  return ns;
}

/* ================= 5. LEGAL MOVE GENERATION ============================== */
function inCheck(st, color){
  const k = findKing(st.board, color);
  if (k < 0) return false;
  return isAttacked(st, k, opp(color));
}

/** All fully-legal moves for the side to move (king-safety filtered). */
function legalMoves(st){
  const us = st.turn, out = [];
  for (const m of genPseudo(st, false)){
    const ns = makeMove(st, m);
    if (!inCheck(ns, us)) out.push(m);
  }
  return out;
}
function legalMovesFrom(st, from){
  return legalMoves(st).filter(m => m.from === from);
}
function moveToUci(m){
  return sqName(m.from) + sqName(m.to) + (m.promo ? m.promo.toLowerCase() : '');
}

/* ======================= 6. GAME STATUS ================================== */
/**
 * Returns { over, result, reason, check }
 * result: 'w' | 'b' | 'draw' | null
 */
function gameStatus(st, history){
  const moves = legalMoves(st);
  const check = inCheck(st, st.turn);
  if (moves.length === 0){
    if (check) return { over:true, result: opp(st.turn), reason:'checkmate', check:true, moves };
    return { over:true, result:'draw', reason:'stalemate', check:false, moves };
  }
  if (st.half >= 100) return { over:true, result:'draw', reason:'fifty-move rule', check, moves };
  if (insufficientMaterial(st.board)) return { over:true, result:'draw', reason:'insufficient material', check, moves };
  if (history && isThreefold(history, st)) return { over:true, result:'draw', reason:'threefold repetition', check, moves };
  return { over:false, result:null, reason:null, check, moves };
}

function insufficientMaterial(board){
  const pieces = [];
  for (let i = 0; i < 64; i++) if (board[i]) pieces.push({ p: board[i], sq: i });
  if (pieces.length > 4) return false;
  const nonKings = pieces.filter(x => typeOf(x.p) !== 'K');
  if (nonKings.length === 0) return true;                       // K vs K
  if (nonKings.length === 1){
    const t = typeOf(nonKings[0].p);
    return t === 'B' || t === 'N';                              // K+B vs K, K+N vs K
  }
  if (nonKings.length === 2){
    const a = nonKings[0], b = nonKings[1];
    const ta = typeOf(a.p), tb = typeOf(b.p);
    // K+B vs K+B with both bishops on the same colour complex
    if (ta === 'B' && tb === 'B' && colorOf(a.p) !== colorOf(b.p)){
      const ca = ((a.sq >> 3) + (a.sq & 7)) & 1;
      const cb = ((b.sq >> 3) + (b.sq & 7)) & 1;
      return ca === cb;
    }
  }
  return false;
}

/** Threefold repetition: compare reduced position keys. */
function posKey(st){
  return st.board.join('') + '|' + st.turn + '|' + st.castling + '|' + st.ep +
         '|' + (st.castling ? '' : ''); // ep included only when relevant — conservative
}
function isThreefold(history, st){
  const key = posKey(st);
  let n = 1;
  for (let i = history.length - 1; i >= 0; i--){
    if (posKey(history[i]) === key) n++;
    if (n >= 3) return true;
  }
  return false;
}

/* ==================== 7. ALGEBRAIC NOTATION ============================== */
function toSAN(st, m){
  const legal = legalMoves(st);
  const t = typeOf(m.piece);
  let san;

  if (m.flag === 'castle-k') san = 'O-O';
  else if (m.flag === 'castle-q') san = 'O-O-O';
  else {
    const dest = sqName(m.to);
    if (t === 'P'){
      san = m.capture ? FILES[m.from & 7] + 'x' + dest : dest;
      if (m.flag === 'ep') san += ' e.p.';
    } else {
      // disambiguation
      const rivals = legal.filter(o => o.to === m.to && typeOf(o.piece) === t &&
                                       (o.from !== m.from) && o.promo === m.promo);
      let dis = '';
      if (rivals.length){
        const sameFile = rivals.some(o => (o.from & 7) === (m.from & 7));
        const sameRank = rivals.some(o => (o.from >> 3) === (m.from >> 3));
        if (!sameFile) dis = FILES[m.from & 7];
        else if (!sameRank) dis = String(8 - (m.from >> 3));
        else dis = sqName(m.from);
      }
      san = t + dis + (m.capture ? 'x' : '') + dest;
    }
    if (m.promo) san += '=' + m.promo;
  }

  const ns = makeMove(st, m);
  const status = gameStatus(ns, null);
  if (status.check){
    if (status.moves.length === 0) san += '#';
    else san += '+';
  }
  return san;
}

/* ======================= 8. EVALUATION =================================== */
/* Piece-square tables (white perspective, index 0 = a8 ... 63 = h1) */
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],
  KE: [
   -50,-40,-30,-20,-20,-30,-40,-50,
   -30,-20,-10,  0,  0,-10,-20,-30,
   -30,-10, 20, 30, 30, 20,-10,-30,
   -30,-10, 30, 40, 40, 30,-10,-30,
   -30,-10, 30, 40, 40, 30,-10,-30,
   -30,-10, 20, 30, 30, 20,-10,-30,
   -30,-30,  0,  0,  0,  0,-30,-30,
   -50,-30,-30,-30,-30,-30,-30,-50]
};

/** Static evaluation, from the perspective of the side to move (negamax). */
function evaluate(st){
  const b = st.board;
  let score = 0, nonPawn = 0;
  let wb = 0, bb = 0;
  const pawnFilesW = [0,0,0,0,0,0,0,0], pawnFilesB = [0,0,0,0,0,0,0,0];
  const pawnsW = [], pawnsB = [];

  for (let sq = 0; sq < 64; sq++){
    const p = b[sq];
    if (!p) continue;
    const t = typeOf(p), w = colorOf(p) === 'w';
    if (t !== 'P' && t !== 'K') nonPawn += P_VAL[t];
    if (t === 'B'){ w ? wb++ : bb++; }
    if (t === 'P'){ (w ? pawnsW : pawnsB).push(sq); (w ? pawnFilesW : pawnFilesB)[sq & 7]++; }
  }
  const endgame = nonPawn < 1500;

  for (let sq = 0; sq < 64; sq++){
    const p = b[sq];
    if (!p) continue;
    const t = typeOf(p), w = colorOf(p) === 'w';
    const idx = w ? sq : (sq ^ 56);
    const table = (t === 'K') ? (endgame ? PST.KE : PST.K) : PST[t];
    const v = P_VAL[t] + table[idx];
    score += w ? v : -v;
  }

  // small positional terms
  for (let f = 0; f < 8; f++){
    if (pawnFilesW[f] > 1) score += (pawnFilesW[f] - 1) * CFG.W_PAWN_STRUCT.DOUBLED;
    if (pawnFilesB[f] > 1) score -= (pawnFilesB[f] - 1) * CFG.W_PAWN_STRUCT.DOUBLED;
    const wIso = pawnFilesW[f] > 0 && (f === 0 || !pawnFilesW[f-1]) && (f === 7 || !pawnFilesW[f+1]);
    const bIso = pawnFilesB[f] > 0 && (f === 0 || !pawnFilesB[f-1]) && (f === 7 || !pawnFilesB[f+1]);
    if (wIso) score += CFG.W_PAWN_STRUCT.ISOLATED;
    if (bIso) score -= CFG.W_PAWN_STRUCT.ISOLATED;
  }
  if (wb >= 2) score += CFG.W_BISHOP_PAIR;
  if (bb >= 2) score -= CFG.W_BISHOP_PAIR;

  // passed pawns (simple: no enemy pawn on file or adjacent files ahead)
  for (const sq of pawnsW){
    const f = sq & 7, r = sq >> 3;
    let passed = true;
    for (let rr = 0; rr < r; rr++)
      for (let ff = Math.max(0,f-1); ff <= Math.min(7,f+1); ff++)
        if (b[rr*8+ff] === 'p') { passed = false; break; }
    if (passed) score += 12 + (6 - r) * 5;
  }
  for (const sq of pawnsB){
    const f = sq & 7, r = sq >> 3;
    let passed = true;
    for (let rr = r + 1; rr < 8; rr++)
      for (let ff = Math.max(0,f-1); ff <= Math.min(7,f+1); ff++)
        if (b[rr*8+ff] === 'P') { passed = false; break; }
    if (passed) score -= 12 + (r - 1) * 5;
  }

  score += CFG.W_TEMPO * (st.turn === 'w' ? 1 : -1);
  return st.turn === 'w' ? score : -score;
}

/* ========================= 9. SEARCH (AI) ================================ */
const ABORT = { abort: true };
let nodesSearched = 0;

function orderMoves(st, moves){
  for (const m of moves){
    let s = 0;
    if (m.capture) s += 100 * P_VAL[typeOf(m.capture)] - P_VAL[typeOf(m.piece)];
    if (m.promo) s += P_VAL[m.promo] + 50;
    if (m.flag === 'castle-k' || m.flag === 'castle-q') s += 30;
    m._s = s;
  }
  moves.sort((a, b) => b._s - a._s);
  return moves;
}

function checkTime(start, limit){
  if (performance.now() - start > limit) throw ABORT;
}

function quiesce(st, alpha, beta, qd, start, limit, ply){
  nodesSearched++;
  if ((nodesSearched & 511) === 0) checkTime(start, limit);

  const stand = evaluate(st);
  if (stand >= beta) return beta;
  if (stand > alpha) alpha = stand;
  if (qd <= 0 || ply > 40) return alpha;

  const caps = orderMoves(st, legalMoves(st).filter(m => m.capture || m.promo));
  for (const m of caps){
    const ns = makeMove(st, m);
    const sc = -quiesce(ns, -beta, -alpha, qd - 1, start, limit, ply + 1);
    if (sc >= beta) return beta;
    if (sc > alpha) alpha = sc;
  }
  return alpha;
}

function negamax(st, depth, alpha, beta, ply, start, limit){
  nodesSearched++;
  if ((nodesSearched & 511) === 0) checkTime(start, limit);

  const moves = legalMoves(st);
  if (moves.length === 0){
    if (inCheck(st, st.turn)) return -MATE + ply;   // prefer faster mates
    return 0;                                        // stalemate
  }
  if (st.half >= 100 || insufficientMaterial(st.board)){
    // 50-move / dead position — score as a draw unless a mate is on the board
    return 0;
  }
  if (depth <= 0) return quiesce(st, alpha, beta, CFG.AI_MAX_QUIESCE, start, limit, ply);

  orderMoves(st, moves);
  let best = -INF;
  // check extension: look one ply deeper when in check
  const ext = inCheck(st, st.turn) ? 1 : 0;

  for (const m of moves){
    const ns = makeMove(st, m);
    const sc = -negamax(ns, depth - 1 + ext, -beta, -alpha, ply + 1, start, limit);
    if (sc > best) best = sc;
    if (sc > alpha) alpha = sc;
    if (alpha >= beta) break;
  }
  return best;
}

/** Iterative deepening root search. Returns { move, score, depth, nodes, ms }. */
function findBestMove(st, maxDepth, timeLimit){
  const start = performance.now();
  nodesSearched = 0;
  let best = null, bestScore = 0, reachedDepth = 0;
  let prevMove = null;

  const rootMoves = legalMoves(st);
  if (!rootMoves.length) return null;

  for (let d = 1; d <= maxDepth; d++){
    const moves = orderMoves(st, legalMoves(st));
    if (prevMove){
      const i = moves.findIndex(m => m.from === prevMove.from && m.to === prevMove.to && m.promo === prevMove.promo);
      if (i > 0) moves.unshift(moves.splice(i, 1)[0]);
    }
    let alpha = -INF, localBest = moves[0], localScore = -INF;
    let completed = true;
    try {
      for (const m of moves){
        const ns = makeMove(st, m);
        const sc = -negamax(ns, d - 1, -INF, -alpha, 1, start, timeLimit);
        if (sc > localScore){ localScore = sc; localBest = m; }
        if (sc > alpha) alpha = sc;
      }
    } catch (e){
      if (e !== ABORT) throw e;
      completed = false;
    }
    if (completed){
      best = localBest; bestScore = localScore; reachedDepth = d; prevMove = localBest;
      if (Math.abs(localScore) > MATE - 500) break;  // mate found — stop early
    } else if (!best){
      best = localBest; bestScore = localScore; reachedDepth = Math.max(reachedDepth, 1);
      break;
    } else break;
  }
  return { move: best, score: bestScore, depth: reachedDepth,
           nodes: nodesSearched, ms: Math.round(performance.now() - start) };
}

/* =========================== 10. AUDIO =================================== */
let audioCtx = null;
function ac(){
  if (G.muted) return null;
  const AC = window.AudioContext || window.webkitAudioContext;
  if (!AC) return null;
  if (!audioCtx) audioCtx = new AC();
  if (audioCtx.state === 'suspended') { try { audioCtx.resume(); } catch (e) {} }
  return audioCtx;
}
// create/resume the context on the first real user gesture (browser autoplay policy)
window.addEventListener('pointerdown', function primeAudio(){ ac(); }, { once: true });
function tone(freq, dur, type, vol, delay){
  if (G.muted) return;
  const c = ac(); if (!c) return;
  const t0 = c.currentTime + (delay || 0);
  const o = c.createOscillator(), g = c.createGain();
  o.type = type || 'sine'; o.frequency.setValueAtTime(freq, t0);
  g.gain.setValueAtTime(0.0001, t0);
  g.gain.exponentialRampToValueAtTime(vol, t0 + 0.008);
  g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
  o.connect(g); g.connect(c.destination);
  o.start(t0); o.stop(t0 + dur + 0.02);
}
function noise(dur, vol, delay){
  if (G.muted) return;
  const c = ac(); if (!c) return;
  const len = Math.floor(c.sampleRate * dur);
  const buf = c.createBuffer(1, len, c.sampleRate);
  const d = buf.getChannelData(0);
  for (let i = 0; i < len; i++) d[i] = (Math.random() * 2 - 1) * (1 - i / len);
  const src = c.createBufferSource(); src.buffer = buf;
  const g = c.createGain(); g.gain.value = vol;
  const f = c.createBiquadFilter(); f.type = 'lowpass'; f.frequency.value = 1400;
  src.connect(f); f.connect(g); g.connect(c.destination);
  src.start(c.currentTime + (delay || 0));
}
const SFX = {
  move(){ tone(320, 0.07, 'triangle', 0.13); noise(0.05, 0.05); },
  capture(){ tone(180, 0.13, 'square', 0.10); noise(0.10, 0.12); },
  check(){ tone(760, 0.10, 'sine', 0.13); tone(1020, 0.13, 'sine', 0.10, 0.10); },
  castle(){ tone(300, 0.06, 'triangle', 0.12); tone(400, 0.07, 'triangle', 0.11, 0.07); },
  end(){ tone(520, 0.16, 'sine', 0.13); tone(392, 0.20, 'sine', 0.12, 0.15); tone(262, 0.34, 'sine', 0.11, 0.32); }
};

/* ==================== 11. RENDERING / UI ================================= */
const GLYPH = { K:'\u265A', Q:'\u265B', R:'\u265C', B:'\u265D', N:'\u265E', P:'\u265F' };

const G = {
  state: parseFen(START_FEN),
  history: [],     // state before each move (undo + repetition)
  moves: [],
  sans: [],
  playerColor: 'w',
  flipped: false,
  muted: false,
  depth: 2,
  selected: -1,
  thinking: false,
  over: false,
  pendingPromo: null,
  legal: [],
  status: null,
  lastMove: null
};

const boardEl   = document.getElementById('board');
const frameEl   = document.getElementById('boardFrame');
const promoEl   = document.getElementById('promo');
const bannerEl  = document.getElementById('banner');
const statusEl  = document.getElementById('statusLine');
const movesEl   = document.getElementById('movesList');
const trayWEl   = document.getElementById('trayW');
const trayBEl   = document.getElementById('trayB');
const turnDot   = document.getElementById('turnDot');
const turnTxt   = document.getElementById('turnTxt');
const sideSel   = document.getElementById('sideSel');
const depthSel  = document.getElementById('depthSel');
const sqEls = new Array(64);

/* --- board construction / layout --- */
function buildBoard(){
  boardEl.innerHTML = '';
  for (let sq = 0; sq < 64; sq++){
    const d = document.createElement('div');
    const light = (((sq >> 3) + (sq & 7)) & 1) === 0;
    d.className = 'sq ' + (light ? 'light' : 'dark');
    d.dataset.sq = sq;
    sqEls[sq] = d;
    boardEl.appendChild(d);
  }
  layoutBoard();
}
function layoutBoard(){
  for (let sq = 0; sq < 64; sq++){
    const r = sq >> 3, f = sq & 7;
    const el = sqEls[sq];
    el.style.gridRow = (G.flipped ? 8 - r : r + 1);
    el.style.gridColumn = (G.flipped ? 8 - f : f + 1);
    el.innerHTML = '';
    const showRank = G.flipped ? f === 7 : f === 0;
    const showFile = G.flipped ? r === 0 : r === 7;
    if (showRank){
      const s = document.createElement('span'); s.className = 'coord r';
      s.textContent = String(8 - r); el.appendChild(s);
    }
    if (showFile){
      const s = document.createElement('span'); s.className = 'coord f';
      s.textContent = FILES[f]; el.appendChild(s);
    }
  }
  fitFont();
  renderBoard();
}
function fitFont(){
  const w = boardEl.clientWidth || 480;
  boardEl.style.fontSize = (w / 8 * 0.86) + 'px';
}
window.addEventListener('resize', () => { fitFont(); if (G.pendingPromo) hidePromo(); });

/* --- board render --- */
let dragHiddenSq = -1;
function renderBoard(){
  const st = G.state;
  const kingSq = G.status && G.status.check ? findKing(st.board, st.turn) : -1;
  const lm = G.lastMove;
  const targets = G.selected >= 0 ? G.legal.filter(m => m.from === G.selected) : null;

  for (let sq = 0; sq < 64; sq++){
    const el = sqEls[sq];
    el.querySelectorAll('.pc,.hint').forEach(n => n.remove());
    el.classList.remove('last','sel','check','over');

    if (lm && (sq === lm.from || sq === lm.to)) el.classList.add('last');
    if (sq === G.selected) el.classList.add('sel');
    if (sq === kingSq) el.classList.add('check');

    const p = st.board[sq];
    if (p){
      const s = document.createElement('span');
      s.className = 'pc ' + (colorOf(p) === 'w' ? 'w' : 'b');
      s.textContent = GLYPH[typeOf(p)];
      if (sq === dragHiddenSq) s.style.opacity = '0.2';
      el.appendChild(s);
    }
    if (targets && sq !== G.selected && dragHiddenSq < 0){
      const hit = targets.find(m => m.to === sq);
      if (hit){
        const h = document.createElement('span');
        h.className = 'hint' + (hit.capture || hit.flag === 'ep' ? ' cap' : '');
        el.appendChild(h);
      }
    }
  }
}

/* --- panel render --- */
function renderPanel(){
  const st = G.state, stt = G.status;

  // turn indicator
  turnDot.className = 'dot' + (st.turn === 'b' ? ' b' : '') + (G.thinking ? ' think' : '');
  if (stt && stt.over){
    turnTxt.textContent = 'Game over';
  } else if (G.thinking){
    turnTxt.textContent = 'Computer thinking…';
  } else {
    turnTxt.textContent = (st.turn === 'w' ? 'White' : 'Black') + ' to move' + (stt && stt.check ? ' — check!' : '');
  }

  // status line
  if (stt && stt.over){
    statusEl.innerHTML = '<b>' + resultText(stt) + '</b>';
  } else if (stt && stt.check){
    statusEl.innerHTML = '<b>' + (st.turn === 'w' ? 'White' : 'Black') + ' is in check</b> · ' +
      G.legal.length + ' legal moves';
  } else {
    statusEl.innerHTML = (G.playerColor === 'both' ? 'Analysis mode' :
      (st.turn === G.playerColor ? 'Your move' : 'Computer to move')) +
      ' · ' + G.legal.length + ' legal moves';
  }

  // captured trays
  const capW = [], capB = [];
  let matW = 0, matB = 0;
  for (const m of G.moves){
    if (m.capture){
      // trayW = pieces captured BY White (black pieces), trayB = the reverse
      if (colorOf(m.piece) === 'w'){ capW.push(m.capture); matW += P_VAL[typeOf(m.capture)]; }
      else { capB.push(m.capture); matB += P_VAL[typeOf(m.capture)]; }
    }
    if (m.promo){
      if (colorOf(m.piece) === 'w') matW += P_VAL[m.promo] - 100; else matB += P_VAL[m.promo] - 100;
    }
  }
  capW.sort((a,b) => P_VAL[typeOf(a)] - P_VAL[typeOf(b)]);
  capB.sort((a,b) => P_VAL[typeOf(a)] - P_VAL[typeOf(b)]);
  trayWEl.innerHTML = capW.map(p => '<span class="pc ' + (colorOf(p) === 'w' ? 'w' : 'b') + '">' + GLYPH[typeOf(p)] + '</span>').join('');
  trayBEl.innerHTML = capB.map(p => '<span class="pc ' + (colorOf(p) === 'w' ? 'w' : 'b') + '">' + GLYPH[typeOf(p)] + '</span>').join('');
  const diff = matW - matB;
  if (diff > 0) trayWEl.innerHTML += '<span class="adv">+' + Math.round(diff/100) + '</span>';
  else if (diff < 0) trayBEl.innerHTML += '<span class="adv">+' + Math.round(-diff/100) + '</span>';

  // move list
  if (!G.sans.length){
    movesEl.innerHTML = '<div class="movesEmpty">No moves yet — White begins.</div>';
  } else {
    let html = '';
    for (let i = 0; i < G.sans.length; i += 2){
      const n = (i / 2) + 1;
      const w = G.sans[i] || '';
      const b = G.sans[i + 1] || '';
      const wCur = i === G.sans.length - 1 ? ' cur' : '';
      const bCur = i + 1 === G.sans.length - 1 ? ' cur' : '';
      html += '<div class="mvrow"><span class="num">' + n + '.</span>' +
              '<span class="mv' + wCur + '">' + w + '</span>' +
              '<span class="mv' + bCur + '">' + b + '</span></div>';
    }
    movesEl.innerHTML = html;
    movesEl.scrollTop = movesEl.scrollHeight;
  }

  // buttons
  document.getElementById('undoBtn').disabled = G.history.length === 0 || G.thinking;
  document.getElementById('muteBtn').textContent = G.muted ? '🔇 Muted' : '🔊 Sound';
}

function resultText(stt){
  if (stt.result === 'draw') return 'Draw — ' + stt.reason;
  return (stt.result === 'w' ? 'White' : 'Black') + ' wins by ' + stt.reason;
}

function refresh(){
  G.legal = legalMoves(G.state);
  G.status = gameStatus(G.state, G.history);
  if (!G.status.over) G.status.moves = G.legal;
  renderBoard();
  renderPanel();
}

function showBanner(){
  if (!G.status.over) { bannerEl.classList.add('hidden'); return; }
  bannerEl.classList.remove('hidden');
  bannerEl.innerHTML = '<div class="big">' + resultText(G.status) + '</div>' +
    '<div class="sub">' + (G.state.turn === 'w' ? 'White' : 'Black') + ' to move</div>' +
    '<button id="againBtn">New Game</button>';
  document.getElementById('againBtn').onclick = newGame;
}
function hideBanner(){ bannerEl.classList.add('hidden'); }

/* --- promotion picker --- */
function showPromo(from, to){
  const opts = G.legal.filter(m => m.from === from && m.to === to && m.promo);
  if (!opts.length) return;
  G.pendingPromo = { from, to };
  promoEl.innerHTML = '';
  promoEl.classList.remove('hidden');
  for (const q of ['Q','R','B','N']){
    const m = opts.find(x => x.promo === q);
    if (!m) continue;
    const btn = document.createElement('button');
    const sp = document.createElement('span');
    sp.className = 'pc ' + (G.state.turn === 'w' ? 'w' : 'b');
    sp.textContent = GLYPH[q];
    btn.appendChild(sp);
    btn.title = 'Promote to ' + q;
    btn.onclick = (ev) => { ev.stopPropagation(); hidePromo(); doMove(m); };
    promoEl.appendChild(btn);
  }
  // position over the promotion square
  const sr = sqEls[to].getBoundingClientRect();
  const fr = frameEl.getBoundingClientRect();
  const w = promoEl.offsetWidth, h = promoEl.offsetHeight;
  let left = sr.left - fr.left + sr.width / 2 - w / 2;
  let top  = sr.bottom - fr.top + 6;
  if (top + h > fr.height - 2) top = sr.top - fr.top - h - 6;
  left = Math.max(2, Math.min(left, fr.width - w - 2));
  top  = Math.max(2, Math.min(top, fr.height - h - 2));
  promoEl.style.left = left + 'px';
  promoEl.style.top = top + 'px';
}
function hidePromo(){
  G.pendingPromo = null;
  promoEl.classList.add('hidden');
  promoEl.innerHTML = '';
}

/* --- interaction: click + drag --- */
function squareAt(clientX, clientY){
  const r = boardEl.getBoundingClientRect();
  const x = clientX - r.left, y = clientY - r.top;
  if (x < 0 || y < 0 || x >= r.width || y >= r.height) return -1;
  let col = Math.floor(x / (r.width / 8));
  let row = Math.floor(y / (r.height / 8));
  if (G.flipped){ col = 7 - col; row = 7 - row; }
  return row * 8 + col;
}
function isHumanTurn(){
  if (G.over || G.thinking || G.pendingPromo) return false;
  if (G.playerColor === 'both') return true;
  return G.state.turn === G.playerColor;
}
function findMove(from, to){
  return G.legal.find(m => m.from === from && m.to === to) || null;
}

let drag = null, ghostEl = null;
boardEl.addEventListener('pointerdown', (e) => {
  const sq = squareAt(e.clientX, e.clientY);
  if (sq < 0) return;
  if (!isHumanTurn()) { G.selected = -1; renderBoard(); return; }

  if (G.selected >= 0){
    const mv = findMove(G.selected, sq);
    if (mv){
      e.preventDefault();
      cancelDrag();
      if (mv.promo){ G.selected = -1; renderBoard(); showPromo(mv.from, mv.to); }
      else doMove(mv);
      return;
    }
    if (sq === G.selected){ G.selected = -1; renderBoard(); return; }
  }
  const p = G.state.board[sq];
  if (p && colorOf(p) === G.state.turn){
    G.selected = sq;
    drag = { from: sq, x: e.clientX, y: e.clientY, active: false, moved: false };
    renderBoard();
    e.preventDefault();
  } else {
    G.selected = -1;
    renderBoard();
  }
});
window.addEventListener('pointermove', (e) => {
  if (!drag) return;
  const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
  if (!drag.active && (dx * dx + dy * dy) > 16){
    drag.active = true;
    dragHiddenSq = drag.from;
    const p = G.state.board[drag.from];
    ghostEl = document.createElement('div');
    ghostEl.id = 'ghost';
    ghostEl.style.fontSize = boardEl.style.fontSize;
    ghostEl.innerHTML = '<span class="pc ' + (colorOf(p) === 'w' ? 'w' : 'b') + '">' + GLYPH[typeOf(p)] + '</span>';
    document.body.appendChild(ghostEl);
    renderBoard();
  }
  if (drag.active){
    ghostEl.style.left = e.clientX + 'px';
    ghostEl.style.top  = e.clientY + 'px';
    const sq = squareAt(e.clientX, e.clientY);
    for (let i = 0; i < 64; i++) sqEls[i].classList.remove('over');
    if (sq >= 0 && findMove(drag.from, sq)) sqEls[sq].classList.add('over');
  }
});
window.addEventListener('pointerup', (e) => {
  if (!drag) return;
  const d = drag;
  drag = null;
  dragHiddenSq = -1;
  for (let i = 0; i < 64; i++) sqEls[i].classList.remove('over');
  if (d.active){
    const sq = squareAt(e.clientX, e.clientY);
    const mv = sq >= 0 ? findMove(d.from, sq) : null;
    cancelDrag();
    if (mv){
      if (mv.promo){ G.selected = -1; renderBoard(); showPromo(mv.from, mv.to); }
      else doMove(mv);
    } else {
      G.selected = d.from;
      renderBoard();
    }
  } else {
    renderBoard();
  }
});
window.addEventListener('pointercancel', () => { cancelDrag(); renderBoard(); });
function cancelDrag(){
  if (ghostEl){ ghostEl.remove(); ghostEl = null; }
  dragHiddenSq = -1;
}

/* --- applying a move --- */
function doMove(m, opts){
  opts = opts || {};
  if (G.over || G.pendingPromo) return false;
  hidePromo();

  const san = toSAN(G.state, m);
  G.history.push(cloneState(G.state));
  G.moves.push(m);
  G.sans.push(san);
  G.state = makeMove(G.state, m);
  G.lastMove = m;
  G.selected = -1;
  cancelDrag();

  if (opts.sfx !== false){
    if (m.flag === 'castle-k' || m.flag === 'castle-q') SFX.castle();
    else if (m.capture || m.flag === 'ep') SFX.capture();
    else SFX.move();
  }

  refresh();
  if (G.status.check && !G.status.over && opts.sfx !== false) SFX.check();
  if (G.status.over && opts.sfx !== false) SFX.end();

  showBanner();
  save();
  if (!opts.noAI && G.status.over === false) maybeAI();
  return true;
}

/* --- AI turn --- */
let aiToken = 0;
function maybeAI(){
  if (G.over || G.playerColor === 'both') return;
  if (G.state.turn === G.playerColor) return;
  G.thinking = true;
  renderPanel();
  const token = ++aiToken;
  const expect = toFen(G.state);
  setTimeout(() => runAI(token, expect), 40);
}
function runAI(token, expect){
  // abort if the position changed underneath us (undo / new game / manual move)
  if (token !== aiToken || G.over || !G.thinking) return;
  if (toFen(G.state) !== expect) { G.thinking = false; renderPanel(); return; }
  if (G.playerColor !== 'both' && G.state.turn === G.playerColor) { G.thinking = false; renderPanel(); return; }
  const deadline = performance.now() + CFG.AI_TIME_MS + 800;
  let res = null;
  try {
    res = findBestMove(G.state, CFG.DEPTHS[G.depth] || 2, CFG.AI_TIME_MS);
  } catch (err){
    console.error('AI error', err);
  }
  G.thinking = false;
  if (performance.now() > deadline) console.warn('AI exceeded soft deadline');
  if (!res || !res.move){ refresh(); showBanner(); return; }
  // safety: never play an illegal move
  const ok = G.legal.some(x => x.from === res.move.from && x.to === res.move.to && x.promo === res.move.promo);
  if (!ok){
    console.error('AI produced an illegal move — falling back', res.move);
    const fallback = G.legal[Math.floor(Math.random() * G.legal.length)];
    doMove(fallback);
    return;
  }
  doMove(res.move);
}

/* --- controls --- */
function popOne(){
  if (!G.history.length) return false;
  G.state = G.history.pop();
  G.moves.pop();
  G.sans.pop();
  G.lastMove = G.moves.length ? G.moves[G.moves.length - 1] : null;
  return true;
}
function undo(){
  if (!G.history.length || G.thinking) return;
  G.over = false;
  hideBanner();
  popOne();
  if (G.playerColor !== 'both' && G.state.turn !== G.playerColor) popOne();
  G.selected = -1;
  G.thinking = false;
  refresh();
  save();
}
function newGame(){
  G.state = parseFen(START_FEN);
  G.history = []; G.moves = []; G.sans = [];
  G.selected = -1; G.over = false; G.thinking = false;
  G.lastMove = null; G.pendingPromo = null;
  G.playerColor = sideSel.value;
  G.depth = +depthSel.value;
  G.flipped = (G.playerColor === 'b');
  hideBanner(); hidePromo();
  layoutBoard(); refresh(); save();
  maybeAI();
}

/* ======================= 12. PERSISTENCE ================================= */
const SAVE_KEY = 'chess.engine.save.v1';
function save(){
  try {
    localStorage.setItem(SAVE_KEY, JSON.stringify({
      state: G.state, history: G.history, moves: G.moves, sans: G.sans,
      playerColor: G.playerColor, flipped: G.flipped, muted: G.muted, depth: G.depth
    }));
  } catch (e){ /* storage may be unavailable */ }
}
function load(){
  try {
    const raw = localStorage.getItem(SAVE_KEY);
    if (!raw) return false;
    const d = JSON.parse(raw);
    if (!d || !d.state || !Array.isArray(d.state.board) || d.state.board.length !== 64) return false;
    G.state = d.state;
    G.history = d.history || [];
    G.moves = d.moves || [];
    G.sans = d.sans || [];
    G.playerColor = d.playerColor || 'w';
    G.flipped = !!d.flipped;
    G.muted = !!d.muted;
    G.depth = d.depth || 2;
    G.lastMove = G.moves.length ? G.moves[G.moves.length - 1] : null;
    return true;
  } catch (e){ return false; }
}
function clearSave(){ try { localStorage.removeItem(SAVE_KEY); } catch(e){} }

/* ====================== 13. DEBUG INTERFACE ============================== */
window.game = {
  get state(){ return G.state; },
  fen(){ return toFen(G.state); },
  legalMoves(){ return legalMoves(G.state).map(moveToUci); },
  legalMovesDetailed(){ return legalMoves(G.state).map(m => ({
    from: sqName(m.from), to: sqName(m.to), uci: moveToUci(m),
    piece: m.piece, capture: m.capture, promo: m.promo, flag: m.flag
  })); },
  san(){ return G.sans.slice(); },
  /** Play a move in UCI-ish form ('e2e4', 'e7e8q'). Returns true if accepted. */
  move(str){
    if (!str || str.length < 4) return false;
    const from = nameSq(str.slice(0,2)), to = nameSq(str.slice(2,4));
    if (from < 0 || to < 0) return false;
    const promo = str.length > 4 ? str[4].toUpperCase() : null;
    const m = G.legal.find(x => x.from === from && x.to === to && (promo ? x.promo === promo : true));
    if (!m) return false;
    return doMove(m, { noAI: true, sfx: false });
  },
  /** Load any FEN (board, turn, castling, ep, clocks). */
  load(fen){
    try {
      const st = parseFen(fen);
      if (findKing(st.board,'w') < 0 || findKing(st.board,'b') < 0) return false;
      G.state = st; G.history = []; G.moves = []; G.sans = [];
      G.selected = -1; G.over = false; G.lastMove = null; G.thinking = false;
      refresh(); save();
      return true;
    } catch(e){ return false; }
  },
  status(){
    const s = gameStatus(G.state, G.history);
    return { over: s.over, result: s.result, reason: s.reason, check: s.check,
             moves: s.moves.length };
  },
  inCheck(color){ return inCheck(G.state, color || G.state.turn); },
  eval(){ return evaluate(G.state); },
  bestMove(depth){ return findBestMove(G.state, depth || 2, CFG.AI_TIME_MS); },
  reset(){ newGame(); clearSave(); },
  board(){ return G.state.board.slice(); },
  turn(){ return G.state.turn; },
  newGame, undo,
  _G: G
};

/* ============================ 14. BOOT =================================== */
function boot(){
  buildBoard();
  if (!load()){
    G.playerColor = 'w';
    G.depth = 2;
  }
  sideSel.value = G.playerColor;
  depthSel.value = String(G.depth);
  document.getElementById('muteBtn').textContent = G.muted ? '🔇 Muted' : '🔊 Sound';

  document.getElementById('newBtn').onclick = newGame;
  document.getElementById('undoBtn').onclick = undo;
  document.getElementById('flipBtn').onclick = () => {
    G.flipped = !G.flipped;
    layoutBoard(); save();
  };
  document.getElementById('muteBtn').onclick = () => {
    G.muted = !G.muted;
    if (!G.muted) SFX.move();
    renderPanel(); save();
  };
  sideSel.onchange = () => {
    G.playerColor = sideSel.value;
    // orient the board to the side you play
    if (G.playerColor === 'b') G.flipped = true;
    else if (G.playerColor === 'w') G.flipped = false;
    layoutBoard(); refresh(); save();
    maybeAI();
  };
  depthSel.onchange = () => { G.depth = +depthSel.value; save(); };

  layoutBoard();
  refresh();
  showBanner();
  // resume an in-progress AI move after a reload
  if (!G.status.over && G.playerColor !== 'both' && G.state.turn !== G.playerColor) maybeAI();
}
document.addEventListener('keydown', (e) => {
  if (e.key === 'z' && (e.ctrlKey || e.metaKey)){ e.preventDefault(); undo(); }
  if (e.key === 'f' && !e.ctrlKey && !e.metaKey){ G.flipped = !G.flipped; layoutBoard(); save(); }
});
boot();
</script>
</body>
</html>
<!-- agent-meta {"model":"deepseek/deepseek-v4.1-flash","provider":"openrouter","persona":"full","sessionId":"9b4bf85f-ada5-4da5-b632-5fb4fd769223","tokensIn":2210189,"tokensOut":60693,"tokensTotal":2270882,"cost":0.048843606,"turns":33,"toolCalls":37,"failedToolCalls":0,"timestamp":"2026-09-10T16:50:12.171Z"} -->