Chess
ox alpha · full

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 36,482 bytes · SHA-256 5054db158788
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — vs Computer</title>
<style>
/* ===================== TUNABLE CONSTANTS (CSS) ===================== */
:root{
--light:#f0d9b5; --dark:#b58863; --light-hi:#f6ebc8; --dark-hi:#c9a876;
--sel:#f7ec5e88; --last:#f7ec5e55; --check:#ff5a4a;
--panel:#2b2620; --panel2:#3a332b; --text:#efe6d8; --accent:#c8a24a;
}
*{box-sizing:border-box; margin:0; padding:0;}
body{
font-family:Georgia,'Times New Roman',serif; background:radial-gradient(1200px 800px at 50% 30%,#4a4238,#241f19);
color:var(--text); min-height:100vh; display:flex; align-items:center; justify-content:center; padding:16px;
}
#app{display:flex; gap:22px; align-items:flex-start; flex-wrap:wrap; justify-content:center;}
h1{font-size:22px; letter-spacing:2px; text-align:center; color:var(--accent); margin-bottom:8px;}
#board-wrap{position:relative;}
#board{
display:grid; grid-template-columns:repeat(8,64px); grid-template-rows:repeat(8,64px);
border:6px solid #171310; border-radius:6px; box-shadow:0 12px 40px #000c, inset 0 0 0 1px #ffffff18;
user-select:none; touch-action:none;
}
.sq{width:64px; height:64px; display:flex; align-items:center; justify-content:center; position:relative; cursor:pointer;}
.sq.light{background:var(--light);} .sq.dark{background:var(--dark);}
.sq.lastmove.light{background:var(--light-hi); box-shadow:inset 0 0 0 3px var(--last);}
.sq.lastmove.dark{background:var(--dark-hi); box-shadow:inset 0 0 0 3px var(--last);}
.sq.selected{box-shadow:inset 0 0 0 4px #e8d44d !important; background:var(--sel);}
.sq.incheck::after{content:''; position:absolute; inset:0; border-radius:50%;
background:radial-gradient(circle, #ff5a4a00 40%, var(--check) 90%); opacity:.85; pointer-events:none;}
.pc{font-size:50px; line-height:1; cursor:grab; z-index:2; text-shadow:0 2px 3px #0007;
transition:transform .08s;}
.pc.w{color:#fafafa; -webkit-text-stroke:1.5px #3a3128; text-shadow:0 2px 3px #0007;} .pc.b{color:#1c1a17;}
.pc:hover{transform:scale(1.06);}
.dot{width:20px; height:20px; border-radius:50%; background:#00000038; position:absolute; z-index:1;}
.ring{width:56px; height:56px; border-radius:50%; border:6px solid #00000038; position:absolute; z-index:1;}
.dragging{position:fixed; z-index:99; pointer-events:none; font-size:50px; line-height:1;
text-shadow:0 4px 10px #000a; transform:translate(-50%,-58%);}
/* side panel */
#panel{width:300px; background:var(--panel); border-radius:10px; padding:14px; box-shadow:0 10px 30px #000a;
display:flex; flex-direction:column; gap:10px;}
#status{font-size:17px; text-align:center; padding:8px; border-radius:6px; background:var(--panel2);}
#status.mate{background:#7a2a22; font-weight:bold;}
#trays{display:flex; justify-content:space-between; font-size:22px; min-height:28px; color:#ddd;}
.tray{display:flex; flex-wrap:wrap; max-width:140px; gap:1px; align-items:center;}
.tray small{font-size:11px; color:#999; margin-left:4px;}
#movelist{background:var(--panel2); border-radius:6px; height:220px; overflow-y:auto; padding:6px 10px;
font-size:14px; font-family:'Segoe UI',sans-serif;}
#movelist .row{display:grid; grid-template-columns:34px 1fr 1fr; padding:2px 0;}
#movelist .num{color:#8a7f6e;} #movelist .mv{cursor:default; padding:0 4px; border-radius:3px;}
#movelist .mv.cur{background:#5a4f3d; color:#fff;}
#controls{display:grid; grid-template-columns:1fr 1fr; gap:8px;}
button{font-family:inherit; background:var(--panel2); color:var(--text); border:1px solid #5a5040;
border-radius:6px; padding:8px 10px; cursor:pointer; font-size:14px;}
button:hover{background:#4a4234; border-color:var(--accent);}
button:disabled{opacity:.4; cursor:default;}
select{font-family:inherit; background:var(--panel2); color:var(--text); border:1px solid #5a5040;
border-radius:6px; padding:8px; font-size:14px; width:100%;}
label{font-size:12px; color:#a89a84;}
/* promotion overlay */
#promo{position:absolute; display:none; flex-direction:column; background:#efe6d8; border-radius:8px;
box-shadow:0 8px 24px #000c; z-index:50; overflow:hidden;}
#promo button{background:none; border:none; font-size:44px; padding:4px 14px; color:#111;}
#promo button:hover{background:var(--accent);}
#banner{position:absolute; inset:0; display:none; align-items:center; justify-content:center;
background:#000000b0; z-index:60; border-radius:6px;}
#banner .card{background:var(--panel); padding:26px 40px; border-radius:12px; text-align:center;
border:1px solid var(--accent); font-size:20px;}
#banner button{margin-top:14px;}
@media (max-width:760px){ #board{grid-template-columns:repeat(8,44px); grid-template-rows:repeat(8,44px);}
.sq{width:44px;height:44px;} .pc{font-size:34px;} .ring{width:38px;height:38px;border-width:4px;} }
</style>
</head>
<body>
<div id="app">
<div>
<h1>♞ CHESS</h1>
<div id="board-wrap">
<div id="board"></div>
<div id="promo"></div>
<div id="banner"><div class="card"><div id="banner-text"></div><button onclick="ui.newGame()">New Game</button></div></div>
</div>
</div>
<div id="panel">
<div id="status">White to move</div>
<div id="trays">
<div class="tray" id="tray-b"></div>
<div class="tray" id="tray-w"></div>
</div>
<div id="movelist"></div>
<div id="controls">
<button id="btn-new" onclick="ui.newGame()">New Game</button>
<button id="btn-undo" onclick="ui.undo()">Undo</button>
<button id="btn-flip" onclick="ui.flip()">Flip Board</button>
<button id="btn-mute" onclick="ui.toggleMute()">🔊 Sound</button>
<div><label>Difficulty</label><select id="sel-depth">
<option value="1">Easy (depth 1)</option>
<option value="2" selected>Medium (depth 2)</option>
<option value="3">Hard (depth 3)</option>
</select></div>
<div><label>Play as</label><select id="sel-side">
<option value="w" selected>White</option>
<option value="b">Black</option>
<option value="both">Both sides</option>
</select></div>
</div>
</div>
</div>
<script>
/* ============================================================
TUNABLE CONSTANTS (JS)
============================================================ */
const CFG = {
pieceValues: { p:100, n:320, b:330, r:500, q:900, k:20000 },
aiDepths: { 1:1, 2:2, 3:3 }, // difficulty -> search depth
quiescence: true, // capture-only search extension
qMaxDepth: 6,
aiMoveDelayMs: 120, // small pause so UI feels natural
flipBlackPerspective: true,
};
/* ============================================================
SECTION 1: ENGINE — 0x88 board representation
Squares: index 0..127, valid if (sq & 0x88) === 0.
rank = sq >> 4, file = sq & 7. a1 = 0, h1 = 7, a8 = 112.
Pieces: 'PNBRQK' white, 'pnbrqk' black, null empty.
============================================================ */
const ENGINE = (() => {
const WHITE='w', BLACK='b';
const PIECE_VAL = CFG.pieceValues;
const KNIGHT_D=[31,33,14,18,-31,-33,-14,-18];
const KING_D=[1,-1,16,-16,15,17,-15,-17];
const BISHOP_D=[15,17,-15,-17];
const ROOK_D=[1,-1,16,-16];
function initialState(){
return {
board: new Array(128).fill(null),
turn: WHITE,
castling: {wk:true,wq:true,bk:true,bq:true},
ep: -1, // en-passant target square (index) or -1
half: 0, // halfmove clock (50-move rule)
full: 1,
history: [], // undo stack: {move, castling, ep, half, full}
kings: {w: 4, b: 116} // king square cache
};
}
function setupStart(st){
const back='RNBQKBNR';
for(let f=0; f<8; f++){
st.board[f] = back[f]; // white back rank
st.board[16+f] = 'P'; // white pawns
st.board[96+f] = 'p'; // black pawns
st.board[112+f] = back[f].toLowerCase();
}
st.kings={w:4, b:116};
}
const colorOf = p => p ? (p === p.toUpperCase() ? WHITE : BLACK) : null;
const typeOf = p => p ? p.toLowerCase() : null;
const sqName = sq => 'abcdefgh'[sq & 7] + (1 + (sq >> 4));
const nameSq = s => (s.charCodeAt(0)-97) + (parseInt(s[1])-1)*16;
const other = c => c===WHITE ? BLACK : WHITE;
function attacked(st, sq, byColor){
// pawns
const dir = byColor===WHITE ? -16 : 16; // attacker sits at sq+dir±1 relative? compute from sq
for(const d of [dir-1, dir+1]){
const f = sq + d;
if(!(f & 0x88)){
const p = st.board[f];
if(p && colorOf(p)===byColor && typeOf(p)==='p') return true;
}
}
// knights
for(const d of KNIGHT_D){
const f=sq+d; if(!(f&0x88)){ const p=st.board[f];
if(p && colorOf(p)===byColor && typeOf(p)==='n') return true; }
}
// king
for(const d of KING_D){
const f=sq+d; if(!(f&0x88)){ const p=st.board[f];
if(p && colorOf(p)===byColor && typeOf(p)==='k') return true; }
}
// sliders
for(const d of BISHOP_D){
let f=sq+d;
while(!(f&0x88)){ const p=st.board[f];
if(p){ if(colorOf(p)===byColor && (typeOf(p)==='b'||typeOf(p)==='q')) return true; break; }
f+=d; }
}
for(const d of ROOK_D){
let f=sq+d;
while(!(f&0x88)){ const p=st.board[f];
if(p){ if(colorOf(p)===byColor && (typeOf(p)==='r'||typeOf(p)==='q')) return true; break; }
f+=d; }
}
return false;
}
const inCheck = (st, color) => attacked(st, st.kings[color], other(color));
function pseudoMoves(st, color){
const moves=[];
const add=(from,to,flags,promo)=>{
moves.push({from,to,piece:st.board[from],captured:flags==='e'?(color===WHITE?'p':'P'):st.board[to],promo:promo||null,flags:flags||'n'});
};
for(let sq=0; sq<128; sq++){
if(sq & 0x88){ sq+=7; continue; }
const p=st.board[sq]; if(!p || colorOf(p)!==color) continue;
const t=typeOf(p);
if(t==='p'){
const dir = color===WHITE ? 16 : -16;
const startRank = color===WHITE ? 1 : 6;
const promoRank = color===WHITE ? 7 : 0;
const one = sq+dir;
if(!(one&0x88) && !st.board[one]){
if((one>>4)===promoRank){ for(const pr of 'qrbn') add(sq,one,'p',color===WHITE?pr.toUpperCase():pr); }
else{
add(sq,one);
const two = sq+2*dir;
if((sq>>4)===startRank && !st.board[two]) add(sq,two,'b'); // double push
}
}
for(const dd of [dir-1,dir+1]){
const to=sq+dd; if(to&0x88) continue;
const tp=st.board[to];
if(tp && colorOf(tp)!==color){
if((to>>4)===promoRank){ for(const pr of 'qrbn') add(sq,to,'p',color===WHITE?pr.toUpperCase():pr); }
else add(sq,to,'c');
} else if(to===st.ep && st.ep>=0){
add(sq,to,'e');
}
}
} else if(t==='n' || t==='k'){
for(const d of (t==='n'?KNIGHT_D:KING_D)){
const to=sq+d; if(to&0x88) continue;
const tp=st.board[to];
if(!tp) add(sq,to); else if(colorOf(tp)!==color) add(sq,to,'c');
}
if(t==='k'){
const home = color===WHITE?4:116;
if(sq===home && !inCheck(st,color)){
const kR=color+'k', qR=color+'q';
// kingside: rook on h-file, squares empty & not attacked
if(st.castling[kR] && !st.board[home+1] && !st.board[home+2]
&& st.board[home+3]===(color===WHITE?'R':'r')
&& !attacked(st,home+1,other(color)) && !attacked(st,home+2,other(color)))
add(sq,home+2,'k');
if(st.castling[qR] && !st.board[home-1] && !st.board[home-2] && !st.board[home-3]
&& st.board[home-4]===(color===WHITE?'R':'r')
&& !attacked(st,home-1,other(color)) && !attacked(st,home-2,other(color)))
add(sq,home-2,'q');
}
}
} else {
const dirs = t==='b'?BISHOP_D : t==='r'?ROOK_D : KING_D; // q = all 8
for(const d of dirs){
let to=sq+d;
while(!(to&0x88)){
const tp=st.board[to];
if(!tp) add(sq,to);
else { if(colorOf(tp)!==color) add(sq,to,'c'); break; }
to+=d;
}
}
}
}
return moves;
}
function makeMove(st, m){
st.history.push({castling:{...st.castling}, ep:st.ep, half:st.half, full:st.full});
const color=colorOf(m.piece), opp=other(color);
const b=st.board;
b[m.from]=null;
b[m.to]= m.promo ? m.promo : m.piece;
if(m.flags==='e') b[m.to + (color===WHITE?-16:16)]=null; // remove ep pawn
if(typeOf(m.piece)==='k'){
st.kings[color]=m.to;
if(m.flags==='k'){ const home=color===WHITE?4:116; b[home+1]=b[home+3]; b[home+3]=null; }
if(m.flags==='q'){ const home=color===WHITE?4:116; b[home-1]=b[home-4]; b[home-4]=null; }
st.castling[color+'k']=false; st.castling[color+'q']=false;
}
// rook moved / captured -> castling rights
const clr=c=>c==='R'?'wk':c==='r'?'bk':null;
if(m.from===7||m.to===7) st.castling.wk=false;
if(m.from===0||m.to===0) st.castling.wq=false;
if(m.from===119||m.to===119) st.castling.bk=false;
if(m.from===112||m.to===112) st.castling.bq=false;
// en passant target
st.ep = (typeOf(m.piece)==='p' && Math.abs(m.to-m.from)===32) ? (m.from+m.to)/2 : -1;
st.half = (typeOf(m.piece)==='p' || m.captured) ? 0 : st.half+1;
if(color===BLACK) st.full++;
st.turn=opp;
}
function unmakeMove(st, m){
const h=st.history.pop(); if(!h) return null;
const color=colorOf(m.piece), b=st.board;
b[m.from]=m.piece;
b[m.to]=null;
if(m.flags==='e') b[m.to + (color===WHITE?-16:16)] = color===WHITE ? 'p' : 'P';
else if(m.captured) b[m.to]=m.captured;
if(typeOf(m.piece)==='k'){
st.kings[color]=m.from;
if(m.flags==='k'){ const home=color===WHITE?4:116; b[home+3]=b[home+1]; b[home+1]=null; }
if(m.flags==='q'){ const home=color===WHITE?4:116; b[home-4]=b[home-1]; b[home-1]=null; }
}
st.castling=h.castling; st.ep=h.ep; st.half=h.half; st.full=h.full;
st.turn=color;
}
function legalMoves(st, color){
color = color || st.turn;
const out=[];
for(const m of pseudoMoves(st,color)){
makeMove(st,m);
if(!inCheck(st,color)) out.push(m);
unmakeMove(st,m);
}
return out;
}
// Game status: 'playing' | 'checkmate' | 'stalemate' | 'draw-50' | 'draw-material'
function insufficientMaterial(st){
const pieces=[];
for(let sq=0; sq<128; sq++){ if(sq&0x88){sq+=7;continue;} const p=st.board[sq]; if(p && typeOf(p)!=='k') pieces.push(typeOf(p)); }
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;
}
function status(st){
const legal = legalMoves(st);
if(legal.length===0) return inCheck(st, st.turn) ? 'checkmate' : 'stalemate';
if(st.half>=100) return 'draw-50';
if(insufficientMaterial(st)) return 'draw-material';
return inCheck(st, st.turn) ? 'check' : 'playing';
}
// SAN notation for a move, given list of legal moves in current position
function toSAN(st, m, legal){
if(m.flags==='k') return 'O-O';
if(m.flags==='q') return 'O-O-O';
const t=typeOf(m.piece);
let s='';
if(t==='p'){
if(m.captured || m.flags==='e') s += 'abcdefgh'[m.from&7] + 'x';
s += sqName(m.to);
if(m.promo) s += '=' + m.promo.toUpperCase();
} else {
s += t.toUpperCase();
// disambiguation
const others = legal.filter(x => x!==m && typeOf(x.piece)===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 += (1+(m.from>>4));
else s += sqName(m.from);
}
if(m.captured) s += 'x';
s += sqName(m.to);
}
// check / mate suffix
makeMove(st,m);
const stt = status(st);
unmakeMove(st,m);
if(stt==='checkmate') s+='#'; else if(stt==='check') s+='+';
return s;
}
// FEN
function toFEN(st){
let rows=[];
for(let r=7;r>=0;r--){
let row='', empty=0;
for(let f=0;f<8;f++){
const p=st.board[r*16+f];
if(p){ if(empty){row+=empty;empty=0;} row+=p; } else empty++;
}
if(empty) row+=empty;
rows.push(row);
}
let cast = (st.castling.wk?'K':'')+(st.castling.wq?'Q':'')+(st.castling.bk?'k':'')+(st.castling.bq?'q':'');
return rows.join('/')+' '+st.turn+' '+(cast||'-')+' '+(st.ep>=0?sqName(st.ep):'-')+' '+st.half+' '+st.full;
}
function fromFEN(fen){
const st=initialState();
const [pos, turn, cast, ep, half, full] = fen.trim().split(/\s+/);
let r=7,f=0;
for(const ch of pos){
if(ch==='/'){ r--; f=0; }
else if(/\d/.test(ch)) f+=+ch;
else { const sq=r*16+f; st.board[sq]=ch; if(ch==='K') st.kings.w=sq; if(ch==='k') st.kings.b=sq; f++; }
}
st.turn = turn==='b'?BLACK:WHITE;
st.castling={wk:cast.includes('K'),wq:cast.includes('Q'),bk:cast.includes('k'),bq:cast.includes('q')};
st.ep = (ep && ep!=='-') ? nameSq(ep) : -1;
st.half = +half||0; st.full = +full||1;
return st;
}
return {WHITE,BLACK,colorOf,typeOf,sqName,nameSq,other,attacked,inCheck,
legalMoves,pseudoMoves,makeMove,unmakeMove,status,toSAN,toFEN,fromFEN,
initialState,setupStart,insufficientMaterial};
})();
</script>
<script>
/* ============================================================
SECTION 2: AI — minimax + alpha-beta, move ordering,
quiescence-lite (captures only), material + PST eval.
============================================================ */
const AI = (() => {
const E = ENGINE;
// Piece-square tables from White's perspective (a8 first row), values in centipawns
const PST = {
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],
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],
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],
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],
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],
k:[-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]
};
const mirror = sq => ((7 - (sq>>4)) << 4) | (sq & 7); // flip rank for black
function evaluate(st){
let score=0;
for(let sq=0; sq<128; sq++){
if(sq & 0x88){ sq+=7; continue; }
const p=st.board[sq]; if(!p) continue;
const t=E.typeOf(p), white=E.colorOf(p)==='w';
const idx = white ? (7-(sq>>4))*8 + (sq&7) : (sq>>4)*8 + (sq&7);
const v = CFG.pieceValues[t] + PST[t][idx];
score += white ? v : -v;
}
return score; // + good for White
}
function orderMoves(moves){
return moves.map(m => ({m, s:
(m.captured ? 1000 + CFG.pieceValues[E.typeOf(m.captured)] - CFG.pieceValues[E.typeOf(m.piece)]/10 : 0)
+ (m.promo ? 900 : 0)}))
.sort((a,b)=>b.s-a.s).map(x=>x.m);
}
function quiesce(st, alpha, beta, color, depth){
const stand = color==='w' ? evaluate(st) : -evaluate(st);
if(depth<=0) return stand;
if(stand >= beta) return beta;
if(stand > alpha) alpha = stand;
const caps = E.pseudoMoves(st,color).filter(m=>m.captured||m.flags==='e');
for(const m of orderMoves(caps)){
E.makeMove(st,m);
if(E.inCheck(st,color)){ E.unmakeMove(st,m); continue; }
const sc = quiesce(st, -beta, -alpha, E.other(color), depth-1);
E.unmakeMove(st,m);
if(sc >= beta) return beta;
if(sc > alpha) alpha = sc;
}
return alpha;
}
function search(st, depth, alpha, beta, color){
const legal = E.legalMoves(st, color);
if(legal.length===0) return E.inCheck(st,color) ? -99999 + (CFG.aiDepths[3]-depth) : 0;
if(depth===0) return CFG.quiescence ? quiesce(st,alpha,beta,color,CFG.qMaxDepth)
: (color==='w'?evaluate(st):-evaluate(st));
for(const m of orderMoves(legal)){
E.makeMove(st,m);
const sc = -search(st, depth-1, -beta, -alpha, E.other(color));
E.unmakeMove(st,m);
if(sc >= beta) return beta;
if(sc > alpha) alpha = sc;
}
return alpha;
}
function bestMove(st, depth){
const color = st.turn;
const legal = E.legalMoves(st, color);
if(!legal.length) return null;
let best=null, bestScore=-Infinity;
for(const m of orderMoves(legal)){
E.makeMove(st,m);
const sc = -search(st, depth-1, -Infinity, Infinity, E.other(color));
E.unmakeMove(st,m);
if(sc > bestScore){ bestScore=sc; best=m; }
}
return best; // never illegal: drawn from legalMoves()
}
return {bestMove, evaluate};
})();
/* ============================================================
SECTION 3: SOUND — Web Audio
============================================================ */
const Sound = (() => {
let ctx=null, muted=false;
function beep(freq, dur, type='sine', gain=0.12){
if(muted) return;
try{
ctx = ctx || new (window.AudioContext||window.webkitAudioContext)();
const o=ctx.createOscillator(), g=ctx.createGain();
o.type=type; o.frequency.value=freq;
g.gain.setValueAtTime(gain, ctx.currentTime);
g.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime+dur);
o.connect(g).connect(ctx.destination);
o.start(); o.stop(ctx.currentTime+dur);
}catch(e){}
}
return {
move: () => beep(320, .09, 'triangle'),
capture: () => { beep(180,.1,'square',.1); setTimeout(()=>beep(140,.12,'square',.08),40); },
check: () => { beep(660,.12,'sawtooth',.08); setTimeout(()=>beep(880,.14,'sawtooth',.07),90); },
end: () => { [440,554,659].forEach((f,i)=>setTimeout(()=>beep(f,.3,'triangle'),i*130)); },
toggleMute(){ muted=!muted; return muted; },
get muted(){ return muted; }
};
})();
/* ============================================================
SECTION 4: GAME CONTROLLER + UI
============================================================ */
const GLYPH = {P:'♟',N:'♞',B:'♝',R:'♜',Q:'♛',K:'♚',p:'♟',n:'♞',b:'♝',r:'♜',q:'♛',k:'♚'};
const LS_KEY = 'chess-game-v1';
const Game = {
st: null,
sanList: [], // [{san, fenAfter}]
humanSide: 'w', // 'w' | 'b' | 'both'
flipped: false,
selected: -1,
legalCache: [],
gameOver: false,
newGame(){
this.st = ENGINE.initialState();
ENGINE.setupStart(this.st);
this.sanList=[]; this.selected=-1; this.gameOver=false;
this.save(); UI.renderAll();
this.maybeAI();
},
load(){
try{
const raw = localStorage.getItem(LS_KEY);
if(!raw) return false;
const d = JSON.parse(raw);
this.st = ENGINE.fromFEN(d.fen);
// rebuild history stack is not needed for undo of past moves beyond SAN list;
// undo uses stored FEN snapshots instead.
this.st.history = [];
this.sanList = d.sanList || [];
this.humanSide = d.humanSide || 'w';
this.flipped = !!d.flipped;
this.gameOver = d.gameOver || false;
this.lastMove = d.lastMove || null;
return true;
}catch(e){ return false; }
},
save(){
localStorage.setItem(LS_KEY, JSON.stringify({
fen: ENGINE.toFEN(this.st),
sanList: this.sanList,
humanSide: this.humanSide,
flipped: this.flipped,
gameOver: this.gameOver,
lastMove: this.lastMove || null
}));
},
findMove(from,to,promo){
return this.legalCache.find(m => m.from===from && m.to===to && (!promo || (m.promo&&m.promo.toLowerCase()===promo)));
},
movesFrom(sq){ return this.legalCache.filter(m=>m.from===sq); },
applyMove(m){
const st=this.st;
const legal = ENGINE.legalMoves(st);
const san = ENGINE.toSAN(st, m, legal);
ENGINE.makeMove(st,m);
this.sanList.push({san, fen: ENGINE.toFEN(st)});
this.selected=-1;
const stat = ENGINE.status(st);
if(stat==='checkmate' || stat==='stalemate' || stat==='draw-50' || stat==='draw-material') this.gameOver=true;
this.save();
UI.renderAll();
if(stat==='checkmate'||stat==='stalemate') Sound.end();
else if(stat==='check') Sound.check();
else if(m.captured||m.flags==='e') Sound.capture();
else Sound.move();
this.maybeAI();
},
maybeAI(){
if(this.gameOver) return;
const aiTurn = this.humanSide==='both' ? false :
(this.st.turn !== this.humanSide);
if(!aiTurn) return;
UI.setStatus('Computer thinking…');
const depth = CFG.aiDepths[+document.getElementById('sel-depth').value] || 2;
setTimeout(()=>{
const m = AI.bestMove(this.st, depth);
if(m) this.applyMove(m);
}, CFG.aiMoveDelayMs);
},
undo(){
if(!this.sanList.length || UI.thinking) return;
// take back moves until it's the human's turn again (usually 2 plies)
let n = (this.humanSide==='both') ? 1 : 2;
n = Math.min(n, this.sanList.length);
for(let i=0;i<n;i++){
this.sanList.pop();
const prevFen = this.sanList.length ? this.sanList[this.sanList.length-1].fen : null;
this.st = prevFen ? ENGINE.fromFEN(prevFen) : (()=>{const s=ENGINE.initialState();ENGINE.setupStart(s);return s;})();
this.st.history=[];
}
this.gameOver=false; this.selected=-1;
this.save(); UI.renderAll();
}
};
const UI = {
thinking:false,
drag:null,
init(){
document.getElementById('sel-side').addEventListener('change', e=>{
Game.humanSide=e.target.value; Game.newGame();
});
Game.load() || Game.newGame();
if(Game.humanSide !== (document.getElementById('sel-side').value))
document.getElementById('sel-side').value = Game.humanSide;
this.renderAll();
Game.maybeAI();
},
boardOrder(){ // square indices in display order
const out=[];
for(let r=0;r<8;r++) for(let f=0;f<8;f++){
const rank = Game.flipped ? r : 7-r;
const file = Game.flipped ? 7-f : f;
out.push(rank*16+file);
}
return out;
},
renderAll(){ this.renderBoard(); this.renderPanel(); },
renderBoard(){
const st=Game.st, board=document.getElementById('board');
Game.legalCache = ENGINE.legalMoves(st);
board.innerHTML='';
const last = Game.sanList.length ? Game.sanList[Game.sanList.length-1] : null;
// derive last-move squares from FEN diff is complex; store on Game instead:
const lm = Game.lastMove || {};
const checkSq = ENGINE.inCheck(st, st.turn) ? st.kings[st.turn] : -1;
const targets = Game.selected>=0 ? Game.movesFrom(Game.selected) : [];
for(const sq of this.boardOrder()){
const div=document.createElement('div');
const light = ((sq>>4)+(sq&7))%2===1;
div.className='sq '+(light?'light':'dark');
div.dataset.sq=sq;
if(sq===lm.from || sq===lm.to) div.classList.add('lastmove');
if(sq===checkSq) div.classList.add('incheck');
if(sq===Game.selected) div.classList.add('selected');
const p=st.board[sq];
if(p){
const pc=document.createElement('span');
pc.className='pc '+(ENGINE.colorOf(p)==='w'?'w':'b');
pc.textContent=GLYPH[p];
div.appendChild(pc);
}
const t=targets.find(m=>m.to===sq);
if(t){
const h=document.createElement('span');
h.className=(t.captured||t.flags==='e')?'ring':'dot';
div.appendChild(h);
}
div.addEventListener('click', ()=>this.onSquareClick(sq));
div.addEventListener('pointerdown', e=>this.onPointerDown(e,sq));
board.appendChild(div);
}
},
renderPanel(){
const st=Game.st, stat=ENGINE.status(st);
const statusEl=document.getElementById('status');
statusEl.classList.remove('mate');
if(Game.gameOver){
let msg;
if(stat==='checkmate') msg=(st.turn==='w'?'Black':'White')+' wins by checkmate';
else if(stat==='stalemate') msg='Draw — stalemate';
else if(stat==='draw-50') msg='Draw — 50-move rule';
else msg='Draw — insufficient material';
statusEl.textContent=msg; statusEl.classList.add('mate');
document.getElementById('banner-text').textContent=msg;
document.getElementById('banner').style.display='flex';
} else {
document.getElementById('banner').style.display='none';
const side = st.turn==='w'?'White':'Black';
statusEl.textContent = side+' to move'+(stat==='check'?' — CHECK!':'');
}
// captured trays
const startCount={p:8,n:2,b:2,r:2,q:1};
const counts={w:{},b:{}};
for(const c of ['w','b']) counts[c]={p:0,n:0,b:0,r:0,q:0};
for(let sq=0;sq<128;sq++){ if(sq&0x88){sq+=7;continue;}
const p=st.board[sq]; if(p && ENGINE.typeOf(p)!=='k') counts[ENGINE.colorOf(p)][ENGINE.typeOf(p)]++; }
// captured by White = missing black pieces
const trayW=document.getElementById('tray-w'), trayB=document.getElementById('tray-b');
trayW.innerHTML=''; trayB.innerHTML='';
let wPts=0,bPts=0;
for(const t of ['q','r','b','n','p']){
const missB=startCount[t]-counts.b[t];
const missW=startCount[t]-counts.w[t];
for(let i=0;i<missB;i++){ trayW.innerHTML+='<span>'+GLYPH[t.toUpperCase()]+'</span>'; wPts+=CFG.pieceValues[t]; }
for(let i=0;i<missW;i++){ trayB.innerHTML+='<span>'+GLYPH[t]+'</span>'; bPts+=CFG.pieceValues[t]; }
}
if(wPts>bPts) trayW.innerHTML+='<small>+'+Math.round((wPts-bPts)/100)+'</small>';
if(bPts>wPts) trayB.innerHTML+='<small>+'+Math.round((bPts-wPts)/100)+'</small>';
// move list
const ml=document.getElementById('movelist');
ml.innerHTML='';
for(let i=0;i<Game.sanList.length;i+=2){
const row=document.createElement('div'); row.className='row';
row.innerHTML='<span class="num">'+(i/2+1)+'.</span>'
+'<span class="mv'+(i===Game.sanList.length-1?' cur':'')+'">'+Game.sanList[i].san+'</span>'
+'<span class="mv'+(i+1===Game.sanList.length-1?' cur':'')+'">'+(Game.sanList[i+1]?Game.sanList[i+1].san:'')+'</span>';
ml.appendChild(row);
}
ml.scrollTop=ml.scrollHeight;
document.getElementById('btn-mute').textContent = Sound.muted?'🔇 Muted':'🔊 Sound';
},
setStatus(t){ if(!Game.gameOver) document.getElementById('status').textContent=t; },
onSquareClick(sq){
if(Game.gameOver || this.thinking) return;
if(Game.humanSide!=='both' && Game.st.turn!==Game.humanSide) return;
const p=Game.st.board[sq];
if(Game.selected>=0){
const ms=Game.movesFrom(Game.selected).filter(m=>m.to===sq);
if(ms.length>1 || (ms.length===1 && ms[0].promo)){ // promotion picker
this.showPromo(ms[0].from, sq, ms);
return;
}
if(ms.length===1){ Game.lastMove={from:ms[0].from,to:ms[0].to}; Game.applyMove(ms[0]); return; }
}
if(p && ENGINE.colorOf(p)===Game.st.turn){
Game.selected = (Game.selected===sq) ? -1 : sq;
} else Game.selected=-1;
this.renderBoard();
},
showPromo(from,to,options){
const el=document.getElementById('promo');
const rect=document.querySelector('[data-sq="'+to+'"]').getBoundingClientRect();
const wrap=document.getElementById('board-wrap').getBoundingClientRect();
const white=Game.st.turn==='w';
el.style.left=(rect.left-wrap.left)+'px';
el.style.top =(Game.flipped? rect.top-wrap.top : rect.top-wrap.top)+'px';
el.style.flexDirection = Game.flipped ? 'column-reverse':'column';
el.style.display='flex';
el.innerHTML='';
for(const t of ['q','r','b','n']){
const b=document.createElement('button');
b.textContent=GLYPH[white?t.toUpperCase():t];
b.onclick=()=>{
el.style.display='none';
const m=options.find(o=>o.promo && o.promo.toLowerCase()===t);
Game.lastMove={from:m.from,to:m.to};
Game.applyMove(m);
};
el.appendChild(b);
}
},
// ---- drag & drop ----
onPointerDown(e, sq){
if(Game.gameOver || this.thinking || e.button!==0) return;
if(Game.humanSide!=='both' && Game.st.turn!==Game.humanSide) return;
const p=Game.st.board[sq];
if(!p || ENGINE.colorOf(p)!==Game.st.turn) return;
Game.selected=sq;
this.renderBoard();
const ghost=document.createElement('div');
ghost.className='dragging'; ghost.textContent=GLYPH[p];
document.body.appendChild(ghost);
const move=x=>{ ghost.style.left=x.clientX+'px'; ghost.style.top=x.clientY+'px'; };
move(e);
const srcEl=document.querySelector('[data-sq="'+sq+'"] .pc');
if(srcEl) srcEl.style.visibility='hidden';
const onMove=ev=>move(ev);
const onUp=ev=>{
window.removeEventListener('pointermove',onMove);
window.removeEventListener('pointerup',onUp);
ghost.remove();
if(srcEl) srcEl.style.visibility='';
const target=document.elementFromPoint(ev.clientX,ev.clientY);
const tsq=target && target.closest ? target.closest('.sq') : null;
if(tsq){
const to=+tsq.dataset.sq;
if(to!==sq){
const ms=Game.movesFrom(sq).filter(m=>m.to===to);
if(ms.length>1 || (ms.length===1 && ms[0].promo)){ this.showPromo(sq,to,ms); return; }
if(ms.length===1){ Game.lastMove={from:sq,to}; Game.applyMove(ms[0]); return; }
}
}
this.renderBoard();
};
window.addEventListener('pointermove',onMove);
window.addEventListener('pointerup',onUp);
},
flip(){ Game.flipped=!Game.flipped; Game.save(); this.renderBoard(); },
toggleMute(){ Sound.toggleMute(); this.renderPanel(); },
newGame(){ Game.newGame(); },
undo(){ Game.undo(); },
};
/* ============================================================
SECTION 5: DEBUG INTERFACE + BOOT
============================================================ */
window.game = {
fen: () => ENGINE.toFEN(Game.st),
move: (uci) => { // 'e2e4', 'e7e8q'
const from=ENGINE.nameSq(uci.slice(0,2)), to=ENGINE.nameSq(uci.slice(2,4));
const promo=uci.length>4?uci[4]:null;
const m=Game.findMove(from,to,promo);
if(!m) return false;
Game.lastMove={from,to};
Game.applyMove(m);
return true;
},
legalMoves: () => ENGINE.legalMoves(Game.st).map(m =>
ENGINE.sqName(m.from)+ENGINE.sqName(m.to)+(m.promo?m.promo.toLowerCase():'')),
status: () => ENGINE.status(Game.st),
turn: () => Game.st.turn,
board: () => Game.st.board,
undo: () => Game.undo(),
newGame: () => Game.newGame(),
eval: () => AI.evaluate(Game.st),
engine: ENGINE
};
window.ui = UI;
window.addEventListener('DOMContentLoaded', ()=>UI.init());
</script>
</body>
</html>
<!-- agent-meta {"model":"stealth/ox-alpha","provider":"openrouter","persona":"full","sessionId":"1b9d9d72-9485-460a-a8cc-18252e69f363","tokensIn":732267,"tokensOut":17564,"tokensTotal":749831,"cost":0,"turns":23,"toolCalls":24,"failedToolCalls":0,"timestamp":"2026-08-21T17:05:32.559Z"} -->