← Chess results

Chess

Ornith 1.5 35B A3B · 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,827 bytes · SHA-256 9b933786cf0c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess vs AI</title>
<style>
:root{
  --bg:#1e1e24; --panel:#2a2a32; --panel2:#33333d;
  --wood-light:#e8d0a8; --wood-dark:#a9773f;
  --highlight:rgba(255,241,132,0.45);
  --check:rgba(231,56,56,0.75);
  --move:rgba(20,20,20,0.35);
  --capture:rgba(231,56,56,0.55);
  --sel:#f6f66b;
  --text:#ececec; --muted:#9a9aa6;
  --accent:#d9a441;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);
  font-family:"Segoe UI",system-ui,-apple-system,sans-serif;}
header{display:flex;align-items:center;gap:14px;padding:12px 20px;
  background:linear-gradient(180deg,#33333d,#26262d);border-bottom:1px solid #000;
  flex-wrap:wrap;}
header h1{font-size:20px;margin:0;font-weight:600;letter-spacing:.5px;}
header h1 .w{color:var(--text);} header h1 .b{color:var(--accent);}
.layout{display:flex;gap:22px;justify-content:center;align-items:flex-start;
  padding:22px;flex-wrap:wrap;}

/* Board */
.board-wrap{position:relative;}
#board{display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);
  width:min(70vmin,520px);height:min(70vmin,520px);
  border:6px solid #5a3d1a;border-radius:6px;box-shadow:0 10px 30px rgba(0,0,0,.5);
  --sq:calc(min(70vmin,520px)/8);}
.cell{position:relative;display:flex;align-items:center;justify-content:center;
  font-size:calc(min(70vmin,520px)/8*0.78);cursor:pointer;user-select:none;
  line-height:1;}
.cell.light{background:var(--wood-light);}
.cell.dark{background:var(--wood-dark);}
.cell.sel{box-shadow:inset 0 0 0 4px var(--sel);background:var(--highlight)!important;}
.cell.lastmove::after{content:"";position:absolute;inset:0;background:var(--highlight);
  pointer-events:none;}
.cell.check{box-shadow:inset 0 0 0 4px var(--check),inset 0 0 18px 4px var(--check);}
.cell .piece{position:relative;z-index:2;transition:transform .05s;
  text-shadow:0 1px 2px rgba(0,0,0,.35);}
.cell .piece.white{color:#fff;border:1px solid #333;border-radius:50%;
  padding:0 1px;box-shadow:0 1px 2px rgba(0,0,0,.4);}
.cell .piece.black{color:#1b1b1b;}
.cell .dot{position:absolute;width:28%;height:28%;border-radius:50%;
  background:var(--move);z-index:1;}
.cell .ring{position:absolute;inset:6%;border:5px solid var(--capture);border-radius:50%;
  z-index:1;pointer-events:none;}
.coord{position:absolute;font-size:11px;font-weight:600;opacity:.8;z-index:3;}
.coord.file{bottom:2px;right:3px;}
.coord.rank{top:2px;left:3px;}
.cell.dragging .piece{opacity:.4;}
#dragGhost{position:fixed;pointer-events:none;z-index:999;font-size:64px;
  text-shadow:0 2px 6px rgba(0,0,0,.5);transform:translate(-50%,-50%);display:none;}

/* Panel */
.panel{width:300px;background:var(--panel);border-radius:10px;padding:16px;
  box-shadow:0 8px 24px rgba(0,0,0,.4);display:flex;flex-direction:column;gap:12px;
  max-height:70vmin;min-height:520px;}
.panel h2{font-size:14px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);
  margin:0 0 4px;}
.turn{display:flex;align-items:center;gap:10px;font-weight:600;padding:8px 12px;
  background:var(--panel2);border-radius:8px;}
.turn .dot2{width:16px;height:16px;border-radius:50%;border:1px solid #000;}
.turn .dot2.white{background:#fff;}
.turn .dot2.black{background:#222;}
.controls{display:flex;flex-direction:column;gap:8px;}
.controls .row{display:flex;gap:8px;flex-wrap:wrap;}
button,select{font-family:inherit;font-size:13px;padding:8px 12px;border-radius:7px;
  border:1px solid #444;background:var(--panel2);color:var(--text);cursor:pointer;
  transition:background .15s,border .15s;}
button:hover,select:hover{background:#3d3d48;}
button.primary{background:var(--accent);color:#20180a;border-color:#b9862f;font-weight:600;}
button.primary:hover{background:#e8b454;}
button:disabled{opacity:.5;cursor:not-allowed;}
.captured{display:flex;flex-wrap:wrap;gap:2px;font-size:22px;min-height:30px;align-items:center;}
.captured .lead{font-size:11px;color:var(--muted);width:100%;}
.moves{background:var(--panel2);border-radius:8px;padding:8px;flex:1;overflow:auto;
  font-family:"SF Mono",Consolas,monospace;font-size:13px;min-height:120px;}
.moves .mv-row{display:flex;gap:6px;padding:2px 4px;border-radius:5px;}
.moves .mv-row:nth-child(odd){background:rgba(255,255,255,.03);}
.moves .mv-num{color:var(--muted);width:26px;}
.moves .mv{padding:0 6px;cursor:default;}
.moves .mv.current{background:var(--accent);color:#20180a;border-radius:4px;}

/* Overlay */
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;
  align-items:center;justify-content:center;z-index:50;}
.overlay.show{display:flex;}
.modal{background:var(--panel);border-radius:12px;padding:20px;box-shadow:0 12px 40px #000;
  text-align:center;}
.modal h3{margin:0 0 12px;}
.promo-opts{display:flex;gap:10px;}
.promo-opts button{font-size:34px;padding:10px 16px;width:64px;height:64px;
  background:var(--wood-light);color:#222;border:2px solid #777;border-radius:10px;}
.promo-opts button:hover{background:var(--sel);border-color:#333;}
.banner{position:fixed;left:50%;top:18px;transform:translateX(-50%);
  background:linear-gradient(180deg,#3a3a44,#2a2a32);border:1px solid #000;
  padding:12px 22px;border-radius:10px;font-size:16px;font-weight:600;
  box-shadow:0 6px 20px #000;display:none;z-index:60;}
.banner.show{display:block;}
.banner small{display:block;font-weight:400;color:var(--muted);font-size:12px;margin-top:3px;}
.hint{font-size:12px;color:var(--muted);}
.status-ok{color:#7ec87e;} .status-err{color:#e73838;}
</style>
</head>
<body>
<header>
  <h1><span class="w">♔</span> <span class="w">Chess</span> <span class="b">vs AI</span></h1>
  <span class="hint" id="statusLine"></span>
</header>

<div class="layout">
  <div class="board-wrap">
    <div id="board"></div>
  </div>
  <div class="panel">
    <div class="turn"><span class="dot2 white" id="turnDot"></span><span id="turnText">White to move</span></div>
    <div>
      <h2>Captured by White</h2>
      <div class="captured" id="capWhite"></div>
    </div>
    <div>
      <h2>Captured by Black</h2>
      <div class="captured" id="capBlack"></div>
    </div>
    <div>
      <h2>Moves</h2>
      <div class="moves" id="moves"></div>
    </div>
    <div class="controls">
      <div class="row">
        <label class="hint" style="align-self:center;">Play as:</label>
        <select id="sideSel">
          <option value="white">White</option>
          <option value="black">Black</option>
          <option value="both">Both (self-play)</option>
          <option value="analyze">Analysis</option>
        </select>
      </div>
      <div class="row">
        <label class="hint" style="align-self:center;">AI depth:</label>
        <select id="depthSel">
          <option value="1">Easy (1)</option>
          <option value="2" selected>Medium (2)</option>
          <option value="3">Hard (3)</option>
        </select>
      </div>
      <div class="row">
        <button id="undoBtn">Undo</button>
        <button id="flipBtn">Flip</button>
        <button id="muteBtn">🔊</button>
      </div>
      <div class="row">
        <button id="newBtn" class="primary">New Game</button>
      </div>
    </div>
  </div>
</div>

<div id="dragGhost"></div>
<div class="overlay" id="promoOverlay"><div class="modal">
  <h3>Promote pawn to:</h3><div class="promo-opts" id="promoOpts"></div>
</div></div>
<div class="banner" id="banner"><span id="bannerText"></span><small id="bannerSub"></small></div>

<script>
"use strict";

/* ============================================================
   TUNABLE CONSTANTS
   ============================================================ */
const CFG = {
  colors: {
    light: "#e8d0a8", dark: "#a9773f", sel: "#f6f66b",
    lastmove: "rgba(255,241,132,0.45)", check: "rgba(231,56,56,0.75)"
  },
  ai: {
    depth: 2,            // 1/2/3
    whiteSide: true,     // AI plays white by default
    maxTimePerMove: 1000 // ms budget (soft)
  },
  eval: {
    pawn:100, knight:320, bishop:330, rook:500, queen:900, king:20000,
    // piece-square tables (from White's perspective, rank 8..1 ordering = a8..h1)
    pstPawn:[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],
    pstKnight:[-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],
    pstBishop:[-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, -10,0,0,0,0,0,0,-10, -20,-10,-10,-10,-10,-10,-10,-20],
    pstRook:[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],
    pstQueen:[-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,0,5,5,5,5,0,-10, -10,0,0,0,0,0,0,-10, -20,-10,-10,-5,-5,-10,-10,-20],
    pstKingMid:[-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]
  },
  saveKey: "chess.save.v1"
};

/* ============================================================
   CHESS ENGINE — board representation: 64-element array (mailbox)
   Index = rank*8 + file, rank 0 = rank 8 (top), file 0 = file a (left)
   Piece = single char: 'P','N','B','R','Q','K' (white) / lowercase (black), '' empty
   ============================================================ */
const FILE_A=0, FILE_H=7, RANK1=0, RANK8=7;
const WHITE='w', BLACK='b';

const PIECE_TYPES = {P:'pawn',N:'knight',B:'bishop',R:'rook',Q:'queen',K:'king'};
const PIECE_SHORT = {pawn:'P',knight:'N',bishop:'B',rook:'R',queen:'Q',king:'K'};
const PST = {
  P: CFG.eval.pstPawn, N: CFG.eval.pstKnight, B: CFG.eval.pstBishop,
  R: CFG.eval.pstRook, Q: CFG.eval.pstQueen, K: CFG.eval.pstKingMid
};

function pieceColor(p){ return p==='' ? null : (p===p.toUpperCase() ? WHITE : BLACK); }
function opp(c){ return c===WHITE ? BLACK : WHITE; }
function inBounds(f,r){ return f>=0&&f<=7&&r>=0&&r<=7; }
const idx=(f,r)=>r*8+f;
const algebraic=(f,r)=>"abcdefgh"[f]+(r+1);

class Chess {
  constructor(state){
    this.reset(state);
  }
  reset(opts){
    opts = opts||{};
    this.board = opts.board ? opts.board.slice() : this.fenToBoard("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1");
    this.turn = opts.turn||WHITE;
    this.castle = opts.castle||{wK:true,wQ:true,bK:true,bQ:true};
    this.ep = opts.ep||null; // {f,r} en passant target square or null
    this.halfmove = opts.halfmove||0;
    this.fullmove = opts.fullmove||1;
    this.history = opts.history||[]; // stack of move records
  }

  // --- FEN / board helpers ---
  fenToBoard(fen){
    const rows = fen.split(' ')[0].split('/');
    const b = new Array(64).fill('');
    for(let r=0;r<8;r++){
      const rowStr = rows[7-r]; // FEN rank 1 is last element (rows[7])
      let f=0;
      for(const ch of rowStr){
        if(/\d/.test(ch)){ f += parseInt(ch,10); }
        else { b[idx(f,r)] = ch; f++; }
      }
    }
    return b;
  }

  pieceAt(f,r){ return inBounds(f,r) ? this.board[idx(f,r)] : undefined; }
  findKing(color){ for(let i=0;i<64;i++){ if(this.board[i]===color+'K') return i; } return -1; }
  kingPos(color){ const i=this.findKing(color); return i<0?null:{f:i%8,r:Math.floor(i/8)}; }

  // --- Attack detection ---
  // Is square (f,r) attacked by `by`?
  isSquareAttacked(f,r,by){
    // Pawn attacks
    const dir = by===WHITE ? -1 : 1; // attacking pawn moves in `dir`; attack comes from r+dir
    for(const df of [-1,1]){
      const pf=f+df, pr=r+dir;
      if(inBounds(pf,pr)){ const p=this.pieceAt(pf,pr); if(p===by+'P') return true; }
    }
    // Knight
    const kmts=[[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
    for(const [df,dr] of kmts){ const nf=f+df,nr=r+dr; if(inBounds(nf,nr)){const p=this.pieceAt(nf,nr); if(p===by+'N') 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(inBounds(nf,nr)){const p=this.pieceAt(nf,nr); if(p===by+'K') return true;}
    }
    // Sliding: bishop/queen (diagonals)
    const diag=[[-1,-1],[-1,1],[1,-1],[1,1]];
    for(const [df,dr] of diag){
      let nf=f+df,nr=r+dr;
      while(inBounds(nf,nr)){ const p=this.pieceAt(nf,nr); if(p!==''){ if(p===by+'B'||p===by+'Q') return true; break;} nf+=df; nr+=dr; }
    }
    // Sliding: rook/queen (orthogonals)
    const orth=[[-1,0],[1,0],[0,-1],[0,1]];
    for(const [df,dr] of orth){
      let nf=f+df,nr=r+dr;
      while(inBounds(nf,nr)){ const p=this.pieceAt(nf,nr); if(p!==''){ if(p===by+'R'||p===by+'Q') return true; break;} nf+=df; nr+=dr; }
    }
    return false;
  }

  inCheck(color){
    const k=this.kingPos(color); if(!k) return false;
    return this.isSquareAttacked(k.f,k.r,opp(color));
  }

  // --- Legal move generation ---
  // Returns array of moves: {from,to,piece,captured,ep,castle,promo,pieceType,...}
  legalMoves(color){
    color = color||this.turn;
    const moves=[];
    for(let i=0;i<64;i++){
      const p=this.board[i]; if(p==='') continue;
      if(pieceColor(p)!==color) continue;
      const f=i%8,r=Math.floor(i/8);
      const pseudo=this.pseudoMovesFrom(f,r);
      for(const m of pseudo){
        if(this.isMoveLegal(m)) moves.push(m);
      }
    }
    return moves;
  }

  // Pseudo-legal moves from a square (no self-check filtering)
  pseudoMovesFrom(f,r){
    const p=this.board[idx(f,r)]; if(p==='') return [];
    const color=pieceColor(p); const type=PIECE_TYPES[p.toUpperCase()];
    const moves=[];
    const enemy=opp(color);
    const add=(tf,tr,extra)=>{
      moves.push(Object.assign({from:{f,r},to:{f:tf,r:tr},piece:p,pieceType:type,
        pieceColor:color, captured:'', ep:false, castle:'', promo:null}, extra||{}));
    };
    if(type==='pawn'){
      const dir=color===WHITE?1:-1;
      const startR = color===WHITE?1:6;
      const promoR = color===WHITE?7:0;
      const oneR=r+dir;
      // forward one
      if(inBounds(f,oneR)&&this.pieceAt(f,oneR)===''){
        if(oneR===promoR) add(f,oneR,{promo:true});
        else add(f,oneR);
        // forward two
        if(r===startR){ const twoR=r+2*dir; if(this.pieceAt(f,twoR)==='') add(f,twoR,{double:true}); }
      }
      // captures
      for(const df of [-1,1]){
        const nf=f+df, nr=r+dir;
        if(!inBounds(nf,nr)) continue;
        const t=this.pieceAt(nf,nr);
        if(t&&pieceColor(t)===enemy){
          if(nr===promoR) add(nf,nr,{promo:true, captured:t});
          else add(nf,nr,{captured:t});
        }
        // en passant
        if(this.ep && this.ep.f===nf && this.ep.r===nr){
          add(nf,nr,{ep:true, captured:(color===WHITE?'p':'P')});
        }
      }
    } else if(type==='knight'){
      const kmts=[[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
      for(const [df,dr] of kmts){ const nf=f+df,nr=r+dr; if(!inBounds(nf,nr))continue; const t=this.pieceAt(nf,nr); if(t===''){add(nf,nr);} else if(pieceColor(t)===enemy){add(nf,nr,{captured:t});} }
    } else if(type==='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(!inBounds(nf,nr))continue;
        const t=this.pieceAt(nf,nr); if(t===''){add(nf,nr);} else if(pieceColor(t)===enemy){add(nf,nr,{captured:t});}
      }
      // Castling
      const homeR = color===WHITE?0:7;
      if(r===homeR && !this.inCheck(color)){
        const rookKR = this.pieceAt(5,homeR), rookQR = this.pieceAt(3,homeR);
        const canK = color===WHITE?this.castle.wK:this.castle.bK;
        const canQ = color===WHITE?this.castle.wQ:this.castle.bQ;
        if(canK && rookKR==='R'+color && !this.isSquareAttacked(4,homeR,enemy)
            && !this.isSquareAttacked(5,homeR,enemy) && !this.isSquareAttacked(6,homeR,enemy)
            && this.pieceAt(5,homeR)==='' && this.pieceAt(6,homeR)===''){
          add(6,homeR,{castle:'K'});
        }
        if(canQ && rookQR==='R'+color && !this.isSquareAttacked(4,homeR,enemy)
            && !this.isSquareAttacked(3,homeR,enemy) && !this.isSquareAttacked(2,homeR,enemy)
            && this.pieceAt(3,homeR)==='' && this.pieceAt(2,homeR)==='' && this.pieceAt(1,homeR)===''){
          add(2,homeR,{castle:'Q'});
        }
      }
    } else {
      // sliding
      let dirs=[];
      if(type==='bishop'||type==='queen') dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
      if(type==='rook'||type==='queen') dirs.push([-1,0],[1,0],[0,-1],[0,1]);
      for(const [df,dr] of dirs){
        let nf=f+df,nr=r+dr;
        while(inBounds(nf,nr)){ const t=this.pieceAt(nf,nr); if(t===''){add(nf,nr);} else { if(pieceColor(t)===enemy) add(nf,nr,{captured:t}); break;} nf+=df; nr+=dr; }
      }
    }
    return moves;
  }

  // Apply a move to a copy of state, return new state object (no mutation)
  applyMove(m){
    const nb=this.board.slice();
    nb[idx(m.to.f,m.to.r)] = m.promo ? (m.pieceColor===WHITE?m.promo:m.promo.toLowerCase()) : nb[idx(m.from.f,m.from.r)];
    nb[idx(m.from.f,m.from.r)] = '';
    // en passant capture removes the captured pawn
    if(m.ep){ nb[idx(m.from.f,m.to.r)] = ''; }
    // castling moves the rook
    if(m.castle){
      const homeR=m.to.r;
      if(m.castle==='K'){ nb[idx(5,homeR)]=nb[idx(7,homeR)]; nb[idx(7,homeR)]=''; }
      else { nb[idx(3,homeR)]=nb[idx(0,homeR)]; nb[idx(0,homeR)]=''; }
    }
    return nb;
  }

  // A move is legal if after applying it, own king is not in check.
  // Also must verify the moving piece actually moves (handles king moving into check, etc.)
  isMoveLegal(m){
    const color=m.pieceColor;
    const nb=this.applyMove(m);
    const saved=this.board;
    this.board=nb;
    const ok=!this.inCheck(color);
    this.board=saved;
    return ok;
  }

  // Make a move on the real board; returns move with SAN; pushes history.
  move(m){
    const color=m.pieceColor;
    const san=this.san(m);
    const snapshot = {
      board:this.board.slice(), turn:this.turn, castle:JSON.parse(JSON.stringify(this.castle)),
      ep:{...this.ep}, halfmove:this.halfmove, fullmove:this.fullmove,
      move:m, san:san, captured: m.captured|| (m.ep?'':undefined)
    };
    // record captured piece for tray
    let capForTray = m.captured;
    if(m.ep) capForTray = (color===WHITE?'p':'P');
    snapshot.trayCapture = capForTray||null;
    snapshot.isCheck = false;
    snapshot.isCheckmate=false; snapshot.isStale=false;

    // Update halfmove clock
    if(m.pieceType==='pawn'||m.captured||m.ep) snapshot.halfBefore=this.halfmove;

    this.board = this.applyMove(m);
    // update castling rights
    this.updateCastleRights(m);
    // update en passant
    if(m.double) this.ep={f:m.from.f, r:(m.from.r+m.to.r)/2};
    else this.ep=null;
    // halfmove / fullmove
    if(m.pieceType==='pawn'||m.captured||m.ep) this.halfmove=0;
    else this.halfmove++;
    if(this.turn===BLACK) this.fullmove++;
    this.turn=opp(color);

    // post-move check/stalemate
    const oppInCheck = this.inCheck(this.turn);
    const oppMoves = this.legalMoves(this.turn);
    snapshot.isCheck = oppInCheck;
    if(oppInCheck && oppMoves.length===0) snapshot.isCheckmate=true;
    if(!oppInCheck && oppMoves.length===0) snapshot.isStale=true;
    if(!snapshot.isCheckmate && !snapshot.isStale){
      const d=this.isDraw();
      if(d.draw){ snapshot.draw=true; snapshot.drawReason=d.drawReason; }
    }

    this.history.push(snapshot);
    return snapshot;
  }

  updateCastleRights(m){
    // if king moved
    if(m.piece==='K'){ this.castle.wK=false; this.castle.wQ=false; }
    if(m.piece==='k'){ this.castle.bK=false; this.castle.bQ=false; }
    // if rook moved from home square (file,rank)
    if(m.from.f===0&&m.from.r===0) this.castle.wQ=false;
    if(m.from.f===7&&m.from.r===0) this.castle.wK=false;
    if(m.from.f===0&&m.from.r===7) this.castle.bQ=false;
    if(m.from.f===7&&m.from.r===7) this.castle.bK=false;
    // if rook captured on home square
    if(m.to.f===0&&m.to.r===0) this.castle.wQ=false;
    if(m.to.f===7&&m.to.r===0) this.castle.wK=false;
    if(m.to.f===0&&m.to.r===7) this.castle.bQ=false;
    if(m.to.f===7&&m.to.r===7) this.castle.bK=false;
    if(m.to.f===7&&m.to.r===0) this.castle.wK=false;
  }

  undo(){
    if(!this.history.length) return false;
    const s=this.history.pop();
    // restore pre-move state from snapshot's saved board (captured piece restored)
    this.board=s.board.slice();
    this.turn=s.turn;
    this.castle=JSON.parse(JSON.stringify(s.castle));
    this.ep={...s.ep};
    this.halfmove=s.halfmove;
    this.fullmove=s.fullmove;
    return true;
  }

  // --- SAN ---
  san(m){
    if(m.castle==='K') return "O-O";
    if(m.castle==='Q') return "O-O-O";
    let s="";
    const t=m.pieceType;
    if(t!=='pawn'){
      s += PIECE_SHORT[t];
      // disambiguation: other same-type pieces that can reach the SAME destination from a DIFFERENT square
      const sameType=this.legalMoves(m.pieceColor).filter(x=>x.pieceType===t&&x.to.f===m.to.f&&x.to.r===m.to.r&&!(x.from.f===m.from.f&&x.from.r===m.from.r));
      if(sameType.length){
        if(sameType.every(x=>x.from.f===m.from.f)) s+=algebraic(0,m.from.r); // same file -> rank
        else if(sameType.every(x=>x.from.r===m.from.r)) s+=algebraic(m.from.f,0); // same rank -> file
        else s+=algebraic(m.from.f,m.from.r);
      }
    } else if(m.captured||m.ep){
      s+=algebraic(m.from.f,0);
    }
    if(m.captured||m.ep) s+="x";
    s+=algebraic(m.to.f,m.to.r);
    if(m.promo) s+="="+m.promo.toUpperCase();
    return s;

  // Finalize SAN with check/mate symbols
  finalSan(snapshot){
    let s=snapshot.san;
    if(snapshot.isCheckmate) s+="#";
    else if(snapshot.isCheck) s+="+";
    return s;
  }

  fen(){
    let fen="";
    for(let r=7;r>=0;r--){
      let empty=0;
      for(let f=0;f<8;f++){ const p=this.board[idx(f,r)]; if(p==='') empty++; else { if(empty){fen+=empty;empty=0;} fen+=p; } }
      if(empty) fen+=empty;
      if(r>0) fen+="/";
    }
    fen+=" "+this.turn+" ";
    const c=this.castle;
    let cs="";
    if(c.wK)cs+="K"; if(c.wQ)cs+="Q"; if(c.bK)cs+="k"; if(c.bQ)cs+="q";
    cs=cs||"-";
    fen+=cs+" ";
    fen+=(this.ep?algebraic(this.ep.f,this.ep.r):"-")+" ";
    fen+=this.halfmove+" "+this.fullmove;
    return fen;
  }

  // Draw detection
  isDraw(){
    // insufficient material
    if(this.insufficientMaterial()) return {draw:true,reason:"insufficient material"};
    if(this.halfmove>=100) return {draw:true,reason:"50-move rule"};
    // stalemate
    if(!this.inCheck(this.turn) && this.legalMoves(this.turn).length===0){
      return {draw:true,reason:"stalemate"};
    }
    return {draw:false};
  }

  insufficientMaterial(){
    const pieces=[];
    for(const p of this.board){ if(p!=='') pieces.push(p); }
    if(pieces.length===2) return true; // K vs K
    if(pieces.length===3){
      // find non-king
      const nk=pieces.filter(x=>x!=='K'&&x!=='k');
      // bishop or knight only
      const onlyBn = pieces.every(x=>x==='K'||x==='k'||x.toUpperCase()==='B'||x.toUpperCase()==='N');
      if(onlyBn && nk.length===1) return true;
    }
    return false;
  }
}

/* ============================================================
   AI — minimax with alpha-beta + quiescence + move ordering
   ============================================================ */
const AI = {
  evaluate(state){
    // return eval from perspective of side to move
    let score=0;
    const b=state.board;
    for(let i=0;i<64;i++){
      const p=b[i]; if(p==='') continue;
      const color=pieceColor(p); const type=PIECE_TYPES[p.toUpperCase()];
      const base=CFG.eval[type.toLowerCase()];
      const pstKey=PIECE_SHORT[type];
      const pst=PST[pstKey];
      // white piece evaluated from white perspective (rank 8 first in table)
      let pv=base;
      pv += color===WHITE ? pst[i] : pst[63-i];
      score += color===WHITE ? pv : -pv;
    }
    return score;
  },

  search(state, depth, alpha, beta, color){
    // color = side to move (maximizer)
    const moves=state.legalMoves(state.turn);
    if(moves.length===0){
      if(state.inCheck(state.turn)) return -100000+ (10-depth); // checkmate, prefer faster
      return 0; // stalemate/draw
    }
    if(depth===0){
      return this.quiesce(state, alpha, beta, color);
    }
    // move ordering: captures first (MVV-LVA-ish)
    const ordered=this.orderMoves(moves);
    let best=-Infinity;
    for(const m of ordered){
      // snapshot for undo-based search
      const snap=state.snapshot();
      state.move(m);
      const val=-this.search(state, depth-1, -beta, -alpha, opp(color));
      state.restore(snap);
      if(val>best) best=val;
      if(best>alpha) alpha=best;
      if(alpha>=beta) break;
    }
    return best;
  },

  quiesce(state, alpha, beta, color){
    const stand=this.evaluate(state);
    if(stand>=beta) return beta;
    if(stand>alpha) alpha=stand;
    const captures=state.legalMoves(state.turn).filter(m=>m.captured||m.ep);
    const ordered=this.orderMoves(captures);
    for(const m of ordered){
      const snap=state.snapshot();
      state.move(m);
      const val=-this.quiesce(state, -beta, -alpha, opp(color));
      state.restore(snap);
      if(val>alpha) alpha=val;
      if(alpha>=beta) return beta;
    }
    return alpha;
  },

  orderMoves(moves){
    return moves.slice().sort((a,b)=>{
      let sa=0,sb=0;
      if(a.captured) sa+=10*(CFG.eval[PIECE_TYPES[a.captured.toUpperCase()]]||0)-CFG.eval[PIECE_TYPES[a.pieceType]]/10;
      if(b.captured) sb+=10*(CFG.eval[PIECE_TYPES[b.captured.toUpperCase()]]||0)-CFG.eval[PIECE_TYPES[b.pieceType]]/10;
      if(a.promo) sa+=900; if(b.promo) sb+=900;
      if(a.castle) sa+=10; if(b.castle) sb+=10;
      return sb-sa;
    });
  },

  bestMove(state){
    const color=state.turn;
    const depth=CFG.ai.depth;
    const moves=state.legalMoves(color);
    if(moves.length===0) return null;
    const ordered=this.orderMoves(moves);
    let best=null; let bestVal=-Infinity;
    const start=Date.now();
    let alpha=-Infinity, beta=Infinity;
    // Iterative deepening-ish: just do fixed depth but time-guard via quiesce only.
    for(const m of ordered){
      const snap=state.snapshot();
      state.move(m);
      const val=-this.search(state, depth-1, -beta, -alpha, opp(color));
      state.restore(snap);
      if(val>bestVal){ bestVal=val; best=m; }
      if(val>alpha) alpha=val;
      // soft time guard
      if(Date.now()-start > CFG.ai.maxTimePerMove) break;
    }
    return best||moves[0];
  }
};

/* Chess snapshot/restore for fast search */
(function(){
  Chess.prototype.snapshot=function(){
    return {
      board:this.board.slice(), turn:this.turn,
      castle:JSON.parse(JSON.stringify(this.castle)),
      ep:this.ep?{...this.ep}:null,
      halfmove:this.halfmove, fullmove:this.fullmove
    };
  };
  Chess.prototype.restore=function(s){
    this.board=s.board.slice(); this.turn=s.turn;
    this.castle=JSON.parse(JSON.stringify(s.castle));
    this.ep=s.ep?{...s.ep}:null; this.halfmove=s.halfmove; this.fullmove=s.fullmove;
  };
})();

/* ============================================================
   UI CONTROLLER
   ============================================================ */
const UI = {
  game:null,
  selected:null,       // {f,r}
  legalForSel:[],      // array of moves
  lastMove:null,       // {from,to}
  flipped:false,
  muted:false,
  aiThinking:false,
  audioCtx:null,

  init(){
    const saved=localStorage.getItem(CFG.saveKey);
    if(saved){
      try{
        const d=JSON.parse(saved);
        this.game=new Chess();
        this.game.board=d.board.slice();
        this.game.turn=d.turn;
        this.game.castle=d.castle;
        this.game.ep=d.ep||null;
        this.game.halfmove=d.halfmove||0;
        this.game.fullmove=d.fullmove||1;
        this.game.history=d.history||[];
        this.lastMove = this.game.history.length? {from:this.game.history[this.game.history.length-1].move.from, to:this.game.history[this.game.history.length-1].move.to}:null;
      }catch(e){ this.newGame(); }
    } else {
      this.newGame();
    }
    this.bindControls();
    this.render();
    this.updateStatus();
    // AI to move?
    if(this.shouldAIMove()) setTimeout(()=>this.aiMove(), 300);
  },

  newGame(){
    this.game=new Chess();
    this.selected=null; this.legalForSel=[]; this.lastMove=null;
    this.aiThinking=false;
    this.hideBanner();
    this.hidePromo();
    this.persist();
    this.render();
    this.updateStatus();
    if(this.shouldAIMove()) setTimeout(()=>this.aiMove(), 300);
  },

  side(){ return document.getElementById('sideSel').value; },
  depth(){ return parseInt(document.getElementById('depthSel').value,10); },

  bindControls(){
    document.getElementById('undoBtn').addEventListener('click',()=>this.undo());
    document.getElementById('flipBtn').addEventListener('click',()=>{ this.flipped=!this.flipped; this.render(); });
    document.getElementById('muteBtn').addEventListener('click',()=>this.toggleMute());
    document.getElementById('newBtn').addEventListener('click',()=>this.newGame());
    document.getElementById('sideSel').addEventListener('change',()=>{
      this.selected=null; this.legalForSel=[];
      this.render();
      if(this.shouldAIMove()) setTimeout(()=>this.aiMove(), 300);
    });
    document.getElementById('depthSel').addEventListener('change',()=>{
      CFG.ai.depth=this.depth();
    });
  },

  shouldAIMove(){
    const s=this.side();
    if(s==='both'||s==='analyze') return false;
    const aiColor = this.game.turn;
    const playerColor = s==='white'?WHITE:BLACK;
    return aiColor!==playerColor;
  },

  aiMove(){
    if(this.aiThinking) return;
    if(this.gameover()) return;
    if(!this.shouldAIMove()) return;
    this.aiThinking=true;
    document.getElementById('statusLine').textContent="AI thinking…";
    // run in a timeout so UI can update
    setTimeout(()=>{
      const t0=Date.now();
      const m=AI.bestMove(this.game);
      if(!m){ this.aiThinking=false; this.updateStatus(); return; }
      this.game.move(m);
      this.lastMove={from:m.from,to:m.to};
      this.aiThinking=false;
      this.playSoundFor(this.game.history[this.game.history.length-1]);
      this.persist();
      this.render();
      this.updateStatus();
      this.maybeAutoScroll();
    }, 30);
  },

  undo(){
    if(this.aiThinking) return;
    // undo both AI and player move
    const before=this.game.history.length;
    // if it's currently AI's turn (shouldn't happen) just undo one
    if(this.game.history.length===0) return;
    // ensure we undo to player's turn: undo AI then player if needed
    this.game.undo();
    if(this.game.turn===this.playerColor() && this.game.history.length>0 && !this.gameover()){
      this.game.undo();
    }
    this.selected=null; this.legalForSel=[];
    this.lastMove = this.game.history.length? {from:this.game.history[this.game.history.length-1].move.from, to:this.game.history[this.game.history.length-1].move.to}:null;
    this.persist();
    this.render();
    this.updateStatus();
  },
  playerColor(){ const s=this.side(); return s==='white'?WHITE:BLACK; },

  /* ---- Interaction ---- */
  onCellClick(f,r){
    if(this.aiThinking || this.gameover()) return;
    if(this.shouldAIMove()) return; // not player's turn
    const p=this.game.pieceAt(f,r);
    if(this.selected){
      // try to make a move
      const mv=this.legalForSel.find(m=>m.to.f===f&&m.to.r===r);
      if(mv){
        this.tryMove(mv);
        return;
      }
    }
    // select own piece
    if(p&&pieceColor(p)===this.game.turn){
      this.selected={f,r};
      this.legalForSel=this.game.legalMoves(this.game.turn).filter(m=>m.from.f===f&&m.from.r===r);
    } else {
      this.selected=null; this.legalForSel=[];
    }
    this.render();
  },

  tryMove(m){
    if(m.promo){
      this.showPromo(m);
    } else {
      this.executeMove(m);
    }
  },

  executeMove(m){
    const snap=this.game.snapshot();
    this.game.move(m);
    this.lastMove={from:m.from,to:m.to};
    this.selected=null; this.legalForSel=[];
    this.playSoundFor(this.game.history[this.game.history.length-1]);
    this.persist();
    this.render();
    this.updateStatus();
    this.maybeAutoScroll();
    if(this.shouldAIMove() && !this.gameover()) this.aiMove();
  },

  showPromo(m){
    const color=m.pieceColor;
    const opts=['Q','R','B','N'];
    const container=document.getElementById('promoOpts');
    container.innerHTML="";
    for(const t of opts){
      const b=document.createElement('button');
      b.textContent = color===WHITE? t : t.toLowerCase();
      b.style.color = color===WHITE?'#222':'#222';
      b.addEventListener('click',()=>{
        this.hidePromo();
        this.executeMove(Object.assign({},m,{promo:t}));
      });
      container.appendChild(b);
    }
    document.getElementById('promoOverlay').classList.add('show');
  },
  hidePromo(){ document.getElementById('promoOverlay').classList.remove('show'); },

  /* ---- Rendering ---- */
  render(){
    const board=document.getElementById('board');
    board.innerHTML="";
    const checkPos = this.game.inCheck(this.game.turn)? this.game.kingPos(this.game.turn):null;
    for(let dr=0;dr<8;dr++){
      for(let df=0;df<8;df++){
        const r=this.flipped?7-dr:dr;
        const f=this.flipped?7-df:df;
        const cell=document.createElement('div');
        cell.className='cell '+((df+dr)%2===0?'light':'dark');
        cell.dataset.f=f; cell.dataset.r=r;
        // last move highlight
        if(this.lastMove && ((this.lastMove.from.f===f&&this.lastMove.from.r===r)||(this.lastMove.to.f===f&&this.lastMove.to.r===r)))
          cell.classList.add('lastmove');
        // selected
        if(this.selected && this.selected.f===f && this.selected.r===r) cell.classList.add('sel');
        // check
        if(checkPos && checkPos.f===f && checkPos.r===r) cell.classList.add('check');
        // piece
        const p=this.game.pieceAt(f,r);
        if(p!==''){
          const pe=document.createElement('span');
          pe.className='piece '+(pieceColor(p)===WHITE?'white':'black');
          pe.textContent=GLYPHS[p];
          pe.draggable=true;
          pe.addEventListener('dragstart',(e)=>this.onDragStart(e,f,r));
          pe.addEventListener('dragend',(e)=>this.onDragEnd(e));
          cell.appendChild(pe);
        }
        // legal move indicators
        if(this.selected){
          const target=this.legalForSel.find(m=>m.to.f===f&&m.to.r===r);
          if(target){
            if(target.captured||target.ep){
              const ring=document.createElement('div'); ring.className='ring'; cell.appendChild(ring);
            } else {
              const dot=document.createElement('div'); dot.className='dot'; cell.appendChild(dot);
            }
          }
        }
        // coordinates
        if(df===0){ const k=document.createElement('span'); k.className='coord rank'; k.textContent=(r+1); cell.appendChild(k); }
        if(dr===7){ const fi=document.createElement('span'); fi.className='coord file'; fi.textContent="abcdefgh"[f]; cell.appendChild(fi); }
        cell.addEventListener('click',(e)=>{ e.preventDefault(); this.onCellClick(f,r); });
        cell.addEventListener('dragover',(e)=>{ e.preventDefault(); });
        cell.addEventListener('drop',(e)=>{ e.preventDefault(); this.onDrop(e,f,r); });
        board.appendChild(cell);
      }
    }
    this.renderTrays();
    this.renderMoves();
    this.renderTurn();
  },

  renderTurn(){
    const dot=document.getElementById('turnDot');
    const txt=document.getElementById('turnText');
    dot.className='dot2 '+(this.game.turn===WHITE?'white':'black');
    const label=this.game.turn===WHITE?'White to move':'Black to move';
    txt.textContent = this.aiThinking? label+' (AI thinking…)' : label;
  },

  renderTrays(){
    // captured by white = black pieces captured; captured by black = white pieces
    const capW=[], capB=[];
    for(const s of this.game.history){
      const c=s.trayCapture;
      if(!c) continue;
      if(c===c.toLowerCase()) capW.push(GLYPHS[c]); // black piece captured by white
      else capB.push(GLYPHS[c]); // white piece captured by black
    }
    document.getElementById('capWhite').innerHTML=capW.map(g=>`<span>${g}</span>`).join('');
    document.getElementById('capBlack').innerHTML=capB.map(g=>`<span>${g}</span>`).join('');
  },

  renderMoves(){
    const el=document.getElementById('moves');
    el.innerHTML="";
    const h=this.game.history;
    for(let i=0;i<h.length;i+=2){
      const row=document.createElement('div'); row.className='mv-row';
      const num=document.createElement('span'); num.className='mv-num'; num.textContent=(i/2+1)+'.';
      row.appendChild(num);
      const white=document.createElement('span'); white.className='mv'; white.textContent=h[i].san;
      row.appendChild(white);
      if(h[i+1]){
        const black=document.createElement('span'); black.className='mv'; black.textContent=h[i+1].san;
        row.appendChild(black);
      }
      el.appendChild(row);
    }
    this.maybeAutoScroll();
  },

  maybeAutoScroll(){
    const el=document.getElementById('moves');
    el.scrollTop=el.scrollHeight;
  },

  updateStatus(){
    const line=document.getElementById('statusLine');
    if(this.gameover()){
      const info=this.gameResult();
      line.innerHTML='<span class="status-ok">'+info.text+'</span>';
      return;
    }
    const turn=this.game.turn===WHITE?'White':'Black';
    let extra='';
    if(this.game.inCheck(this.game.turn)) extra=' — <b>check!</b>';
    line.textContent = turn+' to move'+extra;
    // auto AI
    if(this.shouldAIMove() && !this.aiThinking && !this.gameover()){
      // handled by aiMove call in render/execute
    }
  },

  gameover(){
    const h=this.game.history;
    if(h.length===0) return false;
    const last=h[h.length-1];
    if(last.isCheckmate||last.isStale||last.draw) return true;
    return false;
  },

  gameResult(){
    const h=this.game.history; const last=h[h.length-1];
    if(last.isCheckmate){
      const winner = this.game.turn===WHITE?'Black':'White';
      return {text:'Checkmate — '+winner+' wins!', sub:''};
    }
    if(last.draw){
      return {text:'Draw — '+last.drawReason, sub:''};
    }
    return {text:'',sub:''};
  },

  showBanner(res){
    const b=document.getElementById('banner');
    document.getElementById('bannerText').textContent=res.text;
    document.getElementById('bannerSub').textContent=res.sub||'';
    b.classList.add('show');
  },
  hideBanner(){ document.getElementById('banner').classList.remove('show'); },

  persist(){
    localStorage.setItem(CFG.saveKey, JSON.stringify({
      board:this.game.board.slice(), turn:this.game.turn,
      castle:this.game.castle, ep:this.game.ep,
      halfmove:this.game.halfmove, fullmove:this.game.fullmove,
      history:this.game.history.map(s=>({
        board:s.board, turn:s.turn, castle:s.castle, ep:s.ep,
        halfmove:s.halfmove, fullmove:s.fullmove,
        san:s.san, isCheck:s.isCheck, isCheckmate:s.isCheckmate,
        isStale:s.isStale, draw:s.draw, drawReason:s.drawReason,
        move:s.move, trayCapture:s.trayCapture
      }))
    }));
  },

  toggleMute(){
    this.muted=!this.muted;
    document.getElementById('muteBtn').textContent=this.muted?'🔇':'🔊';
  },

  /* ---- Drag and drop ---- */
  dragInfo:null,
  onDragStart(e,f,r){
    if(this.aiThinking||this.gameover()||this.shouldAIMove()) { e.preventDefault(); return; }
    const p=this.game.pieceAt(f,r);
    if(!p||pieceColor(p)!==this.game.turn) return;
    // set legal moves for this piece
    this.selected={f,r};
    this.legalForSel=this.game.legalMoves(this.game.turn).filter(m=>m.from.f===f&&m.from.r===r);
    this.dragInfo={f,r};
    e.dataTransfer.effectAllowed='move';
    e.dataTransfer.setData('text/plain',f+','+r);
    // ghost handled by browser; use custom ghost
    const ghost=document.getElementById('dragGhost');
    ghost.textContent=GLYPHS[p];
    ghost.style.display='block';
    e.dataTransfer.setDragImage(ghost,32,32);
    this.render();
  },
  onDragEnd(e){
    this.dragInfo=null;
  },
  onDrop(e,f,r){
    if(!this.dragInfo) return;
    const mv=this.legalForSel.find(m=>m.to.f===f&&m.to.r===r);
    if(mv){
      this.selected={f:this.dragInfo.f,r:this.dragInfo.r};
      this.legalForSel=[mv];
      this.tryMove(mv);
    }
    this.dragInfo=null;
  },

  /* ---- Sound ---- */
  ensureAudio(){
    if(!this.audioCtx){
      try{ this.audioCtx=new (window.AudioContext||window.webkitAudioContext)(); }
      catch(e){ this.audioCtx=null; }
    }
    return this.audioCtx;
  },
  beep(freq,dur,type,vol){
    if(this.muted) return;
    const ctx=this.ensureAudio(); if(!ctx) return;
    const o=ctx.createOscillator(), g=ctx.createGain();
    o.type=type||'sine'; o.frequency.value=freq;
    g.gain.value=vol||0.15;
    o.connect(g); g.connect(ctx.destination);
    o.start();
    g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+dur);
    o.stop(ctx.currentTime+dur);
  },
  playSoundFor(snap){
    if(this.muted) return;
    if(snap.isCheckmate){ this.beep(300,0.3,'square',0.2); setTimeout(()=>this.beep(240,0.4,'square',0.2),150); }
    else if(snap.isCheckmate) return;
    else if(snap.isCheck) this.beep(660,0.18,'triangle',0.2);
    else if(snap.captured||snap.ep) this.beep(200,0.12,'square',0.18);
    else this.beep(320,0.08,'sine',0.12);
  }
};

const GLYPHS={
  'P':'♙','N':'♘','B':'♗','R':'♖','Q':'♕','K':'♔',
  'p':'♟','n':'♞','b':'♝','r':'♜','q':'♛','k':'♚'
};

/* ============================================================
   DEBUG INTERFACE on window
   ============================================================ */
window.chessGame = {
  get:()=>UI.game,
  fen:()=>UI.game?UI.game.fen():null,
  legalMoves:()=>UI.game?UI.game.legalMoves(UI.game.turn):[],
  move:(sanOrUc)=>{
    if(!UI.game) return null;
    // accept like 'e2e4' or SAN
    let m=null;
    if(/^[a-h][1-8][a-h][1-8]/.test(sanOrUc)){
      const from={f:sanOrUc.charCodeAt(0)-97,r:parseInt(sanOrUc[1],10)-1};
      const to={f:sanOrUc.charCodeAt(2)-97,r:parseInt(sanOrUc[3],10)-1};
      m=UI.game.legalMoves(UI.game.turn).find(x=>x.from.f===from.f&&x.from.r===from.r&&x.to.f===to.f&&x.to.r===to.r);
    } else {
      m=UI.game.legalMoves(UI.game.turn).find(x=>UI.game.san(x)===sanOrUc);
    }
    if(!m) return null;
    UI.executeMove(m);
    return UI.game.fen();
  },
  newGame:()=>UI.newGame(),
  undo:()=>UI.undo(),
  gameover:()=>UI.gameover(),
  isDraw:()=>UI.game?UI.game.isDraw():{draw:false},
  depth:(d)=>{ if(d!==undefined){CFG.ai.depth=d;} return CFG.ai.depth; },
  setSide:(s)=>{ document.getElementById('sideSel').value=s; document.getElementById('sideSel').dispatchEvent(new Event('change')); },
  aiMove:()=>UI.aiMove()
};

document.addEventListener('DOMContentLoaded',()=>UI.init());
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"06f47a2f-7aba-4852-addf-3f0b148a8190","tokensIn":7585424,"tokensOut":110524,"tokensTotal":7695948,"cost":0,"turns":94,"toolCalls":93,"failedToolCalls":5,"timestamp":"2026-08-23T18:56:37.947Z"} -->