← Chess results

Chess

muse spark 1.2 · basic

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 53,920 bytes · SHA-256 391457993101
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Atlas Chess — Classic</title>
<style>
  :root{
    --bg:#0f1115;
    --panel:#faf7f1;
    --panel2:#fff;
    --light:#f0d9b5;
    --dark:#b58863;
    --light-hi:#f7e9c9;
    --dark-hi:#c99a6b;
    --accent:#c0392b;
    --accent2:#2c3e50;
    --line:#e8ddd0;
    --shadow:0 18px 50px rgba(0,0,0,.35);
    --radius:18px;
  }
  *{box-sizing:border-box}
  body{
    margin:0;
    font-family: ui-serif, Georgia, "Times New Roman", serif;
    background:
      radial-gradient(1200px 600px at 20% -10%, #2a2e3a 0%, transparent 60%),
      radial-gradient(900px 500px at 90% 0%, #3a2f25 0%, transparent 60%),
      var(--bg);
    color:#1a1a1a;
    min-height:100vh;
    display:flex;
    flex-direction:column;
  }
  header{
    max-width:1180px;
    width:100%;
    margin:18px auto 0;
    padding:0 18px;
    display:flex;
    align-items:center;
    justify-content:space-between;
    color:#f5efe6;
  }
  header h1{
    font-size:22px;
    letter-spacing:.14em;
    font-weight:800;
    margin:0;
    display:flex;
    gap:10px;
    align-items:center;
  }
  header h1 span{font-weight:400;opacity:.7;letter-spacing:.06em;font-size:13px}
  .header-actions{display:flex;gap:8px;align-items:center}
  .pill{
    background:rgba(255,255,255,.08);
    border:1px solid rgba(255,255,255,.12);
    color:#f5efe6;
    padding:7px 12px;
    border-radius:999px;
    font-size:12px;
    letter-spacing:.08em;
    text-transform:uppercase;
  }
  .wrap{
    max-width:1180px;
    width:100%;
    margin:18px auto;
    padding:0 18px 28px;
    display:grid;
    grid-template-columns: 1.22fr .78fr;
    gap:22px;
  }
  @media(max-width:900px){ .wrap{grid-template-columns:1fr} }
  .board-card{
    background:linear-gradient(180deg, #fffcf7 0%, #fdf6eb 100%);
    border-radius:var(--radius);
    padding:18px;
    box-shadow:var(--shadow);
    border:1px solid rgba(0,0,0,.06);
  }
  .board-top{
    display:flex;
    align-items:center;
    justify-content:space-between;
    margin-bottom:12px;
    gap:10px;
    flex-wrap:wrap;
  }
  .turn{
    display:flex;
    align-items:center;
    gap:10px;
    font-size:13px;
    letter-spacing:.08em;
    text-transform:uppercase;
    color:#5a4a3a;
    font-weight:700;
  }
  .dot{width:10px;height:10px;border-radius:50%;background:#2ecc71;box-shadow:0 0 0 6px rgba(46,204,113,.15)}
  .dot.black{background:#2c3e50;box-shadow:0 0 0 6px rgba(44,62,80,.15)}
  .status{
    font-size:12px;
    padding:6px 10px;
    border-radius:999px;
    background:#fff;
    border:1px solid var(--line);
    color:#6b5a4a;
  }
  .captured{ display:flex; gap:6px; min-height:24px; align-items:center; flex-wrap:wrap; font-size:18px; line-height:1}
  .captured span{opacity:.9}
  .captured .label{font-size:10px; letter-spacing:.12em; text-transform:uppercase; color:#9a8a7a; margin-right:6px; font-family:system-ui,sans-serif}
  .board-wrap{
    position:relative;
    aspect-ratio:1;
    background:#3a2a1c;
    padding:12px;
    border-radius:14px;
    box-shadow:inset 0 0 0 1px rgba(255,255,255,.08), 0 12px 30px rgba(0,0,0,.25);
  }
  .board{
    display:grid;
    grid-template-columns:repeat(8,1fr);
    grid-template-rows:repeat(8,1fr);
    width:100%;height:100%;
    border-radius:10px;
    overflow:hidden;
    border:4px solid #2b1d12;
    position:relative;
  }
  .sq{
    position:relative;
    display:flex;
    align-items:center;
    justify-content:center;
    font-size:44px;
    line-height:1;
    user-select:none;
    cursor:pointer;
  }
  @media(max-width:560px){ .sq{font-size:34px} }
  .sq.light{background:var(--light)}
  .sq.dark{background:var(--dark)}
  .sq.highlight::after{
    content:"";
    position:absolute;inset:0;
    background:rgba(255,235,120,.62);
    pointer-events:none;
  }
  .sq.last::after{
    content:"";
    position:absolute;inset:0;
    background:rgba(110,180,255,.38);
    pointer-events:none;
  }
  .sq.in-check::after{
    content:"";
    position:absolute;inset:0;
    background:radial-gradient(circle at center, rgba(231,76,60,.9) 0 6px, transparent 7px), rgba(231,76,60,.22);
    pointer-events:none;
    box-shadow:inset 0 0 0 3px #c0392b;
  }
  .sq.can-move::before{
    content:"";
    width:18px;height:18px;
    border-radius:50%;
    background:rgba(0,0,0,.22);
    position:absolute;
    box-shadow:0 1px 0 rgba(255,255,255,.6) inset;
  }
  .sq.can-capture::before{
    content:"";
    position:absolute;inset:6px;
    border-radius:50%;
    border:5px solid rgba(0,0,0,.38);
    background:transparent;
  }
  .sq.selected::after{
    content:"";
    position:absolute;inset:0;
    background:rgba(255,214,90,.55);
    box-shadow:inset 0 0 0 3px #ffb700;
    pointer-events:none;
  }
  .piece{
    position:relative;
    z-index:2;
    filter: drop-shadow(0 1px 0 rgba(0,0,0,.4)) drop-shadow(0 3px 6px rgba(0,0,0,.25));
    transition:transform .08s;
    cursor:grab;
  }
  .piece.w{ color:#ffffff; -webkit-text-stroke:1.2px #2b1d12; text-shadow: 0 1px 0 #2b1d12; }
  .piece.b{ color:#111111; -webkit-text-stroke:.6px #fff; }
  .sq:active .piece{transform:scale(.96)}
  .coords{
    position:absolute;
    font-family:system-ui,sans-serif;
    font-size:10px;
    font-weight:800;
    opacity:.55;
    pointer-events:none;
  }
  .coords.file{bottom:3px;right:4px}
  .coords.rank{top:3px;left:4px}
  /* promotion */
  .promo{
    position:absolute;
    z-index:20;
    background:#fff;
    border-radius:12px;
    box-shadow:0 18px 40px rgba(0,0,0,.35);
    border:1px solid var(--line);
    display:flex;
    overflow:hidden;
  }
  .promo button{
    font-size:32px;
    width:56px;height:56px;
    border:0;
    background:#fff;
    cursor:pointer;
  }
  .promo button:hover{background:#fdf0d5}
  /* side panel */
  .panel{
    display:flex;
    flex-direction:column;
    gap:16px;
  }
  .card{
    background:var(--panel);
    border-radius:var(--radius);
    padding:16px;
    box-shadow:var(--shadow);
    border:1px solid rgba(0,0,0,.06);
  }
  .card h3{
    margin:0 0 12px;
    font-size:11px;
    letter-spacing:.14em;
    text-transform:uppercase;
    color:#8a7a6a;
    display:flex;
    justify-content:space-between;
    align-items:center;
  }
  .controls{ display:grid; grid-template-columns:1fr 1fr; gap:8px}
  .btn{
    padding:10px 12px;
    border-radius:999px;
    border:1px solid var(--line);
    background:#fff;
    font-size:13px;
    font-weight:700;
    letter-spacing:.04em;
    cursor:pointer;
    display:flex;
    align-items:center;
    justify-content:center;
    gap:6px;
  }
  .btn.primary{ background:var(--accent2); color:#fff; border-color:var(--accent2)}
  .btn:active{transform:scale(.98)}
  .row{ display:flex; gap:8px; flex-wrap:wrap; align-items:center}
  select{
    padding:8px 10px;
    border-radius:999px;
    border:1px solid var(--line);
    background:#fff;
    font-size:13px;
  }
  .moves{
    max-height:220px;
    overflow:auto;
    background:#fff;
    border:1px solid var(--line);
    border-radius:12px;
    padding:8px;
  }
  .moves ol{ margin:0; padding:0 0 0 22px; columns:2; column-gap:18px}
  .moves li{
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    font-size:13px;
    padding:3px 6px;
    border-radius:6px;
    break-inside:avoid;
  }
  .moves li.active{ background:#2c3e50; color:#fff}
  .banner{
    padding:14px;
    border-radius:12px;
    text-align:center;
    font-weight:800;
    letter-spacing:.04em;
    display:none;
  }
  .banner.show{ display:block}
  .banner.mate{ background:#c0392b; color:#fff}
  .banner.draw{ background:#f1c40f; color:#2c3e50}
  .banner.check{ background:#eaf6ff; color:#2c3e50; border:1px solid #cfe8ff}
  .small{ font-size:11px; color:#9a8a7a; font-family:system-ui,sans-serif}
  .switch{ display:flex; align-items:center; gap:8px; font-size:13px}
  .toggle{ width:36px;height:22px;background:#ddd;border-radius:999px;position:relative;cursor:pointer; border:1px solid #ccc}
  .toggle.on{ background:#2ecc71; border-color:#27ae60}
  .toggle::after{ content:""; position:absolute; width:16px;height:16px;background:#fff;border-radius:50%;top:2px;left:2px;transition:.18s; box-shadow:0 1px 3px rgba(0,0,0,.3)}
  .toggle.on::after{ left:17px}
  .flip .board{ transform: rotate(180deg)}
  .flip .piece{ transform: rotate(180deg)}
  .flip .sq.can-move::before, .flip .sq.can-capture::before{ transform:rotate(180deg)}
</style>
</head>
<body>
<header>
  <h1>ATLAS ♔ <span>Classic Chess • Wood & Slate</span></h1>
  <div class="header-actions">
    <span class="pill" id="evalPill">Even</span>
    <span class="pill" id="plyPill">Ply 1</span>
  </div>
</header>

<div class="wrap" id="wrap">
  <div class="board-card" id="boardCard">
    <div class="board-top">
      <div class="turn"><span class="dot" id="turnDot"></span> <span id="turnText">White to move</span> <span class="status" id="statusText">Ready</span></div>
      <div class="row">
        <button class="btn" id="flipBtn">⇅ Flip</button>
        <button class="btn" id="muteBtn">🔊 Sound</button>
      </div>
    </div>
    <div class="captured" id="capturedBlack"><span class="label">Captured by White</span></div>
    <div class="board-wrap" id="boardWrap">
      <div class="board" id="board"></div>
      <div id="promoAnchor" style="position:absolute;inset:0;pointer-events:none"></div>
    </div>
    <div class="captured" id="capturedWhite" style="margin-top:10px"><span class="label">Captured by Black</span></div>
    <div class="row" style="margin-top:12px; justify-content:space-between">
      <span class="small">Click or drag to move • Dots = quiet, Rings = capture</span>
      <span class="small" id="fenSmall" style="max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap"></span>
    </div>
  </div>

  <div class="panel">
    <div class="card">
      <h3>Game Controls</h3>
      <div class="controls">
        <button class="btn primary" id="newBtn">↻ New Game</button>
        <button class="btn" id="undoBtn">↩ Undo</button>
      </div>
      <div class="row" style="margin-top:12px">
        <label class="small">Play as</label>
        <select id="colorSel">
          <option value="w">White (human)</option>
          <option value="b">Black (human)</option>
          <option value="both">Both / Analysis</option>
        </select>
        <label class="small">AI Depth</label>
        <select id="depthSel">
          <option value="1">Depth 1 — Quick</option>
          <option value="2" selected>Depth 2 — Balanced</option>
          <option value="3">Depth 3 — Strong</option>
        </select>
      </div>
      <div class="row" style="margin-top:10px">
        <label class="switch"><span class="small">AI Thinking</span> <span id="thinking" class="small" style="color:#c0392b; display:none">● thinking…</span></label>
        <div style="margin-left:auto" class="switch">
          <span class="small">Flip board</span>
          <div class="toggle" id="flipToggle"></div>
        </div>
      </div>
    </div>

    <div class="card">
      <h3>Move List <span class="small" id="moveCount">0 moves</span></h3>
      <div class="moves" id="movesBox"><ol id="moveList"></ol></div>
      <div id="banner" class="banner" style="margin-top:10px"></div>
    </div>

    <div class="card">
      <h3>How to verify</h3>
      <div class="small" style="line-height:1.6">
        • Start position has 20 legal moves<br>
        • Fool's mate: <code>f3 e5 g4 Qh4#</code> → checkmate<br>
        • En passant appears only one move<br>
        • Pin test: exposes king → rejected<br>
        Use <code>game.legalMoves().length</code>, <code>game.fen()</code>, <code>game.move('e2e4')</code> in console.
      </div>
    </div>
  </div>
</div>

<script>
/* =======================
   TUNABLE CONSTANTS
   ======================= */
const COLORS = {
  light: '#f0d9b5',
  dark: '#b58863',
  highlight: 'rgba(255,235,120,.62)',
  lastMove: 'rgba(110,180,255,.38)',
  check: 'rgba(231,76,60,.9)'
};
const AI_DEPTHS = { 1:1, 2:2, 3:3 };
const EVAL_WEIGHTS = {
  P:100, N:320, B:330, R:500, Q:900, K:20000,
  mobility: 2
};
// Piece-square tables (from white perspective, idx 0=a1)
// Simple bonuses
const PST = (() => {
  const mk = (arr)=>arr;
  // rank 8 to 1? our idx 0=a1 bottom. So PST arrays indexed by idx.
  // We'll define by rank order bottom to top.
  const 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
  ];
  const 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
  ];
  const 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
  ];
  const 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
  ];
  const 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
  ];
  const k_mid = [
    -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
  ];
  return {P:p,N:n,B:b,R:r,Q:q,K:k_mid};
})();

/* =======================
   ENGINE - board representation & move generation
   ======================= */
class Engine {
  constructor(){
    this.reset();
  }
  reset(){
    // board 64, 0=a1
    this.board = new Array(64).fill(null);
    const back = ['R','N','B','Q','K','B','N','R'];
    for(let c=0;c<8;c++){
      this.board[c] = {type:back[c], color:'w'};
      this.board[c+8] = {type:'P', color:'w'};
      this.board[48+c] = {type:'P', color:'b'};
      this.board[56+c] = {type:back[c], color:'b'};
    }
    this.turn='w';
    this.castling={K:true,Q:true,k:true,q:true};
    this.enPassant=null;
    this.halfmove=0;
    this.fullmove=1;
    this.history=[];
    this.moveHistory=[]; // SAN
    this.captured={w:[],b:[]};
  }
  cloneBoard(){ return this.board.map(p=>p?{...p}:null); }
  idxToAlg(idx){
    const c=idx%8, r=Math.floor(idx/8);
    return String.fromCharCode(97+c)+(r+1);
  }
  algToIdx(alg){
    const c=alg.charCodeAt(0)-97;
    const r=parseInt(alg[1])-1;
    return r*8+c;
  }
  findKing(color){
    for(let i=0;i<64;i++) if(this.board[i] && this.board[i].type==='K' && this.board[i].color===color) return i;
    return -1;
  }
  isOnBoard(r,c){ return r>=0&&r<8&&c>=0&&c<8; }
  pieceAt(idx){ return this.board[idx]; }
  isAttacked(sq, byColor){
    const r=Math.floor(sq/8), c=sq%8;
    // pawn attacks
    const dir = byColor==='w'? -1 : 1; // pawn that attacks sq must be one rank behind?
    // if byColor white, pawn is south of sq (r-1)
    const pr = r + (byColor==='w' ? -1 : 1);
    // Actually white pawns move north (+1). To attack sq, white pawn must be one rank south: r-1
    // So pr = r -1 for white? Let's think: sq at r=3 (rank4). White pawn at r=2 attacks r=3. So byColor w => pr = r-1 . byColor b => pr = r+1
    const pawnRank = byColor==='w' ? r-1 : r+1;
    for(let dc of [-1,1]){
      const pc = c+dc;
      if(this.isOnBoard(pawnRank,pc)){
        const idx=pawnRank*8+pc;
        const p=this.board[idx];
        if(p && p.color===byColor && p.type==='P') return true;
      }
    }
    // knights
    const knightDeltas=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
    for(let [dr,dc] of knightDeltas){
      const nr=r+dr,nc=c+dc;
      if(this.isOnBoard(nr,nc)){
        const p=this.board[nr*8+nc];
        if(p && p.color===byColor && p.type==='N') return true;
      }
    }
    // bishops / queens diagonals
    const diag=[[-1,-1],[-1,1],[1,-1],[1,1]];
    for(let [dr,dc] of diag){
      let nr=r+dr,nc=c+dc;
      while(this.isOnBoard(nr,nc)){
        const idx=nr*8+nc;
        const p=this.board[idx];
        if(p){
          if(p.color===byColor && (p.type==='B' || p.type==='Q')) return true;
          break;
        }
        nr+=dr; nc+=dc;
      }
    }
    // rooks / queens orthogonal
    const orth=[[-1,0],[1,0],[0,-1],[0,1]];
    for(let [dr,dc] of orth){
      let nr=r+dr,nc=c+dc;
      while(this.isOnBoard(nr,nc)){
        const idx=nr*8+nc;
        const p=this.board[idx];
        if(p){
          if(p.color===byColor && (p.type==='R' || p.type==='Q')) return true;
          break;
        }
        nr+=dr; nc+=dc;
      }
    }
    // king
    for(let dr=-1;dr<=1;dr++) for(let dc=-1;dc<=1;dc++){
      if(dr===0&&dc===0) continue;
      const nr=r+dr,nc=c+dc;
      if(this.isOnBoard(nr,nc)){
        const p=this.board[nr*8+nc];
        if(p && p.color===byColor && p.type==='K') return true;
      }
    }
    return false;
  }
  inCheck(color){
    const k=this.findKing(color);
    if(k===-1) return false;
    return this.isAttacked(k, color==='w'?'b':'w');
  }
  // generate pseudo moves
  pseudoMoves(){
    const moves=[];
    const us=this.turn;
    const them= us==='w'?'b':'w';
    for(let idx=0;idx<64;idx++){
      const p=this.board[idx];
      if(!p || p.color!==us) continue;
      const r=Math.floor(idx/8), c=idx%8;
      if(p.type==='P'){
        const dir = us==='w'?1:-1;
        const startRank = us==='w'?1:6;
        const promoRank = us==='w'?7:0;
        // forward 1
        let nr=r+dir;
        if(this.isOnBoard(nr,c) && !this.board[nr*8+c]){
          if(nr===promoRank){
            for(let pr of ['Q','R','B','N']) moves.push({from:idx,to:nr*8+c,promotion:pr,isCapture:false});
          } else {
            moves.push({from:idx,to:nr*8+c});
            // double
            if(r===startRank){
              let nr2=r+2*dir;
              if(!this.board[nr2*8+c]) moves.push({from:idx,to:nr2*8+c, isDouble:true});
            }
          }
        }
        // captures
        for(let dc of [-1,1]){
          const nc=c+dc;
          if(!this.isOnBoard(nr,nc)) continue;
          const to=nr*8+nc;
          const target=this.board[to];
          if(target && target.color===them){
            if(nr===promoRank){
              for(let pr of ['Q','R','B','N']) moves.push({from:idx,to, promotion:pr, isCapture:true});
            } else moves.push({from:idx,to,isCapture:true});
          }
          // en passant
          if(this.enPassant!==null && to===this.enPassant){
            moves.push({from:idx,to,isEnPassant:true,isCapture:true});
          }
        }
      } else if(p.type==='N'){
        const deltas=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
        for(let [dr,dc] of deltas){
          const nr=r+dr,nc=c+dc;
          if(!this.isOnBoard(nr,nc)) continue;
          const to=nr*8+nc;
          const t=this.board[to];
          if(!t) moves.push({from:idx,to});
          else if(t.color===them) moves.push({from:idx,to,isCapture:true});
        }
      } else if(p.type==='B' || p.type==='R' || p.type==='Q'){
        const dirs=[];
        if(p.type==='B' || p.type==='Q') dirs.push(...[[-1,-1],[-1,1],[1,-1],[1,1]]);
        if(p.type==='R' || p.type==='Q') dirs.push(...[[-1,0],[1,0],[0,-1],[0,1]]);
        for(let [dr,dc] of dirs){
          let nr=r+dr,nc=c+dc;
          while(this.isOnBoard(nr,nc)){
            const to=nr*8+nc;
            const t=this.board[to];
            if(!t) moves.push({from:idx,to});
            else {
              if(t.color===them) moves.push({from:idx,to,isCapture:true});
              break;
            }
            nr+=dr; nc+=dc;
          }
        }
      } else if(p.type==='K'){
        for(let dr=-1;dr<=1;dr++) for(let dc=-1;dc<=1;dc++){
          if(dr===0&&dc===0) continue;
          const nr=r+dr,nc=c+dc;
          if(!this.isOnBoard(nr,nc)) continue;
          const to=nr*8+nc;
          const t=this.board[to];
          if(!t) moves.push({from:idx,to});
          else if(t.color===them) moves.push({from:idx,to,isCapture:true});
        }
        // castling
        if(!this.inCheck(us)){
          const rank = us==='w'?0:7;
          // kingside
          if( (us==='w' && this.castling.K) || (us==='b' && this.castling.k)){
            const f= rank*8+5, g= rank*8+6;
            const rookIdx= rank*8+7;
            if(!this.board[f] && !this.board[g] && this.board[rookIdx] && this.board[rookIdx].type==='R'){
              if(!this.isAttacked(f, them) && !this.isAttacked(g, them)){
                moves.push({from:idx,to:g,isCastling:'K'});
              }
            }
          }
          if( (us==='w' && this.castling.Q) || (us==='b' && this.castling.q)){
            const d= rank*8+3, c1= rank*8+2, b= rank*8+1;
            const rookIdx= rank*8+0;
            if(!this.board[d] && !this.board[c1] && !this.board[b] && !this.board[rookIdx]){
              // Actually b can be occupied? standard requires b empty for queen side? No, only c,d must be empty; b can be anything? But spec says squares between empty, so c,d,b all empty for comprehensive. Common rule: squares between empty includes b. Keep check b empty.
              // Alternative: allow b occupied? but still castling requires b empty in official? For queen side, squares between king and rook are b,c,d. So all must be empty.
              // So check.
              if(!this.isAttacked(d, them) && !this.isAttacked(c1, them)){
                // also rook exists
                if(this.board[rookIdx] && this.board[rookIdx].type==='R')
                  moves.push({from:idx,to:c1,isCastling:'Q'});
              }
            } else if(!this.board[d] && !this.board[c1]){
              // fallback if b not required? Keep first condition, but handle b piece case: if b occupied then can't castle anyway
              // We'll not add extra
            }
          }
        }
      }
    }
    return moves;
  }
  legalMoves(){
    const pseudo=this.pseudoMoves();
    const legal=[];
    const us=this.turn;
    for(let m of pseudo){
      const state=this.makeMove(m, true); // test
      const kingStill = this.findKing(us);
      // after making move, turn has flipped, but check king of mover
      const attacked = this.isAttacked(kingStill, us==='w'?'b':'w');
      this.unmakeMove(state);
      if(!attacked) legal.push(m);
    }
    // move ordering: captures/promotions first
    legal.sort((a,b)=>{
      const ac = (a.isCapture?10:0)+(a.promotion?9:0)+(a.isCastling?1:0);
      const bc = (b.isCapture?10:0)+(b.promotion?9:0)+(b.isCastling?1:0);
      return bc-ac;
    });
    return legal;
  }
  makeMove(move, testMode=false){
    // save state
    const saved={
      board:this.cloneBoard(),
      turn:this.turn,
      castling:{...this.castling},
      enPassant:this.enPassant,
      halfmove:this.halfmove,
      fullmove:this.fullmove,
      captured: testMode?null: {w:[...this.captured.w], b:[...this.captured.b]},
      moveHistory: testMode?null:[...this.moveHistory]
    };
    const us=this.turn;
    const them=us==='w'?'b':'w';
    const fromPiece=this.board[move.from];
    let capturedPiece=null;
    let isCapture = !!move.isCapture;
    // en passant capture
    if(move.isEnPassant){
      const capIdx = us==='w'? move.to-8 : move.to+8;
      capturedPiece=this.board[capIdx];
      this.board[capIdx]=null;
    } else {
      capturedPiece=this.board[move.to];
    }
    // handle capture tracking
    if(!testMode && capturedPiece){
      // store captured for UI (store type)
      const capColor=capturedPiece.color;
      // captured by us -> push to our captured list? We maintain w = white captured black pieces etc? We'll map
      if(us==='w') this.captured.w.push(capturedPiece.type);
      else this.captured.b.push(capturedPiece.type);
    }
    // move piece
    this.board[move.to]= fromPiece;
    this.board[move.from]=null;
    // promotion
    if(move.promotion){
      this.board[move.to]={type:move.promotion, color:us};
    }
    // castling rook
    if(move.isCastling){
      const rank= us==='w'?0:7;
      if(move.isCastling==='K'){
        const rf= rank*8+7, rt= rank*8+5;
        this.board[rt]= this.board[rf];
        this.board[rf]=null;
      } else {
        const rf= rank*8+0, rt= rank*8+3;
        this.board[rt]= this.board[rf];
        this.board[rf]=null;
      }
    }
    // update castling rights
    if(fromPiece.type==='K'){
      if(us==='w'){ this.castling.K=false; this.castling.Q=false; }
      else { this.castling.k=false; this.castling.q=false; }
    }
    if(fromPiece.type==='R'){
      if(us==='w'){
        if(move.from===0) this.castling.Q=false;
        if(move.from===7) this.castling.K=false;
      } else {
        if(move.from===56) this.castling.q=false;
        if(move.from===63) this.castling.k=false;
      }
    }
    // if captured rook on original square, remove rights
    if(capturedPiece && capturedPiece.type==='R'){
      if(move.to===0) this.castling.Q=false;
      if(move.to===7) this.castling.K=false;
      if(move.to===56) this.castling.q=false;
      if(move.to===63) this.castling.k=false;
      // en passant capture rook?
      if(move.isEnPassant){
        const capIdx = us==='w'? move.to-8 : move.to+8;
        if(capIdx===0) this.castling.Q=false;
        if(capIdx===7) this.castling.K=false;
        if(capIdx===56) this.castling.q=false;
        if(capIdx===63) this.castling.k=false;
      }
    }
    // en passant square
    if(fromPiece.type==='P' && Math.abs(move.to - move.from)===16){
      this.enPassant = us==='w' ? move.from+8 : move.from-8;
    } else {
      this.enPassant=null;
    }
    // halfmove
    if(fromPiece.type==='P' || isCapture) this.halfmove=0;
    else this.halfmove++;
    // fullmove
    if(us==='b') this.fullmove++;
    this.turn=them;
    // SAN generation for history if not test
    if(!testMode){
      const san=this.moveToSAN(move, capturedPiece, saved);
      this.moveHistory.push(san);
    }
    return saved;
  }
  unmakeMove(saved){
    this.board=saved.board;
    this.turn=saved.turn;
    this.castling=saved.castling;
    this.enPassant=saved.enPassant;
    this.halfmove=saved.halfmove;
    this.fullmove=saved.fullmove;
    if(saved.captured){ this.captured=saved.captured; }
    if(saved.moveHistory){ this.moveHistory=saved.moveHistory; }
  }
  moveToSAN(move, capturedPiece, saved){
    const us=saved.turn;
    const piece=this.board[move.to] ? this.board[move.to] : saved.board[move.from];
    // actually after make, board has piece
    // castling
    if(move.isCastling) return move.isCastling==='K' ? 'O-O' : 'O-O-O';
    let san='';
    const isCap = !!capturedPiece || move.isEnPassant;
    // disambiguation
    if(piece.type!=='P'){
      san+=piece.type;
      // find other pieces that could move to same square
      // need to generate legal moves for disam? Use pseudo for same piece type
      // simplistic: check files/ranks
      const pseudo = (() => {
        // generate attacks for disam by brute force: see other pieces of same type that can go to target
        let others=[];
        for(let i=0;i<64;i++){
          if(i===move.from) continue;
          const p=saved.board[i];
          if(!p || p.color!==us || p.type!==piece.type) continue;
          // can p move to move.to ignoring check? Use ray logic simplified: check if move is in pseudo for that piece
          // brute: generate pseudo for that piece alone
          // We'll approximate by reusing pseudoMoves but filter
        }
        return others;
      })();
      // proper disambiguation: we compute legal moves before move
      // Instead compute attackers
      let candidates=[];
      // temporary generate legal moves before move to find disambiguation
      // We have saved board: generate pseudo from saved state, filter? Easier: brute check sliding
      for(let i=0;i<64;i++){
        if(i===move.from) continue;
        const p=saved.board[i];
        if(!p || p.color!==us || p.type!==piece.type) continue;
        if(this.canPieceMove(i, move.to, saved.board, saved.enPassant, saved.castling, isCap)) candidates.push(i);
      }
      if(candidates.length>0){
        const sameFile=candidates.some(c=> c%8===move.from%8);
        const sameRank=candidates.some(c=> Math.floor(c/8)===Math.floor(move.from/8));
        if(sameFile && sameRank) san+= this.idxToAlg(move.from);
        else if(sameFile) san+= (Math.floor(move.from/8)+1);
        else if(sameRank) san+= String.fromCharCode(97+ move.from%8);
        else san+= String.fromCharCode(97+ move.from%8);
      }
    } else {
      if(isCap) san+= String.fromCharCode(97+ move.from%8);
    }
    if(isCap) san+='x';
    san+= this.idxToAlg(move.to);
    if(move.promotion) san+='='+move.promotion;
    if(move.isEnPassant) san+=' e.p.';
    // check / mate
    const them= us==='w'?'b':'w';
    const kingPos=this.findKing(them);
    const inChk = kingPos!==-1 && this.isAttacked(kingPos, us);
    if(inChk){
      const lm=this.legalMoves(); // after move, legal moves for them
      san+= lm.length===0 ? '#' : '+';
    }
    return san;
  }
  canPieceMove(from,to,board,enPass,castling,isCap){
    // helper for disambiguation: can piece at from go to to ignoring king safety? approximate
    const p=board[from];
    if(!p) return false;
    const r=Math.floor(from/8), c=from%8;
    const tr=Math.floor(to/8), tc=to%8;
    const dr=tr-r, dc=tc-c;
    if(p.type==='N'){
      return (Math.abs(dr)===2 && Math.abs(dc)===1) || (Math.abs(dr)===1 && Math.abs(dc)===2);
    }
    if(p.type==='K'){
      return Math.max(Math.abs(dr),Math.abs(dc))===1;
    }
    if(p.type==='P'){
      const dir=p.color==='w'?1:-1;
      if(dc===0 && !isCap) return dr===dir || dr===2*dir;
      if(Math.abs(dc)===1 && dr===dir) return true;
      return false;
    }
    if(p.type==='B' || p.type==='R' || p.type==='Q'){
      const isDiag=Math.abs(dr)===Math.abs(dc);
      const isOrth=dr===0 || dc===0;
      if(p.type==='B' && !isDiag) return false;
      if(p.type==='R' && !isOrth) return false;
      if(p.type==='Q' && !(isDiag||isOrth)) return false;
      const stepR= dr===0?0: dr/Math.abs(dr);
      const stepC= dc===0?0: dc/Math.abs(dc);
      let nr=r+stepR, nc=c+stepC;
      while(nr!==tr || nc!==tc){
        if(board[nr*8+nc]) return false;
        nr+=stepR; nc+=stepC;
      }
      return true;
    }
    return false;
  }
  fen(){
    let fen='';
    for(let r=7;r>=0;r--){
      let empty=0;
      for(let c=0;c<8;c++){
        const p=this.board[r*8+c];
        if(!p) empty++;
        else{
          if(empty>0){ fen+=empty; empty=0; }
          const ch=p.type;
          fen+= p.color==='w'? ch : ch.toLowerCase();
        }
      }
      if(empty>0) fen+=empty;
      if(r>0) fen+='/';
    }
    fen+= ` ${this.turn} `;
    let cs='';
    if(this.castling.K) cs+='K';
    if(this.castling.Q) cs+='Q';
    if(this.castling.k) cs+='k';
    if(this.castling.q) cs+='q';
    if(cs==='') cs='-';
    fen+= cs+' ';
    fen+= this.enPassant!==null ? this.idxToAlg(this.enPassant) : '-';
    fen+= ` ${this.halfmove} ${this.fullmove}`;
    return fen;
  }
  loadFen(fen){
    const parts=fen.split(' ');
    const rows=parts[0].split('/');
    this.board=new Array(64).fill(null);
    for(let r=7, idx=0; r>=0; r--){
      const row=rows[7-r];
      let c=0;
      for(let ch of row){
        if(ch>='1'&& ch<='8'){ c+= parseInt(ch); }
        else{
          const color= ch===ch.toUpperCase()?'w':'b';
          const type= ch.toUpperCase();
          this.board[r*8+c]={type,color};
          c++;
        }
      }
    }
    this.turn=parts[1];
    const cs=parts[2];
    this.castling={K:cs.includes('K'), Q:cs.includes('Q'), k:cs.includes('k'), q:cs.includes('q')};
    this.enPassant= parts[3]!=='-' ? this.algToIdx(parts[3]) : null;
    this.halfmove=parseInt(parts[4]);
    this.fullmove=parseInt(parts[5]);
  }
  isInsufficient(){
    const pieces=this.board.filter(p=>p);
    if(pieces.length===2) return true;
    if(pieces.length===3){
      const minor=pieces.find(p=>p.type==='B'||p.type==='N');
      if(minor) return true;
    }
    return false;
  }
  gameOver(){
    const legal=this.legalMoves();
    if(legal.length===0){
      return this.inCheck(this.turn) ? {over:true, result: this.turn==='w'?'0-1':'1-0', reason:'checkmate'} : {over:true, result:'½-½', reason:'stalemate'};
    }
    if(this.isInsufficient()) return {over:true, result:'½-½', reason:'insufficient material'};
    if(this.halfmove>=100) return {over:true,result:'½-½',reason:'50-move rule'};
    return {over:false};
  }
  // for debug: move string e2e4 or e7e8q
  moveUCI(uci){
    if(uci.length<4) return false;
    const from=this.algToIdx(uci.slice(0,2));
    const to=this.algToIdx(uci.slice(2,4));
    const promo= uci.length>4 ? uci[4].toUpperCase() : null;
    const legal=this.legalMoves();
    const found=legal.find(m=> m.from===from && m.to===to && (promo? m.promotion===promo : !m.promotion || true));
    // if promo specified must match
    let target=null;
    if(promo) target=legal.find(m=> m.from===from && m.to===to && m.promotion===promo);
    else {
      // if multiple promotion options, default to queen
      const candidates=legal.filter(m=> m.from===from && m.to===to);
      if(candidates.length===0) return false;
      target=candidates[0];
    }
    if(!target) return false;
    this.makeMove(target);
    return true;
  }
}

/* =======================
   EVALUATION & AI
   ======================= */
function evaluate(engine){
  if(engine.isInsufficient()) return 0;
  let score=0;
  for(let i=0;i<64;i++){
    const p=engine.board[i];
    if(!p) continue;
    let val=0;
    if(p.type==='P') val=EVAL_WEIGHTS.P + PST.P[i];
    else if(p.type==='N') val=EVAL_WEIGHTS.N + PST.N[i];
    else if(p.type==='B') val=EVAL_WEIGHTS.B + PST.B[i];
    else if(p.type==='R') val=EVAL_WEIGHTS.R + PST.R[i];
    else if(p.type==='Q') val=EVAL_WEIGHTS.Q + PST.Q[i];
    else if(p.type==='K') val=EVAL_WEIGHTS.K + PST.K[i];
    // flip for black? PST defined for white. For black, mirror vertically
    if(p.color==='b'){
      const mirrored = (7 - Math.floor(i/8))*8 + (i%8);
      let pstVal=0;
      if(p.type==='P') pstVal=PST.P[mirrored];
      else if(p.type==='N') pstVal=PST.N[mirrored];
      else if(p.type==='B') pstVal=PST.B[mirrored];
      else if(p.type==='R') pstVal=PST.R[mirrored];
      else if(p.type==='Q') pstVal=PST.Q[mirrored];
      else if(p.type==='K') pstVal=PST.K[mirrored];
      // recompute val for black
      if(p.type==='P') val=EVAL_WEIGHTS.P + pstVal;
      else if(p.type==='N') val=EVAL_WEIGHTS.N + pstVal;
      else if(p.type==='B') val=EVAL_WEIGHTS.B + pstVal;
      else if(p.type==='R') val=EVAL_WEIGHTS.R + pstVal;
      else if(p.type==='Q') val=EVAL_WEIGHTS.Q + pstVal;
      else if(p.type==='K') val=EVAL_WEIGHTS.K + pstVal;
      score-=val;
    } else {
      score+=val;
    }
  }
  // mobility bonus
  const turn=engine.turn;
  engine.turn='w'; const wMob=engine.legalMoves().length;
  engine.turn='b'; const bMob=engine.legalMoves().length;
  engine.turn=turn;
  score+= (wMob - bMob)*EVAL_WEIGHTS.mobility;
  return engine.turn==='w' ? score : -score; // perspective of side to move? Actually we want white positive. For minimax we return from side to move? We'll keep white positive and minimax will handle.
}
function searchBest(engine, depth){
  // returns best move
  const legal=engine.legalMoves();
  if(legal.length===0) return null;
  // sort captures first already
  let best=null;
  let bestScore = -Infinity;
  // iterative evaluate each move
  for(let m of legal){
    const saved=engine.makeMove(m, true);
    const score = -negamax(engine, depth-1, -Infinity, Infinity);
    engine.unmakeMove(saved);
    if(score > bestScore){
      bestScore=score;
      best=m;
    }
  }
  return best;
}
function negamax(engine, depth, alpha, beta){
  const over=engine.gameOver();
  if(over.over){
    if(over.reason==='checkmate') return -99999 - depth; // losing
    return 0;
  }
  if(depth===0){
    // quiescence: only captures?
    // evaluation from white perspective, convert to side to move
    const ev=evaluate(engine);
    // evaluate returns white positive, but we need side to move perspective
    // evaluate already flips? Actually we made evaluate return white positive always, so convert:
    // white positive => if side to move white, positive is good; if black, positive is bad => -ev
    return engine.turn==='w' ? ev : -ev;
  }
  const moves=engine.legalMoves();
  // move ordering already captures first
  let best=-Infinity;
  for(let m of moves){
    const saved=engine.makeMove(m,true);
    const score= -negamax(engine, depth-1, -beta, -alpha);
    engine.unmakeMove(saved);
    if(score>best) best=score;
    if(best>alpha) alpha=best;
    if(alpha>=beta) break;
  }
  return best;
}

/* =======================
   UI / RENDERING
   ======================= */
const engine=new Engine();
let selected=null;
let legalTargets=new Map(); // to->move
let lastMove=null;
let flip=false;
let muted=false;
let playerColor='w';
let aiDepth=2;
let promoMove=null; // pending promotion move without promotion piece
let audioCtx=null;

const boardEl=document.getElementById('board');
const turnDot=document.getElementById('turnDot');
const turnText=document.getElementById('turnText');
const statusText=document.getElementById('statusText');
const moveList=document.getElementById('moveList');
const banner=document.getElementById('banner');
const capturedWhite=document.getElementById('capturedWhite');
const capturedBlack=document.getElementById('capturedBlack');
const boardWrap=document.getElementById('boardWrap');
const fenSmall=document.getElementById('fenSmall');
const evalPill=document.getElementById('evalPill');
const plyPill=document.getElementById('plyPill');
const thinkingEl=document.getElementById('thinking');

function ensureAudio(){
  if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
}
function playTone(freq, dur, type='sine', gain=0.15){
  if(muted) return;
  ensureAudio();
  const o=audioCtx.createOscillator();
  const g=audioCtx.createGain();
  o.type=type; o.frequency.value=freq;
  g.gain.value=gain;
  o.connect(g); g.connect(audioCtx.destination);
  o.start();
  g.gain.exponentialRampToValueAtTime(0.0001, audioCtx.currentTime+dur);
  o.stop(audioCtx.currentTime+dur);
}
function soundMove(isCap, isCheck){
  if(isCheck) { playTone(880,0.35,'square',0.12); setTimeout(()=>playTone(1100,0.2,'sine',0.12),120); }
  else if(isCap) playTone(220,0.12,'triangle',0.2);
  else playTone(440,0.08,'sine',0.08);
}

function squareColor(idx){
  const r=Math.floor(idx/8), c=idx%8;
  return (r+c)%2===0 ? 'dark' : 'light'; // a1 dark (0+0 even) => dark matches standard? Usually a1 dark? Actually a1 is dark square? In standard, a1 is dark? Wait h1 is light, a1 is dark? No, a1 is dark rook? Actually board: a1 is dark? No, a1 is dark square? Let's define: a1 is dark (black) corner is dark? Standard: white on right, h1 light, a1 dark. So our dark/light mapping: (r+c)%2==0 dark gives a1 dark correct. Good.
}
const pieceGlyph={ 'K':{w:'♔',b:'♚'}, 'Q':{w:'♕',b:'♛'}, 'R':{w:'♖',b:'♜'}, 'B':{w:'♗',b:'♝'}, 'N':{w:'♘',b:'♞'}, 'P':{w:'♙',b:'♟'} };

function render(){
  boardEl.innerHTML='';
  const legal= selected!==null ? engine.legalMoves().filter(m=>m.from===selected) : [];
  legalTargets.clear();
  legal.forEach(m=> legalTargets.set(m.to,m));
  const inChk=engine.inCheck(engine.turn);
  const kingPos=engine.findKing(engine.turn);
  const lastFrom=lastMove?lastMove.from:null, lastTo=lastMove?lastMove.to:null;
  for(let r=7;r>=0;r--){
    for(let c=0;c<8;c++){
      const idx=r*8+c;
      const sq=document.createElement('div');
      sq.className='sq '+(squareColor(idx)==='light'?'light':'dark');
      sq.dataset.idx=idx;
      if(idx===lastFrom || idx===lastTo) sq.classList.add('last');
      if(inChk && idx===kingPos) sq.classList.add('in-check');
      if(idx===selected) sq.classList.add('selected');
      if(legalTargets.has(idx)){
        const mv=legalTargets.get(idx);
        const isCap = !!engine.board[idx] || mv.isEnPassant;
        sq.classList.add(isCap?'can-capture':'can-move');
      }
      if((r===7 && c===0) || (r===0 && c===7) || true){
        // coordinates
        if(r===0){
          const f=document.createElement('span');
          f.className='coords file';
          f.textContent=String.fromCharCode(97+c);
          f.style.color= squareColor(idx)==='light' ? 'var(--dark)' : 'var(--light)';
          // adjust contrast
          sq.appendChild(f);
        }
        if(c===0){
          const rk=document.createElement('span');
          rk.className='coords rank';
          rk.textContent=(r+1);
          rk.style.color= squareColor(idx)==='light' ? 'var(--dark)' : 'var(--light)';
          sq.appendChild(rk);
        }
      }
      const p=engine.board[idx];
      if(p){
        const span=document.createElement('span');
        span.className='piece '+(p.color==='w'?'w':'b');
        span.textContent=pieceGlyph[p.type][p.color];
        span.draggable=true;
        span.dataset.idx=idx;
        span.addEventListener('dragstart', e=>{
          if(engine.gameOver().over) { e.preventDefault(); return; }
          if(playerColor!=='both' && p.color!==playerColor && engine.turn===playerColor) {
            // allow selecting opponent? No
          }
          e.dataTransfer.setData('text/plain', idx);
          setTimeout(()=>span.style.opacity='0.3',0);
        });
        span.addEventListener('dragend', ()=> span.style.opacity='1');
        sq.appendChild(span);
      }
      sq.addEventListener('click', ()=> onSquareClick(idx));
      sq.addEventListener('dragover', e=> e.preventDefault());
      sq.addEventListener('drop', e=>{
        e.preventDefault();
        const from=parseInt(e.dataTransfer.getData('text/plain'));
        if(isNaN(from)) return;
        tryMove(from, idx);
      });
      boardEl.appendChild(sq);
    }
  }
  // turn
  turnDot.className='dot '+(engine.turn==='w'?'':'black');
  turnText.textContent= (engine.turn==='w'?'White':'Black')+' to move';
  const over=engine.gameOver();
  if(over.over){
    banner.className='banner show '+(over.reason==='checkmate'?'mate':'draw');
    banner.textContent= over.reason==='checkmate' ? `Checkmate — ${over.result}` : `Draw — ${over.reason} (${over.result})`;
    statusText.textContent= over.reason;
  } else if(inChk){
    banner.className='banner show check';
    banner.textContent='Check!';
    statusText.textContent='check';
  } else {
    banner.className='banner';
    banner.textContent='';
    statusText.textContent= engine.turn==='w'?'White':'Black';
  }
  // move list
  moveList.innerHTML='';
  engine.moveHistory.forEach((san,i)=>{
    const li=document.createElement('li');
    const num=Math.floor(i/2)+1;
    const isWhite=i%2===0;
    li.textContent= (isWhite? num+'. ' : '') + san;
    if(i===engine.moveHistory.length-1) li.classList.add('active');
    moveList.appendChild(li);
  });
  document.getElementById('movesBox').scrollTop=9999;
  document.getElementById('moveCount').textContent= engine.moveHistory.length+' plies';
  // captured
  capturedBlack.innerHTML='<span class="label">Captured by White</span>'+ engine.captured.w.map(t=> `<span>${pieceGlyph[t].b}</span>`).join('');
  capturedWhite.innerHTML='<span class="label">Captured by Black</span>'+ engine.captured.b.map(t=> `<span>${pieceGlyph[t].w}</span>`).join('');
  fenSmall.textContent=engine.fen();
  plyPill.textContent='Ply '+ engine.moveHistory.length;
  const ev=evaluate(engine);
  const evStr = ev>0 ? `+${(ev/100).toFixed(1)} White` : ev<0 ? `${(ev/100).toFixed(1)} Black` : 'Even';
  evalPill.textContent=evStr;
  // flip class
  document.getElementById('boardCard').classList.toggle('flip', flip);
  document.getElementById('flipToggle').classList.toggle('on', flip);
  saveState();
}
function onSquareClick(idx){
  const over=engine.gameOver();
  if(over.over) return;
  const p=engine.board[idx];
  // if we have selection and clicked legal target -> move
  if(selected!==null && legalTargets.has(idx)){
    tryMove(selected, idx);
    return;
  }
  // otherwise select piece of side to move
  if(p && p.color===engine.turn){
    // if analysis mode or it's player's turn? Allow selection always but AI move guard later
    if(playerColor!=='both' && engine.turn!==playerColor){
      // it's AI turn, don't allow human selection? But allow if both
      // still prevent
      return;
    }
    selected=idx;
    render();
  } else {
    selected=null;
    render();
  }
}
function tryMove(from,to){
  const legal=engine.legalMoves();
  const candidates=legal.filter(m=> m.from===from && m.to===to);
  if(candidates.length===0){
    // maybe selected wrong, select new
    const p=engine.board[to];
    if(p && p.color===engine.turn) { selected=to; render(); } else { selected=null; render(); }
    return;
  }
  // promotion needs picker if multiple
  if(candidates.length>1 && candidates[0].promotion){
    showPromotionPicker(from,to,candidates);
    return;
  }
  const move=candidates[0];
  executeMove(move);
}
function showPromotionPicker(from,to,moves){
  promoMove={from,to,moves};
  // create overlay near square
  const anchor=document.getElementById('promoAnchor');
  anchor.innerHTML='';
  anchor.style.pointerEvents='auto';
  const promo=document.createElement('div');
  promo.className='promo';
  // position over target square: compute square rect
  const sqEl=[...boardEl.children].find(el=> parseInt(el.dataset.idx)===to);
  const wrapRect=boardWrap.getBoundingClientRect();
  const sqRect=sqEl.getBoundingClientRect();
  const left= sqRect.left - wrapRect.left;
  const top= sqRect.top - wrapRect.top;
  promo.style.left= left+'px';
  promo.style.top= (top - 10)+'px';
  const isWhite=engine.turn==='w';
  const options=['Q','R','B','N'];
  options.forEach(t=>{
    const btn=document.createElement('button');
    btn.textContent= pieceGlyph[t][isWhite?'w':'b'];
    btn.title=t;
    btn.onclick=()=>{
      anchor.innerHTML='';
      anchor.style.pointerEvents='none';
      const chosen=moves.find(m=>m.promotion===t);
      executeMove(chosen);
    };
    promo.appendChild(btn);
  });
  anchor.appendChild(promo);
}
function executeMove(move){
  const us=engine.turn;
  const wasCapture= !!engine.board[move.to] || move.isEnPassant;
  const savedTurn=engine.turn;
  const state=engine.makeMove(move);
  lastMove=move;
  selected=null;
  const inChk=engine.inCheck(engine.turn);
  soundMove(wasCapture, inChk);
  render();
  // check AI turn
  setTimeout(maybeAiMove, 250);
}
function maybeAiMove(){
  const over=engine.gameOver();
  if(over.over) return;
  if(playerColor==='both') return;
  if(engine.turn===playerColor) return;
  thinkingEl.style.display='inline';
  // run search chunked via setTimeout
  setTimeout(()=>{
    const best=searchBest(engine, aiDepth);
    thinkingEl.style.display='none';
    if(!best) { render(); return; }
    const wasCap= !!engine.board[best.to] || best.isEnPassant;
    engine.makeMove(best);
    lastMove=best;
    const inChk=engine.inCheck(engine.turn);
    soundMove(wasCap, inChk);
    render();
  }, 40);
}

/* persistence */
function saveState(){
  const data={
    fen:engine.fen(),
    moveHistory:engine.moveHistory,
    captured:engine.captured,
    lastMove,
    flip, playerColor, aiDepth
  };
  localStorage.setItem('atlas_chess_v2', JSON.stringify(data));
}
function loadState(){
  const raw=localStorage.getItem('atlas_chess_v2');
  if(!raw) return;
  try{
    const d=JSON.parse(raw);
    if(d.fen) engine.loadFen(d.fen);
    if(d.moveHistory) engine.moveHistory=d.moveHistory;
    if(d.captured) engine.captured=d.captured;
    if(d.lastMove) lastMove=d.lastMove;
    if(typeof d.flip==='boolean') flip=d.flip;
    if(d.playerColor) playerColor=d.playerColor;
    if(d.aiDepth) aiDepth=d.aiDepth;
    // sync selects
    document.getElementById('colorSel').value=playerColor;
    document.getElementById('depthSel').value=String(aiDepth);
  }catch(e){}
}

/* controls */
document.getElementById('newBtn').onclick=()=>{
  engine.reset();
  lastMove=null; selected=null; promoMove=null;
  document.getElementById('promoAnchor').innerHTML='';
  render();
  saveState();
};
document.getElementById('undoBtn').onclick=()=>{
  // undo both player and AI if needed: pop two moves? spec: takes back both player and AI move
  // We'll undo up to 2 ply if AI involved, else 1
  const hist=engine.history; // not used
  // We can rebuild from moveHistory by resetting and replaying?
  // Simpler: we maintain stack via engine states? We didn't push history stack properly for undo via makeMove saved states.
  // Instead we store FENs for each move: easier to rebuild by replaying SAN? Alternative store board snapshots array.
  // We'll implement undo by restoring from localStorage stack? Let's maintain undo stack in engine via array of saved states.
  // Our engine.makeMove returns saved snapshot but we discard. Let's instead push onto array.
  // For now, implement undo by loading previous FENs from history of FENs we can reconstruct by resetting and replaying moves minus 1 or 2.
  const n= engine.moveHistory.length;
  if(n===0) return;
  const toRemove = (playerColor==='both') ? 1 : 2;
  const keep = Math.max(0, n - toRemove);
  // replay
  const movesToKeep = engine.moveHistory.slice(0, keep);
  // Need to replay using SAN? We'll replay by re-generating from start using stored FEN? Easier: reset and replay via moveUCI? But SAN not UCI.
  // Instead we saved FEN before moves? Let's brute: reset and replay by storing move objects? We don't have them.
  // Alternative: maintain fenStack array.
  if(!window._fenStack) window._fenStack=[];
  // Fallback: if we have fenStack, pop
  if(window._fenStack && window._fenStack.length>0){
    // _fenStack holds FEN after each move plus initial
    const targetIdx = Math.max(0, window._fenStack.length - 1 - toRemove);
    const fen = window._fenStack[targetIdx];
    // truncate stack
    window._fenStack = window._fenStack.slice(0, targetIdx+1);
    engine.loadFen(fen);
    engine.moveHistory = engine.moveHistory.slice(0, keep);
    // captured rebuild: not perfect but restore from fen doesn't have captured. We'll reset captured based on material diff? Simplify clear and rebuild from moves? For now reset captured and recompute from board diff vs start? Approx: clear
    // We'll keep captured as stored truncated? Let's store capturedStack too.
    if(window._capStack) { engine.captured = JSON.parse(JSON.stringify(window._capStack[targetIdx] || {w:[],b:[]})) ; window._capStack=window._capStack.slice(0,targetIdx+1); }
    lastMove=null;
    selected=null;
    render();
    return;
  }
  // if no stack, just reset
  engine.reset();
  render();
};
document.getElementById('flipBtn').onclick=()=>{ flip=!flip; render(); };
document.getElementById('flipToggle').onclick=()=>{ flip=!flip; render(); };
document.getElementById('muteBtn').onclick=(e)=>{
  muted=!muted;
  e.target.textContent= muted? '🔇 Muted' : '🔊 Sound';
};
document.getElementById('colorSel').onchange=(e)=>{
  playerColor=e.target.value;
  saveState();
  render();
  maybeAiMove();
};
document.getElementById('depthSel').onchange=(e)=>{
  aiDepth=parseInt(e.target.value);
  saveState();
};

// debug interface
window.game={
  fen:()=>engine.fen(),
  move:(uci)=>{ const ok=engine.moveUCI(uci); if(ok){ lastMove={from:engine.algToIdx(uci.slice(0,2)), to:engine.algToIdx(uci.slice(2,4))}; render(); } return ok; },
  legalMoves:()=>engine.legalMoves().map(m=> engine.idxToAlg(m.from)+engine.idxToAlg(m.to)+(m.promotion?m.promotion.toLowerCase():'')),
  engine,
  reset:()=>{ engine.reset(); render(); },
  board:()=>engine.board
};

// maintain fen stack for undo
window._fenStack=[engine.fen()];
window._capStack=[JSON.parse(JSON.stringify(engine.captured))];
const origMake=engine.makeMove.bind(engine);
engine.makeMove=function(m,test){
  const res=origMake(m,test);
  if(!test){
    window._fenStack.push(engine.fen());
    window._capStack.push(JSON.parse(JSON.stringify(engine.captured)));
  }
  return res;
};

// init
loadState();
if(engine.moveHistory.length===0){ window._fenStack=[engine.fen()]; window._capStack=[JSON.parse(JSON.stringify(engine.captured))]; }
else {
  // rebuild stacks from current state? For reload, recreate stacks length = moves+1 but we only have final. To enable undo after reload, start fresh stack
  window._fenStack=[new Engine().fen(), engine.fen()];
  window._capStack=[{w:[],b:[]}, JSON.parse(JSON.stringify(engine.captured))];
}
render();
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2","provider":"openrouter","persona":"basic","sessionId":"0b76a854-6fcb-429f-88ff-2222963a7246","tokensIn":1137,"tokensOut":18670,"tokensTotal":19807,"cost":0.08076875,"turns":1,"toolCalls":0,"failedToolCalls":0,"timestamp":"2026-08-06T15:07:45.634Z"} -->