← Chess results

Chess

muse spark 1.3 contributor · 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 47,192 bytes · SHA-256 db1046e8a01c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Grandmaster — Chess vs Computer</title>
<style>
:root{
  --light:#ecd9b0; --dark:#a5714b; --light2:#f0dcb4; --dark2:#96683f;
  --bg:#14110d; --panel:#1e1a14; --panel2:#262019; --gold:#d4a94e; --gold2:#f2cd7e;
  --ink:#efe6d3; --muted:#a89a80; --green:#7fa650; --red:#e05252;
}
*{box-sizing:border-box; -webkit-tap-highlight-color:transparent;}
html,body{margin:0;padding:0;background:radial-gradient(1200px 800px at 50% -10%,#2b241a 0%,var(--bg) 60%);color:var(--ink);font-family:Georgia,'Times New Roman',serif;min-height:100vh;}
header{display:flex;align-items:center;justify-content:space-between;padding:14px 22px;max-width:1180px;margin:0 auto;}
.brand{display:flex;align-items:center;gap:12px;}
.brand .knight{font-size:34px;filter:drop-shadow(0 2px 6px rgba(0,0,0,.6));}
.brand h1{font-size:22px;margin:0;letter-spacing:3px;font-weight:700;color:var(--gold2);}
.brand h1 small{display:block;font-size:11px;letter-spacing:4px;color:var(--muted);font-family:system-ui,sans-serif;}
.top-controls{display:flex;gap:8px;align-items:center;font-family:system-ui,sans-serif;}
button,select{background:#2e271c;border:1px solid #4a3f2c;color:var(--ink);border-radius:10px;padding:8px 12px;font-size:13px;cursor:pointer;transition:.15s;}
button:hover,select:hover{border-color:var(--gold);transform:translateY(-1px);}
button.primary{background:linear-gradient(180deg,#e8bc62,#c99635);color:#231a08;font-weight:700;border-color:#8a6a24;}
button:disabled{opacity:.45;cursor:default;transform:none;}
main{max-width:1180px;margin:0 auto;padding:6px 22px 30px;display:grid;grid-template-columns:minmax(320px,640px) 340px;gap:22px;justify-content:center;}
@media(max-width:900px){main{grid-template-columns:1fr;} header{flex-wrap:wrap;gap:10px;}}
.board-wrap{position:relative;}
.turn-bar{display:flex;align-items:center;gap:10px;margin-bottom:10px;font-family:system-ui,sans-serif;font-size:13px;}
.turn-dot{width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 0 10px #fff;}
.turn-dot.b{background:#111;box-shadow:0 0 0 2px #888;}
#statusText{color:var(--ink);} #statusText b{color:var(--gold2);}
.board-outer{background:linear-gradient(180deg,#3a2d1d,#211a10);padding:14px;border-radius:16px;border:1px solid #4a3c26;box-shadow:0 20px 60px rgba(0,0,0,.55), inset 0 1px 0 rgba(255,255,255,.08);}
.coords{display:flex;justify-content:space-between;padding:0 4px 6px;color:var(--muted);font-size:11px;font-family:system-ui,sans-serif;}
#board{display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);aspect-ratio:1/1;border-radius:10px;overflow:hidden;border:3px solid #120e08;position:relative;touch-action:none;user-select:none;}
.sq{position:relative;display:flex;align-items:center;justify-content:center;cursor:pointer;}
.sq.light{background:linear-gradient(180deg,var(--light2),var(--light));}
.sq.dark{background:linear-gradient(180deg,var(--dark),var(--dark2));}
.sq.last::after{content:'';position:absolute;inset:0;background:rgba(212,169,78,.45);pointer-events:none;}
.sq.selected::after{content:'';position:absolute;inset:0;background:rgba(46,160,67,.45);box-shadow:inset 0 0 0 3px #2ea043;pointer-events:none;}
.sq.check::before{content:'';position:absolute;inset:6%;border-radius:18%;background:radial-gradient(circle,rgba(255,40,40,.85) 20%,rgba(255,40,40,0) 70%);filter:blur(1px);pointer-events:none;}
.sq.check{box-shadow:inset 0 0 0 4px #ff2d2d, inset 0 0 24px #ff2d2d;}
.piece{font-size:clamp(28px,6.2vw,52px);line-height:1;pointer-events:none;z-index:2;transition:transform .08s;}
.piece.w{color:#faf5e8;text-shadow:0 0 2px #000,0 2px 0 #000,-1px 0 0 #000,1px 0 0 #000,0 -1px 0 #000,0 4px 8px rgba(0,0,0,.6);}
.piece.b{color:#15110c;text-shadow:0 0 2px rgba(255,255,255,.35),0 1px 0 #fff3,0 4px 8px rgba(0,0,0,.5);}
.hint-dot{position:absolute;width:26%;height:26%;border-radius:50%;background:rgba(20,60,20,.38);box-shadow:0 0 0 3px rgba(20,60,20,.15);pointer-events:none;z-index:3;}
.hint-ring{position:absolute;inset:6%;border-radius:50%;border:5px solid rgba(200,30,30,.75);pointer-events:none;z-index:3;}
.sq.hover-target{outline:3px solid #2ea043;outline-offset:-3px;}
#dragPiece{position:fixed;pointer-events:none;z-index:99;font-size:52px;display:none;transform:translate(-50%,-58%);filter:drop-shadow(0 10px 12px rgba(0,0,0,.6));}
.panel{background:linear-gradient(180deg,var(--panel2),var(--panel));border:1px solid #4a3c26;border-radius:16px;padding:14px;display:flex;flex-direction:column;gap:12px;min-height:400px;box-shadow:0 20px 60px rgba(0,0,0,.4);}
.panel h3{margin:0;font-size:11px;letter-spacing:2px;color:var(--muted);font-family:system-ui,sans-serif;}
.trays{display:grid;grid-template-columns:1fr 1fr;gap:8px;}
.tray{background:#14110c;border:1px solid #3a3122;border-radius:10px;padding:8px;min-height:56px;}
.tray .cap{font-size:20px;line-height:1.2;letter-spacing:1px;}
.tray .cap.w{color:#faf5e8;text-shadow:0 1px 0 #000;}
.tray .cap.b{color:#8d8270;}
.tray .score{font-family:system-ui,sans-serif;font-size:11px;color:var(--gold2);}
#moves{background:#100d08;border:1px solid #3a3122;border-radius:10px;height:220px;overflow-y:auto;padding:8px 10px;font-family:system-ui,sans-serif;font-size:13px;}
#moves table{width:100%;border-collapse:collapse;}
#moves td{padding:3px 6px;border-radius:6px;}
#moves tr:nth-child(odd) td{background:rgba(255,255,255,.03);}
#moves td.num{color:var(--muted);width:34px;}
#moves td.cur{background:rgba(212,169,78,.25);}
.controls-grid{display:grid;grid-template-columns:1fr 1fr;gap:8px;font-family:system-ui,sans-serif;}
.controls-grid .row{display:flex;gap:8px;grid-column:1/-1;}
label.small{font-size:12px;color:var(--muted);font-family:system-ui,sans-serif;display:flex;align-items:center;gap:6px;}
select{width:100%;}
#banner{position:absolute;inset:14px;border-radius:12px;display:none;align-items:center;justify-content:center;flex-direction:column;gap:8px;background:rgba(10,8,5,.82);backdrop-filter:blur(4px);z-index:20;text-align:center;padding:20px;}
#banner.show{display:flex;}
#banner h2{margin:0;font-size:34px;color:var(--gold2);letter-spacing:1px;}
#banner p{margin:0;color:var(--ink);font-family:system-ui,sans-serif;}
#promo{position:absolute;z-index:30;display:none;background:#1e1a14;border:2px solid var(--gold);border-radius:12px;padding:8px;gap:6px;box-shadow:0 12px 30px rgba(0,0,0,.7);}
#promo.show{display:flex;}
#promo button{font-size:34px;padding:4px 10px;line-height:1;background:#2e271c;}
#promo button:hover{background:var(--gold);color:#000;}
.thinking{display:none;font-family:system-ui,sans-serif;font-size:12px;color:var(--gold2);align-items:center;gap:8px;}
.thinking.show{display:flex;}
.spinner{width:14px;height:14px;border:2px solid #55482f;border-top-color:var(--gold2);border-radius:50%;animation:spin 0.7s linear infinite;}
@keyframes spin{to{transform:rotate(360deg);}}
.footer-note{max-width:1180px;margin:0 auto;padding:0 22px 30px;color:var(--muted);font-size:12px;font-family:system-ui,sans-serif;}
kbd{background:#0006;border:1px solid #555;border-radius:4px;padding:0 5px;}
</style>
</head>
<body>
<header>
  <div class="brand"><div class="knight">♞</div><h1>GRANDMASTER<small>CHESS vs COMPUTER</small></h1></div>
  <div class="top-controls">
    <button id="btnNew" class="primary">+ New Game</button>
    <button id="btnUndo">↩ Undo</button>
    <button id="btnFlip">⇄ Flip</button>
    <button id="btnSound">🔊</button>
  </div>
</header>
<main>
  <div class="board-wrap">
    <div class="turn-bar"><div id="turnDot" class="turn-dot"></div><div id="statusText">White to move</div><div style="flex:1"></div><div id="think" class="thinking"><div class="spinner"></div><span id="thinkText">AI thinking…</span></div></div>
    <div class="board-outer">
      <div class="coords" id="topCoords"></div>
      <div id="board"></div>
      <div class="coords" id="botCoords"></div>
      <div id="banner"><h2 id="bannerTitle">Checkmate</h2><p id="bannerSub">White wins</p><button id="btnAgain" class="primary">New Game</button></div>
      <div id="promo"></div>
    </div>
  </div>
  <div class="panel">
    <div>
      <h3>CAPTURED</h3>
      <div class="trays" style="margin-top:6px">
        <div class="tray"><div style="font-size:11px;color:var(--muted);font-family:system-ui">BY WHITE</div><div id="capByW" class="cap b"></div><div id="scoreW" class="score"></div></div>
        <div class="tray"><div style="font-size:11px;color:var(--muted);font-family:system-ui">BY BLACK</div><div id="capByB" class="cap w"></div><div id="scoreB" class="score"></div></div>
      </div>
    </div>
    <div>
      <h3>MOVES</h3>
      <div id="moves" style="margin-top:6px"><table id="movesTable"></table></div>
    </div>
    <div class="controls-grid">
      <label class="small">Play as<br></label><label class="small">Difficulty<br></label>
      <select id="selColor"><option value="w">White (you first)</option><option value="b">Black (AI first)</option><option value="both">Analysis — both sides</option></select>
      <select id="selDepth"><option value="1">Easy — depth 1</option><option value="2" selected>Normal — depth 2</option><option value="3">Hard — depth 3</option></select>
      <div class="row">
        <button id="btnHint" style="flex:1">💡 Hint</button>
        <button id="btnResign" style="flex:1">🏳 Resign</button>
      </div>
    </div>
    <div style="font-size:12px;color:var(--muted);font-family:system-ui;line-height:1.5">Click or drag to move. Dots = moves, ring = capture.<br>FEN: <span id="fenPreview" style="word-break:break-all;color:#7a6f58"></span></div>
  </div>
</main>
<div class="footer-note">Fully rule-complete engine: castling, en passant, promotion, pins, check / mate / stalemate, 50-move & insufficient-material draws. Progress auto-saves to this browser.</div>
<div id="dragPiece"></div>
<script>
'use strict';
/* ============================================================
   1. TUNABLE CONSTANTS
============================================================ */
const CONFIG = {
  AI_DEPTH_DEFAULT: 2,
  AI_DEPTHS: {1:1, 2:2, 3:3},
  QUIESCE_DEPTH: 2,
  VAL: { P:100, N:320, B:330, R:500, Q:900, K:0 },
  COLORS: { light:'#ecd9b0', dark:'#a5714b' },
  SAVE_KEY: 'grandmaster_chess_v1',
  THINK_DELAY_MS: 60
};
const GLYPH = { P:'♟', N:'♞', B:'♝', R:'♜', Q:'♛', K:'♚' };
const FILES = 'abcdefgh';

/* ============================================================
   2. SOUND (Web Audio, no assets)
============================================================ */
const Sound = {
  enabled: true, ctx:null,
  ensure(){ if(!this.ctx){ try{ this.ctx = new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } if(this.ctx&&this.ctx.state==='suspended') this.ctx.resume(); },
  tone(freq,dur,type='sine',gain=0.15,when=0){ if(!this.enabled) return; this.ensure(); if(!this.ctx) return;
    const t=this.ctx.currentTime+when, o=this.ctx.createOscillator(), g=this.ctx.createGain();
    o.type=type; o.frequency.value=freq; g.gain.setValueAtTime(gain,t); g.gain.exponentialRampToValueAtTime(0.001,t+dur);
    o.connect(g); g.connect(this.ctx.destination); o.start(t); o.stop(t+dur+0.02); },
  move(){ this.tone(520,0.09,'triangle',0.12); this.tone(780,0.07,'triangle',0.06,0.03); },
  capture(){ this.tone(220,0.12,'sawtooth',0.12); this.tone(140,0.15,'square',0.08,0.02); },
  check(){ this.tone(880,0.12,'sine',0.14); this.tone(880,0.12,'sine',0.14,0.14); },
  castle(){ this.tone(400,0.08,'triangle',0.12); this.tone(600,0.1,'triangle',0.12,0.07); },
  end(win){ [523,659,784,1046].forEach((f,i)=>this.tone(f,0.18,'triangle',0.12,i*0.11)); },
  notify(){ this.tone(660,0.1,'sine',0.1); }
};

/* ============================================================
   3. ENGINE — board + move generation (separated from UI)
   Representation: board[64], idx = rankRow*8+file (row0=rank8 top).
   Piece: {c:'w'|'b', t:'P'|'N'|'B'|'R'|'Q'|'K'}
============================================================ */
function idx(r,c){ return r*8+c; }
function rc(i){ return [Math.floor(i/8), i%8]; }
function sqName(i){ const [r,c]=rc(i); return FILES[c]+(8-r); }
function sqIndex(name){ const c=FILES.indexOf(name[0]), rank=+name[1]; return (8-rank)*8+c; }
function inB(r,c){ return r>=0&&r<8&&c>=0&&c<8; }
function opp(c){ return c==='w'?'b':'w'; }

function initialBoard(){
  const order=['R','N','B','Q','K','B','N','R'];
  const b=new Array(64).fill(null);
  for(let c=0;c<8;c++){
    b[idx(0,c)]={c:'b',t:order[c]}; b[idx(1,c)]={c:'b',t:'P'};
    b[idx(6,c)]={c:'w',t:'P'}; b[idx(7,c)]={c:'w',t:order[c]};
  }
  return b;
}
function newState(){
  return { board: initialBoard(), turn:'w', castling:{K:true,Q:true,k:true,q:true}, ep:null, half:0, full:1 };
}
function cloneState(s){
  return { board: s.board.map(p=>p?{c:p.c,t:p.t}:null), turn:s.turn, castling:{...s.castling}, ep:s.ep, half:s.half, full:s.full };
}

// --- attack detection ---
function isAttacked(board, sq, by){
  const [r,c]=rc(sq);
  // pawns
  const pr = by==='w' ? r+1 : r-1;
  for(const dc of [-1,1]){ const cc=c+dc; if(inB(pr,cc)){ const p=board[idx(pr,cc)]; if(p&&p.c===by&&p.t==='P') return true; } }
  // knights
  for(const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]){
    const rr=r+dr,cc=c+dc; if(inB(rr,cc)){ const p=board[idx(rr,cc)]; if(p&&p.c===by&&p.t==='N') return true; }
  }
  // king
  for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){ if(!dr&&!dc)continue; const rr=r+dr,cc=c+dc;
    if(inB(rr,cc)){ const p=board[idx(rr,cc)]; if(p&&p.c===by&&p.t==='K') return true; } }
  // sliders
  const dirs=[[-1,-1],[-1,1],[1,-1],[1,1]];
  for(const [dr,dc] of dirs){ let rr=r+dr,cc=c+dc;
    while(inB(rr,cc)){ const p=board[idx(rr,cc)]; if(p){ if(p.c===by&&(p.t==='B'||p.t==='Q')) return true; break; } rr+=dr;cc+=dc; } }
  const dirs2=[[-1,0],[1,0],[0,-1],[0,1]];
  for(const [dr,dc] of dirs2){ let rr=r+dr,cc=c+dc;
    while(inB(rr,cc)){ const p=board[idx(rr,cc)]; if(p){ if(p.c===by&&(p.t==='R'||p.t==='Q')) return true; break; } rr+=dr;cc+=dc; } }
  return false;
}
function kingSq(board,color){ for(let i=0;i<64;i++){ const p=board[i]; if(p&&p.c===color&&p.t==='K') return i; } return -1; }
function inCheck(state,color){ const k=kingSq(state.board,color); if(k<0) return false; return isAttacked(state.board,k,opp(color)); }

// --- pseudo move generation ---
function pseudoMoves(state, from=null){
  const moves=[]; const b=state.board;
  const squares = from!=null ? [from] : [...Array(64).keys()].filter(i=>b[i]&&b[i].c===state.turn);
  for(const f of squares){
    const p=b[f]; if(!p||p.c!==state.turn) continue;
    const [r,c]=rc(f);
    if(p.t==='P'){
      const dir = p.c==='w'?-1:1;
      const startRow = p.c==='w'?6:1;
      const lastRow = p.c==='w'?0:7;
      const oneR=r+dir;
      if(inB(oneR,c)&&!b[idx(oneR,c)]){
        if(oneR===lastRow){ for(const pr of ['Q','R','B','N']) moves.push({from:f,to:idx(oneR,c),piece:{...p},promotion:pr,isCapture:false,flag:'promo'}); }
        else { moves.push({from:f,to:idx(oneR,c),piece:{...p},isCapture:false,flag:'normal'});
          if(r===startRow){ const twoR=r+2*dir; if(!b[idx(twoR,c)]) moves.push({from:f,to:idx(twoR,c),piece:{...p},isCapture:false,flag:'double'}); } }
      }
      for(const dc of [-1,1]){
        const cc=c+dc, rr=r+dir; if(!inB(rr,cc)) continue;
        const t=idx(rr,cc), tp=b[t];
        if(tp&&tp.c!==p.c){
          if(rr===lastRow){ for(const pr of ['Q','R','B','N']) moves.push({from:f,to:t,piece:{...p},captured:{...tp},promotion:pr,isCapture:true,flag:'promo'}); }
          else moves.push({from:f,to:t,piece:{...p},captured:{...tp},isCapture:true,flag:'normal'});
        } else if(!tp && state.ep===t){
          moves.push({from:f,to:t,piece:{...p},isCapture:true,flag:'ep'});
        }
      }
    } else if(p.t==='N'){
      for(const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]){
        const rr=r+dr,cc=c+dc; if(!inB(rr,cc))continue; const t=idx(rr,cc),tp=b[t];
        if(!tp) moves.push({from:f,to:t,piece:{...p},isCapture:false,flag:'normal'});
        else if(tp.c!==p.c) moves.push({from:f,to:t,piece:{...p},captured:{...tp},isCapture:true,flag:'normal'});
      }
    } else if(p.t==='K'){
      for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){ if(!dr&&!dc)continue;
        const rr=r+dr,cc=c+dc; if(!inB(rr,cc))continue; const t=idx(rr,cc),tp=b[t];
        if(!tp) moves.push({from:f,to:t,piece:{...p},isCapture:false,flag:'normal'});
        else if(tp.c!==p.c) moves.push({from:f,to:t,piece:{...p},captured:{...tp},isCapture:true,flag:'normal'});
      }
      // castling
      const home = p.c==='w'?7:0;
      if(r===home&&c===4&&!inCheck(state,p.c)){
        const rights = p.c==='w'?['K','Q']:['k','q'];
        // kingside
        if(state.castling[rights[0]] && !b[idx(home,5)] && !b[idx(home,6)]){
          const rook=b[idx(home,7)];
          if(rook&&rook.c===p.c&&rook.t==='R'){
            if(!isAttacked(b,idx(home,5),opp(p.c))&&!isAttacked(b,idx(home,6),opp(p.c)))
              moves.push({from:f,to:idx(home,6),piece:{...p},isCapture:false,flag:'castleK'});
          }
        }
        if(state.castling[rights[1]] && !b[idx(home,3)] && !b[idx(home,2)] && !b[idx(home,1)]){
          const rook=b[idx(home,0)];
          if(rook&&rook.c===p.c&&rook.t==='R'){
            if(!isAttacked(b,idx(home,3),opp(p.c))&&!isAttacked(b,idx(home,2),opp(p.c)))
              moves.push({from:f,to:idx(home,2),piece:{...p},isCapture:false,flag:'castleQ'});
          }
        }
      }
    } else {
      const sets = p.t==='B'?[[-1,-1],[-1,1],[1,-1],[1,1]] : p.t==='R'?[[-1,0],[1,0],[0,-1],[0,1]] : [[-1,-1],[-1,1],[1,-1],[1,1],[-1,0],[1,0],[0,-1],[0,1]];
      for(const [dr,dc] of sets){ let rr=r+dr,cc=c+dc;
        while(inB(rr,cc)){ const t=idx(rr,cc),tp=b[t];
          if(!tp) moves.push({from:f,to:t,piece:{...p},isCapture:false,flag:'normal'});
          else { if(tp.c!==p.c) moves.push({from:f,to:t,piece:{...p},captured:{...tp},isCapture:true,flag:'normal'}); break; }
          rr+=dr;cc+=dc; } }
    }
  }
  return moves;
}

// --- apply move (mutates state), returns undo ---
function applyMove(state,m){
  const undo={castling:{...state.castling},ep:state.ep,half:state.half,full:state.full,captured:null,epCaptured:null,movedPiece:{...state.board[m.from]},rookFrom:-1,rookTo:-1,rookPiece:null};
  const b=state.board; const mover=b[m.from];
  // ep captured pawn
  if(m.flag==='ep'){
    const [tr,tc]=rc(m.to); const capR = mover.c==='w'?tr+1:tr-1;
    undo.epCaptured={sq:idx(capR,tc),piece:{...b[idx(capR,tc)]}};
    b[idx(capR,tc)]=null;
  } else if(b[m.to]){ undo.captured={...b[m.to]}; }
  b[m.from]=null;
  let placed={c:mover.c,t:m.promotion||mover.t};
  b[m.to]=placed;
  if(m.flag==='castleK'){ const [r]=rc(m.from); const home=r; undo.rookFrom=idx(home,7);undo.rookTo=idx(home,5); undo.rookPiece={...b[undo.rookFrom]}; b[undo.rookTo]=b[undo.rookFrom]; b[undo.rookFrom]=null; }
  if(m.flag==='castleQ'){ const [r]=rc(m.from); const home=r; undo.rookFrom=idx(home,0);undo.rookTo=idx(home,3); undo.rookPiece={...b[undo.rookFrom]}; b[undo.rookTo]=b[undo.rookFrom]; b[undo.rookFrom]=null; }
  // castling rights
  if(mover.t==='K'){ if(mover.c==='w'){state.castling.K=false;state.castling.Q=false;} else {state.castling.k=false;state.castling.q=false;} }
  // rook moved from corner
  const clearRight=(sq,right)=>{ if(m.from===sq) state.castling[right]=false; if(m.to===sq) state.castling[right]=false; };
  clearRight(idx(7,0),'Q'); clearRight(idx(7,7),'K'); clearRight(idx(0,0),'q'); clearRight(idx(0,7),'k');
  // ep square
  state.ep=null;
  if(mover.t==='P'&&m.flag==='double'){ const [fr,fc]=rc(m.from); const [tr]=rc(m.to); state.ep=idx((fr+tr)/2,fc); }
  // halfmove
  if(mover.t==='P'||m.isCapture||m.flag==='ep') state.half=0; else state.half++;
  // fullmove
  if(state.turn==='b') state.full++;
  state.turn=opp(state.turn);
  return undo;
}
function unapplyMove(state,m,undo){
  state.turn=opp(state.turn);
  state.castling=undo.castling; state.ep=undo.ep; state.half=undo.half; state.full=undo.full;
  const b=state.board;
  b[m.from]=undo.movedPiece?{...undo.movedPiece}:null;
  b[m.to]=null;
  if(m.flag==='ep'&&undo.epCaptured){ b[undo.epCaptured.sq]={...undo.epCaptured.piece}; }
  else if(undo.captured){ b[m.to]={...undo.captured}; }
  if((m.flag==='castleK'||m.flag==='castleQ')&&undo.rookFrom>=0){ b[undo.rookFrom]={...undo.rookPiece}; b[undo.rookTo]=null; }
}

// --- legal moves (generator-based validation) ---
function legalMoves(state, from=null){
  const pseudos=pseudoMoves(state,from);
  const out=[];
  for(const m of pseudos){
    const moverColor=state.turn;
    const u=applyMove(state,m);
    const k=kingSq(state.board,moverColor);
    const ok = k>=0 ? !isAttacked(state.board,k,opp(moverColor)) : true;
    unapplyMove(state,m,u);
    if(ok) out.push(m);
  }
  return out;
}

// --- status ---
function gameStatus(state){
  const moves=legalMoves(state);
  const check=inCheck(state,state.turn);
  if(moves.length===0){
    if(check) return {over:true,result:state.turn==='w'?'0-1':'1-0',reason:'checkmate',inCheck:true};
    return {over:true,result:'1/2-1/2',reason:'stalemate',inCheck:false};
  }
  if(state.half>=100 && moves.length>0) return {over:true,result:'1/2-1/2',reason:'fifty-move rule',inCheck:check};
  if(moves.length>0 && isInsufficient(state.board)) return {over:true,result:'1/2-1/2',reason:'insufficient material',inCheck:check};
  return {over:false,inCheck:check,moves: moves.length};
}
function isInsufficient(board){
  let minors=0;
  for(let i=0;i<64;i++){ const p=board[i]; if(!p)continue;
    if(p.t==='P'||p.t==='R'||p.t==='Q') return false;
    if(p.t==='B'||p.t==='N') minors++;
  }
  return minors<=1; // KvsK (0), KBvsK / KNvsK (1)
}

// --- FEN ---
function toFEN(state){
  let rows=[];
  for(let r=0;r<8;r++){ let s='',empty=0;
    for(let c=0;c<8;c++){ const p=state.board[idx(r,c)];
      if(!p) empty++;
      else { if(empty){s+=empty;empty=0;} s+= p.c==='w'?p.t:p.t.toLowerCase(); } }
    if(empty)s+=empty; rows.push(s); }
  let cast=''; if(state.castling.K)cast+='K'; if(state.castling.Q)cast+='Q'; if(state.castling.k)cast+='k'; if(state.castling.q)cast+='q'; if(!cast)cast='-';
  return `${rows.join('/')} ${state.turn} ${cast} ${state.ep==null?'-':sqName(state.ep)} ${state.half} ${state.full}`;
}
function parseFEN(fen){
  const parts=fen.trim().split(/\s+/);
  const rows=parts[0].split('/');
  const board=new Array(64).fill(null);
  for(let r=0;r<8;r++){ let c=0;
    for(const ch of rows[r]){ if(/\d/.test(ch)){ c+=+ch; } else { board[idx(r,c)]={c:ch===ch.toUpperCase()?'w':'b',t:ch.toUpperCase()}; c++; } } }
  const castStr=parts[2]||'-';
  return { board, turn:parts[1]||'w',
    castling:{K:castStr.includes('K'),Q:castStr.includes('Q'),k:castStr.includes('k'),q:castStr.includes('q')},
    ep:(!parts[3]||parts[3]==='-')?null:sqIndex(parts[3]), half:+(parts[4]||0), full:+(parts[5]||1) };
}

// --- SAN ---
function moveToSAN(state, m){
  if(m.flag==='castleK') return 'O-O';
  if(m.flag==='castleQ') return 'O-O-O';
  const b=state.board; const p=b[m.from];
  let san='';
  const dest=sqName(m.to);
  if(p.t==='P'){
    if(m.isCapture) san+=FILES[m.from%8]+'x';
    san+=dest;
    if(m.promotion) san+='='+m.promotion;
    if(m.flag==='ep') san+=' e.p.';
  } else {
    san+= p.t==='N'?'N':p.t;
    // disambiguation
    const others=legalMoves(state).filter(x=>x.to===m.to&&x.from!==m.from&&state.board[x.from]&&state.board[x.from].t===p.t);
    if(others.length){
      const [fr,fc]=rc(m.from);
      const sameFile=others.some(x=>rc(x.from)[1]===fc);
      const sameRank=others.some(x=>rc(x.from)[0]===fr);
      if(!sameFile) san+=FILES[fc];
      else if(!sameRank) san+=(8-fr);
      else san+=FILES[fc]+(8-fr);
    }
    if(m.isCapture) san+='x';
    san+=dest;
  }
  // check / mate suffix (needs post-move state)
  const u=applyMove(state,m);
  const st=gameStatus(state);
  unapplyMove(state,m,u);
  if(st.reason==='checkmate') san+='#';
  else if(st.inCheck) san+='+';
  return san;
}
function uciOf(m){ return sqName(m.from)+sqName(m.to)+(m.promotion?m.promotion.toLowerCase():''); }
function findMoveByUCI(state, uci){
  uci=uci.trim();
  if(uci.length<4) return null;
  const from=sqIndex(uci.slice(0,2)), to=sqIndex(uci.slice(2,4));
  const promo=uci.length>4?uci[4].toUpperCase():null;
  const cands=legalMoves(state,from).filter(m=>m.to===to);
  if(!cands.length) return null;
  if(promo) return cands.find(m=>m.promotion===promo)||null;
  const nonPromo=cands.filter(m=>!m.promotion);
  if(nonPromo.length) return nonPromo[0];
  return cands[0]; // promotion default queen handled by UI picker; engine picks queen
}

/* ============================================================
   4. AI — negamax + alpha-beta + move ordering + quiescence-lite
============================================================ */
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,-5,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]};
function evaluateBoard(state){
  let s=0;
  for(let i=0;i<64;i++){ const p=state.board[i]; if(!p)continue;
    const v=CONFIG.VAL[p.t]+pstVal(p.t,i,p.c);
    s += p.c==='w'? v : -v;
  }
  return s;
}
// mirror helper: black PST index = (7-r)*8+c
function pstVal(t,i,color){ const r=Math.floor(i/8),c=i%8; const mi=color==='w'?i:(7-r)*8+c; return PST[t][mi]; }
function orderMoves(moves,state){
  for(const m of moves){
    let s=0;
    if(m.promotion) s+=900;
    if(m.isCapture||m.flag==='ep'){
      const victim = m.flag==='ep'?CONFIG.VAL.P : (state.board[m.to]?CONFIG.VAL[state.board[m.to].t]:0);
      const attacker=CONFIG.VAL[state.board[m.from].t];
      s+=10000+victim-attacker/10;
    }
    if(m.flag==='castleK'||m.flag==='castleQ') s+=50;
    m._s=s;
  }
  moves.sort((a,b)=>b._s-a._s);
  return moves;
}
let nodes=0;
function quiesce(state,alpha,beta,depth){
  nodes++;
  // NOTE: stand-pat must be from the side-to-move's perspective (negamax convention)
  const stm=state.turn;
  const stand=(stm==='w'?evaluateBoard(state):-evaluateBoard(state));
  if(depth<=0) return stand;
  if(stand>=beta) return beta;
  if(stand>alpha) alpha=stand;
  const caps=orderMoves(legalMoves(state).filter(m=>m.isCapture||m.promotion),state);
  for(const m of caps){
    const u=applyMove(state,m);
    const sc=-quiesce(state,-beta,-alpha,depth-1);
    unapplyMove(state,m,u);
    if(sc>=beta) return beta;
    if(sc>alpha) alpha=sc;
  }
  return alpha;
}
function negamax(state,depth,alpha,beta,color){
  nodes++;
  const st=gameStatus(state);
  if(st.over){
    if(st.reason==='checkmate') return -100000-depth; // prefer faster mate
    return 0;
  }
  if(depth<=0) return quiesce(state,alpha,beta,CONFIG.QUIESCE_DEPTH);
  const moves=orderMoves(legalMoves(state),state);
  let best=-Infinity;
  for(const m of moves){
    const u=applyMove(state,m);
    const sc=-negamax(state,depth-1,-beta,-alpha,opp(color));
    unapplyMove(state,m,u);
    if(sc>best)best=sc;
    if(sc>alpha)alpha=sc;
    if(alpha>=beta)break;
  }
  return best;
}
function findBestMove(state,depth){
  nodes=0;
  const turn=state.turn;
  const moves=orderMoves(legalMoves(state),state);
  if(!moves.length) return null;
  if(moves.length===1) return {move:moves[0],score:0,nodes:1};
  let best=null,bestScore=-Infinity,alpha=-Infinity;
  for(const m of moves){
    const u=applyMove(state,m);
    const sc=-negamax(state,depth-1,-Infinity,-alpha,opp(turn));
    unapplyMove(state,m,u);
    if(sc>bestScore){bestScore=sc;best=m;}
    if(sc>alpha)alpha=sc;
  }
  return {move:best,score:bestScore,nodes};
}
// Chunked async search: one root-move per time slice so UI never freezes
function findBestMoveAsync(state,depth,onDone){
  const moves=orderMoves(legalMoves(state),state);
  if(!moves.length){ onDone(null); return; }
  if(moves.length===1){ onDone({move:moves[0],score:0,nodes:1}); return; }
  nodes=0;
  let i=0,best=moves[0],bestScore=-Infinity,alpha=-Infinity;
  const turn=state.turn;
  function step(){
    const t0=performance.now();
    while(i<moves.length){
      const m=moves[i++];
      const u=applyMove(state,m);
      const sc=-negamax(state,depth-1,-Infinity,-alpha,opp(turn));
      unapplyMove(state,m,u);
      if(sc>bestScore){bestScore=sc;best=m;}
      if(sc>alpha)alpha=sc;
      if(performance.now()-t0>40) break; // yield to UI
    }
    if(i<moves.length){ setTimeout(step,0); }
    else onDone({move:best,score:bestScore,nodes});
  }
  step();
}

/* ============================================================
   5. GAME CONTROLLER + PERSISTENCE
============================================================ */
const Game = {
  state: newState(),
  history: [],      // {move, san, undo}
  sanList: [],
  flipped:false,
  playerColor:'w',  // 'w','b','both'
  depth: CONFIG.AI_DEPTH_DEFAULT,
  selected:null,
  legalForSelected:[],
  lastMove:null,
  gameOver:null,
  aiThinking:false,
  pendingPromo:null,

  reset(){
    this.state=newState(); this.history=[]; this.sanList=[]; this.selected=null;
    this.legalForSelected=[]; this.lastMove=null; this.gameOver=null; this.pendingPromo=null;
    this.save(); renderAll(); maybeAI();
  },
  save(){
    try{
      localStorage.setItem(CONFIG.SAVE_KEY, JSON.stringify({
        state:{board:this.state.board,turn:this.state.turn,castling:this.state.castling,ep:this.state.ep,half:this.state.half,full:this.state.full},
        sanList:this.sanList,
        uciList:this.history.map(h=>uciOf(h.move)),
        playerColor:this.playerColor, depth:this.depth, flipped:this.flipped
      }));
    }catch(e){}
  },
  load(){
    try{
      const raw=localStorage.getItem(CONFIG.SAVE_KEY); if(!raw) return false;
      const d=JSON.parse(raw); if(!d||!d.state||!d.state.board) return false;
      this.state={board:d.state.board.map(p=>p?{c:p.c,t:p.t}:null),turn:d.state.turn,castling:d.state.castling,ep:d.state.ep,half:d.state.half,full:d.state.full};
      this.playerColor=d.playerColor||'w'; this.depth=d.depth||2; this.flipped=!!d.flipped;
      // rebuild history by replaying UCI
      this.history=[]; this.sanList=[];
      const tmp=newState();
      if(d.uciList&&d.uciList.length){
        let ok=true;
        for(const u of d.uciList){ const m=findMoveByUCI(tmp,u); if(!m){ok=false;break;} const san=moveToSAN(tmp,m); const undo=applyMove(tmp,m); this.history.push({move:m,san,undo}); this.sanList.push(san); }
        if(ok){ this.state=tmp; }
        else { this.history=[]; this.sanList=d.sanList||[]; }
      }
      this.lastMove=this.history.length?this.history[this.history.length-1].move:null;
      this.updateStatus();
      return true;
    }catch(e){ return false; }
  },
  tryMove(from,to,promoChoice){
    if(this.gameOver) return false;
    const cands=legalMoves(this.state,from).filter(m=>m.to===to);
    if(!cands.length) return false;
    let chosen;
    if(cands[0].promotion){
      if(!promoChoice){ this.pendingPromo={from,to}; showPromoPicker(from,to,this.state.turn); return 'promo'; }
      chosen=cands.find(m=>m.promotion===promoChoice)||cands.find(m=>m.promotion==='Q');
    } else chosen=cands[0];
    this.doMove(chosen);
    return true;
  },
  doMove(m){
    const san=moveToSAN(this.state,m);
    const undo=applyMove(this.state,m);
    this.history.push({move:{...m},san,undo});
    this.sanList.push(san);
    this.lastMove={...m}; this.selected=null; this.legalForSelected=[]; this.pendingPromo=null;
    hidePromo();
    // sounds
    const st=gameStatus(this.state);
    if(st.over){ this.gameOver=st; Sound.end(); }
    else if(st.inCheck) Sound.check();
    else if(m.isCapture||m.flag==='ep') Sound.capture();
    else if(m.flag==='castleK'||m.flag==='castleQ') Sound.castle();
    else Sound.move();
    if(!st.over) this.gameOver=null; else this.gameOver=st;
    this.updateStatus();
    this.save(); renderAll();
    maybeAI();
  },
  undo(){
    this.aiThinking=false; $('think').classList.remove('show');
    if(!this.history.length) return;
    const take=()=>{
      const h=this.history.pop(); this.sanList.pop();
      unapplyMove(this.state,h.move,h.undo);
    };
    if(this.playerColor==='both'){ take(); }
    else {
      // take back AI + player (2 ply) if possible
      take();
      if(this.history.length && !this.gameOver) {
        // if it's now AI's turn still (e.g., only 1 move played), take one is enough; else take second
        const aiColor = opp(this.playerColor);
        if(this.state.turn===aiColor || this.history.length===0){ /* after 1 take, turn flipped; if AI already moved this restores player turn */ }
        // If after single take it's still not player's turn, take another
        if(this.state.turn!==this.playerColor && this.history.length) take();
      }
    }
    this.gameOver=null;
    this.lastMove=this.history.length?this.history[this.history.length-1].move:null;
    this.selected=null; this.legalForSelected=[];
    this.updateStatus(); this.save(); renderAll();
  },
  updateStatus(){
    const st=gameStatus(this.state);
    if(st.over){ this.gameOver=st; }
    return st;
  }
};

/* ============================================================
   6. RENDERING + INTERACTION
============================================================ */
const $=id=>document.getElementById(id);
const boardEl=$('board'), movesTable=$('movesTable');

function displayIdx(r,c){ // board-grid position given logical r,c + flip
  if(!Game.flipped) return idx(r,c);
  return idx(7-r,7-c);
}
function logicalFromDisplay(di){
  if(!Game.flipped) return di;
  const [r,c]=rc(di); return idx(7-r,7-c);
}
function buildBoard(){
  boardEl.innerHTML='';
  for(let d=0;d<64;d++){
    const div=document.createElement('div');
    div.className='sq'; div.dataset.d=d;
    div.innerHTML='<span class="piece"></span>';
    boardEl.appendChild(div);
  }
  boardEl.querySelectorAll('.sq').forEach(sq=>{
    sq.addEventListener('pointerdown', onDown);
  });
  window.addEventListener('pointermove', onDragMove);
  window.addEventListener('pointerup', onUp);
}
let drag=null;
function onDown(e){
  if(Game.aiThinking) return;
  const d=+e.currentTarget.dataset.d;
  const sq=logicalFromDisplay(d);
  const piece=Game.state.board[sq];
  const myTurn = Game.playerColor==='both' || Game.state.turn===Game.playerColor;
  if(Game.selected==null){
    if(piece&&myTurn&&piece.c===Game.state.turn){
      select(sq);
      startDrag(e,sq);
    }
  } else {
    // attempt move to sq
    if(sq===Game.selected){ deselect(); return; }
    const res=Game.tryMove(Game.selected,sq);
    if(res==='promo'){ startDragCancel(); return; }
    if(res){ deselect(); return; }
    // reselect
    if(piece&&piece.c===Game.state.turn&&myTurn){ select(sq); startDrag(e,sq); }
    else deselect();
  }
  if(e.pointerId!=null){ try{e.currentTarget.setPointerCapture(e.pointerId);}catch(_){} }
}
function startDrag(e,sq){
  const p=Game.state.board[sq]; if(!p) return;
  drag={from:sq,el:$('dragPiece')};
  drag.el.textContent=GLYPH[p.t];
  drag.el.style.color=p.c==='w'?'#faf5e8':'#15110c';
  drag.el.style.textShadow=p.c==='w'?'0 0 2px #000,0 2px 0 #000,-1px 0 0 #000,1px 0 0 #000':'0 0 2px rgba(255,255,255,.35)';
  drag.el.style.display='block';
  moveDrag(e);
}
function moveDrag(e){ if(drag){ drag.el.style.left=e.clientX+'px'; drag.el.style.top=e.clientY+'px'; } }
function onDragMove(e){ moveDrag(e);
  boardEl.querySelectorAll('.sq').forEach(s=>s.classList.remove('hover-target'));
  if(drag&&e.clientX!=null){
    const el=document.elementFromPoint(e.clientX,e.clientY);
    const sqEl=el&&el.closest?el.closest('.sq'):null;
    if(sqEl&&boardEl.contains(sqEl)) sqEl.classList.add('hover-target');
  }
}
function onUp(e){
  if(!drag) return;
  const el=document.elementFromPoint(e.clientX,e.clientY);
  const sqEl=el&&el.closest?el.closest('.sq'):null;
  const from=drag.from;
  $('dragPiece').style.display='none';
  boardEl.querySelectorAll('.sq').forEach(s=>s.classList.remove('hover-target'));
  const d=drag; drag=null;
  if(!sqEl||!boardEl.contains(sqEl)) { renderAll(); return; }
  const to=logicalFromDisplay(+sqEl.dataset.d);
  if(to===from){ return; } // click handled by selection already
  const myTurn = Game.playerColor==='both' || Game.state.turn===Game.playerColor;
  if(!myTurn) return;
  Game.selected=from;
  Game.legalForSelected=legalMoves(Game.state,from);
  const res=Game.tryMove(from,to);
  if(!res){ select(from); Sound.notify(); }
}
function startDragCancel(){ if(drag){$('dragPiece').style.display='none'; drag=null;} }
function select(sq){ Game.selected=sq; Game.legalForSelected=legalMoves(Game.state,sq); renderAll(); }
function deselect(){ Game.selected=null; Game.legalForSelected=[]; renderAll(); }

function renderAll(){
  renderBoard(); renderSide(); renderCoords();
  $('selColor').value=Game.playerColor; $('selDepth').value=String(Game.depth);
  $('fenPreview').textContent=toFEN(Game.state);
}
function renderCoords(){
  const f=Game.flipped?'hgfedcba':'abcdefgh';
  $('topCoords').innerHTML=[...f].map(x=>`<span>${x}</span>`).join('');
  $('botCoords').innerHTML=[...f].map(x=>`<span>${x}</span>`).join('');
}
function renderBoard(){
  const st=gameStatus(Game.state);
  const checkSq = st.inCheck? kingSq(Game.state.board,Game.state.turn):-1;
  for(let d=0;d<64;d++){
    const el=boardEl.children[d];
    const sq=logicalFromDisplay(d);
    const [r,c]=rc(sq);
    const isLight=(r+c)%2===0;
    el.className='sq '+(isLight?'light':'dark');
    // last move
    if(Game.lastMove&&(Game.lastMove.from===sq||Game.lastMove.to===sq)) el.classList.add('last');
    if(Game.selected===sq) el.classList.add('selected');
    if(sq===checkSq) el.classList.add('check');
    const p=Game.state.board[sq];
    const pe=el.querySelector('.piece');
    if(p){ pe.textContent=GLYPH[p.t]; pe.className='piece '+p.c; pe.style.visibility=(drag&&drag.from===sq)?'hidden':'visible'; }
    else { pe.textContent=''; }
    // remove old hints
    el.querySelectorAll('.hint-dot,.hint-ring').forEach(n=>n.remove());
    if(Game.selected!=null){
      const m=Game.legalForSelected.find(m=>m.to===sq);
      if(m){
        const dot=document.createElement('div');
        if(m.isCapture||Game.state.board[sq]){ dot.className='hint-ring'; }
        else dot.className='hint-dot';
        el.appendChild(dot);
      }
    }
  }
  // turn + banner
  const dot=$('turnDot');
  dot.className='turn-dot'+(Game.state.turn==='b'?' b':'');
  let txt = Game.state.turn==='w'?'White to move':'Black to move';
  if(st.over){
    if(st.reason==='checkmate') txt=`Checkmate — ${Game.state.turn==='w'?'Black':'White'} wins`;
    else if(st.reason==='stalemate') txt='Stalemate — draw';
    else txt=`Draw — ${st.reason}`;
  } else if(st.inCheck) txt+=` — <b>check!</b>`;
  if(Game.playerColor!=='both'&&!st.over) txt+=` (you: ${Game.playerColor==='w'?'White':'Black'})`;
  $('statusText').innerHTML=txt;
  const b=$('banner');
  if(st.over){ b.classList.add('show');
    $('bannerTitle').textContent = st.reason==='checkmate'?'Checkmate ♔':st.reason==='stalemate'?'Stalemate':'Draw';
    $('bannerSub').textContent = st.reason==='checkmate'?`${st.result==='1-0'?'White':'Black'} wins (${st.result})`:`${st.reason} (${st.result})`;
  } else b.classList.remove('show');
}
function renderSide(){
  // captured trays: missing pieces
  const start={P:8,N:2,B:2,R:2,Q:1};
  const count={w:{P:0,N:0,B:0,R:0,Q:0},b:{P:0,N:0,B:0,R:0,Q:0}};
  for(const p of Game.state.board){ if(p&&p.t!=='K') count[p.c][p.t]++; }
  let byW='',byB='',matW=0,matB=0;
  const order=['Q','R','B','N','P'];
  for(const t of order){
    const missB=start[t]-count.b[t]; // black pieces captured by white
    const missW=start[t]-count.w[t];
    byW+=GLYPH[t].repeat(Math.max(0,missB));
    byB+=GLYPH[t].repeat(Math.max(0,missW));
    matW+=Math.max(0,missB)*CONFIG.VAL[t]/100; matB+=Math.max(0,missW)*CONFIG.VAL[t]/100;
  }
  $('capByW').textContent=byW||'—'; $('capByB').textContent=byB||'—';
  const diff=matW-matB;
  $('scoreW').textContent = diff>0?`up +${diff.toFixed(0)}`:'';
  $('scoreB').textContent = diff<0?`up +${(-diff).toFixed(0)}`:'';
  // moves table
  let html='';
  for(let i=0;i<Game.sanList.length;i+=2){
    const n=i/2+1, w=Game.sanList[i], b=Game.sanList[i+1]||'';
    const curW=(i===Game.sanList.length-1)?' class="cur"':'';
    const curB=(i+1===Game.sanList.length-1)?' class="cur"':'';
    html+=`<tr><td class="num">${n}.</td><td${curW}>${w}</td><td${curB}>${b}</td></tr>`;
  }
  movesTable.innerHTML=html||'<tr><td class="num">—</td><td>No moves yet</td><td></td></tr>';
  const box=$('moves'); box.scrollTop=box.scrollHeight;
}

// --- promotion picker ---
function showPromoPicker(from,to,color){
  const box=$('promo'); box.innerHTML='';
  for(const t of ['Q','R','B','N']){
    const btn=document.createElement('button'); btn.textContent=GLYPH[t];
    btn.style.color=color==='w'?'#faf5e8':'#15110c';
    btn.onclick=(e)=>{ e.stopPropagation(); Game.tryMove(from,to,t); };
    box.appendChild(btn);
  }
  // position near target square
  const dDisp = (()=>{ for(let d=0;d<64;d++) if(logicalFromDisplay(d)===to) return d; return 0; })();
  const sqEl=boardEl.children[dDisp];
  const wrap=boardEl.getBoundingClientRect(), r=sqEl.getBoundingClientRect(), outer=document.querySelector('.board-outer').getBoundingClientRect();
  box.classList.add('show');
  box.style.left=(r.left-outer.left+r.width/2-90)+'px';
  box.style.top=(r.top-outer.top-10)+'px';
}
function hidePromo(){ $('promo').classList.remove('show'); }

// --- AI hookup ---
function isAITurn(){
  if(Game.gameOver) return false;
  if(Game.playerColor==='both') return false;
  return Game.state.turn!==Game.playerColor;
}
function maybeAI(){
  if(!isAITurn()){ $('think').classList.remove('show'); Game.aiThinking=false; return; }
  $('think').classList.add('show');
  $('thinkText').textContent=`AI thinking (depth ${Game.depth})…`;
  Game.aiThinking=true;
  const turnAtStart=Game.state.turn;
  setTimeout(()=>{
    try{
      const snap=cloneState(Game.state);
      findBestMoveAsync(snap,Game.depth,(res)=>{
        Game.aiThinking=false;
        $('think').classList.remove('show');
        if(!res||!res.move){ Game.updateStatus(); renderAll(); return; }
        if(Game.state.turn!==turnAtStart||!isAITurn()){ renderAll(); return; } // user undid/new-game meanwhile
        // map move back to live state (find equal from/to/promo)
        const live=legalMoves(Game.state).find(m=>m.from===res.move.from&&m.to===res.move.to&&(m.promotion||null)===(res.move.promotion||null));
        if(live) Game.doMove(live);
        else renderAll();
      });
    }catch(err){ Game.aiThinking=false; $('think').classList.remove('show'); console.error(err); }
  }, CONFIG.THINK_DELAY_MS);
}

/* ============================================================
   7. WIRING
============================================================ */
$('btnNew').onclick=()=>{ Game.reset(); };
$('btnAgain').onclick=()=>{ Game.reset(); };
$('btnUndo').onclick=()=>Game.undo();
$('btnFlip').onclick=()=>{ Game.flipped=!Game.flipped; Game.save(); renderAll(); };
$('btnSound').onclick=(e)=>{ Sound.enabled=!Sound.enabled; e.target.textContent=Sound.enabled?'🔊':'🔇'; if(Sound.enabled)Sound.move(); };
$('selColor').onchange=(e)=>{ Game.playerColor=e.target.value; Game.save(); renderAll(); maybeAI(); };
$('selDepth').onchange=(e)=>{ Game.depth=+e.target.value; Game.save(); };
$('btnResign').onclick=()=>{ if(Game.gameOver)return; const winner=Game.state.turn==='w'?'Black':'White';
  Game.gameOver={over:true,result:Game.state.turn==='w'?'0-1':'1-0',reason:'resignation'}; renderAll(); Sound.end(); };
$('btnHint').onclick=()=>{
  if(Game.gameOver||Game.aiThinking) return;
  $('think').classList.add('show'); $('thinkText').textContent='Hint…';
  setTimeout(()=>{ const r=findBestMove(cloneState(Game.state),Math.min(2,Game.depth)); $('think').classList.remove('show');
    if(r&&r.move){ select(r.move.from); Sound.notify(); } },50);
};
document.addEventListener('pointerdown',e=>{ const pr=$('promo'); if(pr.classList.contains('show')&&!pr.contains(e.target)&&Game.pendingPromo){ hidePromo(); Game.pendingPromo=null; Game.selected=null; Game.legalForSelected=[]; renderAll(); } },true);

/* ============================================================
   8. DEBUG INTERFACE (headless probing)
============================================================ */
window.game={
  fen:()=>toFEN(Game.state),
  setFen:(f)=>{ Game.state=parseFEN(f); Game.history=[]; Game.sanList=[]; Game.lastMove=null; Game.gameOver=null; Game.updateStatus(); Game.save(); renderAll(); return toFEN(Game.state); },
  move:(uci)=>{ const m=findMoveByUCI(Game.state,uci); if(!m) return false; Game.selected=null; Game.doMove(m); return true; },
  legalMoves:(sq)=>{ if(sq){ const i=(typeof sq==='string')?sqIndex(sq):sq; return legalMoves(Game.state,i).map(uciOf); } return legalMoves(Game.state).map(uciOf); },
  status:()=>gameStatus(Game.state),
  perft:(d)=>perft(Game.state,d),
  state:()=>Game.state, reset:()=>Game.reset(), undo:()=>Game.undo(),
  setMode:(m)=>{ Game.playerColor=m; Game.aiThinking=false; try{$('think').classList.remove('show');}catch(_){} Game.save(); renderAll(); },
  aiPick:(d)=>{ const r=findBestMove(cloneState(Game.state),d||Game.depth); return r&&r.move?uciOf(r.move):null; },
  san:()=>Game.sanList.slice()
};
function perft(state,depth){
  if(depth===0) return 1;
  let n=0; const ms=legalMoves(state);
  for(const m of ms){ const u=applyMove(state,m); n+=perft(state,depth-1); unapplyMove(state,m,u); }
  return n;
}

/* boot */
buildBoard();
const had=Game.load();
Game.updateStatus();
renderAll();
if(!had) Game.save();
if(isAITurn()) maybeAI();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.3-contributor","provider":"openrouter","persona":"full","sessionId":"a2fc4043-9f1e-4c2a-b92f-2715796fdfc6","tokensIn":4633167,"tokensOut":54396,"tokensTotal":4687563,"cost":0.05522776999999999,"turns":69,"toolCalls":68,"failedToolCalls":1,"timestamp":"2026-09-03T04:39:19.962Z"} -->