← Chess results

Chess

muse spark 1.2 contributor · full

Initial view of Chess

Browser check: Artifact runtime error. This is a technical check, not a quality review.

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 43,869 bytes · SHA-256 d857b99856aa
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — Grandmaster Board</title>
<style>
/* ============ TUNABLE CONSTANTS (CSS) ============ */
:root{
  --light:#f0d9b5;
  --dark:#b58863;
  --light-hi:#f7e8c9;
  --dark-hi:#c9a27a;
  --select:#f6e58d;
  --legal-dot:#2f3542;
  --capture-ring:#e74c3c;
  --last-move:#a8d08d;
  --check-glow:#ff4757;
  --bg:#1a1a1e;
  --panel:#26262b;
  --panel2:#2f2f35;
  --text:#eee;
  --muted:#9a9aa0;
  --accent:#d4a574;
  --border:#3a3a42;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
header{padding:14px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);background:#1e1e22;flex-wrap:wrap;gap:10px}
header h1{font-size:18px;letter-spacing:0.12em;text-transform:uppercase;font-weight:700}
header h1 span{color:var(--accent)}
.hdr-right{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:1px solid var(--border);background:var(--panel2);color:var(--text);padding:7px 12px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s}
.btn:hover{background:#3a3a42;transform:translateY(-1px)}
.btn.primary{background:var(--accent);color:#1a1a1e;border-color:var(--accent)}
.btn.small{padding:5px 9px;font-size:12px}
select.btn{padding:6px 8px}
.layout{flex:1;display:flex;gap:20px;padding:18px;max-width:1280px;margin:0 auto;width:100%;align-items:flex-start;justify-content:center}
.board-wrap{position:relative;flex-shrink:0}
.board{width:min(68vh, 560px);height:min(68vh,560px);display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);border:6px solid #3e2f1f;border-radius:6px;overflow:hidden;box-shadow:0 20px 50px rgba(0,0,0,.5), 0 0 0 1px rgba(255,255,255,.06);background:#3e2f1f;user-select:none;touch-action:none}
.square{position:relative;display:flex;align-items:center;justify-content:center;font-size:42px;cursor:pointer;transition:background .08s}
.square.light{background:var(--light)}
.square.dark{background:var(--dark)}
.square.last-move.light{background:#c8e6a8}
.square.last-move.dark{background:#a3c48a}
.square.selected.light{background:var(--select)!important}
.square.selected.dark{background:#e6c86a!important}
.square.check{box-shadow:inset 0 0 0 4px var(--check-glow), inset 0 0 18px rgba(255,71,87,.6)}
.square.legal::after{content:'';width:16px;height:16px;background:rgba(47,53,66,.72);border-radius:50%;position:absolute;pointer-events:none}
.square.legal.capture::after{width:92%;height:92%;background:transparent;border:4px solid rgba(231,76,60,.85);border-radius:6px}
.coord{position:absolute;font-size:10px;font-weight:700;opacity:.55;pointer-events:none}
.coord.file{bottom:2px;right:4px}
.coord.rank{top:2px;left:3px}
.square.dark .coord{color:var(--light)}
.square.light .coord{color:var(--dark)}
.piece{width:100%;height:100%;display:flex;align-items:center;justify-content:center;z-index:2;cursor:grab;transition:transform .08s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))}
.piece.dragging{position:absolute;z-index:100;pointer-events:none;transform:scale(1.08);filter:drop-shadow(0 8px 16px rgba(0,0,0,.5))}
.piece.w{color:#f5f5f0;text-shadow:-1.2px -1.2px 0 #1a1a1a, 1.2px -1.2px 0 #1a1a1a, -1.2px 1.2px 0 #1a1a1a, 1.2px 1.2px 0 #1a1a1a, 0 0 3px #000, 0 2px 4px rgba(0,0,0,.9);filter:drop-shadow(0 1px 1px rgba(0,0,0,.8))}
.piece.b{color:#0d0d0d;text-shadow:-0.8px -0.8px 0 #e8dcc8, 0.8px -0.8px 0 #e8dcc8, -0.8px 0.8px 0 #e8dcc8, 0.8px 0.8px 0 #e8dcc8, 0 1px 3px rgba(0,0,0,.7)}
.side{width:340px;flex-shrink:0;display:flex;flex-direction:column;gap:14px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:14px}
.card h3{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}
.status{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--panel2);border-radius:8px;border:1px solid var(--border)}
.turn-dot{width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 0 0 2px var(--border);flex-shrink:0}
.turn-dot.b{background:#111;border:1px solid #555}
.captured{display:flex;flex-wrap:wrap;gap:2px;min-height:22px;font-size:18px;line-height:1}
.move-list{max-height:220px;overflow-y:auto;display:flex;flex-direction:column;gap:1px;background:var(--bg);border-radius:8px;padding:6px;border:1px solid var(--border)}
.move-row{display:grid;grid-template-columns:28px 1fr 1fr;gap:4px;padding:4px 6px;border-radius:6px;font-size:13px;font-family:'Consolas','Courier New',monospace}
.move-row:nth-child(odd){background:rgba(255,255,255,.03)}
.move-row span.num{color:var(--muted)}
.controls{display:flex;flex-wrap:wrap;gap:8px}
.banner{padding:12px;text-align:center;border-radius:8px;font-weight:700;letter-spacing:.04em;display:none}
.banner.show{display:block}
.banner.check{background:rgba(255,71,87,.15);border:1px solid rgba(255,71,87,.4);color:#ff6b81}
.banner.mate{background:#d4a574;color:#1a1a1e}
.banner.draw{background:#57606f;color:#fff}
.promotion-overlay{position:absolute;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:50;border-radius:6px}
.promotion-overlay.show{display:flex}
.promo-box{background:#fff;border-radius:10px;padding:10px;display:flex;gap:6px;box-shadow:0 10px 30px rgba(0,0,0,.4)}
.promo-box button{width:56px;height:56px;font-size:36px;border:2px solid #ddd;border-radius:8px;background:#f8f8f8;cursor:pointer}
.promo-box button:hover{border-color:var(--accent);background:#fff}
.flip-label{font-size:11px;color:var(--muted)}
@media(max-width:900px){
  .layout{flex-direction:column;align-items:center}
  .side{width:100%;max-width:560px}
  .board{width:min(92vw,560px);height:min(92vw,560px)}
}
</style>
</head>
<body>
<header>
  <h1>♔ Chess <span>Grandmaster</span></h1>
  <div class="hdr-right">
    <label class="flip-label">Play as <select id="colorSel" class="btn small"><option value="w">White</option><option value="b">Black</option><option value="both">Both / Analysis</option></select></label>
    <label class="flip-label">AI <select id="diffSel" class="btn small"><option value="1">Easy (1)</option><option value="2" selected>Medium (2)</option><option value="3">Hard (3)</option></select></label>
    <button class="btn small" id="flipBtn">⇅ Flip Board</button>
    <button class="btn small" id="muteBtn">🔊 Sound</button>
  </div>
</header>
<div class="layout">
  <div class="board-wrap" id="boardWrap">
    <div class="board" id="board"></div>
    <div class="promotion-overlay" id="promoOverlay"><div class="promo-box" id="promoBox"></div></div>
  </div>
  <div class="side">
    <div class="card">
      <div class="status" id="statusBox"><div class="turn-dot" id="turnDot"></div><div><div id="turnText" style="font-weight:700;font-size:14px">White to move</div><div id="subText" style="font-size:12px;color:var(--muted)">Game in progress</div></div></div>
      <div class="banner" id="banner"></div>
    </div>
    <div class="card">
      <h3>Captured by White <span id="matW" style="color:var(--accent);font-size:12px"></span></h3>
      <div class="captured" id="capWhite"></div>
      <h3 style="margin-top:10px">Captured by Black <span id="matB" style="color:var(--accent);font-size:12px"></span></h3>
      <div class="captured" id="capBlack"></div>
    </div>
    <div class="card">
      <h3>Moves <span id="moveCount" style="text-transform:none;letter-spacing:0;color:var(--muted)"></span></h3>
      <div class="move-list" id="moveList"><div style="padding:20px;text-align:center;color:var(--muted);font-size:13px">No moves yet</div></div>
    </div>
    <div class="card">
      <div class="controls">
        <button class="btn" id="undoBtn">↩ Undo</button>
        <button class="btn" id="hintBtn">💡 Hint</button>
        <button class="btn primary" id="newBtn">+ New Game</button>
      </div>
    </div>
  </div>
</div>

<script>
/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
const CONFIG = {
  AI_DEPTH_EASY: 1,
  AI_DEPTH_MED: 2,
  AI_DEPTH_HARD: 3,
  PIECE_VALUES: {p:100, n:320, b:330, r:500, q:900, k:20000},
  // PSTs are from white perspective; will be mirrored for black
};

// Unicode pieces
const GLYPH = {
  'wK':'♔','wQ':'♕','wR':'♖','wB':'♗','wN':'♘','wP':'♙',
  'bK':'♚','bQ':'♛','bR':'♜','bB':'♝','bN':'♞','bP':'♟'
};
const PIECE_LETTER = {p:'', n:'N', b:'B', r:'R', q:'Q', k:'K'};
const FILES = ['a','b','c','d','e','f','g','h'];

/* ============================================================
   BOARD REPRESENTATION
   ============================================================ */
// board[0]=a8 ... board[63]=h1
// piece = 'wP','bK' etc or null
let state = null;
let history = [];
let selected = null; // index
let legalTargets = []; // array of move objects for selected
let flipped = false;
let muted = false;
let pendingPromo = null;
let aiThinking = false;

function initialState(){
  const b = Array(64).fill(null);
  const back = ['R','N','B','Q','K','B','N','R'];
  for(let f=0;f<8;f++){
    b[f] = 'b'+back[f];
    b[8+f] = 'bP';
    b[48+f] = 'wP';
    b[56+f] = 'w'+back[f];
  }
  return {
    board:b,
    turn:'w',
    castling:{wK:true,wQ:true,bK:true,bQ:true},
    enPassant:null,
    halfmove:0,
    fullmove:1,
    lastMove:null
  };
}

function cloneState(s){
  return {
    board:s.board.slice(),
    turn:s.turn,
    castling:{...s.castling},
    enPassant:s.enPassant,
    halfmove:s.halfmove,
    fullmove:s.fullmove,
    lastMove:s.lastMove ? {...s.lastMove} : null
  };
}

function idxToRC(i){ return [Math.floor(i/8), i%8]; }
function rcToIdx(r,c){ return r*8+c; }
function idxToAlg(i){ const [r,c]=idxToRC(i); return FILES[c]+(8-r); }
function algToIdx(a){ const f=FILES.indexOf(a[0]); const r=8-parseInt(a[1]); return r*8+f; }
function pieceColor(p){ return p ? p[0] : null; }
function pieceType(p){ return p ? p[1].toLowerCase() : null; }
function opposite(c){ return c==='w'?'b':'w'; }

/* ============================================================
   ATTACK DETECTION & KING FIND
   ============================================================ */
function findKing(board, color){
  const k = color+'K';
  for(let i=0;i<64;i++) if(board[i]===k) return i;
  return -1;
}
function isSquareAttacked(board, sq, byColor){
  const [r,c]=idxToRC(sq);
  // pawn attacks
  const dir = byColor==='w' ? -1 : 1;
  // pawns attack diagonally forward (from pawn perspective). So to attack sq, pawn is one rank behind.
  // If byColor is white, white pawns are below sq (higher row number) because white moves up (decreasing r)
  // White pawn at (r+1,c-1) or (r+1,c+1) attacks sq
  // Black pawn at (r-1,c-1) or (r-1,c+1) attacks sq
  const pr = r + (byColor==='w' ? 1 : -1);
  if(pr>=0 && pr<8){
    if(c>0 && board[rcToIdx(pr,c-1)]===byColor+'P') return true;
    if(c<7 && board[rcToIdx(pr,c+1)]===byColor+'P') return true;
  }
  // knights
  const kn=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
  for(const [dr,dc] of kn){
    const nr=r+dr,nc=c+dc;
    if(nr>=0&&nr<8&&nc>=0&&nc<8 && board[rcToIdx(nr,nc)]===byColor+'N') return true;
  }
  // king
  for(let dr=-1;dr<=1;dr++) for(let dc=-1;dc<=1;dc++) if(dr||dc){
    const nr=r+dr,nc=c+dc;
    if(nr>=0&&nr<8&&nc>=0&&nc<8 && board[rcToIdx(nr,nc)]===byColor+'K') return true;
  }
  // sliding pieces
  const dirs = [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1]];
  for(let d=0;d<8;d++){
    const [dr,dc]=dirs[d];
    let nr=r+dr,nc=c+dc;
    while(nr>=0&&nr<8&&nc>=0&&nc<8){
      const p=board[rcToIdx(nr,nc)];
      if(p){
        if(pieceColor(p)===byColor){
          const t=pieceType(p);
          if(d<4 && (t==='r'||t==='q')) return true;
          if(d>=4 && (t==='b'||t==='q')) return true;
        }
        break;
      }
      nr+=dr; nc+=dc;
    }
  }
  return false;
}
function isInCheck(board, color){
  const k=findKing(board,color);
  if(k===-1) return false;
  return isSquareAttacked(board,k,opposite(color));
}

/* ============================================================
   MOVE GENERATION
   ============================================================ */
function generatePseudoMoves(s){
  const moves=[];
  const board=s.board;
  const us=s.turn, them=opposite(us);
  for(let from=0;from<64;from++){
    const p=board[from];
    if(!p || pieceColor(p)!==us) continue;
    const type=pieceType(p);
    const [r,c]=idxToRC(from);
    if(type==='p'){
      const dir = us==='w' ? -1 : 1;
      const startRank = us==='w' ? 6 : 1;
      const promoRank = us==='w' ? 0 : 7;
      // single push
      const nr=r+dir;
      if(nr>=0&&nr<8){
        const to=rcToIdx(nr,c);
        if(!board[to]){
          if(nr===promoRank){
            for(const promo of ['q','r','b','n']) moves.push({from,to,promotion:promo,capture:null,isEnPassant:false,isCastle:false,isDouble:false});
          } else {
            moves.push({from,to,promotion:null,capture:null,isEnPassant:false,isCastle:false,isDouble:false});
            // double push
            if(r===startRank){
              const nr2=r+dir*2;
              const to2=rcToIdx(nr2,c);
              if(!board[to2]) moves.push({from,to:to2,promotion:null,capture:null,isEnPassant:false,isCastle:false,isDouble:true});
            }
          }
        }
        // captures
        for(const dc of [-1,1]){
          const nc=c+dc;
          if(nc<0||nc>7) continue;
          const capIdx=rcToIdx(nr,nc);
          const target=board[capIdx];
          if(target && pieceColor(target)===them){
            if(nr===promoRank){
              for(const promo of ['q','r','b','n']) moves.push({from,to:capIdx,promotion:promo,capture:target,isEnPassant:false,isCastle:false,isDouble:false});
            } else {
              moves.push({from,to:capIdx,promotion:null,capture:target,isEnPassant:false,isCastle:false,isDouble:false});
            }
          }
          // en passant
          if(s.enPassant!==null && capIdx===s.enPassant){
            moves.push({from,to:capIdx,promotion:null,capture:them+'P',isEnPassant:true,isCastle:false,isDouble:false});
          }
        }
      }
    } else if(type==='n'){
      const del=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
      for(const [dr,dc] of del){
        const nr=r+dr,nc=c+dc;
        if(nr<0||nr>7||nc<0||nc>7) continue;
        const to=rcToIdx(nr,nc);
        const t=board[to];
        if(!t || pieceColor(t)===them) moves.push({from,to,promotion:null,capture:t||null,isEnPassant:false,isCastle:false,isDouble:false});
      }
    } else if(type==='b' || type==='r' || type==='q'){
      const dirs=[];
      if(type==='b'||type==='q') dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
      if(type==='r'||type==='q') dirs.push([-1,0],[1,0],[0,-1],[0,1]);
      for(const [dr,dc] of dirs){
        let nr=r+dr,nc=c+dc;
        while(nr>=0&&nr<8&&nc>=0&&nc<8){
          const to=rcToIdx(nr,nc);
          const t=board[to];
          if(!t) moves.push({from,to,promotion:null,capture:null,isEnPassant:false,isCastle:false,isDouble:false});
          else {
            if(pieceColor(t)===them) moves.push({from,to,promotion:null,capture:t,isEnPassant:false,isCastle:false,isDouble:false});
            break;
          }
          nr+=dr; nc+=dc;
        }
      }
    } else if(type==='k'){
      for(let dr=-1;dr<=1;dr++) for(let dc=-1;dc<=1;dc++) if(dr||dc){
        const nr=r+dr,nc=c+dc;
        if(nr<0||nr>7||nc<0||nc>7) continue;
        const to=rcToIdx(nr,nc);
        const t=board[to];
        if(!t || pieceColor(t)===them) moves.push({from,to,promotion:null,capture:t||null,isEnPassant:false,isCastle:false,isDouble:false});
      }
      // castling
      const kingStart = us==='w' ? 60 : 4;
      if(from===kingStart && !isInCheck(board,us)){
        // kingside
        const canK = us==='w' ? s.castling.wK : s.castling.bK;
        if(canK){
          const f1=from+1, f2=from+2;
          const rookFrom=from+3;
          if(!board[f1] && !board[f2] && board[rookFrom]===us+'R'){
            if(!isSquareAttacked(board,f1,them) && !isSquareAttacked(board,f2,them)){
              moves.push({from,to:f2,promotion:null,capture:null,isEnPassant:false,isCastle:'K',isDouble:false});
            }
          }
        }
        const canQ = us==='w' ? s.castling.wQ : s.castling.bQ;
        if(canQ){
          const f1=from-1,f2=from-2,f3=from-3;
          const rookFrom=from-4;
          if(!board[f1] && !board[f2] && !board[f3] && board[rookFrom]===us+'R'){
            if(!isSquareAttacked(board,f1,them) && !isSquareAttacked(board,f2,them)){
              moves.push({from,to:f2,promotion:null,capture:null,isEnPassant:false,isCastle:'Q',isDouble:false});
            }
          }
        }
      }
    }
  }
  return moves;
}

function generateLegalMoves(s){
  const pseudo=generatePseudoMoves(s);
  const legal=[];
  for(const m of pseudo){
    const ns=applyMove(s,m);
    // after move, the moving side's king must not be in check
    if(!isInCheck(ns.board, s.turn)){
      legal.push(m);
    }
  }
  return legal;
}

function applyMove(s,m){
  const ns=cloneState(s);
  const board=ns.board;
  const us=s.turn, them=opposite(us);
  const piece=board[m.from];
  // handle en passant capture
  if(m.isEnPassant){
    const [r,c]=idxToRC(m.to);
    const capRow = r + (us==='w' ? 1 : -1);
    const capIdx=rcToIdx(capRow,c);
    board[capIdx]=null;
  }
  // handle castling rook move
  if(m.isCastle){
    if(m.isCastle==='K'){
      const rookFrom=m.from+3, rookTo=m.from+1;
      board[rookTo]=board[rookFrom];
      board[rookFrom]=null;
    } else {
      const rookFrom=m.from-4, rookTo=m.from-1;
      board[rookTo]=board[rookFrom];
      board[rookFrom]=null;
    }
  }
  // move piece (with promotion)
  board[m.to]= m.promotion ? us + m.promotion.toUpperCase() : piece;
  board[m.from]=null;

  // update castling rights
  if(pieceType(piece)==='k'){
    if(us==='w'){ ns.castling.wK=false; ns.castling.wQ=false; }
    else { ns.castling.bK=false; ns.castling.bQ=false; }
  }
  if(pieceType(piece)==='r'){
    if(us==='w'){
      if(m.from===56) ns.castling.wQ=false;
      if(m.from===63) ns.castling.wK=false;
    } else {
      if(m.from===0) ns.castling.bQ=false;
      if(m.from===7) ns.castling.bK=false;
    }
  }
  // if capture removed rook on starting square, remove castling right
  if(m.capture && pieceType(m.capture)==='r'){
    const capSq = m.isEnPassant ? (()=>{const [r,c]=idxToRC(m.to); return rcToIdx(r+(us==='w'?1:-1),c)})() : m.to;
    // Actually for en passant capture is pawn, not rook, so ignore
    if(!m.isEnPassant){
      if(capSq===56) ns.castling.wQ=false;
      if(capSq===63) ns.castling.wK=false;
      if(capSq===0) ns.castling.bQ=false;
      if(capSq===7) ns.castling.bK=false;
    }
  }
  // en passant square
  if(m.isDouble){
    const [r,c]=idxToRC(m.from);
    ns.enPassant = rcToIdx(r + (us==='w'?-1:1), c);
  } else {
    ns.enPassant=null;
  }
  // halfmove clock
  if(pieceType(piece)==='p' || m.capture) ns.halfmove=0;
  else ns.halfmove = s.halfmove+1;

  ns.turn = them;
  if(us==='b') ns.fullmove = s.fullmove+1;
  ns.lastMove = {...m, piece, san:null};
  return ns;
}

/* ============================================================
   GAME STATUS
   ============================================================ */
function isInsufficientMaterial(board){
  const pieces=[];
  for(let i=0;i<64;i++) if(board[i]) pieces.push(board[i]);
  if(pieces.length===2) return true; // K vs K
  if(pieces.length===3){
    const types=pieces.map(p=>pieceType(p)).sort().join('');
    if(types==='kn' || types==='kb') return true; // K+N vs K or K+B vs K (k is king)
    // actually pieces include two kings + one minor: check
    const minors=pieces.filter(p=>pieceType(p)!=='k');
    if(minors.length===1 && (pieceType(minors[0])==='b' || pieceType(minors[0])==='n')) return true;
  }
  return false;
}
function gameStatus(s){
  const legal=generateLegalMoves(s);
  const inCheck=isInCheck(s.board,s.turn);
  if(legal.length===0){
    if(inCheck) return {over:true, result: s.turn==='w' ? '0-1' : '1-0', reason: 'Checkmate'};
    else return {over:true, result:'1/2-1/2', reason:'Stalemate'};
  }
  if(isInsufficientMaterial(s.board)) return {over:true, result:'1/2-1/2', reason:'Insufficient material'};
  if(s.halfmove>=100) return {over:true, result:'1/2-1/2', reason:'50-move rule'};
  if(inCheck) return {over:false, inCheck:true};
  return {over:false, inCheck:false};
}

/* ============================================================
   SAN GENERATION
   ============================================================ */
function moveToSAN(s, m){
  if(m.isCastle) return m.isCastle==='K' ? 'O-O' : 'O-O-O';
  const board=s.board;
  const piece=board[m.from];
  const type=pieceType(piece);
  const isCap = !!m.capture || m.isEnPassant;
  let san='';
  if(type==='p'){
    if(isCap) san+=FILES[m.from%8];
  } else {
    san+=PIECE_LETTER[type];
    // disambiguation
    const pseudo=generatePseudoMoves(s).filter(x=> {
      const p2=board[x.from];
      return p2 && pieceType(p2)===type && pieceColor(p2)===s.turn && x.to===m.to && x.from!==m.from;
    });
    // need to check if any other same piece could also move to same square legally? use pseudo for simplicity
    if(pseudo.length>0){
      const sameFile=pseudo.some(x=> x.from%8===m.from%8);
      const sameRank=pseudo.some(x=> Math.floor(x.from/8)===Math.floor(m.from/8));
      if(!sameFile) san+=FILES[m.from%8];
      else if(!sameRank) san+= (8-Math.floor(m.from/8));
      else san+=FILES[m.from%8]+(8-Math.floor(m.from/8));
    }
  }
  if(isCap) san+='x';
  san+=idxToAlg(m.to);
  if(m.promotion) san+='='+m.promotion.toUpperCase();
  if(m.isEnPassant) san+=' e.p.';
  // check / mate suffix
  const ns=applyMove(s,m);
  const oppInCheck=isInCheck(ns.board, ns.turn);
  if(oppInCheck){
    const oppLegal=generateLegalMoves(ns);
    san+= oppLegal.length===0 ? '#' : '+';
  }
  return san;
}

/* ============================================================
   EVALUATION & AI
   ============================================================ */
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,10,10,10,10,0,-10, -10,5,5,10,12,5,5,-10, -10,0,5,10,10,5,0,-10, -10,5,5,5,5,5,5,-10, -10,0,5,0,0,5,0,-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],
};
function evaluateBoard(board){
  let score=0;
  for(let i=0;i<64;i++){
    const p=board[i];
    if(!p) continue;
    const type=pieceType(p);
    const val=CONFIG.PIECE_VALUES[type];
    // PST index: board index 0=a8 corresponds to PST index 0
    const pstVal = PST[type] ? PST[type][i] : 0;
    if(pieceColor(p)==='w') score += val + pstVal;
    else score -= val + (PST[type] ? PST[type][63-i] : 0); // mirror for black
  }
  return score;
}
function orderMoves(moves, board){
  // MVV-LVA: captures first
  return moves.slice().sort((a,b)=>{
    const ca = a.capture ? CONFIG.PIECE_VALUES[pieceType(a.capture)] : 0;
    const cb = b.capture ? CONFIG.PIECE_VALUES[pieceType(b.capture)] : 0;
    if(cb!==ca) return cb-ca;
    if(a.promotion && !b.promotion) return -1;
    if(!a.promotion && b.promotion) return 1;
    return 0;
  });
}
function minimax(s, depth, alpha, beta, maximizing){
  const status=gameStatus(s);
  if(status.over){
    if(status.reason==='Checkmate'){
      // side to move is mated => bad for side to move
      return s.turn==='w' ? -99999 : 99999;
    }
    return 0;
  }
  if(depth===0){
    const ev=evaluateBoard(s.board);
    return ev;
  }
  const moves=orderMoves(generateLegalMoves(s), s.board);
  if(moves.length===0) return evaluateBoard(s.board);
  if(maximizing){
    let maxEval=-Infinity;
    for(const m of moves){
      const ns=applyMove(s,m);
      const ev=minimax(ns, depth-1, alpha, beta, false);
      maxEval=Math.max(maxEval, ev);
      alpha=Math.max(alpha, ev);
      if(beta<=alpha) break;
    }
    return maxEval;
  } else {
    let minEval=Infinity;
    for(const m of moves){
      const ns=applyMove(s,m);
      const ev=minimax(ns, depth-1, alpha, beta, true);
      minEval=Math.min(minEval, ev);
      beta=Math.min(beta, ev);
      if(beta<=alpha) break;
    }
    return minEval;
  }
}
function findBestMove(s, depth){
  const moves=orderMoves(generateLegalMoves(s), s.board);
  if(moves.length===0) return null;
  let best=null;
  let bestScore = s.turn==='w' ? -Infinity : Infinity;
  const maximizing = s.turn==='w';
  for(const m of moves){
    const ns=applyMove(s,m);
    const score=minimax(ns, depth-1, -Infinity, Infinity, !maximizing);
    if(maximizing){
      if(score>bestScore){ bestScore=score; best=m; }
    } else {
      if(score<bestScore){ bestScore=score; best=m; }
    }
  }
  return best;
}
// Chunked AI to avoid freezing UI
function findBestMoveAsync(s, depth, callback){
  const moves=orderMoves(generateLegalMoves(s), s.board);
  if(moves.length===0){ callback(null); return; }
  let best=null;
  let bestScore = s.turn==='w' ? -Infinity : Infinity;
  const maximizing = s.turn==='w';
  let i=0;
  function step(){
    if(i>=moves.length){ callback(best); return; }
    const m=moves[i];
    const ns=applyMove(s,m);
    const score=minimax(ns, depth-1, -Infinity, Infinity, !maximizing);
    if(maximizing){ if(score>bestScore){bestScore=score; best=m;}}
    else { if(score<bestScore){bestScore=score; best=m;}}
    i++;
    if(i%2===0) setTimeout(step, 0);
    else step();
  }
  setTimeout(step, 10);
}

/* ============================================================
   PERSISTENCE
   ============================================================ */
function saveState(){
  const data={state, history, flipped, humanColor: document.getElementById('colorSel').value, diff: document.getElementById('diffSel').value};
  localStorage.setItem('chess_save', JSON.stringify(data));
}
function loadState(){
  try{
    const raw=localStorage.getItem('chess_save');
    if(!raw) return false;
    const d=JSON.parse(raw);
    if(d.state && d.state.board && d.state.board.length===64){
      state=d.state;
      history=d.history||[];
      flipped=!!d.flipped;
      if(d.humanColor) document.getElementById('colorSel').value=d.humanColor;
      if(d.diff) document.getElementById('diffSel').value=d.diff;
      return true;
    }
  }catch(e){}
  return false;
}

/* ============================================================
   FEN & DEBUG INTERFACE
   ============================================================ */
function boardToFEN(s){
  let fen='';
  for(let r=0;r<8;r++){
    let empty=0;
    for(let c=0;c<8;c++){
      const p=s.board[r*8+c];
      if(!p) empty++;
      else {
        if(empty){ fen+=empty; empty=0; }
        const ch=pieceType(p);
        fen+= pieceColor(p)==='w' ? ch.toUpperCase() : ch;
      }
    }
    if(empty) fen+=empty;
    if(r<7) fen+='/';
  }
  fen+=' '+(s.turn)+' ';
  let cs='';
  if(s.castling.wK) cs+='K';
  if(s.castling.wQ) cs+='Q';
  if(s.castling.bK) cs+='k';
  if(s.castling.bQ) cs+='q';
  fen+= cs||'-';
  fen+=' '+(s.enPassant!==null ? idxToAlg(s.enPassant) : '-');
  fen+=' '+s.halfmove+' '+s.fullmove;
  return fen;
}
window.game = {
  fen: ()=> boardToFEN(state),
  board: ()=> state.board.slice(),
  state: ()=> cloneState(state),
  legalMoves: ()=> generateLegalMoves(state).map(m=> ({from:idxToAlg(m.from), to:idxToAlg(m.to), promotion:m.promotion, san: moveToSAN(state,m), raw:m})),
  move: (uci)=>{
    // uci like e2e4 or e7e8q
    if(!uci || uci.length<4) return false;
    const from=algToIdx(uci.slice(0,2));
    const to=algToIdx(uci.slice(2,4));
    const promo=uci.length>4 ? uci[4].toLowerCase() : null;
    const legal=generateLegalMoves(state);
    const found=legal.find(m=> m.from===from && m.to===to && (m.promotion||null)===(promo||null));
    if(!found){
      // try without promo spec if pawn promotion and only one promo? allow any
      const alt=legal.find(m=> m.from===from && m.to===to);
      if(!alt) return false;
      if(alt.promotion && !promo) return false;
      doMove(alt);
      return true;
    }
    doMove(found);
    return true;
  },
  setFEN: (fen)=>{ /* simple */ },
  perft: (depth)=>{ /* count nodes */ let cnt=0; function perftRec(s,d){ if(d===0){cnt++; return;} const ms=generateLegalMoves(s); for(const m of ms) perftRec(applyMove(s,m), d-1);} perftRec(state, depth); return cnt; }
};

/* ============================================================
   SOUND
   ============================================================ */
let audioCtx=null;
function playSound(type){
  if(muted) return;
  try{
    if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
    const o=audioCtx.createOscillator();
    const g=audioCtx.createGain();
    o.connect(g); g.connect(audioCtx.destination);
    const now=audioCtx.currentTime;
    if(type==='move'){ o.frequency.value=420; g.gain.setValueAtTime(.18,now); g.gain.exponentialRampToValueAtTime(.001, now+.12); o.start(now); o.stop(now+.12); }
    else if(type==='capture'){ o.frequency.value=220; g.gain.setValueAtTime(.22,now); g.gain.exponentialRampToValueAtTime(.001, now+.18); o.start(now); o.stop(now+.18); const o2=audioCtx.createOscillator(); const g2=audioCtx.createGain(); o2.connect(g2); g2.connect(audioCtx.destination); o2.frequency.value=330; g2.gain.setValueAtTime(.12,now); g2.gain.exponentialRampToValueAtTime(.001, now+.15); o2.start(now+.02); o2.stop(now+.17); }
    else if(type==='check'){ o.frequency.value=880; g.gain.setValueAtTime(.2,now); g.gain.exponentialRampToValueAtTime(.001, now+.35); o.start(now); o.stop(now+.35); }
  }catch(e){}
}

/* ============================================================
   RENDERING
   ============================================================ */
const boardEl=document.getElementById('board');
const turnDot=document.getElementById('turnDot');
const turnText=document.getElementById('turnText');
const subText=document.getElementById('subText');
const banner=document.getElementById('banner');
const moveListEl=document.getElementById('moveList');
const capWhiteEl=document.getElementById('capWhite');
const capBlackEl=document.getElementById('capBlack');

function render(){
  boardEl.innerHTML='';
  const lastFrom=state.lastMove ? state.lastMove.from : -1;
  const lastTo=state.lastMove ? state.lastMove.to : -1;
  const kingInCheck = isInCheck(state.board, state.turn) ? findKing(state.board, state.turn) : -1;
  for(let i=0;i<64;i++){
    const dispIdx = flipped ? 63-i : i;
    const sqIdx = dispIdx; // actual board index to display at this cell
    // But we iterate visual order: visual position v = i, board index b = flipped?63-i:i
    const bIdx = flipped ? 63 - i : i;
    const [r,c]=idxToRC(bIdx);
    const isLight = (r+c)%2===0;
    const div=document.createElement('div');
    div.className='square '+(isLight?'light':'dark');
    div.dataset.idx=bIdx;
    if(bIdx===lastFrom || bIdx===lastTo) div.classList.add('last-move');
    if(bIdx===kingInCheck) div.classList.add('check');
    if(bIdx===selected) div.classList.add('selected');
    if(legalTargets.some(m=> m.to===bIdx)){
      div.classList.add('legal');
      const mv=legalTargets.find(m=> m.to===bIdx);
      if(mv.capture) div.classList.add('capture');
    }
    // coords
    if((flipped? r===0 : r===7) ) {
      const f=document.createElement('span'); f.className='coord file'; f.textContent=FILES[c]; div.appendChild(f);
    }
    if((flipped? c===7 : c===0)){
      const rk=document.createElement('span'); rk.className='coord rank'; rk.textContent=8-r; div.appendChild(rk);
    }
    const p=state.board[bIdx];
    if(p){
      const pe=document.createElement('div');
      pe.className='piece '+(pieceColor(p)==='w'?'w':'b');
      pe.textContent=GLYPH[p];
      pe.draggable=true;
      pe.dataset.idx=bIdx;
      // drag events
      pe.addEventListener('dragstart', onDragStart);
      pe.addEventListener('dragend', onDragEnd);
      div.appendChild(pe);
    }
    div.addEventListener('click', ()=> onSquareClick(bIdx));
    div.addEventListener('dragover', e=> e.preventDefault());
    div.addEventListener('drop', e=>{ e.preventDefault(); onDrop(bIdx); });
    boardEl.appendChild(div);
  }
  // status
  turnDot.className='turn-dot '+(state.turn==='w'?'w':'b');
  const status=gameStatus(state);
  if(status.over){
    if(status.reason==='Checkmate'){
      turnText.textContent = status.result==='1-0' ? 'White wins!' : 'Black wins!';
      subText.textContent = 'Checkmate';
      banner.textContent = `♔ Checkmate — ${status.result==='1-0'?'White':'Black'} wins`;
      banner.className='banner show mate';
    } else {
      turnText.textContent='Draw';
      subText.textContent=status.reason;
      banner.textContent=`½–½ Draw — ${status.reason}`;
      banner.className='banner show draw';
    }
  } else {
    banner.className='banner';
    if(status.inCheck){
      turnText.textContent=(state.turn==='w'?'White':'Black')+' in check!';
      subText.textContent='Must get out of check';
      banner.textContent='Check!';
      banner.className='banner show check';
    } else {
      turnText.textContent=(state.turn==='w'?'White':'Black')+' to move';
      subText.textContent= state.turn==='w' ? 'White pieces' : 'Black pieces';
    }
  }
  renderCaptured();
  renderMoveList();
  saveState();
  maybeTriggerAI();
}

function renderCaptured(){
  // compute captured by diff from initial material
  const count = {w:{p:0,n:0,b:0,r:0,q:0}, b:{p:0,n:0,b:0,r:0,q:0}};
  const initCounts = {w:{p:8,n:2,b:2,r:2,q:1,k:1}, b:{p:8,n:2,b:2,r:2,q:1,k:1}};
  const cur={w:{p:0,n:0,b:0,r:0,q:0,k:0}, b:{p:0,n:0,b:0,r:0,q:0,k:0}};
  for(const p of state.board) if(p){ cur[pieceColor(p)][pieceType(p)]++; }
  // captured by white = black pieces missing
  function listFor(capturer){
    const victim = capturer==='w' ? 'b' : 'w';
    let html='';
    let val=0;
    for(const t of ['p','n','b','r','q']){
      const missing = initCounts[victim][t] - (cur[victim][t]||0);
      // promotions can create extra pieces, but ignore for display
      const n=Math.max(0, missing);
      for(let i=0;i<n;i++) html+= GLYPH[victim+t]+' ';
      val+= n*CONFIG.PIECE_VALUES[t];
    }
    return {html, val};
  }
  const wCap=listFor('w');
  const bCap=listFor('b');
  capWhiteEl.innerHTML = wCap.html || '<span style="color:var(--muted);font-size:13px">—</span>';
  capBlackEl.innerHTML = bCap.html || '<span style="color:var(--muted);font-size:13px">—</span>';
  document.getElementById('matW').textContent = wCap.val? '+'+wCap.val : '';
  document.getElementById('matB').textContent = bCap.val? '+'+bCap.val : '';
}

function renderMoveList(){
  if(history.length===0){ moveListEl.innerHTML='<div style="padding:20px;text-align:center;color:var(--muted);font-size:13px">No moves yet<br><span style="font-size:11px">White moves first</span></div>'; document.getElementById('moveCount').textContent=''; return; }
  let html='';
  for(let i=0;i<history.length;i+=2){
    const n=Math.floor(i/2)+1;
    const w=history[i]?.san||'';
    const b=history[i+1]?.san||'';
    html+=`<div class="move-row"><span class="num">${n}.</span><span>${w}</span><span>${b}</span></div>`;
  }
  moveListEl.innerHTML=html;
  moveListEl.scrollTop=moveListEl.scrollHeight;
  document.getElementById('moveCount').textContent=history.length+' moves';
}

/* ============================================================
   INTERACTION
   ============================================================ */
let dragFrom=null;
function onSquareClick(idx){
  if(aiThinking) return;
  const status=gameStatus(state);
  if(status.over) return;
  // if pending promotion, ignore
  if(pendingPromo) return;
  const p=state.board[idx];
  // if selected and clicked legal target -> move
  if(selected!==null && legalTargets.some(m=> m.to===idx)){
    const mv=legalTargets.find(m=> m.to===idx);
    attemptMove(mv);
    return;
  }
  // select own piece
  if(p && pieceColor(p)===state.turn){
    // check if human is allowed to move this color
    const human=document.getElementById('colorSel').value;
    if(human!=='both' && human!==state.turn){
      // not human's turn, ignore select
      return;
    }
    selected=idx;
    const all=generateLegalMoves(state);
    legalTargets=all.filter(m=> m.from===idx);
    render();
  } else {
    selected=null; legalTargets=[];
    render();
  }
}
function onDragStart(e){
  const idx=parseInt(e.target.dataset.idx);
  const p=state.board[idx];
  if(!p || pieceColor(p)!==state.turn) { e.preventDefault(); return; }
  const human=document.getElementById('colorSel').value;
  if(human!=='both' && human!==state.turn){ e.preventDefault(); return; }
  dragFrom=idx;
  selected=idx;
  legalTargets=generateLegalMoves(state).filter(m=> m.from===idx);
  render();
  setTimeout(()=> e.target.classList.add('dragging'),0);
}
function onDragEnd(e){ e.target.classList.remove('dragging'); }
function onDrop(toIdx){
  if(dragFrom===null) return;
  const mv=legalTargets.find(m=> m.to===toIdx);
  if(mv) attemptMove(mv);
  dragFrom=null;
}

function attemptMove(m){
  // check promotion needed? If pawn to last rank but no promotion specified, show picker
  const piece=state.board[m.from];
  if(pieceType(piece)==='p'){
    const [r]=idxToRC(m.to);
    const promoRank = pieceColor(piece)==='w' ? 0 : 7;
    if(r===promoRank && !m.promotion){
      // need promotion picker
      pendingPromo=m;
      showPromotionPicker(m.to, pieceColor(piece));
      return;
    }
  }
  doMove(m);
}
function showPromotionPicker(sq, color){
  const overlay=document.getElementById('promoOverlay');
  const box=document.getElementById('promoBox');
  box.innerHTML='';
  for(const t of ['q','r','b','n']){
    const btn=document.createElement('button');
    btn.textContent=GLYPH[color+t.toUpperCase()];
    btn.onclick=()=>{
      overlay.classList.remove('show');
      const mv={...pendingPromo, promotion:t};
      // need to set capture correctly if capture move
      const target=state.board[pendingPromo.to];
      if(target) mv.capture=target;
      pendingPromo=null;
      doMove(mv);
    };
    box.appendChild(btn);
  }
  overlay.classList.add('show');
}
document.getElementById('promoOverlay').addEventListener('click', e=>{
  if(e.target.id==='promoOverlay'){ e.target.classList.remove('show'); pendingPromo=null; }
});

function doMove(m){
  const san=moveToSAN(state,m);
  const ns=applyMove(state,m);
  // sound
  const isCap = !!m.capture || m.isEnPassant;
  const givesCheck=isInCheck(ns.board, ns.turn);
  if(givesCheck) playSound('check');
  else if(isCap) playSound('capture');
  else playSound('move');
  // push history
  history.push({from:m.from,to:m.to,san, uci: idxToAlg(m.from)+idxToAlg(m.to)+(m.promotion||''), fenBefore: boardToFEN(state)});
  state=ns;
  selected=null; legalTargets=[];
  render();
}

function undo(){
  if(history.length===0) return;
  // undo up to 2 moves if AI involved, but spec says undo takes back both player and AI move
  // We'll undo 2 if possible, else 1
  const human=document.getElementById('colorSel').value;
  let steps = 1;
  if(human!=='both' && history.length>=2) steps=2;
  // reconstruct from initial by replaying history minus steps
  const remaining=history.slice(0, history.length-steps);
  let s=initialState();
  const newHist=[];
  for(const h of remaining){
    const legal=generateLegalMoves(s);
    const mv=legal.find(m=> m.from===h.from && m.to===h.to && (m.promotion||null)===(h.uci.length>4?h.uci[4]:null) || (m.from===h.from && m.to===h.to && !m.promotion && h.uci.length===4));
    // fallback: find by from/to
    let found=legal.find(m=> m.from===h.from && m.to===h.to);
    if(h.uci.length>4) found=legal.find(m=> m.from===h.from && m.to===h.to && m.promotion===h.uci[4]);
    if(!found) found=legal.find(m=> m.from===h.from && m.to===h.to);
    if(found){
      const san=moveToSAN(s,found);
      s=applyMove(s,found);
      newHist.push({from:found.from,to:found.to,san, uci: idxToAlg(found.from)+idxToAlg(found.to)+(found.promotion||'')});
    }
  }
  state=s;
  history=newHist;
  selected=null; legalTargets=[]; pendingPromo=null;
  document.getElementById('promoOverlay').classList.remove('show');
  render();
}

function newGame(){
  state=initialState();
  history=[];
  selected=null; legalTargets=[];
  document.getElementById('promoOverlay').classList.remove('show');
  render();
}

function getAIDepth(){
  const v=document.getElementById('diffSel').value;
  if(v==='1') return CONFIG.AI_DEPTH_EASY;
  if(v==='2') return CONFIG.AI_DEPTH_MED;
  return CONFIG.AI_DEPTH_HARD;
}
function maybeTriggerAI(){
  if(aiThinking || pendingPromo) return;
  const status=gameStatus(state);
  if(status.over) return;
  const human=document.getElementById('colorSel').value;
  if(human==='both') return;
  if(state.turn===human) return;
  // AI turn
  aiThinking=true;
  subText.textContent='AI thinking...';
  const depth=getAIDepth();
  findBestMoveAsync(cloneState(state), depth, (best)=>{
    aiThinking=false;
    if(!best) { render(); return; }
    // ensure still AI turn (user may have changed settings)
    if(state.turn!==human && human!=='both'){
      doMove(best);
    } else render();
  });
}

/* ============================================================
   INIT
   ============================================================ */
document.getElementById('flipBtn').onclick=()=>{ flipped=!flipped; render(); };
document.getElementById('muteBtn').onclick=(e)=>{ muted=!muted; e.target.textContent= muted ? '🔇 Muted' : '🔊 Sound'; };
document.getElementById('undoBtn').onclick=undo;
document.getElementById('newBtn').onclick=newGame;
document.getElementById('hintBtn').onclick=()=>{
  const moves=generateLegalMoves(state);
  if(moves.length===0) return;
  const best=findBestMove(state, 2);
  if(best){
    selected=best.from;
    legalTargets=[best];
    render();
    setTimeout(()=>{ selected=null; legalTargets=[]; render(); }, 1200);
  }
};
document.getElementById('colorSel').onchange=()=>{ saveState(); render(); };
document.getElementById('diffSel').onchange=saveState;

if(!loadState()){
  state=initialState();
  history=[];
}
render();

// expose for probe
window._state=()=>state;
window._history=()=>history;
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"91c5090c-4a14-4384-8dc0-4eada87b7d98","tokensIn":314801,"tokensOut":19524,"tokensTotal":334325,"cost":0.008381294,"turns":12,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-08-23T00:31:33.077Z"} -->