Chess
GLM 5.3 Flash · full

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"} -->