← Chess results

Chess

Ornith 1.5 9B GGUF · 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 44,473 bytes · SHA-256 2a568cbce18c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — AI Opponent</title>
<style>
  :root{
    --sq:72px;
    --light:#e8d0a9;
    --dark:#a57c50;
    --panel:#2b2620;
    --panel2:#3a332b;
    --ink:#f4ecd8;
    --accent:#d9a441;
    --move:#26607a;
    --capture:#7a2638;
    --last:#b8d06e;
    --check:#cf3a3a;
  }
  *{box-sizing:border-box;}
  html,body{margin:0;height:100%;}
  body{
    background:radial-gradient(circle at 50% 0%, #3a3326, #1c1813);
    color:var(--ink);
    font-family:"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
    display:flex;flex-direction:column;align-items:center;
    min-height:100vh;padding:16px;gap:14px;
  }
  h1{font-weight:600;letter-spacing:1px;margin:4px 0 0;color:var(--accent);font-size:20px;}
  #app{display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap;justify-content:center;}
  #board-wrap{display:flex;flex-direction:column;align-items:center;gap:8px;}
  #turn-indicator{
    font-size:14px;letter-spacing:.5px;padding:4px 12px;border-radius:20px;
    background:var(--panel2);border:1px solid #555044;min-height:28px;
  }
  #board{
    display:grid;grid-template-columns:repeat(8,var(--sq));grid-template-rows:repeat(8,var(--sq));
    border:6px solid #6b4f2a;border-radius:4px;box-shadow:0 10px 30px rgba(0,0,0,.5);
    user-select:none;-webkit-user-select:none;
  }
  .sq{
    position:relative;width:var(--sq);height:var(--sq);
    display:flex;align-items:center;justify-content:center;
    cursor:default;overflow:hidden;
  }
  .sq.light{background:var(--light);}
  .sq.dark{background:var(--dark);}
  .sq.light.check,.sq.dark.check{box-shadow:inset 0 0 0 4px var(--check);}
  .sq.selected{box-shadow:inset 0 0 0 4px var(--accent);}
  .sq.last{box-shadow:inset 0 0 0 4px var(--last);}
  .sq .piece{font-size:calc(var(--sq)*0.82);line-height:1;pointer-events:none;
    filter:drop-shadow(0 1px 1px rgba(0,0,0,.4));}
  .piece-white{color:#fff;}
  .piece-black{color:#201a12;}
  .piece-black{transform:scaleX(-1);}
  .sq.move::after{content:"";position:absolute;width:30%;height:30%;margin:35%;
    background:rgba(38,96,122,.72);border-radius:50%;}
  .sq.capture{box-shadow:inset 0 0 0 5px var(--capture);}
  .sq.capture::after{content:"";position:absolute;inset:6%;border:5px solid rgba(122,38,56,.6);border-radius:50%;}
  #panel{
    width:300px;background:var(--panel);border:1px solid #555044;border-radius:8px;
    padding:14px;display:flex;flex-direction:column;gap:12px;
    box-shadow:0 10px 30px rgba(0,0,0,.4);
  }
  #banner{
    text-align:center;font-weight:700;padding:10px;border-radius:6px;
    background:rgba(217,164,65,.15);border:1px solid var(--accent);color:var(--accent);
    min-height:20px;display:none;line-height:1.3;
  }
  #captured-black{min-height:26px;color:#e8d0a9;font-size:26px;line-height:1.2;
    padding:4px 6px;border-radius:4px;background:var(--panel2);letter-spacing:1px;}
  #moves-wrap{flex:1;display:flex;flex-direction:column;min-height:120px;}
  #moves-title{font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#c9bfa8;margin:2px 0;}
  #move-list{flex:1;overflow-y:auto;font-family:"Cascadia Code","Consolas",monospace;font-size:13px;
    line-height:1.5;display:grid;grid-template-columns:36px 1fr 1fr;gap:2px 8px;align-content:start;}
  #move-list .mv{color:#8f8674;padding:2px 4px;border-radius:3px;cursor:default;}
  #move-list .mv.hl{background:rgba(184,208,110,.25);color:var(--ink);}
  #move-list .mv .n{color:#7a7160;}
  #captured-white{color:#f4ecd8;font-size:26px;line-height:1.2;padding:4px 6px;
    border-radius:4px;background:var(--panel2);letter-spacing:1px;min-height:26px;}
  #controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
  button{background:var(--panel2);color:var(--ink);border:1px solid #555044;border-radius:6px;
    padding:8px 12px;cursor:pointer;font-size:14px;transition:.15s;}
  button:hover{background:#4a4236;border-color:var(--accent);}
  button.active{background:var(--accent);color:#201a12;}
  select,label{font-size:13px;}
  select{background:var(--panel2);color:var(--ink);border:1px solid #555044;border-radius:6px;padding:6px;}
  label{display:flex;align-items:center;gap:6px;cursor:pointer;}
  #promo-overlay{
    position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:50;
  }
  #promo-overlay.show{display:flex;}
  #promo-box{background:var(--panel);border:2px solid var(--accent);border-radius:10px;padding:16px 18px;
    display:flex;gap:10px;box-shadow:0 20px 50px rgba(0,0,0,.6);}
  #promo-box .promot{
    width:76px;height:76px;display:flex;align-items:center;justify-content:center;
    font-size:52px;background:var(--panel2);border:2px solid #555044;border-radius:8px;cursor:pointer;
    color:#201a12;filter:drop-shadow(0 1px 1px rgba(0,0,0,.4));
  }
  #promo-box .promot:hover{border-color:var(--accent);transform:translateY(-2px);}
  #promo-box .promot.piece-white{color:#fff;}
  #promo-box .promot.piece-black{transform:scaleX(-1);color:#201a12;}
  #panel::-webkit-scrollbar{width:8px;}
  #panel::-webkit-scrollbar-thumb{background:#555044;border-radius:4px;}
  @media (max-width:820px){:root{--sq:11vw;}#panel{width:80vw;}}
</style>
</head>
<body>
  <h1>♞ CHESS &nbsp;vs&nbsp; AI</h1>
  <div id="app">
    <div id="board-wrap">
      <div id="turn-indicator">White to move</div>
      <div id="board" data-orientation="normal"></div>
    </div>
    <div id="panel">
      <div id="banner"></div>
      <div id="captured-black" title="Pieces captured by White"></div>
      <div id="moves-wrap">
        <div id="moves-title">Moves</div>
        <div id="move-list"></div>
      </div>
      <div id="captured-white" title="Pieces captured by Black"></div>
      <div id="controls">
        <button id="btn-flip" title="Flip board">⇅ Flip</button>
        <button id="btn-undo" title="Undo (player + AI)">↶ Undo</button>
        <button id="btn-new" title="New game">+ New</button>
        <select id="difficulty" title="AI difficulty">
          <option value="1">Easy (Depth 1)</option>
          <option value="2">Medium (Depth 2)</option>
          <option value="3" selected>Hard (Depth 3)</option>
        </select>
        <label title="Mute sound effects"><input type="checkbox" id="mute"> Mute</label>
      </div>
    </div>
  </div>
  <div id="promo-overlay"><div id="promo-box"></div></div>

<script>
/* =========================================================================
   CONSTANTS
   ========================================================================= */
const WHITE = 0, BLACK = 1;
const PAWN = 0, KNIGHT = 1, BISHOP = 2, ROOK = 3, QUEEN = 4, KING = 5;
const TYPES = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING];
const GLYPHS = ['♙','♟','♘','♞','♗','♝','♕','♛','♔','♚'];
const FILES = 'abcdefgh';
const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';

// Tunable eval weights (centipawns)
const EVAL = {
  PAWN: 100, KNIGHT: 320, BISHOP: 330, ROOK: 500, QUEEN: 900,
  KING: 20000
};
// Piece-square tables (white perspective). Index = rank from white's side (0 = a1).
const PST = [
  // Pawn
  [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]
];
const PST_KING = [
  [-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]
];
// PST_KING[5] used in middlegame, PST_KING[7] in endgame.

/* =========================================================================
   BOARD REPRESENTATION
   Pieces stored as single bytes: [color<<4 | type]. 0 = empty.
   board[0] = a1 (rank1, white's back rank), board[63] = h8.
   index = rank*8 + file, rank 0 = rank1.
   ========================================================================= */
/* =========================================================================
   GAME STATE
   ========================================================================= */
const State = {
  board: new Uint8Array(64),
  turn: WHITE,
  castling: 0,          // bit0 K,wK, bit1 Q,wQ, bit2 k,bK, bit3 q,bQ
  ep: -1,               // en-passant target square (-1 = none)
  halfmove: 0,          // for 50-move rule
  fullmove: 1,
  history: [],          // stack of applied moves (for undo)
  moved: new Set()      // squares that have moved (a piece's origin/destination)
};

function clearState(){
  State.board.fill(0);
  State.turn = WHITE;
  State.castling = 0b1111;
  State.ep = -1;
  State.halfmove = 0;
  State.fullmove = 1;
  State.history.length = 0;
  State.moved.clear();
  setupStart();
}
function setupStart(){
  const back = [ROOK,KNIGHT,BISHOP,QUEEN,KING,BISHOP,KNIGHT,ROOK];
  for(let f=0;f<8;f++){
    State.board[f] = WHITE<<4|PAWN;
    State.board[8+f] = WHITE<<4|back[f];
    State.board[47+f] = BLACK<<4|back[f];
    State.board[56+f] = BLACK<<4|PAWN;
  }
}

// color of a piece byte
function color(b){return b>>4;}
function type(b){return b&15;}
function isWhite(b){return color(b)===WHITE;}
function isBlack(b){return color(b)===BLACK;}
function empty(b){return b===0;}
function clr(bit){return State.castling & bit; }
function setc(bit){State.castling |= bit;}
function clrb(bit){State.castling &= ~bit;}
function hasCastling(){return State.castling!==0;}

// square helpers
function sq(f,r){return r*8+f;}
function fileOf(s){return s&7;}
function rankOf(s){return s>>3;}
function opp(c){return c===WHITE?BLACK:WHITE;}

// is the given color in check?
function inCheck(c, b){
  if(!b) b = State.board;
  const ksq = findKing(c, b);
  if(ksq<0) return false;
  return attacked(opp(c), ksq, b);
}
function findKing(c, b){
  const t = c<<4|KING;
  for(let i=0;i<64;i++) if(b[i]===t) return i;
  return -1;
}

/* =========================================================================
   ATTACK GENERATION (does `color` attack `sq` on board `b`?)
   Uses pseudo-legality (ignores own pieces for sliders, but blockers matter).
   ========================================================================= */
function attacked(byColor, sq, b){
  if(!b) b = State.board;
  const f = sq&7, r = sq>>3;
  // Pawn attacks
  const pT = byColor===WHITE?PAWN:BLACK; // pawns moving toward rank0 for white
  // A white pawn on (f-1,r+1) or (f+1,r+1) attacks sq at (f,r)
  for(const df of [-1,1]){
    const pf = f+df, pr = r+1;
    if(pf>=0&&pf<8&&pr<8){
      const pb = b[pr*8+pf];
      if(color(pb)===byColor&&type(pb)===PAWN) return true;
    }
  }
  // Knight
  const kMoves=[[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
  for(const[df,dr] of kMoves){
    const nf=f+df, nr=r+dr;
    if(nf>=0&&nf<8&&nr>=0&&nr<8){
      const nb=b[nr*8+nf];
      if(color(nb)===byColor&&type(nb)===KNIGHT) return true;
    }
  }
  // King
  for(let df=-1;df<=1;df++)for(let dr=-1;dr<=1;dr++){
    if(df===0&&dr===0) continue;
    const nf=f+df, nr=r+dr;
    if(nf>=0&&nf<8&&nr>=0&&nr<8){
      const nb=b[nr*8+nf];
      if(color(nb)===byColor&&type(nb)===KING) return true;
    }
  }
  // Bishop/Queen (diagonals)
  for(const[df,dr] of [[1,1],[1,-1],[-1,1],[-1,-1]]){
    let nf=f+df, nr=r+dr, ok=true;
    while(nf>=0&&nf<8&&nr>=0&&nr<8){
      const nb=b[nr*8+nf];
      if(nb!==0){ if(color(nb)===byColor&&type(nb)!==BISHOP&&type(nb)!==QUEEN) break; return true; }
      nf+=df; nr+=dr;
    }
  }
  // Rook/Queen (orthogonals)
  for(const[df,dr] of [[1,0],[-1,0],[0,1],[0,-1]]){
    let nf=f+df, nr=r+dr, ok=true;
    while(nf>=0&&nf<8&&nr>=0&&nr<8){
      const nb=b[nr*8+nf];
      if(nb!==0){ if(color(nb)===byColor&&type(nb)!==ROOK&&type(nb)!==QUEEN) break; return true; }
      nf+=df; nr+=dr;
    }
  }
  return false;
}
/* =========================================================================
   MOVE GENERATION
   A move: { from, to, piece, color, flags, promotion }
   flags: castleKing, castleQueen, ep, promo
   ========================================================================= */
// Pseudo-legal moves for a color (ignores whether own king is left in check).
function genPseudoMoves(c, b){
  const moves = [];
  const enemy = opp(c);
  const push = (from,to,extra)=>moves.push({from,to,piece:b[from],color:c,flags:0, promotion:extra||null});
  for(let s=0;s<64;s++){
    const p=b[s]; if(!p) continue; if(color(p)!==c) continue;
    const t=type(p);
    if(t===PAWN){
      const fr=rankOf(s);
      const dir = c===WHITE?1:-1;
      const startR = c===WHITE?1:6;
      const endR = c===WHITE?6:1;
      const f=fileOf(s);
      // forward push
      const one = sq(f, fr+dir);
      if(empty(b[one])){
        let promoR = c===WHITE?7:0;
        if(fr+dir!==promoR){ push(s,one,0); }
        else { for(const pr of TYPES.slice(0,4)) push(s,one,pr); }
        // double push
        if(fr===startR){
          const two=sq(f, fr+2*dir);
          if(empty(b[two])) push(s,two,0);
        }
      }
      // captures
      for(const df of [-1,1]){
        const cf=f+df; if(cf<0||cf>7) continue;
        const cs=sq(cf,fr+dir);
        if(color(b[cs])===enemy){
          let promoR=promoR; promoR=c===WHITE?7:0;
          if(fr+dir===promoR) for(const pr of TYPES.slice(0,4)) push(s,cs,pr);
          else push(s,cs,0);
        } else if(empty(b[cs])){
          // en passant
          if(s===State.ep){ push(s,cs,0); }
        }
      }
    } else if(t===KNIGHT){
      const f=fileOf(s);
      const offs=[[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
      for(const[df,dr] of offs){
        const nf=f+df,nr=rankOf(s)+dr;
        if(nf>=0&&nf<8&&nr>=0&&nr<8){
          const nb=b[nr*8+nf];
          if(nb===0||color(nb)===enemy) push(s,nr*8+nf,0);
        }
      }
    } else if(t===KING){
      const f=fileOf(s);
      for(let df=-1;df<=1;df++)for(let dr=-1;dr<=1;dr++){
        if(df===0&&dr===0) continue;
        const nf=f+df,nr=rankOf(s)+dr;
        if(nf>=0&&nf<8&&nr>=0&&nr<8){
          const nb=b[nr*8+nf];
          if(nb===0||color(nb)===enemy) push(s,nr*8+nf,0);
        }
      }
      // castling
      if(inCheck(c,b)) {} // can't castle out of check
      const backRank = c===WHITE?0:7;
      const rookHome = c===WHITE?sq(0,backRank):sq(7,backRank);
      const kDir = c===WHITE?1:-1;
      const wK = clr(0b1), wQ = clr(0b10), bK = clr(0b100), bQ = clr(0b100|0b10);
      const hasKside = (c===WHITE)? wK && b[rookHome]===c<<4|ROOK : bK && b[rookHome]===c<<4|ROOK;
      const hasQside = (c===WHITE)? wQ && b[rookHome]===c<<4|ROOK : bQ && b[rookHome]===c<<4|ROOK;
      if(hasKside){
        const ksq=findKing(c,b);
        const between=[sq(3,backRank),sq(2,backRank)];
        let safe=true;
        for(const bs of between){ if(!empty(b[bs])||attacked(opp(c),bs,b)){safe=false;break;} }
        if(ksq===sq(4,backRank) && safe && !attacked(opp(c),sq(5,backRank),b) && !attacked(opp(c),sq(6,backRank),b)){
          push(sq(4,backRank),sq(6,backRank),c<<4|KING,0);
        }
      }
      if(hasQside){
        const ksq=findKing(c,b);
        const between=[sq(5,backRank),sq(4,backRank),sq(3,backRank)];
        let safe=true;
        for(const bs of between){ if(!empty(b[bs])||attacked(opp(c),bs,b)){safe=false;break;} }
        if(ksq===sq(4,backRank) && safe && !attacked(opp(c),sq(3,backRank),b) && !attacked(opp(c),sq(2,backRank),b)){
          push(sq(4,backRank),sq(2,backRank),c<<4|KING,0);
        }
      }
    } else {
      // sliders: bishop/rook/queen
      const f=fileOf(s);
      const dirs=[];
      if(t===BISHOP||t===QUEEN) dirs.push([1,1],[1,-1],[-1,1],[-1,-1]);
      if(t===ROOK||t===QUEEN) dirs.push([1,0],[-1,0],[0,1],[0,-1]);
      for(const[df,dr] of dirs){
        let nf=f+df,nr=rankOf(s)+dr;
        while(nf>=0&&nf<8&&nr>=0&&nr<8){
          const nb=b[nr*8+nf];
          if(nb===0){ push(s,nr*8+nf,0); }
          else { if(color(nb)===enemy) push(s,nr*8+nf,0); break; }
          nf+=df; nr+=dr;
        }
      }
    }
  }
  return moves;
}

// Full legal move generation: filter out moves leaving own king in check,
// and add castling if not already added by pseudo-gen (we handle castle flags here).
function legalMoves(c){
  if(c===undefined) c=State.turn;
  const b=State.board;
  let moves=genPseudoMoves(c,b);
  const legal=[];
  const enemy=opp(c);
  for(const m of moves){
    // apply to a scratch board
    const nb=applyMoveTo(m,b,State.castling,State.ep,c,true);
    if(!inCheck(c,nb)) legal.push(m);
  }
  return legal;
}
/* =========================================================================
   applyMoveTo: apply a move to a scratch board `b`, returning a new state
   object {board, castling, ep, turn, halfmove, fullmove} (or the state object
   itself when `own` is false — used by the real game).
   ========================================================================= */
function applyMoveTo(m, b, castling, ep, c, own){
  const nb = new Uint8Array(b);
  nb[m.to] = 0;
  const t=type(m.piece);
  // handle en passant capture (pawn removed from its square)
  let captured = false;
  if(t===PAWN && Math.abs(fileOf(m.from)-fileOf(m.to))===1 && empty(nb[m.to]) && m.to===ep){
    captured = true;
    nb[ep]=0;
  }
  // promotion
  const promoType = m.promotion!==null? m.promotion : (c===WHITE?QUEEN:QUEEN);
  nb[m.to] = c<<4|promoType;
  // castling rook movement
  if(t===KING && Math.abs(fileOf(m.from)-fileOf(m.to))===2){
    const backRank=rankOf(m.from);
    if(m.to===sq(6,backRank)){ // king side
      nb[rookSquare(c,backRank)]=0;
      nb[sq(5,backRank)] = c<<4|ROOK;
    } else if(m.to===sq(2,backRank)){ // queen side
      nb[rookSquare(c,backRank)]=0;
      nb[sq(3,backRank)] = c<<4|ROOK;
    }
  }
  const nCast = updateCastling(castling, m.from, m.to, t);
  // en-passant target for next move
  let nEp=-1;
  if(t===PAWN && Math.abs(rankOf(m.from)-rankOf(m.to))===2){
    nEp = (rankOf(m.from)+rankOf(m.to))/2*8+fileOf(m.from);
  }
  if(own){
    return { board:nb, castling:nCast, ep:nEp, turn:opp(c), halfmove:State.halfmove+1, fullmove:State.fullmove+1 };
  }
  // mutate real state
  b[m.to]=nb[m.to]; b[m.from]=0;
  castling=nCast;
  if(captured){ b[ep]=0; }
  ep=nEp;
  State.turn=opp(c);
  State.halfmove++; State.fullmove++;
  return State;
}

function rookSquare(c,backRank){
  return c===WHITE? sq(0,backRank): sq(7,backRank);
}

// update castling rights after a move
function updateCastling(castling, from, to, t){
  let n = castling;
  const backRank=rankOf(from);
  const isKing = t===KING;
  if(isKing){
    if(castling===WHITE){ clrb(0b1); clrb(0b10); }
    else { clrb(0b100); clrb(0b100|0b10); }
    // bK bit is 0b10(2), bQ is 0b100(4)
  }
  // rook moved OR rook's home square vacated
  const f=fileOf(from);
  if(castling===WHITE){
    if(from===sq(0,0)||to===sq(0,0)) clrb(0b1);
    if(from===sq(7,0)||to===sq(7,0)) clrb(0b10);
  } else {
    if(from===sq(0,7)||to===sq(0,7)) clrb(0b100);
    if(from===sq(7,7)||to===sq(7,7)) clrb(0b100|0b10);
  }
  return n;
}
/* =========================================================================
   GAME LOGIC (applies a legal move to the real state, tracks history, AI turn)
   ========================================================================= */
const Game = {
  // apply a legal move from state to the live board
  applyMove(m){
    const from = m.from, to = m.to;
    const prev = {
      board: State.board.slice(),
      turn: State.turn,
      castling: State.castling,
      ep: State.ep,
      halfmove: State.halfmove,
      fullmove: State.fullmove
    };
    applyMoveTo(m, State.board, State.castling, State.ep, State.turn, true);
    State.moved.add(from);
    State.moved.add(to);
    State.history.push({ move:m, prev });
  },

  // undo everything back to the initial position
  newGame(){
    clearState();
  },

  // take back one ply (player or AI move)
  undo(){
    if(State.history.length===0) return;
    const h = State.history.pop();
    State.board = h.prev.board;
    State.turn = h.prev.turn;
    State.castling = h.prev.castling;
    State.ep = h.prev.ep;
    State.halfmove = h.prev.halfmove;
    State.fullmove = h.prev.fullmove;
    State.moved.clear();
  },

  // ---- game status ----
  gameOver(){
    const legal = legalMoves(State.turn);
    if(legal.length===0){
      if(inCheck(State.turn)) return { over:true, type:'checkmate', winner: opp(State.turn) };
      return { over:true, type:'stalemate' };
    }
    // insufficient material (excluding kings)
    if(insufficient()) return { over:true, type:'insufficient' };
    // 50-move rule
    if(State.halfmove>=100) return { over:true, type:'fifty' };
    // threefold handled in FEN check
    return { over:false };
  },

  // ---- AI ----
  runAI(){
    const depth = parseInt(document.getElementById('difficulty').value,10);
    const legal = legalMoves(State.turn);
    if(legal.length===0) return;
    const mv = searchBest(legal, depth);
    if(mv) this.applyMove(mv);
  }
};

function epSq(){ return State.ep; }
function insufficient(){
  const pieces=[];
  for(let i=0;i<64;i++){
    const b=State.board[i]; if(b===0) continue;
    const t=type(b);
    if(t===KING) continue;
    // K+B vs K or K+N vs K
    if(pieces.length===0){ pieces.push(t); continue; }
    const hasBishop=t===BISHOP;
    const hasKnight=t===KNIGHT;
    if(pieces[0]===BISHOP && (t===BISHOP||t===KNIGHT)) return true;
    if(pieces[0]===KNIGHT && (t===KNIGHT||t===BISHOP)) return true;
    return false;
  }
  return false;
}
/* =========================================================================
   EVALUATION
   Returns centipawns from WHITE's perspective (positive = good for White).
   Uses material + piece-square tables. Adjusts king table for endgame.
   ========================================================================= */
function evaluate(){
  let score=0;
  let wCount=0, bCount=0;
  for(let i=0;i<64;i++){
    const b=State.board[i];
    if(b===0) continue;
    const c=color(b);
    if(c===WHITE){ score+=EVAL[type(b)] + PST[type(b)][i]; wCount++; }
    else { score-=EVAL[type(b)] + PST[type(b)][63-i]; bCount++; }
  }
  // endgame king adjustment when few pieces remain
  const phase = (wCount+bCount<=8)?7:5;
  score += (isWhitePos(score)>0? PST_KING[phase][findKing(WHITE)] : 0);
  // (king PST already added for whites above; subtract blacks symmetrically)
  const bk=findKing(BLACK);
  score -= PST_KING[phase][63-bk];
  return score;
}
function isWhitePos(s){ return s>=0; }

/* =========================================================================
   AI SEARCH — minimax with alpha-beta pruning.
   Move ordering: captures first (improves pruning), then others.
   Depth chosen for ~<=1s per move on typical hardware.
   ========================================================================= */
/* =========================================================================
   AI SEARCH — minimax with alpha-beta pruning.
   Move ordering: captures first (improves pruning), then promotions, others.
   Depth chosen for ~<=1s per move on typical hardware.
   ========================================================================= */
function searchBest(legalMovesArr, depth){
  let best=null, bestScore=-Infinity;
  const ordered = orderMoves(legalMovesArr);
  let alpha=-Infinity, beta=Infinity;
  for(const m of ordered){
    const info = nodeSearch(ordered, depth-1, alpha, beta, m, 0);
    if(info && info.score>bestScore){
      bestScore=info.score; best=info.move;
      if(info.score>=beta) break; // beta cutoff
    }
  }
  return best;
}

// nodeSearch: search `moves` for the side to move (State.turn), going `depth`
// plies deep. Returns best move found + score.
function nodeSearch(moves, depth, alpha, beta, root, ply){
  if(depth===0){ return { score: evaluate() }; }
  // if opponent has no legal moves, we are in checkmate
  const oppMoves = legalMoves(opp(State.turn));
  if(oppMoves.length===0){
    return { score: inCheck(State.turn)? -200000-ply : 200000+ply };
  }
  let bestScore=-Infinity;
  for(const m of moves){
    const info = searchOne(oppMoves, depth-1, alpha, beta, m, ply+1);
    if(info && info.score>bestScore){
      bestScore=info.score;
      if(info.score>=beta) break; // beta cutoff
    }
    if(info && info.score>alpha) alpha=info.score;
  }
  return { score: bestScore };
}

// searchOne: apply move `root` for current side, search opponent's reply.
function searchOne(moves, depth, alpha, beta, root, ply){
  const m=root;
  const nb=State.board;
  const res=applyScratch(m,nb);
  State.board=res.board; State.castling=res.castling; State.ep=res.ep;
  const score=nodeSearch(moves, depth, alpha, beta, null, ply+1);
  State.board=nb; State.castling=State.castling; State.ep=State.ep;
  return score;
}

function applyScratch(m,b){
  const nb=new Uint8Array(b);
  nb[m.to]=0;
  // en passant capture
  if(type(m.piece)===PAWN && Math.abs(fileOf(m.from)-fileOf(m.to))===1 && empty(nb[m.to]) && m.to===State.ep){
    nb[epSq()]=0;
  }
  // promotion
  const promoType=type(m.piece)===PAWN?m.promotion||QUEEN:type(m.piece);
  nb[m.to]=m.color<<4|promoType;
  // castling rook movement
  if(type(m.piece)===KING && Math.abs(fileOf(m.from)-fileOf(m.to))===2){
    const br=rankOf(m.from);
    if(m.to===sq(6,br)){ nb[rookSquare(m.color,br)]=0; nb[sq(5,br)]=m.color<<4|ROOK; }
    else if(m.to===sq(2,br)){ nb[rookSquare(m.color,br)]=0; nb[sq(3,br)]=m.color<<4|ROOK; }
  }
  const nCast=updateCastling(State.castling,m.from,m.to,type(m.piece));
  let nEp=-1;
  if(type(m.piece)===PAWN && Math.abs(rankOf(m.from)-rankOf(m.to))===2){
    nEp=(rankOf(m.from)+rankOf(m.to))/2*8+fileOf(m.from);
  }
  return { board:nb, castling:nCast, ep:nEp };
}

// order moves: captures first (descending material), then promotions, others
function orderMoves(moves){
  const byMaterial={KNIGHT:320,BISHOP:330,ROOK:500,QUEEN:900,PAWN:100};
  return moves.slice().sort((a,b)=>{
    const am=a.to!==a.from? byMaterial[type(a.piece)] : (type(a.piece)===PAWN?10:0);
    const bm=b.to!==b.from? byMaterial[type(b.piece)] : (type(b.piece)===PAWN?10:0);
    return bm-am;
  });
}
/* =========================================================================
   FEN SERIALIZATION
   ========================================================================= */
function toFen(){
  let rows='';
  for(let r=0;r<8;r++){
    let empty=0;
    for(let f=0;f<8;f++){
      const b=State.board[r*8+f];
      if(b===0) empty++; else { if(empty) { rows+=empty; empty=0; } rows+=GLYPHS[b>>4]; }
    }
    if(empty) rows+=empty;
    rows+=' /';
  }
  rows=rows.slice(0,-2);
  const legal=legalMoves(State.turn);
  let cr='KQkq';
  if(State.castling&0b1) cr+='K'; if(State.castling&0b10) cr+='Q';
  if(State.castling&0b100) cr+='k'; if(State.castling&0b110) cr+='q';
  if(cr==='KQkq') cr='-';
  let ep='-'; if(State.ep>=0) ep=FILES[fileOf(State.ep)]+(8-rankOf(State.ep));
  return `${rows} ${State.turn===WHITE?'w':'b'} ${cr} ${ep} ${State.halfmove} ${State.fullmove}`;
}
function fromFen(fen){
  const parts=fen.split(' ');
  const boardArr=parts[0].split(' ').map(row=>{
    const u=new Uint8Array(8); let f=0;
    for(const ch of row){ if(ch==='/') continue; if(ch>= '1'&&ch<='8') f+=+ch; else f=u[f++]=GLYPH_INDEX[ch]; }
    return u;
  });
  const board=new Uint8Array(64);
  for(let r=0;r<8;r++) for(let f=0;f<8;f++) board[r*8+f]=boardArr[r][f];
  State.board=board;
  State.turn=parts[1]==='b'?BLACK:WHITE;
  State.castling=parseCastling(parts[2]);
  State.ep=parts[3]==='-'?-1:epSquare(parts[3]);
  State.halfmove=+parts[4];
  State.fullmove=+parts[5];
  State.history.length=0;
  State.moved.clear();
}
function parseCastling(s){
  let n=0;
  if(s.includes('K')) n|=0b1; if(s.includes('Q')) n|=0b10;
  if(s.includes('k')) n|=0b100; if(s.includes('q')) n|=0b110;
  return n;
}
function glyphToType(g){
  const idx=TYPES.indexOf(g);
  if(idx>=0) return TYPES[idx];
  return 0;
}
function epSquare(s){
  const f=FILES.indexOf(s[0]), r=8-(+s[1]);
  return sq(f,r);
}

/* =========================================================================
   DEBUG INTERFACE (exposed on window)
   game.fen(), game.move('e2e4'), game.legalMoves()
   ========================================================================= */
window.game = {
  fen: toFen,
  legalMoves: ()=>legalMoves(State.turn),
  move: function(san){
    const m = parseSan(san);
    if(!m){ console.error('illegal/unknown move:', san); return false; }
    Game.applyMove(m);
    onGameChange();
    return true;
  },
  _board: State.board,
  _turn: State.turn,
  _castling: State.castling,
  _ep: State.ep,
  _halfmove: State.halfmove,
  _fullmove: State.fullmove,
  reset: ()=>Game.newGame()
};

/* =========================================================================
   SAN NOTATION
   ========================================================================= */
function moveToSan(m, prevBoard){
  const t=type(m.piece), c=State.turn;
  const file=FILES[fileOf(m.to)], rank=8-rankOf(m.to);
  let san='';
  if(t===KING) san='K';
  else if(t===KNIGHT) san='N';
  else if(t===PAWN){ san=FILES[fileOf(m.from)]; }
  else san=t.toUpperCase();
  // capture
  const isCapture = prevBoard[m.to]!==0 || (t===PAWN && m.to===State.ep);
  if(isCapture){
    san+='x';
    if(t!==PAWN && t!==KING){
      const f2=FILES[fileOf(m.from)];
      if(f2!==file) san+=f2;
    }
  }
  // disambiguation for non-king pieces on capture or when needed
  if(t!==KING && t!==PAWN && !isCapture){
    // check if other same-type pieces could move here
    const others=legalMoves(c).filter(o=>type(o.piece)===t && o.to===m.to && !(o.from===m.from));
    if(others.length>1){
      const sameFile=others.some(o=>fileOf(o.from)===fileOf(m.from));
      if(!sameFile) san+=FILES[fileOf(m.from)];
      else if(rankOf(others[0].from)!==rankOf(m.from)) san+=8-rankOf(m.from);
    }
  }
  // promotion
  if(m.promotion!==null) san+='='+GLYPHS[m.promotion];
  // check / mate
  const after=applyMoveTo(m, prevBoard, State.castling, State.ep, c, true);
  const opp=opp(c);
  if(t===KING && Math.abs(fileOf(m.from)-fileOf(m.to))===2){
    san = m.to===sq(6,rankOf(m.from))?'O-O':'O-O-O';
    if(inCheck(opp,after)) san+='#';
    return san;
  }
  if(legalMoves(opp).length===0){
    san+='#';
  } else if(inCheck(opp,after)){
    san+='+';
  }
  // en passant
  if(t===PAWN && m.to===State.ep) san+=' e.p.';
  return san;
}
function parseSan(san){
  // try each legal move and match its SAN
  const moves=legalMoves(State.turn);
  for(const m of moves){
    const prev=State.board.slice();
    const s=moveToSan(m, prev);
    if(s===san) return m;
  }
  return null;
}

/* =========================================================================
   SOUND EFFECTS (Web Audio)
   ========================================================================= */
const Sound = {
  ctx:null, muted:false,
  init(){ if(!this.ctx){ try{ this.ctx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } },
  tone(freq,dur,type='sine',vol=0.15){
    if(this.muted||!this.ctx) return;
    const o=this.ctx.createOscillator(), g=this.ctx.createGain();
    o.type=type; o.frequency.value=freq;
    g.gain.value=vol;
    o.connect(g); g.connect(this.ctx.destination);
    o.start();
    g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime+dur);
    o.stop(this.ctx.currentTime+dur);
  },
  move(){ this.tone(320,0.06,'triangle'); },
  capture(){ this.tone(180,0.12,'square',0.12); },
  check(){ this.tone(440,0.18,'sawtooth',0.1); },
  promo(){ this.tone(523,0.1,'triangle'); setTimeout(()=>this.tone(784,0.15,'triangle'),120); },
  mate(){ this.tone(300,0.4,'sawtooth',0.14); setTimeout(()=>this.tone(200,0.5,'sawtooth',0.14),250); }
};
/* =========================================================================
   RENDERING
   ========================================================================= */
const boardEl = document.getElementById('board');
const RANK_LABELS = ['8','7','6','5','4','3','2','1'];
const FILE_LABELS = ['a','b','c','d','e','f','g','h'];

let orientation = 'normal'; // 'normal' (white bottom) or 'flipped'
let selected = null;        // selected square
let legalForSelected = [];  // legal moves from selected square
let gameOverFlag = false;
let moveList = [];          // parsed move history for display
let capturedByWhite = [];   // black pieces captured by white
let capturedByBlack = [];   // white pieces captured by black
let lastMoveFrom = -1, lastMoveTo = -1;

function renderBoard(){
  boardEl.innerHTML='';
  const rows = orientation==='normal' ? [0,1,2,3,4,5,6,7] : [7,6,5,4,3,2,1,0];
  const cols = orientation==='normal' ? [0,1,2,3,4,5,6,7] : [7,6,5,4,3,2,1,0];
  for(const r of rows){
    for(const f of cols){
      const s = r*8+f;
      const sq=document.createElement('div');
      sq.className='sq '+ ( (r+f)%2===0 ? 'light':'dark' );
      sq.dataset.sq=s;
      sq.addEventListener('click', ()=>onSquareClick(s));
      sq.addEventListener('mousedown', e=>onMouseDown(e,s));
      sq.addEventListener('mouseup', e=>onMouseUp(e,s));
      sq.addEventListener('dragover', e=>e.preventDefault());
      sq.addEventListener('drop', e=>{ e.preventDefault(); onDrop(e,s); });
      boardEl.appendChild(sq);
      boardEl.children[s]._sq=s;
    }
  }
}

// re-render board visuals from state
function paintBoard(){
  const b=State.board;
  const legal=legalMoves(State.turn);
  const moveSet = new Set(legal.map(m=>m.to));
  // last move highlight
  let lastFrom=-1,lastTo=-1;
  if(State.history.length){ lastFrom=State.history[State.history.length-1].move.from; lastTo=State.history[State.history.length-1].move.to; }
  const children=boardEl.children;
  for(let s=0;s<64;s++){
    const el=children[s]; if(!el) continue;
    const isLast = (s===lastFrom||s===lastTo);
    el.classList.toggle('last', isLast);
    el.classList.toggle('selected', s===selected);
    const m=legal.find(x=>x.from===s);
    el.classList.toggle('move', !!m && !isCapture(m));
    el.classList.toggle('capture', !!m && isCapture(m));
    const piece=b[s];
    const glyphEl=el.querySelector('.piece');
    if(piece){
      if(!glyphEl){ const g=document.createElement('div'); g.className='piece '+(isWhite(piece)?'piece-white':'piece-black'); el.appendChild(g); }
      glyphEl.textContent=GLYPHS[piece>>4];
      glyphEl.className='piece '+(isWhite(piece)?'piece-white':'piece-black');
    } else if(glyphEl){ glyphEl.remove(); }
    // check glow
    if(type(piece)===KING && inCheck(color(piece))){ el.classList.add('check'); } else el.classList.remove('check');
  }
}

function isCapture(m){ return b(State.board)[m.to]!==0 || (type(m.piece)===PAWN && m.to===State.ep); }
function b(arr){ return arr; }

function updateTurnIndicator(){
  const el=document.getElementById('turn-indicator');
  el.textContent = State.turn===WHITE?'White to move':'Black to move';
}

// ---- move list ----
function renderMoveList(){
  const list=document.getElementById('move-list');
  list.innerHTML='';
  moveList.forEach((mv,i)=>{
    const num=Math.floor(i/2)+1;
    const pair=[i*2+1,i*2+2];
    const isCur = pair.includes(i);
    const isDone = gameOverFlag || pair.every(p=>p>=moveList.length);
    if(isCur && !isDone) list.children[pair[0]]?.classList.add('hl');
    else list.children[pair[0]]?.classList.remove('hl');
    const n=document.createElement('span'); n.className='mv'; n.innerHTML=`<span class="n">${num}.</span> `;
    list.appendChild(n);
    const cells=[mv.white||'', mv.black||''];
    cells.forEach(txt=>{ if(txt){ const s=document.createElement('span'); s.className='mv'; s.textContent=txt; list.appendChild(s);} });
  });
}

// ---- captured pieces ----
function renderCaptured(){
  document.getElementById('captured-black').textContent = capturedByWhite.map(g=>GLYPHS[g]).join('');
  document.getElementById('captured-white').textContent = capturedByBlack.map(g=>GLYPHS[g]).join('');
}

function trackCaptures(m){
  const b=State.board;
  const prev=State.history[State.history.length-1].prev.board;
  const t=type(m.piece);
  if(t===PAWN && m.to===State.ep){
    // captured pawn is on ep square of the PREVIOUS state
    const pb=prev;
    if(pb[m.ep]!==0){ const c=color(pb[m.ep]); const cap=(c===WHITE?capturedByWhite:capturedByBlack); // black captured by white => white tray
      // pawn captured by white goes to white tray
      if(c===BLACK) capturedByWhite.push(pb[m.ep]>>4);
      else capturedByBlack.push(pb[m.ep]>>4);
    }
  } else {
    const cap=b[m.to];
    if(cap!==0){
      if(color(cap)===BLACK) capturedByWhite.push(cap>>4);
      else capturedByBlack.push(cap>>4);
    }
  }
}

// ---- banner / game over ----
function updateBanner(){
  const st=Game.gameOver();
  const banner=document.getElementById('banner');
  if(st.over){
    gameOverFlag=true;
    let msg;
    if(st.type==='checkmate'){
      msg = `Checkmate! ${st.winner===WHITE?'White':'Black'} wins.`;
      Sound.mate();
    } else if(st.type==='stalemate'){ msg='Stalemate — draw.'; }
    else if(st.type==='insufficient'){ msg='Draw — insufficient material.'; }
    else if(st.type==='fifty'){ msg='Draw — 50-move rule.'; }
    banner.textContent=msg;
    banner.style.display='block';
  } else {
    banner.style.display='none';
  }
}

/* =========================================================================
   GAME LOOP / ORCHESTRATION
   ========================================================================= */
function onGameChange(){
  updateTurnIndicator();
  paintBoard();
  renderMoveList();
  renderCaptured();
  updateBanner();
  saveGame();
}

function playAIMove(){
  const st=Game.gameOver();
  if(st.over) return;
  if(State.turn!==BLACK) return;
  const legal=legalMoves(State.turn);
  if(legal.length===0) return;
  // run AI in chunks to avoid freezing
  let depth=parseInt(document.getElementById('difficulty').value,10);
  const mv=searchBest(legal, depth);
  if(mv){
    Game.applyMove(mv);
    onGameChange();
  }
}

// ---- persistence ----
function saveGame(){
  try{
    const data={
      fen:toFen(),
      history:moveList,
      capturedByWhite, capturedByBlack,
      orientation, selected, lastMoveFrom, lastMoveTo,
      gameOverFlag
    };
    localStorage.setItem('chessgame', JSON.stringify(data));
  }catch(e){}
}
function loadGame(){
  try{
    const raw=localStorage.getItem('chessgame');
    if(!raw) return false;
    const data=JSON.parse(raw);
    fromFen(data.fen);
    moveList=data.history||[];
    capturedByWhite=data.capturedByWhite||[];
    capturedByBlack=data.capturedByBlack||[];
    orientation=data.orientation||'normal';
    lastMoveFrom=data.lastMoveFrom||-1;
    lastMoveTo=data.lastMoveTo||-1;
    gameOverFlag=data.gameOverFlag||false;
    return true;
  }catch(e){ return false; }
}
/* =========================================================================
   INTERACTION
   ========================================================================= */
function onSquareClick(s){
  if(gameOverFlag) return;
  if(State.turn!==WHITE) return;
  const piece=State.board[s];
  if(selected===null){
    if(piece && color(piece)===WHITE){
      selected=s;
      legalForSelected=legalMoves(WHITE).filter(m=>m.from===s);
      paintBoard();
    }
    return;
  }
  // clicking same square deselects
  if(selected===s){ selected=null; legalForSelected=[]; paintBoard(); return; }
  // find matching move
  const m=legalForSelected.find(x=>x.to===s);
  if(m){ doPlayerMove(m); return; }
  // click elsewhere: reselect if it's one of our pieces
  if(piece && color(piece)===WHITE){
    selected=s; legalForSelected=legalMoves(WHITE).filter(x=>x.from===s);
    paintBoard();
  } else {
    selected=null; legalForSelected=[]; paintBoard();
  }
}

function onDragStart(e,s){
  if(gameOverFlag||State.turn!==WHITE) return;
  if(State.board[s] && color(State.board[s])===WHITE){
    selected=s; legalForSelected=legalMoves(WHITE).filter(x=>x.from===s);
    paintBoard();
    e.dataTransfer.effectAllowed='move';
    try{ e.dataTransfer.setData('text/plain',''); }catch(_){}
  }
}
function onDrop(e,s){
  e.preventDefault();
  if(gameOverFlag||State.turn!==WHITE) return;
  if(selected!==null){
    const m=legalForSelected.find(x=>x.to===s);
    if(m) doPlayerMove(m);
  }
  selected=null; legalForSelected=[]; paintBoard();
}
function onMouseUp(e,s){ /* nothing extra */ }

function doPlayerMove(m){
  // promotion picker for pawn promotions
  if(type(m.piece)===PAWN && (rankOf(m.to)===0 || rankOf(m.to)===7)){
    showPromotion(m);
    return;
  }
  executeMove(m);
}

function executeMove(m){
  const prev=State.history[State.history.length-1].prev.board;
  // track captured pieces
  const cap=b(State.board)[m.to];
  if(cap!==0){
    if(color(cap)===BLACK) capturedByWhite.push(cap>>4);
    else capturedByBlack.push(cap>>4);
  } else if(type(m.piece)===PAWN && m.to===State.ep){
    // en passant: captured pawn on ep square of previous board
    const epPiece=prev[m.ep];
    if(epPiece!==0){
      if(color(epPiece)===BLACK) capturedByWhite.push(epPiece>>4);
      else capturedByBlack.push(epPiece>>4);
    }
  }
  Game.applyMove(m);
  Sound.move();
  if(isCapture(m)) Sound.capture();
  const after=State.board;
  if(inCheck(opp(State.turn),after) && legalMoves(opp(State.turn)).length===0){
    Sound.mate();
  } else if(inCheck(opp(State.turn),after)){
    Sound.check();
  }
  // build SAN
  const san=moveToSan(m, prev);
  addMoveToList(san);
  onGameChange();
  // AI responds
  if(!Game.gameOver().over && State.turn===BLACK){
    setTimeout(playAIMove, 60);
  }
}

function addMoveToList(san){
  moveList.push({ n:Math.floor(moveList.length/2)+1, white:san, black:'' });
  renderMoveList();
}

/* =========================================================================
   PROMOTION PICKER
   ========================================================================= */
function showPromotion(m){
  const overlay=document.getElementById('promo-overlay');
  const box=document.getElementById('promo-box');
  box.innerHTML='';
  for(const pt of [QUEEN,ROOK,BISHOP,KNIGHT]){
    const b=document.createElement('div');
    b.className='promot '+(State.turn===WHITE?'piece-white':'piece-black');
    b.textContent=GLYPHS[pt];
    b.addEventListener('click', ()=>{
      m.promotion=pt;
      overlay.classList.remove('show');
      executeMove(m);
    });
    box.appendChild(b);
  }
  overlay.classList.add('show');
  Sound.promo();
}

/* =========================================================================
   CONTROLS
   ========================================================================= */
document.getElementById('btn-flip').addEventListener('click', ()=>{
  orientation = orientation==='normal'?'flipped':'normal';
  selected=null; legalForSelected=[];
  renderBoard(); onGameChange();
});
document.getElementById('btn-undo').addEventListener('click', ()=>{
  Game.undo();
  moveList=moveList.filter(mv=>!(mv.black && State.history.length===0));
  // reset captured arrays to match
  capturedByWhite=[]; capturedByBlack=[];
  selected=null; legalForSelected=[]; gameOverFlag=false;
  renderBoard(); onGameChange();
});
document.getElementById('btn-new').addEventListener('click', ()=>{
  Game.newGame();
  moveList=[]; capturedByWhite=[]; capturedByBlack=[];
  orientation='normal'; selected=null; legalForSelected=[]; gameOverFlag=false;
  renderBoard(); onGameChange();
});
document.getElementById('mute').addEventListener('change', e=>{
  Sound.muted=e.target.checked;
});

/* =========================================================================
   INITIALIZATION
   ========================================================================= */
renderBoard();
loadGame();
if(!State.history.length){
  clearState();
  onGameChange();
}
// ensure AI move if loaded position has black to move
if(!gameOverFlag && State.turn===BLACK){
  setTimeout(playAIMove, 100);
}


</script>
</body>
</html>
<!-- agent-meta {"model":"ornith-ai/Ornith-1.5-9B-GGUF","provider":"brigid","persona":"full","sessionId":"67814691-24a0-4657-ae2f-c24361f303b3","tokensIn":7893483,"tokensOut":67247,"tokensTotal":7960730,"cost":0,"turns":121,"toolCalls":120,"failedToolCalls":9,"timestamp":"2026-08-24T03:51:07.744Z"} -->