← Chess results

Chess

ox alpha · 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 36,482 bytes · SHA-256 5054db158788
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — vs Computer</title>
<style>
/* ===================== TUNABLE CONSTANTS (CSS) ===================== */
:root{
  --light:#f0d9b5; --dark:#b58863; --light-hi:#f6ebc8; --dark-hi:#c9a876;
  --sel:#f7ec5e88; --last:#f7ec5e55; --check:#ff5a4a;
  --panel:#2b2620; --panel2:#3a332b; --text:#efe6d8; --accent:#c8a24a;
}
*{box-sizing:border-box; margin:0; padding:0;}
body{
  font-family:Georgia,'Times New Roman',serif; background:radial-gradient(1200px 800px at 50% 30%,#4a4238,#241f19);
  color:var(--text); min-height:100vh; display:flex; align-items:center; justify-content:center; padding:16px;
}
#app{display:flex; gap:22px; align-items:flex-start; flex-wrap:wrap; justify-content:center;}
h1{font-size:22px; letter-spacing:2px; text-align:center; color:var(--accent); margin-bottom:8px;}
#board-wrap{position:relative;}
#board{
  display:grid; grid-template-columns:repeat(8,64px); grid-template-rows:repeat(8,64px);
  border:6px solid #171310; border-radius:6px; box-shadow:0 12px 40px #000c, inset 0 0 0 1px #ffffff18;
  user-select:none; touch-action:none;
}
.sq{width:64px; height:64px; display:flex; align-items:center; justify-content:center; position:relative; cursor:pointer;}
.sq.light{background:var(--light);} .sq.dark{background:var(--dark);}
.sq.lastmove.light{background:var(--light-hi); box-shadow:inset 0 0 0 3px var(--last);}
.sq.lastmove.dark{background:var(--dark-hi); box-shadow:inset 0 0 0 3px var(--last);}
.sq.selected{box-shadow:inset 0 0 0 4px #e8d44d !important; background:var(--sel);}
.sq.incheck::after{content:''; position:absolute; inset:0; border-radius:50%;
  background:radial-gradient(circle, #ff5a4a00 40%, var(--check) 90%); opacity:.85; pointer-events:none;}
.pc{font-size:50px; line-height:1; cursor:grab; z-index:2; text-shadow:0 2px 3px #0007;
  transition:transform .08s;}
.pc.w{color:#fafafa; -webkit-text-stroke:1.5px #3a3128; text-shadow:0 2px 3px #0007;} .pc.b{color:#1c1a17;}
.pc:hover{transform:scale(1.06);}
.dot{width:20px; height:20px; border-radius:50%; background:#00000038; position:absolute; z-index:1;}
.ring{width:56px; height:56px; border-radius:50%; border:6px solid #00000038; position:absolute; z-index:1;}
.dragging{position:fixed; z-index:99; pointer-events:none; font-size:50px; line-height:1;
  text-shadow:0 4px 10px #000a; transform:translate(-50%,-58%);}
/* side panel */
#panel{width:300px; background:var(--panel); border-radius:10px; padding:14px; box-shadow:0 10px 30px #000a;
  display:flex; flex-direction:column; gap:10px;}
#status{font-size:17px; text-align:center; padding:8px; border-radius:6px; background:var(--panel2);}
#status.mate{background:#7a2a22; font-weight:bold;}
#trays{display:flex; justify-content:space-between; font-size:22px; min-height:28px; color:#ddd;}
.tray{display:flex; flex-wrap:wrap; max-width:140px; gap:1px; align-items:center;}
.tray small{font-size:11px; color:#999; margin-left:4px;}
#movelist{background:var(--panel2); border-radius:6px; height:220px; overflow-y:auto; padding:6px 10px;
  font-size:14px; font-family:'Segoe UI',sans-serif;}
#movelist .row{display:grid; grid-template-columns:34px 1fr 1fr; padding:2px 0;}
#movelist .num{color:#8a7f6e;} #movelist .mv{cursor:default; padding:0 4px; border-radius:3px;}
#movelist .mv.cur{background:#5a4f3d; color:#fff;}
#controls{display:grid; grid-template-columns:1fr 1fr; gap:8px;}
button{font-family:inherit; background:var(--panel2); color:var(--text); border:1px solid #5a5040;
  border-radius:6px; padding:8px 10px; cursor:pointer; font-size:14px;}
button:hover{background:#4a4234; border-color:var(--accent);}
button:disabled{opacity:.4; cursor:default;}
select{font-family:inherit; background:var(--panel2); color:var(--text); border:1px solid #5a5040;
  border-radius:6px; padding:8px; font-size:14px; width:100%;}
label{font-size:12px; color:#a89a84;}
/* promotion overlay */
#promo{position:absolute; display:none; flex-direction:column; background:#efe6d8; border-radius:8px;
  box-shadow:0 8px 24px #000c; z-index:50; overflow:hidden;}
#promo button{background:none; border:none; font-size:44px; padding:4px 14px; color:#111;}
#promo button:hover{background:var(--accent);}
#banner{position:absolute; inset:0; display:none; align-items:center; justify-content:center;
  background:#000000b0; z-index:60; border-radius:6px;}
#banner .card{background:var(--panel); padding:26px 40px; border-radius:12px; text-align:center;
  border:1px solid var(--accent); font-size:20px;}
#banner button{margin-top:14px;}
@media (max-width:760px){ #board{grid-template-columns:repeat(8,44px); grid-template-rows:repeat(8,44px);}
  .sq{width:44px;height:44px;} .pc{font-size:34px;} .ring{width:38px;height:38px;border-width:4px;} }
</style>
</head>
<body>
<div id="app">
  <div>
    <h1>♞ CHESS</h1>
    <div id="board-wrap">
      <div id="board"></div>
      <div id="promo"></div>
      <div id="banner"><div class="card"><div id="banner-text"></div><button onclick="ui.newGame()">New Game</button></div></div>
    </div>
  </div>
  <div id="panel">
    <div id="status">White to move</div>
    <div id="trays">
      <div class="tray" id="tray-b"></div>
      <div class="tray" id="tray-w"></div>
    </div>
    <div id="movelist"></div>
    <div id="controls">
      <button id="btn-new" onclick="ui.newGame()">New Game</button>
      <button id="btn-undo" onclick="ui.undo()">Undo</button>
      <button id="btn-flip" onclick="ui.flip()">Flip Board</button>
      <button id="btn-mute" onclick="ui.toggleMute()">🔊 Sound</button>
      <div><label>Difficulty</label><select id="sel-depth">
        <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><label>Play as</label><select id="sel-side">
        <option value="w" selected>White</option>
        <option value="b">Black</option>
        <option value="both">Both sides</option>
      </select></div>
    </div>
  </div>
</div>
<script>
/* ============================================================
   TUNABLE CONSTANTS (JS)
   ============================================================ */
const CFG = {
  pieceValues:  { p:100, n:320, b:330, r:500, q:900, k:20000 },
  aiDepths:     { 1:1, 2:2, 3:3 },      // difficulty -> search depth
  quiescence:   true,                    // capture-only search extension
  qMaxDepth:    6,
  aiMoveDelayMs: 120,                    // small pause so UI feels natural
  flipBlackPerspective: true,
};

/* ============================================================
   SECTION 1: ENGINE — 0x88 board representation
   Squares: index 0..127, valid if (sq & 0x88) === 0.
   rank = sq >> 4, file = sq & 7. a1 = 0, h1 = 7, a8 = 112.
   Pieces: 'PNBRQK' white, 'pnbrqk' black, null empty.
   ============================================================ */
const ENGINE = (() => {
  const WHITE='w', BLACK='b';
  const PIECE_VAL = CFG.pieceValues;
  const KNIGHT_D=[31,33,14,18,-31,-33,-14,-18];
  const KING_D=[1,-1,16,-16,15,17,-15,-17];
  const BISHOP_D=[15,17,-15,-17];
  const ROOK_D=[1,-1,16,-16];

  function initialState(){
    return {
      board: new Array(128).fill(null),
      turn: WHITE,
      castling: {wk:true,wq:true,bk:true,bq:true},
      ep: -1,               // en-passant target square (index) or -1
      half: 0,              // halfmove clock (50-move rule)
      full: 1,
      history: [],          // undo stack: {move, castling, ep, half, full}
      kings: {w: 4, b: 116} // king square cache
    };
  }
  function setupStart(st){
    const back='RNBQKBNR';
    for(let f=0; f<8; f++){
      st.board[f]        = back[f];      // white back rank
      st.board[16+f]     = 'P';          // white pawns
      st.board[96+f]     = 'p';          // black pawns
      st.board[112+f]    = back[f].toLowerCase();
    }
    st.kings={w:4, b:116};
  }
  const colorOf = p => p ? (p === p.toUpperCase() ? WHITE : BLACK) : null;
  const typeOf  = p => p ? p.toLowerCase() : null;
  const sqName  = sq => 'abcdefgh'[sq & 7] + (1 + (sq >> 4));
  const nameSq  = s => (s.charCodeAt(0)-97) + (parseInt(s[1])-1)*16;
  const other   = c => c===WHITE ? BLACK : WHITE;

  function attacked(st, sq, byColor){
    // pawns
    const dir = byColor===WHITE ? -16 : 16; // attacker sits at sq+dir±1 relative? compute from sq
    for(const d of [dir-1, dir+1]){
      const f = sq + d;
      if(!(f & 0x88)){
        const p = st.board[f];
        if(p && colorOf(p)===byColor && typeOf(p)==='p') return true;
      }
    }
    // knights
    for(const d of KNIGHT_D){
      const f=sq+d; if(!(f&0x88)){ const p=st.board[f];
        if(p && colorOf(p)===byColor && typeOf(p)==='n') return true; }
    }
    // king
    for(const d of KING_D){
      const f=sq+d; if(!(f&0x88)){ const p=st.board[f];
        if(p && colorOf(p)===byColor && typeOf(p)==='k') return true; }
    }
    // sliders
    for(const d of BISHOP_D){
      let f=sq+d;
      while(!(f&0x88)){ const p=st.board[f];
        if(p){ if(colorOf(p)===byColor && (typeOf(p)==='b'||typeOf(p)==='q')) return true; break; }
        f+=d; }
    }
    for(const d of ROOK_D){
      let f=sq+d;
      while(!(f&0x88)){ const p=st.board[f];
        if(p){ if(colorOf(p)===byColor && (typeOf(p)==='r'||typeOf(p)==='q')) return true; break; }
        f+=d; }
    }
    return false;
  }
  const inCheck = (st, color) => attacked(st, st.kings[color], other(color));

  function pseudoMoves(st, color){
    const moves=[];
    const add=(from,to,flags,promo)=>{
      moves.push({from,to,piece:st.board[from],captured:flags==='e'?(color===WHITE?'p':'P'):st.board[to],promo:promo||null,flags:flags||'n'});
    };
    for(let sq=0; sq<128; sq++){
      if(sq & 0x88){ sq+=7; continue; }
      const p=st.board[sq]; if(!p || colorOf(p)!==color) continue;
      const t=typeOf(p);
      if(t==='p'){
        const dir = color===WHITE ? 16 : -16;
        const startRank = color===WHITE ? 1 : 6;
        const promoRank = color===WHITE ? 7 : 0;
        const one = sq+dir;
        if(!(one&0x88) && !st.board[one]){
          if((one>>4)===promoRank){ for(const pr of 'qrbn') add(sq,one,'p',color===WHITE?pr.toUpperCase():pr); }
          else{
            add(sq,one);
            const two = sq+2*dir;
            if((sq>>4)===startRank && !st.board[two]) add(sq,two,'b'); // double push
          }
        }
        for(const dd of [dir-1,dir+1]){
          const to=sq+dd; if(to&0x88) continue;
          const tp=st.board[to];
          if(tp && colorOf(tp)!==color){
            if((to>>4)===promoRank){ for(const pr of 'qrbn') add(sq,to,'p',color===WHITE?pr.toUpperCase():pr); }
            else add(sq,to,'c');
          } else if(to===st.ep && st.ep>=0){
            add(sq,to,'e');
          }
        }
      } else if(t==='n' || t==='k'){
        for(const d of (t==='n'?KNIGHT_D:KING_D)){
          const to=sq+d; if(to&0x88) continue;
          const tp=st.board[to];
          if(!tp) add(sq,to); else if(colorOf(tp)!==color) add(sq,to,'c');
        }
        if(t==='k'){
          const home = color===WHITE?4:116;
          if(sq===home && !inCheck(st,color)){
            const kR=color+'k', qR=color+'q';
            // kingside: rook on h-file, squares empty & not attacked
            if(st.castling[kR] && !st.board[home+1] && !st.board[home+2]
              && st.board[home+3]===(color===WHITE?'R':'r')
              && !attacked(st,home+1,other(color)) && !attacked(st,home+2,other(color)))
              add(sq,home+2,'k');
            if(st.castling[qR] && !st.board[home-1] && !st.board[home-2] && !st.board[home-3]
              && st.board[home-4]===(color===WHITE?'R':'r')
              && !attacked(st,home-1,other(color)) && !attacked(st,home-2,other(color)))
              add(sq,home-2,'q');
          }
        }
      } else {
        const dirs = t==='b'?BISHOP_D : t==='r'?ROOK_D : KING_D; // q = all 8
        for(const d of dirs){
          let to=sq+d;
          while(!(to&0x88)){
            const tp=st.board[to];
            if(!tp) add(sq,to);
            else { if(colorOf(tp)!==color) add(sq,to,'c'); break; }
            to+=d;
          }
        }
      }
    }
    return moves;
  }

  function makeMove(st, m){
    st.history.push({castling:{...st.castling}, ep:st.ep, half:st.half, full:st.full});
    const color=colorOf(m.piece), opp=other(color);
    const b=st.board;
    b[m.from]=null;
    b[m.to]= m.promo ? m.promo : m.piece;
    if(m.flags==='e') b[m.to + (color===WHITE?-16:16)]=null; // remove ep pawn
    if(typeOf(m.piece)==='k'){
      st.kings[color]=m.to;
      if(m.flags==='k'){ const home=color===WHITE?4:116; b[home+1]=b[home+3]; b[home+3]=null; }
      if(m.flags==='q'){ const home=color===WHITE?4:116; b[home-1]=b[home-4]; b[home-4]=null; }
      st.castling[color+'k']=false; st.castling[color+'q']=false;
    }
    // rook moved / captured -> castling rights
    const clr=c=>c==='R'?'wk':c==='r'?'bk':null;
    if(m.from===7||m.to===7) st.castling.wk=false;
    if(m.from===0||m.to===0) st.castling.wq=false;
    if(m.from===119||m.to===119) st.castling.bk=false;
    if(m.from===112||m.to===112) st.castling.bq=false;
    // en passant target
    st.ep = (typeOf(m.piece)==='p' && Math.abs(m.to-m.from)===32) ? (m.from+m.to)/2 : -1;
    st.half = (typeOf(m.piece)==='p' || m.captured) ? 0 : st.half+1;
    if(color===BLACK) st.full++;
    st.turn=opp;
  }
  function unmakeMove(st, m){
    const h=st.history.pop(); if(!h) return null;
    const color=colorOf(m.piece), b=st.board;
    b[m.from]=m.piece;
    b[m.to]=null;
    if(m.flags==='e') b[m.to + (color===WHITE?-16:16)] = color===WHITE ? 'p' : 'P';
    else if(m.captured) b[m.to]=m.captured;
    if(typeOf(m.piece)==='k'){
      st.kings[color]=m.from;
      if(m.flags==='k'){ const home=color===WHITE?4:116; b[home+3]=b[home+1]; b[home+1]=null; }
      if(m.flags==='q'){ const home=color===WHITE?4:116; b[home-4]=b[home-1]; b[home-1]=null; }
    }
    st.castling=h.castling; st.ep=h.ep; st.half=h.half; st.full=h.full;
    st.turn=color;
  }

  function legalMoves(st, color){
    color = color || st.turn;
    const out=[];
    for(const m of pseudoMoves(st,color)){
      makeMove(st,m);
      if(!inCheck(st,color)) out.push(m);
      unmakeMove(st,m);
    }
    return out;
  }

  // Game status: 'playing' | 'checkmate' | 'stalemate' | 'draw-50' | 'draw-material'
  function insufficientMaterial(st){
    const pieces=[];
    for(let sq=0; sq<128; sq++){ if(sq&0x88){sq+=7;continue;} const p=st.board[sq]; if(p && typeOf(p)!=='k') pieces.push(typeOf(p)); }
    if(pieces.length===0) return true;                       // K vs K
    if(pieces.length===1 && (pieces[0]==='b'||pieces[0]==='n')) return true; // K+minor vs K
    return false;
  }
  function status(st){
    const legal = legalMoves(st);
    if(legal.length===0) return inCheck(st, st.turn) ? 'checkmate' : 'stalemate';
    if(st.half>=100) return 'draw-50';
    if(insufficientMaterial(st)) return 'draw-material';
    return inCheck(st, st.turn) ? 'check' : 'playing';
  }

  // SAN notation for a move, given list of legal moves in current position
  function toSAN(st, m, legal){
    if(m.flags==='k') return 'O-O';
    if(m.flags==='q') return 'O-O-O';
    const t=typeOf(m.piece);
    let s='';
    if(t==='p'){
      if(m.captured || m.flags==='e') s += 'abcdefgh'[m.from&7] + 'x';
      s += sqName(m.to);
      if(m.promo) s += '=' + m.promo.toUpperCase();
    } else {
      s += t.toUpperCase();
      // disambiguation
      const others = legal.filter(x => x!==m && typeOf(x.piece)===t && x.to===m.to && x.from!==m.from);
      if(others.length){
        const sameFile = others.some(x => (x.from&7)===(m.from&7));
        const sameRank = others.some(x => (x.from>>4)===(m.from>>4));
        if(!sameFile) s += 'abcdefgh'[m.from&7];
        else if(!sameRank) s += (1+(m.from>>4));
        else s += sqName(m.from);
      }
      if(m.captured) s += 'x';
      s += sqName(m.to);
    }
    // check / mate suffix
    makeMove(st,m);
    const stt = status(st);
    unmakeMove(st,m);
    if(stt==='checkmate') s+='#'; else if(stt==='check') s+='+';
    return s;
  }

  // FEN
  function toFEN(st){
    let rows=[];
    for(let r=7;r>=0;r--){
      let row='', empty=0;
      for(let f=0;f<8;f++){
        const p=st.board[r*16+f];
        if(p){ if(empty){row+=empty;empty=0;} row+=p; } else empty++;
      }
      if(empty) row+=empty;
      rows.push(row);
    }
    let cast = (st.castling.wk?'K':'')+(st.castling.wq?'Q':'')+(st.castling.bk?'k':'')+(st.castling.bq?'q':'');
    return rows.join('/')+' '+st.turn+' '+(cast||'-')+' '+(st.ep>=0?sqName(st.ep):'-')+' '+st.half+' '+st.full;
  }
  function fromFEN(fen){
    const st=initialState();
    const [pos, turn, cast, ep, half, full] = fen.trim().split(/\s+/);
    let r=7,f=0;
    for(const ch of pos){
      if(ch==='/'){ r--; f=0; }
      else if(/\d/.test(ch)) f+=+ch;
      else { const sq=r*16+f; st.board[sq]=ch; if(ch==='K') st.kings.w=sq; if(ch==='k') st.kings.b=sq; f++; }
    }
    st.turn = turn==='b'?BLACK:WHITE;
    st.castling={wk:cast.includes('K'),wq:cast.includes('Q'),bk:cast.includes('k'),bq:cast.includes('q')};
    st.ep = (ep && ep!=='-') ? nameSq(ep) : -1;
    st.half = +half||0; st.full = +full||1;
    return st;
  }

  return {WHITE,BLACK,colorOf,typeOf,sqName,nameSq,other,attacked,inCheck,
    legalMoves,pseudoMoves,makeMove,unmakeMove,status,toSAN,toFEN,fromFEN,
    initialState,setupStart,insufficientMaterial};
})();
</script>
<script>
/* ============================================================
   SECTION 2: AI — minimax + alpha-beta, move ordering,
   quiescence-lite (captures only), material + PST eval.
   ============================================================ */
const AI = (() => {
  const E = ENGINE;
  // Piece-square tables from White's perspective (a8 first row), values in centipawns
  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]
  };
  const mirror = sq => ((7 - (sq>>4)) << 4) | (sq & 7); // flip rank for black

  function evaluate(st){
    let score=0;
    for(let sq=0; sq<128; sq++){
      if(sq & 0x88){ sq+=7; continue; }
      const p=st.board[sq]; if(!p) continue;
      const t=E.typeOf(p), white=E.colorOf(p)==='w';
      const idx = white ? (7-(sq>>4))*8 + (sq&7) : (sq>>4)*8 + (sq&7);
      const v = CFG.pieceValues[t] + PST[t][idx];
      score += white ? v : -v;
    }
    return score; // + good for White
  }

  function orderMoves(moves){
    return moves.map(m => ({m, s:
      (m.captured ? 1000 + CFG.pieceValues[E.typeOf(m.captured)] - CFG.pieceValues[E.typeOf(m.piece)]/10 : 0)
      + (m.promo ? 900 : 0)}))
      .sort((a,b)=>b.s-a.s).map(x=>x.m);
  }

  function quiesce(st, alpha, beta, color, depth){
    const stand = color==='w' ? evaluate(st) : -evaluate(st);
    if(depth<=0) return stand;
    if(stand >= beta) return beta;
    if(stand > alpha) alpha = stand;
    const caps = E.pseudoMoves(st,color).filter(m=>m.captured||m.flags==='e');
    for(const m of orderMoves(caps)){
      E.makeMove(st,m);
      if(E.inCheck(st,color)){ E.unmakeMove(st,m); continue; }
      const sc = quiesce(st, -beta, -alpha, E.other(color), depth-1);
      E.unmakeMove(st,m);
      if(sc >= beta) return beta;
      if(sc > alpha) alpha = sc;
    }
    return alpha;
  }

  function search(st, depth, alpha, beta, color){
    const legal = E.legalMoves(st, color);
    if(legal.length===0) return E.inCheck(st,color) ? -99999 + (CFG.aiDepths[3]-depth) : 0;
    if(depth===0) return CFG.quiescence ? quiesce(st,alpha,beta,color,CFG.qMaxDepth)
                                        : (color==='w'?evaluate(st):-evaluate(st));
    for(const m of orderMoves(legal)){
      E.makeMove(st,m);
      const sc = -search(st, depth-1, -beta, -alpha, E.other(color));
      E.unmakeMove(st,m);
      if(sc >= beta) return beta;
      if(sc > alpha) alpha = sc;
    }
    return alpha;
  }

  function bestMove(st, depth){
    const color = st.turn;
    const legal = E.legalMoves(st, color);
    if(!legal.length) return null;
    let best=null, bestScore=-Infinity;
    for(const m of orderMoves(legal)){
      E.makeMove(st,m);
      const sc = -search(st, depth-1, -Infinity, Infinity, E.other(color));
      E.unmakeMove(st,m);
      if(sc > bestScore){ bestScore=sc; best=m; }
    }
    return best; // never illegal: drawn from legalMoves()
  }
  return {bestMove, evaluate};
})();

/* ============================================================
   SECTION 3: SOUND — Web Audio
   ============================================================ */
const Sound = (() => {
  let ctx=null, muted=false;
  function beep(freq, dur, type='sine', gain=0.12){
    if(muted) return;
    try{
      ctx = ctx || new (window.AudioContext||window.webkitAudioContext)();
      const o=ctx.createOscillator(), g=ctx.createGain();
      o.type=type; o.frequency.value=freq;
      g.gain.setValueAtTime(gain, ctx.currentTime);
      g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime+dur);
      o.connect(g).connect(ctx.destination);
      o.start(); o.stop(ctx.currentTime+dur);
    }catch(e){}
  }
  return {
    move:    () => beep(320, .09, 'triangle'),
    capture: () => { beep(180,.1,'square',.1); setTimeout(()=>beep(140,.12,'square',.08),40); },
    check:   () => { beep(660,.12,'sawtooth',.08); setTimeout(()=>beep(880,.14,'sawtooth',.07),90); },
    end:     () => { [440,554,659].forEach((f,i)=>setTimeout(()=>beep(f,.3,'triangle'),i*130)); },
    toggleMute(){ muted=!muted; return muted; },
    get muted(){ return muted; }
  };
})();

/* ============================================================
   SECTION 4: GAME CONTROLLER + UI
   ============================================================ */
const GLYPH = {P:'♟',N:'♞',B:'♝',R:'♜',Q:'♛',K:'♚',p:'♟',n:'♞',b:'♝',r:'♜',q:'♛',k:'♚'};
const LS_KEY = 'chess-game-v1';

const Game = {
  st: null,
  sanList: [],          // [{san, fenAfter}]
  humanSide: 'w',       // 'w' | 'b' | 'both'
  flipped: false,
  selected: -1,
  legalCache: [],
  gameOver: false,

  newGame(){
    this.st = ENGINE.initialState();
    ENGINE.setupStart(this.st);
    this.sanList=[]; this.selected=-1; this.gameOver=false;
    this.save(); UI.renderAll();
    this.maybeAI();
  },
  load(){
    try{
      const raw = localStorage.getItem(LS_KEY);
      if(!raw) return false;
      const d = JSON.parse(raw);
      this.st = ENGINE.fromFEN(d.fen);
      // rebuild history stack is not needed for undo of past moves beyond SAN list;
      // undo uses stored FEN snapshots instead.
      this.st.history = [];
      this.sanList = d.sanList || [];
      this.humanSide = d.humanSide || 'w';
      this.flipped = !!d.flipped;
      this.gameOver = d.gameOver || false;
      this.lastMove = d.lastMove || null;
      return true;
    }catch(e){ return false; }
  },
  save(){
    localStorage.setItem(LS_KEY, JSON.stringify({
      fen: ENGINE.toFEN(this.st),
      sanList: this.sanList,
      humanSide: this.humanSide,
      flipped: this.flipped,
      gameOver: this.gameOver,
      lastMove: this.lastMove || null
    }));
  },

  findMove(from,to,promo){
    return this.legalCache.find(m => m.from===from && m.to===to && (!promo || (m.promo&&m.promo.toLowerCase()===promo)));
  },
  movesFrom(sq){ return this.legalCache.filter(m=>m.from===sq); },

  applyMove(m){
    const st=this.st;
    const legal = ENGINE.legalMoves(st);
    const san = ENGINE.toSAN(st, m, legal);
    ENGINE.makeMove(st,m);
    this.sanList.push({san, fen: ENGINE.toFEN(st)});
    this.selected=-1;
    const stat = ENGINE.status(st);
    if(stat==='checkmate' || stat==='stalemate' || stat==='draw-50' || stat==='draw-material') this.gameOver=true;
    this.save();
    UI.renderAll();
    if(stat==='checkmate'||stat==='stalemate') Sound.end();
    else if(stat==='check') Sound.check();
    else if(m.captured||m.flags==='e') Sound.capture();
    else Sound.move();
    this.maybeAI();
  },

  maybeAI(){
    if(this.gameOver) return;
    const aiTurn = this.humanSide==='both' ? false :
                   (this.st.turn !== this.humanSide);
    if(!aiTurn) return;
    UI.setStatus('Computer thinking…');
    const depth = CFG.aiDepths[+document.getElementById('sel-depth').value] || 2;
    setTimeout(()=>{
      const m = AI.bestMove(this.st, depth);
      if(m) this.applyMove(m);
    }, CFG.aiMoveDelayMs);
  },

  undo(){
    if(!this.sanList.length || UI.thinking) return;
    // take back moves until it's the human's turn again (usually 2 plies)
    let n = (this.humanSide==='both') ? 1 : 2;
    n = Math.min(n, this.sanList.length);
    for(let i=0;i<n;i++){
      this.sanList.pop();
      const prevFen = this.sanList.length ? this.sanList[this.sanList.length-1].fen : null;
      this.st = prevFen ? ENGINE.fromFEN(prevFen) : (()=>{const s=ENGINE.initialState();ENGINE.setupStart(s);return s;})();
      this.st.history=[];
    }
    this.gameOver=false; this.selected=-1;
    this.save(); UI.renderAll();
  }
};

const UI = {
  thinking:false,
  drag:null,

  init(){
    document.getElementById('sel-side').addEventListener('change', e=>{
      Game.humanSide=e.target.value; Game.newGame();
    });
    Game.load() || Game.newGame();
    if(Game.humanSide !== (document.getElementById('sel-side').value))
      document.getElementById('sel-side').value = Game.humanSide;
    this.renderAll();
    Game.maybeAI();
  },

  boardOrder(){ // square indices in display order
    const out=[];
    for(let r=0;r<8;r++) for(let f=0;f<8;f++){
      const rank = Game.flipped ? r : 7-r;
      const file = Game.flipped ? 7-f : f;
      out.push(rank*16+file);
    }
    return out;
  },

  renderAll(){ this.renderBoard(); this.renderPanel(); },

  renderBoard(){
    const st=Game.st, board=document.getElementById('board');
    Game.legalCache = ENGINE.legalMoves(st);
    board.innerHTML='';
    const last = Game.sanList.length ? Game.sanList[Game.sanList.length-1] : null;
    // derive last-move squares from FEN diff is complex; store on Game instead:
    const lm = Game.lastMove || {};
    const checkSq = ENGINE.inCheck(st, st.turn) ? st.kings[st.turn] : -1;
    const targets = Game.selected>=0 ? Game.movesFrom(Game.selected) : [];
    for(const sq of this.boardOrder()){
      const div=document.createElement('div');
      const light = ((sq>>4)+(sq&7))%2===1;
      div.className='sq '+(light?'light':'dark');
      div.dataset.sq=sq;
      if(sq===lm.from || sq===lm.to) div.classList.add('lastmove');
      if(sq===checkSq) div.classList.add('incheck');
      if(sq===Game.selected) div.classList.add('selected');
      const p=st.board[sq];
      if(p){
        const pc=document.createElement('span');
        pc.className='pc '+(ENGINE.colorOf(p)==='w'?'w':'b');
        pc.textContent=GLYPH[p];
        div.appendChild(pc);
      }
      const t=targets.find(m=>m.to===sq);
      if(t){
        const h=document.createElement('span');
        h.className=(t.captured||t.flags==='e')?'ring':'dot';
        div.appendChild(h);
      }
      div.addEventListener('click', ()=>this.onSquareClick(sq));
      div.addEventListener('pointerdown', e=>this.onPointerDown(e,sq));
      board.appendChild(div);
    }
  },

  renderPanel(){
    const st=Game.st, stat=ENGINE.status(st);
    const statusEl=document.getElementById('status');
    statusEl.classList.remove('mate');
    if(Game.gameOver){
      let msg;
      if(stat==='checkmate') msg=(st.turn==='w'?'Black':'White')+' wins by checkmate';
      else if(stat==='stalemate') msg='Draw — stalemate';
      else if(stat==='draw-50') msg='Draw — 50-move rule';
      else msg='Draw — insufficient material';
      statusEl.textContent=msg; statusEl.classList.add('mate');
      document.getElementById('banner-text').textContent=msg;
      document.getElementById('banner').style.display='flex';
    } else {
      document.getElementById('banner').style.display='none';
      const side = st.turn==='w'?'White':'Black';
      statusEl.textContent = side+' to move'+(stat==='check'?' — CHECK!':'');
    }
    // captured trays
    const startCount={p:8,n:2,b:2,r:2,q:1};
    const counts={w:{},b:{}};
    for(const c of ['w','b']) counts[c]={p:0,n:0,b:0,r:0,q:0};
    for(let sq=0;sq<128;sq++){ if(sq&0x88){sq+=7;continue;}
      const p=st.board[sq]; if(p && ENGINE.typeOf(p)!=='k') counts[ENGINE.colorOf(p)][ENGINE.typeOf(p)]++; }
    // captured by White = missing black pieces
    const trayW=document.getElementById('tray-w'), trayB=document.getElementById('tray-b');
    trayW.innerHTML=''; trayB.innerHTML='';
    let wPts=0,bPts=0;
    for(const t of ['q','r','b','n','p']){
      const missB=startCount[t]-counts.b[t];
      const missW=startCount[t]-counts.w[t];
      for(let i=0;i<missB;i++){ trayW.innerHTML+='<span>'+GLYPH[t.toUpperCase()]+'</span>'; wPts+=CFG.pieceValues[t]; }
      for(let i=0;i<missW;i++){ trayB.innerHTML+='<span>'+GLYPH[t]+'</span>'; bPts+=CFG.pieceValues[t]; }
    }
    if(wPts>bPts) trayW.innerHTML+='<small>+'+Math.round((wPts-bPts)/100)+'</small>';
    if(bPts>wPts) trayB.innerHTML+='<small>+'+Math.round((bPts-wPts)/100)+'</small>';
    // move list
    const ml=document.getElementById('movelist');
    ml.innerHTML='';
    for(let i=0;i<Game.sanList.length;i+=2){
      const row=document.createElement('div'); row.className='row';
      row.innerHTML='<span class="num">'+(i/2+1)+'.</span>'
        +'<span class="mv'+(i===Game.sanList.length-1?' cur':'')+'">'+Game.sanList[i].san+'</span>'
        +'<span class="mv'+(i+1===Game.sanList.length-1?' cur':'')+'">'+(Game.sanList[i+1]?Game.sanList[i+1].san:'')+'</span>';
      ml.appendChild(row);
    }
    ml.scrollTop=ml.scrollHeight;
    document.getElementById('btn-mute').textContent = Sound.muted?'🔇 Muted':'🔊 Sound';
  },

  setStatus(t){ if(!Game.gameOver) document.getElementById('status').textContent=t; },

  onSquareClick(sq){
    if(Game.gameOver || this.thinking) return;
    if(Game.humanSide!=='both' && Game.st.turn!==Game.humanSide) return;
    const p=Game.st.board[sq];
    if(Game.selected>=0){
      const ms=Game.movesFrom(Game.selected).filter(m=>m.to===sq);
      if(ms.length>1 || (ms.length===1 && ms[0].promo)){ // promotion picker
        this.showPromo(ms[0].from, sq, ms);
        return;
      }
      if(ms.length===1){ Game.lastMove={from:ms[0].from,to:ms[0].to}; Game.applyMove(ms[0]); return; }
    }
    if(p && ENGINE.colorOf(p)===Game.st.turn){
      Game.selected = (Game.selected===sq) ? -1 : sq;
    } else Game.selected=-1;
    this.renderBoard();
  },

  showPromo(from,to,options){
    const el=document.getElementById('promo');
    const rect=document.querySelector('[data-sq="'+to+'"]').getBoundingClientRect();
    const wrap=document.getElementById('board-wrap').getBoundingClientRect();
    const white=Game.st.turn==='w';
    el.style.left=(rect.left-wrap.left)+'px';
    el.style.top =(Game.flipped? rect.top-wrap.top : rect.top-wrap.top)+'px';
    el.style.flexDirection = Game.flipped ? 'column-reverse':'column';
    el.style.display='flex';
    el.innerHTML='';
    for(const t of ['q','r','b','n']){
      const b=document.createElement('button');
      b.textContent=GLYPH[white?t.toUpperCase():t];
      b.onclick=()=>{
        el.style.display='none';
        const m=options.find(o=>o.promo && o.promo.toLowerCase()===t);
        Game.lastMove={from:m.from,to:m.to};
        Game.applyMove(m);
      };
      el.appendChild(b);
    }
  },

  // ---- drag & drop ----
  onPointerDown(e, sq){
    if(Game.gameOver || this.thinking || e.button!==0) return;
    if(Game.humanSide!=='both' && Game.st.turn!==Game.humanSide) return;
    const p=Game.st.board[sq];
    if(!p || ENGINE.colorOf(p)!==Game.st.turn) return;
    Game.selected=sq;
    this.renderBoard();
    const ghost=document.createElement('div');
    ghost.className='dragging'; ghost.textContent=GLYPH[p];
    document.body.appendChild(ghost);
    const move=x=>{ ghost.style.left=x.clientX+'px'; ghost.style.top=x.clientY+'px'; };
    move(e);
    const srcEl=document.querySelector('[data-sq="'+sq+'"] .pc');
    if(srcEl) srcEl.style.visibility='hidden';
    const onMove=ev=>move(ev);
    const onUp=ev=>{
      window.removeEventListener('pointermove',onMove);
      window.removeEventListener('pointerup',onUp);
      ghost.remove();
      if(srcEl) srcEl.style.visibility='';
      const target=document.elementFromPoint(ev.clientX,ev.clientY);
      const tsq=target && target.closest ? target.closest('.sq') : null;
      if(tsq){
        const to=+tsq.dataset.sq;
        if(to!==sq){
          const ms=Game.movesFrom(sq).filter(m=>m.to===to);
          if(ms.length>1 || (ms.length===1 && ms[0].promo)){ this.showPromo(sq,to,ms); return; }
          if(ms.length===1){ Game.lastMove={from:sq,to}; Game.applyMove(ms[0]); return; }
        }
      }
      this.renderBoard();
    };
    window.addEventListener('pointermove',onMove);
    window.addEventListener('pointerup',onUp);
  },

  flip(){ Game.flipped=!Game.flipped; Game.save(); this.renderBoard(); },
  toggleMute(){ Sound.toggleMute(); this.renderPanel(); },
  newGame(){ Game.newGame(); },
  undo(){ Game.undo(); },
};

/* ============================================================
   SECTION 5: DEBUG INTERFACE + BOOT
   ============================================================ */
window.game = {
  fen: () => ENGINE.toFEN(Game.st),
  move: (uci) => {           // 'e2e4', 'e7e8q'
    const from=ENGINE.nameSq(uci.slice(0,2)), to=ENGINE.nameSq(uci.slice(2,4));
    const promo=uci.length>4?uci[4]:null;
    const m=Game.findMove(from,to,promo);
    if(!m) return false;
    Game.lastMove={from,to};
    Game.applyMove(m);
    return true;
  },
  legalMoves: () => ENGINE.legalMoves(Game.st).map(m =>
    ENGINE.sqName(m.from)+ENGINE.sqName(m.to)+(m.promo?m.promo.toLowerCase():'')),
  status: () => ENGINE.status(Game.st),
  turn: () => Game.st.turn,
  board: () => Game.st.board,
  undo: () => Game.undo(),
  newGame: () => Game.newGame(),
  eval: () => AI.evaluate(Game.st),
  engine: ENGINE
};
window.ui = UI;
window.addEventListener('DOMContentLoaded', ()=>UI.init());
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"1b9d9d72-9485-460a-a8cc-18252e69f363","tokensIn":732267,"tokensOut":17564,"tokensTotal":749831,"cost":0,"turns":23,"toolCalls":24,"failedToolCalls":0,"timestamp":"2026-08-21T17:05:32.559Z"} -->