← Chess results

Chess

GLM 5.3 Flash · full

Initial view of Chess

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 32,960 bytes · SHA-256 541317f46185
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — vs AI</title>
<style>
/* ============================ THEME / CONSTANTS ============================ */
:root{
  --light:#e8d3ae; --dark:#a97a4e;
  --light2:#f3e6c8; --dark2:#8f6238;
  --sel:#f2d24b88; --last:#e6c34a55; --check:#ff4d3a;
  --panel:#241d16; --panel2:#332a20; --text:#efe4d0; --muted:#b7a888;
  --accent:#d9a441;
}
*{box-sizing:border-box;margin:0;padding:0}
body{
  font-family:Georgia,'Times New Roman',serif;
  background:radial-gradient(1200px 800px at 50% -10%,#3a3128,#191410 70%);
  color:var(--text);min-height:100vh;display:flex;align-items:center;justify-content:center;padding:18px;
}
h1{font-size:20px;letter-spacing:2px;text-transform:uppercase;color:var(--accent);font-weight:normal;margin-bottom:10px}
.wrap{display:flex;gap:22px;align-items:flex-start;flex-wrap:wrap;justify-content:center}
.board-col{display:flex;flex-direction:column;align-items:center;gap:10px}
#board{
  display:grid;grid-template-columns:repeat(8,64px);grid-template-rows:repeat(8,64px);
  border:6px solid #17110c;border-radius:6px;box-shadow:0 14px 40px #000a, 0 0 0 1px #000;
  position:relative;user-select:none;touch-action:none;
}
.sq{width:64px;height:64px;display:flex;align-items:center;justify-content:center;position:relative;cursor:default}
.sq.light{background:var(--light)} .sq.dark{background:var(--dark)}
.sq .pc{font-size:48px;line-height:1;cursor:grab;filter:drop-shadow(0 2px 2px #0007);transition:transform .08s}
.sq .pc.w{color:#faf6ec;text-shadow:0 0 2px #000,0 1px 0 #000}
.sq .pc.b{color:#1d1712;text-shadow:0 0 2px #fff3}
.sq.sel{box-shadow:inset 0 0 0 4px var(--sel)}
.sq.last{outline:inset 0 0 0 4px var(--last)}
.sq.check::after{content:'';position:absolute;inset:0;border-radius:50%;background:radial-gradient(circle,var(--check) 15%,transparent 70%);opacity:.85;animation:pulse 1s infinite alternate}
@keyframes pulse{from{opacity:.55}to{opacity:.95}}
.dot{width:20px;height:20px;border-radius:50%;background:#00000042;box-shadow:inset 0 0 0 2px #00000030;position:absolute;pointer-events:none}
.ring{width:56px;height:56px;border-radius:50%;border:5px solid #00000045;position:absolute;pointer-events:none}
.coord{position:absolute;font-size:10px;color:#0007;font-family:sans-serif}
.coord.f{bottom:2px;right:4px}.coord.r{top:2px;left:4px}
.sq.light .coord{color:#00000066}.sq.dark .coord{color:#ffffff88}
/* promotion picker */
#promo{position:absolute;display:none;flex-direction:column;background:var(--panel2);border:2px solid var(--accent);border-radius:8px;z-index:30;box-shadow:0 8px 24px #000c}
#promo button{width:56px;height:56px;font-size:40px;background:none;border:none;cursor:pointer;color:var(--text)}
#promo button:hover{background:var(--accent);color:#191410}
/* side panel */
.panel{width:300px;display:flex;flex-direction:column;gap:12px}
.card{background:var(--panel);border:1px solid #000;border-radius:10px;padding:12px 14px;box-shadow:0 6px 20px #0008}
.card h2{font-size:12px;letter-spacing:2px;text-transform:uppercase;color:var(--muted);margin-bottom:8px;font-weight:normal}
#status{font-size:17px;min-height:24px}
#status .turn{color:var(--accent)}
#status.over{color:#ff8d7a;font-size:19px}
#movelist{height:180px;overflow-y:auto;font-family:ui-monospace,Consolas,monospace;font-size:14px;line-height:1.7}
#movelist .row{display:grid;grid-template-columns:34px 1fr 1fr;gap:6px}
#movelist .no{color:var(--muted)}
#movelist .mv{cursor:default;padding:0 4px;border-radius:4px}
#movelist .mv.cur{background:var(--accent);color:#191410}
.tray{min-height:26px;font-size:26px;line-height:1;letter-spacing:2px;display:flex;flex-wrap:wrap;gap:1px}
.tray .w{color:#faf6ec;text-shadow:0 0 2px #000}.tray .b{color:#221b14;text-shadow:0 0 2px #fff4}
label{font-size:13px;color:var(--muted)}
select,button.btn{
  font-family:inherit;background:var(--panel2);color:var(--text);border:1px solid #00000088;
  border-radius:6px;padding:6px 10px;font-size:14px;cursor:pointer
}
button.btn:hover,select:hover{border-color:var(--accent)}
.controls{display:flex;flex-wrap:wrap;gap:8px}
.banner{
  position:absolute;inset:0;display:none;align-items:center;justify-content:center;z-index:40;
  background:#0c0906d9;border-radius:4px;text-align:center
}
.banner .msg{font-size:26px;color:var(--accent);letter-spacing:1px;padding:20px}
.banner .sub{font-size:14px;color:var(--muted);margin-top:8px}
.banner button{margin-top:18px}
#thinking{font-size:12px;color:var(--muted);min-height:16px}
@media(max-width:760px){#board{grid-template-columns:repeat(8,44px);grid-template-rows:repeat(8,44px)}.sq{width:44px;height:44px}.sq .pc{font-size:33px}.dot{width:14px;height:14px}.ring{width:38px;height:38px;border-width:4px}}
</style>
</head>
<body>
<div class="wrap">
  <div class="board-col">
    <h1>Chess</h1>
    <div id="board"></div>
  </div>
  <div class="panel">
    <div class="card">
      <div id="status"></div>
      <div id="thinking"></div>
    </div>
    <div class="card">
      <h2>Captured</h2>
      <label>by you</label><div class="tray" id="trayTop"></div>
      <label>by opponent</label><div class="tray" id="trayBot"></div>
    </div>
    <div class="card">
      <h2>Moves</h2>
      <div id="movelist"></div>
    </div>
    <div class="card controls">
      <button class="btn" id="btnNew">New Game</button>
      <button class="btn" id="btnUndo">Undo</button>
      <button class="btn" id="btnFlip">Flip</button>
      <button class="btn" id="btnMute">🔊</button>
      <label>Side
        <select id="selSide">
          <option value="w">Play White</option>
          <option value="b">Play Black</option>
          <option value="2">Two players</option>
        </select>
      </label>
      <label>Level
        <select id="selDepth">
          <option value="1">Easy</option>
          <option value="2" selected>Medium</option>
          <option value="3">Hard</option>
        </select>
      </label>
    </div>
  </div>
</div>
<script>
"use strict";
/* ================================================================
   TUNABLE CONSTANTS
   ================================================================ */
const CFG = {
  square: 64,
  light:'#e8d3ae', dark:'#a97a4e',
  aiDepths:{1:1, 2:2, 3:3},
  aiDelayMs: 60,          // breathing room so UI paints before search
  qDepth: 6,              // quiescence plies
  val:{1:100,2:320,3:330,4:500,5:900,6:0}, // P N B R Q K
  mate:100000
};
const GLYPH = {1:'♟',2:'♞',3:'♝',4:'♜',5:'♛',6:'♚'};
const SAN_PIECE = {2:'N',3:'B',4:'R',5:'Q',6:'K'};

/* ================================================================
   ENGINE — 0x88 board representation
   piece = type(1..6) | color(8=white,0=black). EMPTY=0.
   square index = rank*16 + file ; off-board iff (sq & 0x88)
   ================================================================ */
const P=1,N=2,B=3,R=4,Q=5,K=6, WHITE=8, BLACK=0;
const EMPTY=0;
const KNIGHT_OFF=[31,33,14,18,-31,-33,-14,-18];
const KING_OFF=[1,-1,16,-16,15,17,-15,-17];
const BISHOP_DIR=[15,17,-15,-17];
const ROOK_DIR=[1,-1,16,-16];
const FFLAG={EP:1,CASTLE:2,DOUBLE:4};

function sqName(sq){ return "abcdefgh"[sq&7] + ((sq>>4)+1); }
function nameSq(s){ return (s.charCodeAt(0)-97) + ((s.charCodeAt(1)-49)<<4); }
function colorOf(p){ return p & 8; }

function Chess(){
  this.board = new Int8Array(128);
  this.turn = WHITE;
  this.castle = {K:true,Q:true,k:true,q:true};
  this.ep = -1;            // en-passant target square or -1
  this.half = 0;           // 50-move clock
  this.full = 1;
  this.hist = [];          // {move, captured, castle:{...}, ep, half}
  this.sanHist = [];
}
Chess.prototype.setup = function(){
  const back=[R,N,B,Q,K,B,N,R];
  this.board.fill(EMPTY);
  for(let f=0;f<8;f++){
    this.board[f]        = back[f]|WHITE;
    this.board[16+f]     = P|WHITE;
    this.board[96+f]     = P|BLACK;
    this.board[112+f]    = back[f]|BLACK;
  }
  this.turn=WHITE; this.castle={K:true,Q:true,k:true,q:true};
  this.ep=-1; this.half=0; this.full=1;
  this.hist=[]; this.sanHist=[];
};

/* --- attack detection: is `sq` attacked by side `by`? --- */
Chess.prototype.attacked = function(sq, by){
  const b=this.board;
  // pawns
  if(by===WHITE){
    for(const d of [-15,-17]){ const s=sq+d; if(!(s&0x88) && b[s]===(P|WHITE)) return true; }
  } else {
    for(const d of [15,17]){ const s=sq+d; if(!(s&0x88) && b[s]===(P|BLACK)) return true; }
  }
  for(const d of KNIGHT_OFF){ const s=sq+d; if(!(s&0x88) && b[s]===((N)|by)) return true; }
  for(const d of KING_OFF){ const s=sq+d; if(!(s&0x88) && b[s]===((K)|by)) return true; }
  for(const d of BISHOP_DIR){
    let s=sq+d;
    while(!(s&0x88)){ const p=b[s]; if(p){ if(colorOf(p)===by && (p&7)===B || (p&7)===Q) return true; break;} s+=d; }
  }
  for(const d of ROOK_DIR){
    let s=sq+d;
    while(!(s&0x88)){ const p=b[s]; if(p){ if(colorOf(p)===by && ((p&7)===R || (p&7)===Q)) return true; break;} s+=d; }
  }
  return false;
};
Chess.prototype.kingSq = function(c){
  const t=K|c, b=this.board;
  for(let r=0;r<8;r++)for(let f=0;f<8;f++){ const s=r*16+f; if(b[s]===t) return s; }
  return -1;
};
Chess.prototype.inCheck = function(c){ c=c===undefined?this.turn:c; const k=this.kingSq(c); return k>=0 && this.attacked(k, c===WHITE?BLACK:WHITE); };

/* --- pseudo-legal move generation --- */
Chess.prototype.pseudoMoves = function(color){
  const b=this.board, ms=[];
  const add=(from,to,flags,promo)=>{
    ms.push({from,to,piece:b[from],captured: flags&FFLAG.EP ? (P|(color===WHITE?BLACK:WHITE)) : b[to], promo:promo||0, flags:flags||0});
  };
  for(let r=0;r<8;r++)for(let f=0;f<8;f++){
    const s=r*16+f, p=b[s];
    if(!p || colorOf(p)!==color) continue;
    const t=p&7;
    if(t===P){
      const dir = color===WHITE?16:-16, startR = color===WHITE?1:6, promoR = color===WHITE?7:0;
      const one=s+dir;
      if(!(one&0x88) && !b[one]){
        if((one>>4)===promoR){ for(const pr of [Q,R,B,N]) add(s,one,0,pr); }
        else{
          add(s,one,0);
          const two=s+2*dir;
          if(r===startR && !b[two]) add(s,two,FFLAG.DOUBLE);
        }
      }
      for(const dc of [dir-1,dir+1]){
        const ts=s+dc; if(ts&0x88) continue;
        const tp=b[ts];
        if(tp && colorOf(tp)!==color){
          if((ts>>4)===promoR){ for(const pr of [Q,R,B,N]) add(s,ts,0,pr); } else add(s,ts,0);
        } else if(ts===this.ep && this.ep>=0){
          add(s,ts,FFLAG.EP);
        }
      }
    } else if(t===N || t===K){
      for(const d of (t===N?KNIGHT_OFF:KING_OFF)){
        const ts=s+d; if(ts&0x88) continue;
        const tp=b[ts];
        if(!tp || colorOf(tp)!==color) add(s,ts,0);
      }
      if(t===K){ // castling
        const home = color===WHITE?4:116;
        if(s===home && !this.attacked(home, color===WHITE?BLACK:WHITE)){
          const ck = color===WHITE?this.castle.K:this.castle.k;
          const cq = color===WHITE?this.castle.Q:this.castle.q;
          const enemy = color===WHITE?BLACK:WHITE;
          if(ck && !b[home+1] && !b[home+2] && b[home+3]===(R|color)
             && !this.attacked(home+1,enemy) && !this.attacked(home+2,enemy))
            add(s,home+2,FFLAG.CASTLE);
          if(cq && !b[home-1] && !b[home-2] && !b[home-3] && b[home-4]===(R|color)
             && !this.attacked(home-1,enemy) && !this.attacked(home-2,enemy))
            add(s,home-2,FFLAG.CASTLE);
        }
      }
    } else {
      const dirs = t===B?BISHOP_DIR : t===R?ROOK_DIR : KING_OFF;
      for(const d of dirs){
        let ts=s+d;
        while(!(ts&0x88)){
          const tp=b[ts];
          if(!tp) add(s,ts,0);
          else { if(colorOf(tp)!==color) add(s,ts,0); break; }
          ts+=d;
        }
      }
    }
  }
  return ms;
};

/* --- make / unmake --- */
Chess.prototype.make = function(m){
  const b=this.board, c=colorOf(m.piece);
  this.hist.push({move:m, captured:m.captured, castle:{...this.castle}, ep:this.ep, half:this.half});
  b[m.from]=EMPTY;
  b[m.to]= m.promo ? (m.promo|c) : m.piece;
  if(m.flags & FFLAG.EP) b[m.to + (c===WHITE?-16:16)] = EMPTY;
  if(m.flags & FFLAG.CASTLE){
    const d = m.to>m.from ? 1 : -1;
    const rf = d===1 ? m.to+1 : m.to-2;   // rook from (h-file / a-file)
    const rt = m.to - d;                  // rook to (f-file / d-file)
    b[rt]=b[rf]; b[rf]=EMPTY;
  }
  // castling rights
  if((m.piece&7)===K){ if(c===WHITE){this.castle.K=this.castle.Q=false;} else {this.castle.k=this.castle.q=false;} }
  if(m.from===7||m.to===7) this.castle.K=false;
  if(m.from===0||m.to===0) this.castle.Q=false;
  if(m.from===119||m.to===119) this.castle.k=false;
  if(m.from===112||m.to===112) this.castle.q=false;
  // ep square
  this.ep = (m.flags & FFLAG.DOUBLE) ? (m.from + (c===WHITE?16:-16)) : -1;
  // clocks
  if((m.piece&7)===P || m.captured) this.half=0; else this.half++;
  if(c===BLACK) this.full++;
  this.turn = c===WHITE?BLACK:WHITE;
};
Chess.prototype.unmake = function(){
  const h=this.hist.pop(); if(!h) return null;
  const m=h.move, b=this.board, c=colorOf(m.piece);
  b[m.from]=m.piece;
  b[m.to]=EMPTY;
  if(m.flags & FFLAG.EP) b[m.to + (c===WHITE?-16:16)] = h.captured;
  else if(h.captured) b[m.to]=h.captured;
  if(m.flags & FFLAG.CASTLE){
    const d = m.to>m.from ? 1 : -1;
    const rf = d===1 ? m.to+1 : m.to-2;
    const rt = m.to - d;
    b[rf]=b[rt]; b[rt]=EMPTY;
  }
  this.castle=h.castle; this.ep=h.ep; this.half=h.half;
  if(c===BLACK) this.full--;
  this.turn=c;
  return m;
};

/* --- legal moves (generator-based: UI only ever sees these) --- */
Chess.prototype.legalMoves = function(){
  const c=this.turn, out=[];
  for(const m of this.pseudoMoves(c)){
    this.make(m);
    const k=this.kingSq(c);
    if(!this.attacked(k, c===WHITE?BLACK:WHITE)) out.push(m);
    this.unmake();
  }
  return out;
};
Chess.prototype.movesFrom = function(from){
  return this.legalMoves().filter(m=>m.from===from);
};

/* --- FEN --- */
Chess.prototype.fen = function(){
  let rows=[];
  for(let r=7;r>=0;r--){
    let row='',e=0;
    for(let f=0;f<8;f++){
      const p=this.board[r*16+f];
      if(!p){e++;continue;}
      if(e){row+=e;e=0;}
      const ch=' pnbrqk'[(p&7)];
      row += (p&8)? ch.toUpperCase() : ch;
    }
    if(e)row+=e;
    rows.push(row);
  }
  const c = (this.castle.K?'K':'')+(this.castle.Q?'Q':'')+(this.castle.k?'k':'')+(this.castle.q?'q':'');
  return rows.join('/')+' '+(this.turn===WHITE?'w':'b')+' '+(c||'-')+' '+(this.ep>=0?sqName(this.ep):'-')+' '+this.half+' '+this.full;
};
Chess.prototype.loadFen = function(fen){
  this.board.fill(EMPTY);
  this.turn=WHITE; this.castle={K:true,Q:true,k:true,q:true};
  this.ep=-1; this.half=0; this.full=1; this.hist=[]; this.sanHist=[];
  const parts=fen.split(' ');
  const rows=parts[0].split('/');
  for(let i=0;i<8;i++){
    let r=7-i,f=0;
    for(const ch of rows[i]){
      if(/\d/.test(ch)){f+=+ch;continue;}
      const up=ch===ch.toUpperCase();
      const t=' pnbrqk'.indexOf(ch.toLowerCase());
      this.board[r*16+f]=t|(up?WHITE:BLACK); f++;
    }
  }
  this.turn = parts[1]==='w'?WHITE:BLACK;
  this.castle={K:parts[2].includes('K'),Q:parts[2].includes('Q'),k:parts[2].includes('k'),q:parts[2].includes('q')};
  this.ep = parts[3]==='-'?-1:nameSq(parts[3]);
  this.half=+parts[4]||0; this.full=+parts[5]||1;
};

/* --- SAN --- */
Chess.prototype.san = function(m, legal){
  legal = legal || this.legalMoves();
  let s;
  if(m.flags & FFLAG.CASTLE){
    s = (m.to & 7)===6 ? 'O-O' : 'O-O-O';
  } else {
    const t=m.piece&7;
    if(t===P){
      s = m.captured ? "abcdefgh"[m.from&7]+'x'+sqName(m.to) : sqName(m.to);
      if(m.promo) s+='='+' NBRQ'[m.promo===2?'N':0]||''; // fixed below
      if(m.promo) s = s.replace(/=[^=]*$/,'=' + ({2:'N',3:'B',4:'R',5:'Q'})[m.promo]);
      if(m.flags & FFLAG.EP) s+=' e.p.';
    } else {
      s = SAN_PIECE[t];
      const others = legal.filter(x=>x!==m && (x.piece&7)===t && x.to===m.to && x.from!==m.from);
      if(others.length){
        const sameFile = others.some(x=>(x.from&7)===(m.from&7));
        const sameRank = others.some(x=>(x.from>>4)===(m.from>>4));
        if(!sameFile) s+="abcdefgh"[m.from&7];
        else if(!sameRank) s+=''+((m.from>>4)+1);
        else s+=sqName(m.from);
      }
      if(m.captured) s+='x';
      s+=sqName(m.to);
    }
  }
  this.make(m);
  const oppLegal=this.legalMoves();
  if(this.inCheck()) s += oppLegal.length ? '+' : '#';
  else if(!oppLegal.length) {/*stalemate: no suffix*/}
  this.unmake();
  return s;
};

/* --- game state --- */
Chess.prototype.insufficientMaterial = function(){
  const pieces=[];
  for(let r=0;r<8;r++)for(let f=0;f<8;f++){const p=this.board[r*16+f]; if(p&&(p&7)!==K) pieces.push(p&7);}
  if(pieces.length===0) return true;                 // K vs K
  if(pieces.length===1 && (pieces[0]===B||pieces[0]===N)) return true; // K+minor vs K
  return false;
};
Chess.prototype.gameResult = function(){
  const legal=this.legalMoves();
  if(!legal.length) return this.inCheck() ? {over:true,result:'checkmate',winner:this.turn===WHITE?BLACK:WHITE} : {over:true,result:'stalemate'};
  if(this.insufficientMaterial()) return {over:true,result:'insufficient material'};
  if(this.half>=100) return {over:true,result:'50-move rule'};
  return {over:false};
};

/* ================================================================
   AI — negamax + alpha-beta, capture-first ordering, quiescence
   ================================================================ */
/* piece-square tables, from White's perspective, index (7-r)*8+f i.e. rank8 first */
const PST = {
  1:[ 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],
  2:[-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],
  3:[-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],
  4:[  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],
  5:[-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],
  6:[-30,-40,-40,-50,-50,-40,-40,-30,
     -30,-40,-40,-50,-50,-40,-40,-30,
     -30,-40,-40,-50,-50,-40,-40,-30,
     -30,-40,-40,-50,-50,-40,-40,-30,
     -20,-30,-30,-40,-40,-30,-30,-20,
     -10,-20,-20,-20,-20,-20,-20,-10,
      20, 20,  0,  0,  0,  0, 20, 20,
      20, 30, 10,  0,  0, 10, 30, 20]
};
function pstIdx(sq, color){
  const r=sq>>4, f=sq&7;
  return color===WHITE ? (7-r)*8+f : r*8+f;
}
Chess.prototype.evaluate = function(){
  let score=0;
  for(let r=0;r<8;r++)for(let f=0;f<8;f++){
    const p=this.board[r*16+f]; if(!p) continue;
    const t=p&7, c=p&8;
    const v = CFG.val[t] + PST[t][pstIdx(r*16+f,c)];
    score += c===WHITE ? v : -v;
  }
  return this.turn===WHITE ? score : -score;
};
function moveScore(m){
  let s=0;
  if(m.captured) s = 10*CFG.val[m.captured&7] - CFG.val[m.piece&7] + 1000;
  if(m.promo) s += CFG.val[m.promo];
  return s;
}
Chess.prototype.qsearch = function(alpha, beta, depth){
  const stand = this.evaluate();
  if(depth<=0) return stand;
  if(stand>=beta) return beta;
  if(stand>alpha) alpha=stand;
  const caps = this.legalMoves().filter(m=>m.captured).sort((a,b)=>moveScore(b)-moveScore(a));
  for(const m of caps){
    this.make(m);
    const sc = -this.qsearch(-beta,-alpha,depth-1);
    this.unmake();
    if(sc>=beta) return beta;
    if(sc>alpha) alpha=sc;
  }
  return alpha;
};
Chess.prototype.negamax = function(depth, alpha, beta){
  if(depth===0) return this.qsearch(alpha,beta,CFG.qDepth);
  const moves = this.legalMoves().sort((a,b)=>moveScore(b)-moveScore(a));
  if(!moves.length) return this.inCheck() ? -CFG.mate-depth : 0;
  let best=-Infinity;
  for(const m of moves){
    this.make(m);
    const sc = -this.negamax(depth-1,-beta,-alpha);
    this.unmake();
    if(sc>best) best=sc;
    if(sc>alpha) alpha=sc;
    if(alpha>=beta) break;
  }
  return best;
};
Chess.prototype.bestMove = function(depth){
  const moves = this.legalMoves().sort((a,b)=>moveScore(b)-moveScore(a));
  if(!moves.length) return null;
  let best=moves[0], bestSc=-Infinity;
  for(const m of moves){
    this.make(m);
    const sc = -this.negamax(depth-1,-Infinity,Infinity);
    this.unmake();
    if(sc>bestSc){ bestSc=sc; best=m; }
  }
  return best;
};

/* ================================================================
   UI / RENDERING
   ================================================================ */
const game = new Chess();
game.setup();

let flipped=false, selected=-1, legalForSel=[], playerSide='w', gameOver=false, muted=false, aiThinking=false;
let pendingPromo=null; // {moves:[...], to}

const boardEl=document.getElementById('board');
const promoEl=document.createElement('div'); promoEl.id='promo'; boardEl.appendChild(promoEl);
const bannerEl=document.createElement('div'); bannerEl.className='banner'; boardEl.appendChild(bannerEl);

/* --- sounds (Web Audio) --- */
let actx=null;
function beep(freq, dur, type, vol){
  if(muted) return;
  try{
    actx = actx || new (window.AudioContext||window.webkitAudioContext)();
    const o=actx.createOscillator(), g=actx.createGain();
    o.type=type||'sine'; o.frequency.value=freq;
    g.gain.setValueAtTime(vol||0.15, actx.currentTime);
    g.gain.exponentialRampToValueAtTime(0.001, actx.currentTime+dur);
    o.connect(g); g.connect(actx.destination);
    o.start(); o.stop(actx.currentTime+dur);
  }catch(e){}
}
const sndMove=()=>beep(320,0.08,'triangle');
const sndCap =()=>{beep(180,0.12,'square',0.12); setTimeout(()=>beep(120,0.1,'square',0.1),40);};
const sndChk =()=>{beep(660,0.15,'sawtooth',0.1); setTimeout(()=>beep(880,0.15,'sawtooth',0.08),90);};

/* --- board construction --- */
const squares=[];
for(let vr=0;vr<8;vr++)for(let vf=0;vf<8;vf++){
  const d=document.createElement('div');
  d.className='sq';
  boardEl.insertBefore(d, promoEl);
  squares.push(d);
  d.addEventListener('click',()=>onSquareClick(d.dataset.sq|0));
  d.addEventListener('dragover',e=>{e.preventDefault();});
  d.addEventListener('drop',e=>{e.preventDefault(); onSquareClick(d.dataset.sq|0, true);});
}
function sqToView(sq){ // 0x88 -> view index
  let r=sq>>4, f=sq&7;
  if(flipped){ r=7-r; f=7-f; }
  return r*8+f;
}
function viewToSq(v){
  let r=(v/8)|0, f=v%8;
  if(flipped){ r=7-r; f=7-f; }
  return r*16+f;
}
function render(){
  for(let v=0;v<64;v++){
    const sq=viewToSq(v), el=squares[v];
    el.dataset.sq=sq;
    el.className='sq '+((((sq>>4)+(sq&7))%2===0)?'light':'dark');
    el.innerHTML='';
    // coords
    if((flipped? (sq>>4)===0 : (sq>>4)===7)){
      const c=document.createElement('span'); c.className='coord f'; c.textContent="abcdefgh"[sq&7]; el.appendChild(c);
    }
    if((flipped? (sq&7)===7 : (sq&7)===0)){
      const c=document.createElement('span'); c.className='coord r'; c.textContent=(sq>>4)+1; el.appendChild(c);
    }
    const p=game.board[sq];
    if(p){
      const s=document.createElement('span');
      s.className='pc '+((p&8)?'w':'b');
      s.textContent=GLYPH[p&7];
      s.draggable = canControl(p);
      s.addEventListener('dragstart',e=>{
        if(!canControl(p)){e.preventDefault();return;}
        selected=sq; legalForSel=game.movesFrom(sq); render();
      });
      el.appendChild(s);
    }
    // highlights
    const h=game.hist;
    if(h.length){
      const lm=h[h.length-1].move;
      if(sq===lm.from||sq===lm.to) el.classList.add('last');
    }
    if(selected===sq) el.classList.add('sel');
    if(game.inCheck() && game.board[sq]===(K|game.turn)) el.classList.add('check');
    if(selected>=0){
      for(const m of legalForSel){
        if(m.to===sq){
          const d=document.createElement('div');
          d.className = (m.captured||m.flags&FFLAG.EP) ? 'ring' : 'dot';
          el.appendChild(d);
        }
      }
    }
  }
  renderStatus(); renderMoves(); renderTrays();
}
function canControl(p){
  if(gameOver||aiThinking||pendingPromo) return false;
  const c=(p&8)?'w':'b';
  if(playerSide==='2') return true;
  return c===playerSide && game.turn===(p&8?WHITE:BLACK);
}

/* --- status / move list / trays --- */
function renderStatus(){
  const st=document.getElementById('status');
  if(gameOver){
    st.className='over';
    const r=game.gameResult();
    st.textContent = resultText(r);
    return;
  }
  st.className='';
  const t=game.turn===WHITE?'White':'Black';
  let txt = `<span class="turn">${t}</span> to move`;
  if(game.inCheck()) txt += ' — <b style="color:#ff8d7a">check!</b>';
  st.innerHTML=txt;
}
function resultText(r){
  if(r.result==='checkmate') return 'Checkmate — '+(r.winner===WHITE?'White':'Black')+' wins';
  if(r.result==='stalemate') return 'Draw — stalemate';
  if(r.result==='insufficient material') return 'Draw — insufficient material';
  if(r.result==='50-move rule') return 'Draw — 50-move rule';
  return 'Game over';
}
function renderMoves(){
  const ml=document.getElementById('movelist');
  let html='';
  for(let i=0;i<game.sanHist.length;i+=2){
    html+=`<div class="row"><span class="no">${i/2+1}.</span><span class="mv ${i===game.sanHist.length-1?'cur':''}">${game.sanHist[i]||''}</span><span class="mv ${i+1===game.sanHist.length-1?'cur':''}">${game.sanHist[i+1]||''}</span></div>`;
  }
  ml.innerHTML=html;
  ml.scrollTop=ml.scrollHeight;
}
function renderTrays(){
  const capW=[],capB=[]; // pieces captured BY white / by black
  for(const h of game.hist) if(h.captured) ((h.captured&8)?capW:capB).push(h.captured);
  const order={5:0,4:1,3:2,2:3,1:4};
  const fmt=a=>a.sort((x,y)=>order[x&7]-order[y&7]).map(p=>`<span class="${(p&8)?'w':'b'}">${GLYPH[p&7]}</span>`).join('');
  // trayTop = captured by opponent of player view... show pieces player captured on top
  const mine = playerSide==='b' ? capB : capW;
  const theirs = playerSide==='b' ? capW : capB;
  document.getElementById('trayTop').innerHTML=fmt(mine);
  document.getElementById('trayBot').innerHTML=fmt(theirs);
}

/* --- interaction --- */
function onSquareClick(sq, fromDrop){
  if(gameOver||aiThinking||pendingPromo) return;
  const p=game.board[sq];
  if(selected>=0){
    const cands=legalForSel.filter(m=>m.to===sq);
    if(cands.length){
      if(cands.length>1 && cands[0].promo){ showPromo(cands); return; }
      applyMove(cands[0]); selected=-1; legalForSel=[]; return;
    }
  }
  if(p && canControl(p)){
    selected=sq; legalForSel=game.movesFrom(sq);
  } else { selected=-1; legalForSel=[]; }
  render();
}
function showPromo(cands){
  pendingPromo=cands;
  const to=cands[0].to, v=sqToView(to);
  const r=(v/8)|0, f=v%8;
  const c=colorOf(cands[0].piece);
  const opts=[Q,R,B,N];
  promoEl.innerHTML='';
  for(const t of opts){
    const btn=document.createElement('button');
    btn.textContent=GLYPH[t];
    btn.className=(c&8)?'w':'b';
    btn.style.color=(c&8)?'#faf6ec':'#1d1712';
    btn.onclick=()=>{
      const m=pendingPromo.find(x=>x.promo===t);
      pendingPromo=null; promoEl.style.display='none';
      applyMove(m); selected=-1; legalForSel=[];
    };
    promoEl.appendChild(btn);
  }
  promoEl.style.display='flex';
  promoEl.style.left=(f*64)+'px';
  promoEl.style.top = (r<4 ? r*64 : (8-r)*64-224)+'px';
}
function applyMove(m){
  const san=game.san(m);
  game.make(m);
  game.sanHist.push(san);
  selected=-1; legalForSel=[];
  if(m.captured) sndCap(); else sndMove();
  const res=game.gameResult();
  if(res.over){ gameOver=true; if(res.result==='checkmate') sndChk(); }
  else if(game.inCheck()) sndChk();
  render(); save();
  if(!gameOver) maybeAI();
}
function showBanner(){
  const res=game.gameResult();
  if(!res.over){ bannerEl.style.display='none'; return; }
  bannerEl.innerHTML=`<div><div class="msg">${resultText(res)}</div><div class="sub">Final position after ${game.sanHist.length} moves</div><button class="btn" onclick="newGame()">New Game</button></div>`;
  bannerEl.style.display='flex';
}

/* --- AI driver (deferred so UI paints; search is sync & fast at depth≤3) --- */
function maybeAI(){
  showBanner();
  if(gameOver||playerSide==='2') return;
  const aiIsWhite = playerSide==='b';
  if((game.turn===WHITE)!==aiIsWhite) return;
  aiThinking=true;
  document.getElementById('thinking').textContent='thinking…';
  render();
  setTimeout(()=>{
    const depth=CFG.aiDepths[+document.getElementById('selDepth').value]||2;
    const t0=performance.now();
    const m=game.bestMove(depth);
    aiThinking=false;
    document.getElementById('thinking').textContent='';
    if(!m){ render(); return; }
    const san=game.san(m);
    game.make(m);
    game.sanHist.push(san);
    if(m.captured) sndCap(); else sndMove();
    const res=game.gameResult();
    if(res.over){ gameOver=true; if(res.result==='checkmate') sndChk(); }
    else if(game.inCheck()) sndChk();
    render(); save(); showBanner();
  }, CFG.aiDelayMs);
}

/* --- persistence --- */
function save(){
  try{
    localStorage.setItem('chess.save', JSON.stringify({
      fen:game.fen(), san:game.sanHist, side:playerSide,
      depth:document.getElementById('selDepth').value, muted
    }));
  }catch(e){}
}
function load(){
  try{
    const s=JSON.parse(localStorage.getItem('chess.save')||'null');
    if(!s||!s.fen) return false;
    game.loadFen(s.fen);
    game.sanHist=s.san||[];
    playerSide=s.side||'w';
    document.getElementById('selSide').value=playerSide;
    if(s.depth) document.getElementById('selDepth').value=s.depth;
    muted=!!s.muted; updateMuteBtn();
    return true;
  }catch(e){ return false; }
}

/* --- controls --- */
function newGame(){
  game.setup(); game.sanHist=[];
  gameOver=false; selected=-1; legalForSel=[]; pendingPromo=null;
  bannerEl.style.display='none'; promoEl.style.display='none';
  render(); save();
  maybeAI();
}
window.newGame=newGame;
document.getElementById('btnNew').onclick=()=>{ playerSide=document.getElementById('selSide').value; newGame(); };
document.getElementById('btnUndo').onclick=()=>{
  if(aiThinking) return;
  let n = (playerSide==='2') ? 1 : 2;
  while(n-- && game.hist.length){ game.unmake(); game.sanHist.pop(); }
  gameOver=false; bannerEl.style.display='none'; selected=-1; legalForSel=[];
  render(); save(); maybeAI();
};
document.getElementById('btnFlip').onclick=()=>{ flipped=!flipped; render(); };
function updateMute(){ document.getElementById('btnMute').textContent = muted?'🔇':'🔊'; }
document.getElementById('btnMute').onclick=()=>{ muted=!muted; updateMute(); save(); };
document.getElementById('selSide').onchange=e=>{ playerSide=e.target.value; save(); maybeAI(); };
document.getElementById('selDepth').onchange=()=>save();

/* --- debug interface --- */
window.game = {
  fen:()=>game.fen(),
  move:(s)=>{
    const from=nameSq(s.slice(0,2)), to=nameSq(s.slice(2,4));
    const promoChar=s[4] ? {q:Q,r:R,b:B,n:N}[s[4].toLowerCase()] : 0;
    const cands=game.legalMoves().filter(m=>m.from===from&&m.to===to);
    const m = promoChar ? cands.find(x=>x.promo===promoChar) : cands[0];
    if(!m) return null;
    const san=game.san(m); game.make(m); game.sanHist.push(san);
    const res=game.gameResult(); if(res.over) gameOver=true;
    render(); save(); showBanner(); return san;
  },
  legalMoves:()=>game.legalMoves().map(m=>sqName(m.from)+sqName(m.to)+(m.promo?'='+({2:'n',3:'b',4:'r',5:'q'})[m.promo]:'')),
  board:()=>game.fen(),
  undo:()=>{game.unmake(); game.sanHist.pop(); render(); save();},
  aiMove:(d)=>{const m=game.bestMove(d||2); if(m){const san=game.san(m); game.make(m); game.sanHist.push(san); render(); save();} return m?sqName(m.from)+sqName(m.to):null;},
  engine:game
};

/* --- init --- */
if(!load()) newGame(); else { render(); showBanner(); if(!gameOver) maybeAI(); }
updateMute();
</script>
</body>
</html>
<!-- agent-meta {"model":"z-ai/glm-5.3-flash","provider":"openrouter","persona":"full","sessionId":"c092d189-85f5-403d-aa22-7df09186f34f","tokensIn":985011,"tokensOut":25856,"tokensTotal":1010867,"cost":0.02799377,"turns":29,"toolCalls":29,"failedToolCalls":0,"timestamp":"2026-08-28T02:03:53.649Z"} -->