Chess
Ornith 1.5 35B A3B · 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 43,827 bytes · SHA-256 9b933786cf0c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess vs AI</title>
<style>
:root{
--bg:#1e1e24; --panel:#2a2a32; --panel2:#33333d;
--wood-light:#e8d0a8; --wood-dark:#a9773f;
--highlight:rgba(255,241,132,0.45);
--check:rgba(231,56,56,0.75);
--move:rgba(20,20,20,0.35);
--capture:rgba(231,56,56,0.55);
--sel:#f6f66b;
--text:#ececec; --muted:#9a9aa6;
--accent:#d9a441;
}
*{box-sizing:border-box;}
html,body{margin:0;padding:0;background:var(--bg);color:var(--text);
font-family:"Segoe UI",system-ui,-apple-system,sans-serif;}
header{display:flex;align-items:center;gap:14px;padding:12px 20px;
background:linear-gradient(180deg,#33333d,#26262d);border-bottom:1px solid #000;
flex-wrap:wrap;}
header h1{font-size:20px;margin:0;font-weight:600;letter-spacing:.5px;}
header h1 .w{color:var(--text);} header h1 .b{color:var(--accent);}
.layout{display:flex;gap:22px;justify-content:center;align-items:flex-start;
padding:22px;flex-wrap:wrap;}
/* Board */
.board-wrap{position:relative;}
#board{display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);
width:min(70vmin,520px);height:min(70vmin,520px);
border:6px solid #5a3d1a;border-radius:6px;box-shadow:0 10px 30px rgba(0,0,0,.5);
--sq:calc(min(70vmin,520px)/8);}
.cell{position:relative;display:flex;align-items:center;justify-content:center;
font-size:calc(min(70vmin,520px)/8*0.78);cursor:pointer;user-select:none;
line-height:1;}
.cell.light{background:var(--wood-light);}
.cell.dark{background:var(--wood-dark);}
.cell.sel{box-shadow:inset 0 0 0 4px var(--sel);background:var(--highlight)!important;}
.cell.lastmove::after{content:"";position:absolute;inset:0;background:var(--highlight);
pointer-events:none;}
.cell.check{box-shadow:inset 0 0 0 4px var(--check),inset 0 0 18px 4px var(--check);}
.cell .piece{position:relative;z-index:2;transition:transform .05s;
text-shadow:0 1px 2px rgba(0,0,0,.35);}
.cell .piece.white{color:#fff;border:1px solid #333;border-radius:50%;
padding:0 1px;box-shadow:0 1px 2px rgba(0,0,0,.4);}
.cell .piece.black{color:#1b1b1b;}
.cell .dot{position:absolute;width:28%;height:28%;border-radius:50%;
background:var(--move);z-index:1;}
.cell .ring{position:absolute;inset:6%;border:5px solid var(--capture);border-radius:50%;
z-index:1;pointer-events:none;}
.coord{position:absolute;font-size:11px;font-weight:600;opacity:.8;z-index:3;}
.coord.file{bottom:2px;right:3px;}
.coord.rank{top:2px;left:3px;}
.cell.dragging .piece{opacity:.4;}
#dragGhost{position:fixed;pointer-events:none;z-index:999;font-size:64px;
text-shadow:0 2px 6px rgba(0,0,0,.5);transform:translate(-50%,-50%);display:none;}
/* Panel */
.panel{width:300px;background:var(--panel);border-radius:10px;padding:16px;
box-shadow:0 8px 24px rgba(0,0,0,.4);display:flex;flex-direction:column;gap:12px;
max-height:70vmin;min-height:520px;}
.panel h2{font-size:14px;text-transform:uppercase;letter-spacing:1px;color:var(--muted);
margin:0 0 4px;}
.turn{display:flex;align-items:center;gap:10px;font-weight:600;padding:8px 12px;
background:var(--panel2);border-radius:8px;}
.turn .dot2{width:16px;height:16px;border-radius:50%;border:1px solid #000;}
.turn .dot2.white{background:#fff;}
.turn .dot2.black{background:#222;}
.controls{display:flex;flex-direction:column;gap:8px;}
.controls .row{display:flex;gap:8px;flex-wrap:wrap;}
button,select{font-family:inherit;font-size:13px;padding:8px 12px;border-radius:7px;
border:1px solid #444;background:var(--panel2);color:var(--text);cursor:pointer;
transition:background .15s,border .15s;}
button:hover,select:hover{background:#3d3d48;}
button.primary{background:var(--accent);color:#20180a;border-color:#b9862f;font-weight:600;}
button.primary:hover{background:#e8b454;}
button:disabled{opacity:.5;cursor:not-allowed;}
.captured{display:flex;flex-wrap:wrap;gap:2px;font-size:22px;min-height:30px;align-items:center;}
.captured .lead{font-size:11px;color:var(--muted);width:100%;}
.moves{background:var(--panel2);border-radius:8px;padding:8px;flex:1;overflow:auto;
font-family:"SF Mono",Consolas,monospace;font-size:13px;min-height:120px;}
.moves .mv-row{display:flex;gap:6px;padding:2px 4px;border-radius:5px;}
.moves .mv-row:nth-child(odd){background:rgba(255,255,255,.03);}
.moves .mv-num{color:var(--muted);width:26px;}
.moves .mv{padding:0 6px;cursor:default;}
.moves .mv.current{background:var(--accent);color:#20180a;border-radius:4px;}
/* Overlay */
.overlay{position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;
align-items:center;justify-content:center;z-index:50;}
.overlay.show{display:flex;}
.modal{background:var(--panel);border-radius:12px;padding:20px;box-shadow:0 12px 40px #000;
text-align:center;}
.modal h3{margin:0 0 12px;}
.promo-opts{display:flex;gap:10px;}
.promo-opts button{font-size:34px;padding:10px 16px;width:64px;height:64px;
background:var(--wood-light);color:#222;border:2px solid #777;border-radius:10px;}
.promo-opts button:hover{background:var(--sel);border-color:#333;}
.banner{position:fixed;left:50%;top:18px;transform:translateX(-50%);
background:linear-gradient(180deg,#3a3a44,#2a2a32);border:1px solid #000;
padding:12px 22px;border-radius:10px;font-size:16px;font-weight:600;
box-shadow:0 6px 20px #000;display:none;z-index:60;}
.banner.show{display:block;}
.banner small{display:block;font-weight:400;color:var(--muted);font-size:12px;margin-top:3px;}
.hint{font-size:12px;color:var(--muted);}
.status-ok{color:#7ec87e;} .status-err{color:#e73838;}
</style>
</head>
<body>
<header>
<h1><span class="w">♔</span> <span class="w">Chess</span> <span class="b">vs AI</span></h1>
<span class="hint" id="statusLine"></span>
</header>
<div class="layout">
<div class="board-wrap">
<div id="board"></div>
</div>
<div class="panel">
<div class="turn"><span class="dot2 white" id="turnDot"></span><span id="turnText">White to move</span></div>
<div>
<h2>Captured by White</h2>
<div class="captured" id="capWhite"></div>
</div>
<div>
<h2>Captured by Black</h2>
<div class="captured" id="capBlack"></div>
</div>
<div>
<h2>Moves</h2>
<div class="moves" id="moves"></div>
</div>
<div class="controls">
<div class="row">
<label class="hint" style="align-self:center;">Play as:</label>
<select id="sideSel">
<option value="white">White</option>
<option value="black">Black</option>
<option value="both">Both (self-play)</option>
<option value="analyze">Analysis</option>
</select>
</div>
<div class="row">
<label class="hint" style="align-self:center;">AI depth:</label>
<select id="depthSel">
<option value="1">Easy (1)</option>
<option value="2" selected>Medium (2)</option>
<option value="3">Hard (3)</option>
</select>
</div>
<div class="row">
<button id="undoBtn">Undo</button>
<button id="flipBtn">Flip</button>
<button id="muteBtn">🔊</button>
</div>
<div class="row">
<button id="newBtn" class="primary">New Game</button>
</div>
</div>
</div>
</div>
<div id="dragGhost"></div>
<div class="overlay" id="promoOverlay"><div class="modal">
<h3>Promote pawn to:</h3><div class="promo-opts" id="promoOpts"></div>
</div></div>
<div class="banner" id="banner"><span id="bannerText"></span><small id="bannerSub"></small></div>
<script>
"use strict";
/* ============================================================
TUNABLE CONSTANTS
============================================================ */
const CFG = {
colors: {
light: "#e8d0a8", dark: "#a9773f", sel: "#f6f66b",
lastmove: "rgba(255,241,132,0.45)", check: "rgba(231,56,56,0.75)"
},
ai: {
depth: 2, // 1/2/3
whiteSide: true, // AI plays white by default
maxTimePerMove: 1000 // ms budget (soft)
},
eval: {
pawn:100, knight:320, bishop:330, rook:500, queen:900, king:20000,
// piece-square tables (from White's perspective, rank 8..1 ordering = a8..h1)
pstPawn:[0,0,0,0,0,0,0,0, 50,50,50,50,50,50,50,50, 10,10,20,30,30,20,10,10,
5,5,10,25,25,10,5,5, 0,0,0,20,20,0,0,0, 5,-5,-10,0,0,-10,-5,5, 5,10,10,-20,-20,10,10,5, 0,0,0,0,0,0,0,0],
pstKnight:[-50,-40,-30,-30,-30,-30,-40,-50, -40,-20,0,0,0,0,-20,-40,
-30,0,10,15,15,10,0,-30, -30,5,15,20,20,15,5,-30, -30,0,15,20,20,15,0,-30,
-30,5,10,15,15,10,5,-30, -40,-20,0,5,5,0,-20,-40, -50,-40,-30,-30,-30,-30,-40,-50],
pstBishop:[-20,-10,-10,-10,-10,-10,-10,-20, -10,0,0,0,0,0,0,-10,
-10,0,5,10,10,5,0,-10, -10,5,5,10,10,5,5,-10, -10,0,10,10,10,10,0,-10,
-10,10,10,10,10,10,10,-10, -10,5,0,0,0,0,5,-10, -10,0,0,0,0,0,0,-10, -20,-10,-10,-10,-10,-10,-10,-20],
pstRook:[0,0,0,0,0,0,0,0, 5,10,10,10,10,10,10,5, -5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5,
-5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5, 0,0,0,5,5,0,0,0],
pstQueen:[-20,-10,-10,-5,-5,-10,-10,-20, -10,0,0,0,0,0,0,-10,
-10,0,5,5,5,5,0,-10, -5,0,5,5,5,5,0,-5, 0,0,5,5,5,5,0,-5,
-10,0,5,5,5,5,0,-10, -10,0,0,0,0,0,0,-10, -20,-10,-10,-5,-5,-10,-10,-20],
pstKingMid:[-30,-40,-40,-50,-50,-40,-40,-30, -30,-40,-40,-50,-50,-40,-40,-30,
-30,-40,-40,-50,-50,-40,-40,-30, -30,-40,-40,-50,-50,-40,-40,-30,
-20,-30,-30,-40,-40,-30,-30,-20, -10,-20,-20,-20,-20,-20,-20,-10,
20,20,0,0,0,0,20,20, 20,30,10,0,0,10,30,20]
},
saveKey: "chess.save.v1"
};
/* ============================================================
CHESS ENGINE — board representation: 64-element array (mailbox)
Index = rank*8 + file, rank 0 = rank 8 (top), file 0 = file a (left)
Piece = single char: 'P','N','B','R','Q','K' (white) / lowercase (black), '' empty
============================================================ */
const FILE_A=0, FILE_H=7, RANK1=0, RANK8=7;
const WHITE='w', BLACK='b';
const PIECE_TYPES = {P:'pawn',N:'knight',B:'bishop',R:'rook',Q:'queen',K:'king'};
const PIECE_SHORT = {pawn:'P',knight:'N',bishop:'B',rook:'R',queen:'Q',king:'K'};
const PST = {
P: CFG.eval.pstPawn, N: CFG.eval.pstKnight, B: CFG.eval.pstBishop,
R: CFG.eval.pstRook, Q: CFG.eval.pstQueen, K: CFG.eval.pstKingMid
};
function pieceColor(p){ return p==='' ? null : (p===p.toUpperCase() ? WHITE : BLACK); }
function opp(c){ return c===WHITE ? BLACK : WHITE; }
function inBounds(f,r){ return f>=0&&f<=7&&r>=0&&r<=7; }
const idx=(f,r)=>r*8+f;
const algebraic=(f,r)=>"abcdefgh"[f]+(r+1);
class Chess {
constructor(state){
this.reset(state);
}
reset(opts){
opts = opts||{};
this.board = opts.board ? opts.board.slice() : this.fenToBoard("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w - - 0 1");
this.turn = opts.turn||WHITE;
this.castle = opts.castle||{wK:true,wQ:true,bK:true,bQ:true};
this.ep = opts.ep||null; // {f,r} en passant target square or null
this.halfmove = opts.halfmove||0;
this.fullmove = opts.fullmove||1;
this.history = opts.history||[]; // stack of move records
}
// --- FEN / board helpers ---
fenToBoard(fen){
const rows = fen.split(' ')[0].split('/');
const b = new Array(64).fill('');
for(let r=0;r<8;r++){
const rowStr = rows[7-r]; // FEN rank 1 is last element (rows[7])
let f=0;
for(const ch of rowStr){
if(/\d/.test(ch)){ f += parseInt(ch,10); }
else { b[idx(f,r)] = ch; f++; }
}
}
return b;
}
pieceAt(f,r){ return inBounds(f,r) ? this.board[idx(f,r)] : undefined; }
findKing(color){ for(let i=0;i<64;i++){ if(this.board[i]===color+'K') return i; } return -1; }
kingPos(color){ const i=this.findKing(color); return i<0?null:{f:i%8,r:Math.floor(i/8)}; }
// --- Attack detection ---
// Is square (f,r) attacked by `by`?
isSquareAttacked(f,r,by){
// Pawn attacks
const dir = by===WHITE ? -1 : 1; // attacking pawn moves in `dir`; attack comes from r+dir
for(const df of [-1,1]){
const pf=f+df, pr=r+dir;
if(inBounds(pf,pr)){ const p=this.pieceAt(pf,pr); if(p===by+'P') return true; }
}
// Knight
const kmts=[[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
for(const [df,dr] of kmts){ const nf=f+df,nr=r+dr; if(inBounds(nf,nr)){const p=this.pieceAt(nf,nr); if(p===by+'N') return true;} }
// King
for(let df=-1;df<=1;df++) for(let dr=-1;dr<=1;dr++){
if(df===0&&dr===0) continue;
const nf=f+df,nr=r+dr; if(inBounds(nf,nr)){const p=this.pieceAt(nf,nr); if(p===by+'K') return true;}
}
// Sliding: bishop/queen (diagonals)
const diag=[[-1,-1],[-1,1],[1,-1],[1,1]];
for(const [df,dr] of diag){
let nf=f+df,nr=r+dr;
while(inBounds(nf,nr)){ const p=this.pieceAt(nf,nr); if(p!==''){ if(p===by+'B'||p===by+'Q') return true; break;} nf+=df; nr+=dr; }
}
// Sliding: rook/queen (orthogonals)
const orth=[[-1,0],[1,0],[0,-1],[0,1]];
for(const [df,dr] of orth){
let nf=f+df,nr=r+dr;
while(inBounds(nf,nr)){ const p=this.pieceAt(nf,nr); if(p!==''){ if(p===by+'R'||p===by+'Q') return true; break;} nf+=df; nr+=dr; }
}
return false;
}
inCheck(color){
const k=this.kingPos(color); if(!k) return false;
return this.isSquareAttacked(k.f,k.r,opp(color));
}
// --- Legal move generation ---
// Returns array of moves: {from,to,piece,captured,ep,castle,promo,pieceType,...}
legalMoves(color){
color = color||this.turn;
const moves=[];
for(let i=0;i<64;i++){
const p=this.board[i]; if(p==='') continue;
if(pieceColor(p)!==color) continue;
const f=i%8,r=Math.floor(i/8);
const pseudo=this.pseudoMovesFrom(f,r);
for(const m of pseudo){
if(this.isMoveLegal(m)) moves.push(m);
}
}
return moves;
}
// Pseudo-legal moves from a square (no self-check filtering)
pseudoMovesFrom(f,r){
const p=this.board[idx(f,r)]; if(p==='') return [];
const color=pieceColor(p); const type=PIECE_TYPES[p.toUpperCase()];
const moves=[];
const enemy=opp(color);
const add=(tf,tr,extra)=>{
moves.push(Object.assign({from:{f,r},to:{f:tf,r:tr},piece:p,pieceType:type,
pieceColor:color, captured:'', ep:false, castle:'', promo:null}, extra||{}));
};
if(type==='pawn'){
const dir=color===WHITE?1:-1;
const startR = color===WHITE?1:6;
const promoR = color===WHITE?7:0;
const oneR=r+dir;
// forward one
if(inBounds(f,oneR)&&this.pieceAt(f,oneR)===''){
if(oneR===promoR) add(f,oneR,{promo:true});
else add(f,oneR);
// forward two
if(r===startR){ const twoR=r+2*dir; if(this.pieceAt(f,twoR)==='') add(f,twoR,{double:true}); }
}
// captures
for(const df of [-1,1]){
const nf=f+df, nr=r+dir;
if(!inBounds(nf,nr)) continue;
const t=this.pieceAt(nf,nr);
if(t&&pieceColor(t)===enemy){
if(nr===promoR) add(nf,nr,{promo:true, captured:t});
else add(nf,nr,{captured:t});
}
// en passant
if(this.ep && this.ep.f===nf && this.ep.r===nr){
add(nf,nr,{ep:true, captured:(color===WHITE?'p':'P')});
}
}
} else if(type==='knight'){
const kmts=[[1,2],[2,1],[2,-1],[1,-2],[-1,-2],[-2,-1],[-2,1],[-1,2]];
for(const [df,dr] of kmts){ const nf=f+df,nr=r+dr; if(!inBounds(nf,nr))continue; const t=this.pieceAt(nf,nr); if(t===''){add(nf,nr);} else if(pieceColor(t)===enemy){add(nf,nr,{captured:t});} }
} else if(type==='king'){
for(let df=-1;df<=1;df++) for(let dr=-1;dr<=1;dr++){
if(df===0&&dr===0)continue; const nf=f+df,nr=r+dr; if(!inBounds(nf,nr))continue;
const t=this.pieceAt(nf,nr); if(t===''){add(nf,nr);} else if(pieceColor(t)===enemy){add(nf,nr,{captured:t});}
}
// Castling
const homeR = color===WHITE?0:7;
if(r===homeR && !this.inCheck(color)){
const rookKR = this.pieceAt(5,homeR), rookQR = this.pieceAt(3,homeR);
const canK = color===WHITE?this.castle.wK:this.castle.bK;
const canQ = color===WHITE?this.castle.wQ:this.castle.bQ;
if(canK && rookKR==='R'+color && !this.isSquareAttacked(4,homeR,enemy)
&& !this.isSquareAttacked(5,homeR,enemy) && !this.isSquareAttacked(6,homeR,enemy)
&& this.pieceAt(5,homeR)==='' && this.pieceAt(6,homeR)===''){
add(6,homeR,{castle:'K'});
}
if(canQ && rookQR==='R'+color && !this.isSquareAttacked(4,homeR,enemy)
&& !this.isSquareAttacked(3,homeR,enemy) && !this.isSquareAttacked(2,homeR,enemy)
&& this.pieceAt(3,homeR)==='' && this.pieceAt(2,homeR)==='' && this.pieceAt(1,homeR)===''){
add(2,homeR,{castle:'Q'});
}
}
} else {
// sliding
let dirs=[];
if(type==='bishop'||type==='queen') dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
if(type==='rook'||type==='queen') dirs.push([-1,0],[1,0],[0,-1],[0,1]);
for(const [df,dr] of dirs){
let nf=f+df,nr=r+dr;
while(inBounds(nf,nr)){ const t=this.pieceAt(nf,nr); if(t===''){add(nf,nr);} else { if(pieceColor(t)===enemy) add(nf,nr,{captured:t}); break;} nf+=df; nr+=dr; }
}
}
return moves;
}
// Apply a move to a copy of state, return new state object (no mutation)
applyMove(m){
const nb=this.board.slice();
nb[idx(m.to.f,m.to.r)] = m.promo ? (m.pieceColor===WHITE?m.promo:m.promo.toLowerCase()) : nb[idx(m.from.f,m.from.r)];
nb[idx(m.from.f,m.from.r)] = '';
// en passant capture removes the captured pawn
if(m.ep){ nb[idx(m.from.f,m.to.r)] = ''; }
// castling moves the rook
if(m.castle){
const homeR=m.to.r;
if(m.castle==='K'){ nb[idx(5,homeR)]=nb[idx(7,homeR)]; nb[idx(7,homeR)]=''; }
else { nb[idx(3,homeR)]=nb[idx(0,homeR)]; nb[idx(0,homeR)]=''; }
}
return nb;
}
// A move is legal if after applying it, own king is not in check.
// Also must verify the moving piece actually moves (handles king moving into check, etc.)
isMoveLegal(m){
const color=m.pieceColor;
const nb=this.applyMove(m);
const saved=this.board;
this.board=nb;
const ok=!this.inCheck(color);
this.board=saved;
return ok;
}
// Make a move on the real board; returns move with SAN; pushes history.
move(m){
const color=m.pieceColor;
const san=this.san(m);
const snapshot = {
board:this.board.slice(), turn:this.turn, castle:JSON.parse(JSON.stringify(this.castle)),
ep:{...this.ep}, halfmove:this.halfmove, fullmove:this.fullmove,
move:m, san:san, captured: m.captured|| (m.ep?'':undefined)
};
// record captured piece for tray
let capForTray = m.captured;
if(m.ep) capForTray = (color===WHITE?'p':'P');
snapshot.trayCapture = capForTray||null;
snapshot.isCheck = false;
snapshot.isCheckmate=false; snapshot.isStale=false;
// Update halfmove clock
if(m.pieceType==='pawn'||m.captured||m.ep) snapshot.halfBefore=this.halfmove;
this.board = this.applyMove(m);
// update castling rights
this.updateCastleRights(m);
// update en passant
if(m.double) this.ep={f:m.from.f, r:(m.from.r+m.to.r)/2};
else this.ep=null;
// halfmove / fullmove
if(m.pieceType==='pawn'||m.captured||m.ep) this.halfmove=0;
else this.halfmove++;
if(this.turn===BLACK) this.fullmove++;
this.turn=opp(color);
// post-move check/stalemate
const oppInCheck = this.inCheck(this.turn);
const oppMoves = this.legalMoves(this.turn);
snapshot.isCheck = oppInCheck;
if(oppInCheck && oppMoves.length===0) snapshot.isCheckmate=true;
if(!oppInCheck && oppMoves.length===0) snapshot.isStale=true;
if(!snapshot.isCheckmate && !snapshot.isStale){
const d=this.isDraw();
if(d.draw){ snapshot.draw=true; snapshot.drawReason=d.drawReason; }
}
this.history.push(snapshot);
return snapshot;
}
updateCastleRights(m){
// if king moved
if(m.piece==='K'){ this.castle.wK=false; this.castle.wQ=false; }
if(m.piece==='k'){ this.castle.bK=false; this.castle.bQ=false; }
// if rook moved from home square (file,rank)
if(m.from.f===0&&m.from.r===0) this.castle.wQ=false;
if(m.from.f===7&&m.from.r===0) this.castle.wK=false;
if(m.from.f===0&&m.from.r===7) this.castle.bQ=false;
if(m.from.f===7&&m.from.r===7) this.castle.bK=false;
// if rook captured on home square
if(m.to.f===0&&m.to.r===0) this.castle.wQ=false;
if(m.to.f===7&&m.to.r===0) this.castle.wK=false;
if(m.to.f===0&&m.to.r===7) this.castle.bQ=false;
if(m.to.f===7&&m.to.r===7) this.castle.bK=false;
if(m.to.f===7&&m.to.r===0) this.castle.wK=false;
}
undo(){
if(!this.history.length) return false;
const s=this.history.pop();
// restore pre-move state from snapshot's saved board (captured piece restored)
this.board=s.board.slice();
this.turn=s.turn;
this.castle=JSON.parse(JSON.stringify(s.castle));
this.ep={...s.ep};
this.halfmove=s.halfmove;
this.fullmove=s.fullmove;
return true;
}
// --- SAN ---
san(m){
if(m.castle==='K') return "O-O";
if(m.castle==='Q') return "O-O-O";
let s="";
const t=m.pieceType;
if(t!=='pawn'){
s += PIECE_SHORT[t];
// disambiguation: other same-type pieces that can reach the SAME destination from a DIFFERENT square
const sameType=this.legalMoves(m.pieceColor).filter(x=>x.pieceType===t&&x.to.f===m.to.f&&x.to.r===m.to.r&&!(x.from.f===m.from.f&&x.from.r===m.from.r));
if(sameType.length){
if(sameType.every(x=>x.from.f===m.from.f)) s+=algebraic(0,m.from.r); // same file -> rank
else if(sameType.every(x=>x.from.r===m.from.r)) s+=algebraic(m.from.f,0); // same rank -> file
else s+=algebraic(m.from.f,m.from.r);
}
} else if(m.captured||m.ep){
s+=algebraic(m.from.f,0);
}
if(m.captured||m.ep) s+="x";
s+=algebraic(m.to.f,m.to.r);
if(m.promo) s+="="+m.promo.toUpperCase();
return s;
// Finalize SAN with check/mate symbols
finalSan(snapshot){
let s=snapshot.san;
if(snapshot.isCheckmate) s+="#";
else if(snapshot.isCheck) s+="+";
return s;
}
fen(){
let fen="";
for(let r=7;r>=0;r--){
let empty=0;
for(let f=0;f<8;f++){ const p=this.board[idx(f,r)]; if(p==='') empty++; else { if(empty){fen+=empty;empty=0;} fen+=p; } }
if(empty) fen+=empty;
if(r>0) fen+="/";
}
fen+=" "+this.turn+" ";
const c=this.castle;
let cs="";
if(c.wK)cs+="K"; if(c.wQ)cs+="Q"; if(c.bK)cs+="k"; if(c.bQ)cs+="q";
cs=cs||"-";
fen+=cs+" ";
fen+=(this.ep?algebraic(this.ep.f,this.ep.r):"-")+" ";
fen+=this.halfmove+" "+this.fullmove;
return fen;
}
// Draw detection
isDraw(){
// insufficient material
if(this.insufficientMaterial()) return {draw:true,reason:"insufficient material"};
if(this.halfmove>=100) return {draw:true,reason:"50-move rule"};
// stalemate
if(!this.inCheck(this.turn) && this.legalMoves(this.turn).length===0){
return {draw:true,reason:"stalemate"};
}
return {draw:false};
}
insufficientMaterial(){
const pieces=[];
for(const p of this.board){ if(p!=='') pieces.push(p); }
if(pieces.length===2) return true; // K vs K
if(pieces.length===3){
// find non-king
const nk=pieces.filter(x=>x!=='K'&&x!=='k');
// bishop or knight only
const onlyBn = pieces.every(x=>x==='K'||x==='k'||x.toUpperCase()==='B'||x.toUpperCase()==='N');
if(onlyBn && nk.length===1) return true;
}
return false;
}
}
/* ============================================================
AI — minimax with alpha-beta + quiescence + move ordering
============================================================ */
const AI = {
evaluate(state){
// return eval from perspective of side to move
let score=0;
const b=state.board;
for(let i=0;i<64;i++){
const p=b[i]; if(p==='') continue;
const color=pieceColor(p); const type=PIECE_TYPES[p.toUpperCase()];
const base=CFG.eval[type.toLowerCase()];
const pstKey=PIECE_SHORT[type];
const pst=PST[pstKey];
// white piece evaluated from white perspective (rank 8 first in table)
let pv=base;
pv += color===WHITE ? pst[i] : pst[63-i];
score += color===WHITE ? pv : -pv;
}
return score;
},
search(state, depth, alpha, beta, color){
// color = side to move (maximizer)
const moves=state.legalMoves(state.turn);
if(moves.length===0){
if(state.inCheck(state.turn)) return -100000+ (10-depth); // checkmate, prefer faster
return 0; // stalemate/draw
}
if(depth===0){
return this.quiesce(state, alpha, beta, color);
}
// move ordering: captures first (MVV-LVA-ish)
const ordered=this.orderMoves(moves);
let best=-Infinity;
for(const m of ordered){
// snapshot for undo-based search
const snap=state.snapshot();
state.move(m);
const val=-this.search(state, depth-1, -beta, -alpha, opp(color));
state.restore(snap);
if(val>best) best=val;
if(best>alpha) alpha=best;
if(alpha>=beta) break;
}
return best;
},
quiesce(state, alpha, beta, color){
const stand=this.evaluate(state);
if(stand>=beta) return beta;
if(stand>alpha) alpha=stand;
const captures=state.legalMoves(state.turn).filter(m=>m.captured||m.ep);
const ordered=this.orderMoves(captures);
for(const m of ordered){
const snap=state.snapshot();
state.move(m);
const val=-this.quiesce(state, -beta, -alpha, opp(color));
state.restore(snap);
if(val>alpha) alpha=val;
if(alpha>=beta) return beta;
}
return alpha;
},
orderMoves(moves){
return moves.slice().sort((a,b)=>{
let sa=0,sb=0;
if(a.captured) sa+=10*(CFG.eval[PIECE_TYPES[a.captured.toUpperCase()]]||0)-CFG.eval[PIECE_TYPES[a.pieceType]]/10;
if(b.captured) sb+=10*(CFG.eval[PIECE_TYPES[b.captured.toUpperCase()]]||0)-CFG.eval[PIECE_TYPES[b.pieceType]]/10;
if(a.promo) sa+=900; if(b.promo) sb+=900;
if(a.castle) sa+=10; if(b.castle) sb+=10;
return sb-sa;
});
},
bestMove(state){
const color=state.turn;
const depth=CFG.ai.depth;
const moves=state.legalMoves(color);
if(moves.length===0) return null;
const ordered=this.orderMoves(moves);
let best=null; let bestVal=-Infinity;
const start=Date.now();
let alpha=-Infinity, beta=Infinity;
// Iterative deepening-ish: just do fixed depth but time-guard via quiesce only.
for(const m of ordered){
const snap=state.snapshot();
state.move(m);
const val=-this.search(state, depth-1, -beta, -alpha, opp(color));
state.restore(snap);
if(val>bestVal){ bestVal=val; best=m; }
if(val>alpha) alpha=val;
// soft time guard
if(Date.now()-start > CFG.ai.maxTimePerMove) break;
}
return best||moves[0];
}
};
/* Chess snapshot/restore for fast search */
(function(){
Chess.prototype.snapshot=function(){
return {
board:this.board.slice(), turn:this.turn,
castle:JSON.parse(JSON.stringify(this.castle)),
ep:this.ep?{...this.ep}:null,
halfmove:this.halfmove, fullmove:this.fullmove
};
};
Chess.prototype.restore=function(s){
this.board=s.board.slice(); this.turn=s.turn;
this.castle=JSON.parse(JSON.stringify(s.castle));
this.ep=s.ep?{...s.ep}:null; this.halfmove=s.halfmove; this.fullmove=s.fullmove;
};
})();
/* ============================================================
UI CONTROLLER
============================================================ */
const UI = {
game:null,
selected:null, // {f,r}
legalForSel:[], // array of moves
lastMove:null, // {from,to}
flipped:false,
muted:false,
aiThinking:false,
audioCtx:null,
init(){
const saved=localStorage.getItem(CFG.saveKey);
if(saved){
try{
const d=JSON.parse(saved);
this.game=new Chess();
this.game.board=d.board.slice();
this.game.turn=d.turn;
this.game.castle=d.castle;
this.game.ep=d.ep||null;
this.game.halfmove=d.halfmove||0;
this.game.fullmove=d.fullmove||1;
this.game.history=d.history||[];
this.lastMove = this.game.history.length? {from:this.game.history[this.game.history.length-1].move.from, to:this.game.history[this.game.history.length-1].move.to}:null;
}catch(e){ this.newGame(); }
} else {
this.newGame();
}
this.bindControls();
this.render();
this.updateStatus();
// AI to move?
if(this.shouldAIMove()) setTimeout(()=>this.aiMove(), 300);
},
newGame(){
this.game=new Chess();
this.selected=null; this.legalForSel=[]; this.lastMove=null;
this.aiThinking=false;
this.hideBanner();
this.hidePromo();
this.persist();
this.render();
this.updateStatus();
if(this.shouldAIMove()) setTimeout(()=>this.aiMove(), 300);
},
side(){ return document.getElementById('sideSel').value; },
depth(){ return parseInt(document.getElementById('depthSel').value,10); },
bindControls(){
document.getElementById('undoBtn').addEventListener('click',()=>this.undo());
document.getElementById('flipBtn').addEventListener('click',()=>{ this.flipped=!this.flipped; this.render(); });
document.getElementById('muteBtn').addEventListener('click',()=>this.toggleMute());
document.getElementById('newBtn').addEventListener('click',()=>this.newGame());
document.getElementById('sideSel').addEventListener('change',()=>{
this.selected=null; this.legalForSel=[];
this.render();
if(this.shouldAIMove()) setTimeout(()=>this.aiMove(), 300);
});
document.getElementById('depthSel').addEventListener('change',()=>{
CFG.ai.depth=this.depth();
});
},
shouldAIMove(){
const s=this.side();
if(s==='both'||s==='analyze') return false;
const aiColor = this.game.turn;
const playerColor = s==='white'?WHITE:BLACK;
return aiColor!==playerColor;
},
aiMove(){
if(this.aiThinking) return;
if(this.gameover()) return;
if(!this.shouldAIMove()) return;
this.aiThinking=true;
document.getElementById('statusLine').textContent="AI thinking…";
// run in a timeout so UI can update
setTimeout(()=>{
const t0=Date.now();
const m=AI.bestMove(this.game);
if(!m){ this.aiThinking=false; this.updateStatus(); return; }
this.game.move(m);
this.lastMove={from:m.from,to:m.to};
this.aiThinking=false;
this.playSoundFor(this.game.history[this.game.history.length-1]);
this.persist();
this.render();
this.updateStatus();
this.maybeAutoScroll();
}, 30);
},
undo(){
if(this.aiThinking) return;
// undo both AI and player move
const before=this.game.history.length;
// if it's currently AI's turn (shouldn't happen) just undo one
if(this.game.history.length===0) return;
// ensure we undo to player's turn: undo AI then player if needed
this.game.undo();
if(this.game.turn===this.playerColor() && this.game.history.length>0 && !this.gameover()){
this.game.undo();
}
this.selected=null; this.legalForSel=[];
this.lastMove = this.game.history.length? {from:this.game.history[this.game.history.length-1].move.from, to:this.game.history[this.game.history.length-1].move.to}:null;
this.persist();
this.render();
this.updateStatus();
},
playerColor(){ const s=this.side(); return s==='white'?WHITE:BLACK; },
/* ---- Interaction ---- */
onCellClick(f,r){
if(this.aiThinking || this.gameover()) return;
if(this.shouldAIMove()) return; // not player's turn
const p=this.game.pieceAt(f,r);
if(this.selected){
// try to make a move
const mv=this.legalForSel.find(m=>m.to.f===f&&m.to.r===r);
if(mv){
this.tryMove(mv);
return;
}
}
// select own piece
if(p&&pieceColor(p)===this.game.turn){
this.selected={f,r};
this.legalForSel=this.game.legalMoves(this.game.turn).filter(m=>m.from.f===f&&m.from.r===r);
} else {
this.selected=null; this.legalForSel=[];
}
this.render();
},
tryMove(m){
if(m.promo){
this.showPromo(m);
} else {
this.executeMove(m);
}
},
executeMove(m){
const snap=this.game.snapshot();
this.game.move(m);
this.lastMove={from:m.from,to:m.to};
this.selected=null; this.legalForSel=[];
this.playSoundFor(this.game.history[this.game.history.length-1]);
this.persist();
this.render();
this.updateStatus();
this.maybeAutoScroll();
if(this.shouldAIMove() && !this.gameover()) this.aiMove();
},
showPromo(m){
const color=m.pieceColor;
const opts=['Q','R','B','N'];
const container=document.getElementById('promoOpts');
container.innerHTML="";
for(const t of opts){
const b=document.createElement('button');
b.textContent = color===WHITE? t : t.toLowerCase();
b.style.color = color===WHITE?'#222':'#222';
b.addEventListener('click',()=>{
this.hidePromo();
this.executeMove(Object.assign({},m,{promo:t}));
});
container.appendChild(b);
}
document.getElementById('promoOverlay').classList.add('show');
},
hidePromo(){ document.getElementById('promoOverlay').classList.remove('show'); },
/* ---- Rendering ---- */
render(){
const board=document.getElementById('board');
board.innerHTML="";
const checkPos = this.game.inCheck(this.game.turn)? this.game.kingPos(this.game.turn):null;
for(let dr=0;dr<8;dr++){
for(let df=0;df<8;df++){
const r=this.flipped?7-dr:dr;
const f=this.flipped?7-df:df;
const cell=document.createElement('div');
cell.className='cell '+((df+dr)%2===0?'light':'dark');
cell.dataset.f=f; cell.dataset.r=r;
// last move highlight
if(this.lastMove && ((this.lastMove.from.f===f&&this.lastMove.from.r===r)||(this.lastMove.to.f===f&&this.lastMove.to.r===r)))
cell.classList.add('lastmove');
// selected
if(this.selected && this.selected.f===f && this.selected.r===r) cell.classList.add('sel');
// check
if(checkPos && checkPos.f===f && checkPos.r===r) cell.classList.add('check');
// piece
const p=this.game.pieceAt(f,r);
if(p!==''){
const pe=document.createElement('span');
pe.className='piece '+(pieceColor(p)===WHITE?'white':'black');
pe.textContent=GLYPHS[p];
pe.draggable=true;
pe.addEventListener('dragstart',(e)=>this.onDragStart(e,f,r));
pe.addEventListener('dragend',(e)=>this.onDragEnd(e));
cell.appendChild(pe);
}
// legal move indicators
if(this.selected){
const target=this.legalForSel.find(m=>m.to.f===f&&m.to.r===r);
if(target){
if(target.captured||target.ep){
const ring=document.createElement('div'); ring.className='ring'; cell.appendChild(ring);
} else {
const dot=document.createElement('div'); dot.className='dot'; cell.appendChild(dot);
}
}
}
// coordinates
if(df===0){ const k=document.createElement('span'); k.className='coord rank'; k.textContent=(r+1); cell.appendChild(k); }
if(dr===7){ const fi=document.createElement('span'); fi.className='coord file'; fi.textContent="abcdefgh"[f]; cell.appendChild(fi); }
cell.addEventListener('click',(e)=>{ e.preventDefault(); this.onCellClick(f,r); });
cell.addEventListener('dragover',(e)=>{ e.preventDefault(); });
cell.addEventListener('drop',(e)=>{ e.preventDefault(); this.onDrop(e,f,r); });
board.appendChild(cell);
}
}
this.renderTrays();
this.renderMoves();
this.renderTurn();
},
renderTurn(){
const dot=document.getElementById('turnDot');
const txt=document.getElementById('turnText');
dot.className='dot2 '+(this.game.turn===WHITE?'white':'black');
const label=this.game.turn===WHITE?'White to move':'Black to move';
txt.textContent = this.aiThinking? label+' (AI thinking…)' : label;
},
renderTrays(){
// captured by white = black pieces captured; captured by black = white pieces
const capW=[], capB=[];
for(const s of this.game.history){
const c=s.trayCapture;
if(!c) continue;
if(c===c.toLowerCase()) capW.push(GLYPHS[c]); // black piece captured by white
else capB.push(GLYPHS[c]); // white piece captured by black
}
document.getElementById('capWhite').innerHTML=capW.map(g=>`<span>${g}</span>`).join('');
document.getElementById('capBlack').innerHTML=capB.map(g=>`<span>${g}</span>`).join('');
},
renderMoves(){
const el=document.getElementById('moves');
el.innerHTML="";
const h=this.game.history;
for(let i=0;i<h.length;i+=2){
const row=document.createElement('div'); row.className='mv-row';
const num=document.createElement('span'); num.className='mv-num'; num.textContent=(i/2+1)+'.';
row.appendChild(num);
const white=document.createElement('span'); white.className='mv'; white.textContent=h[i].san;
row.appendChild(white);
if(h[i+1]){
const black=document.createElement('span'); black.className='mv'; black.textContent=h[i+1].san;
row.appendChild(black);
}
el.appendChild(row);
}
this.maybeAutoScroll();
},
maybeAutoScroll(){
const el=document.getElementById('moves');
el.scrollTop=el.scrollHeight;
},
updateStatus(){
const line=document.getElementById('statusLine');
if(this.gameover()){
const info=this.gameResult();
line.innerHTML='<span class="status-ok">'+info.text+'</span>';
return;
}
const turn=this.game.turn===WHITE?'White':'Black';
let extra='';
if(this.game.inCheck(this.game.turn)) extra=' — <b>check!</b>';
line.textContent = turn+' to move'+extra;
// auto AI
if(this.shouldAIMove() && !this.aiThinking && !this.gameover()){
// handled by aiMove call in render/execute
}
},
gameover(){
const h=this.game.history;
if(h.length===0) return false;
const last=h[h.length-1];
if(last.isCheckmate||last.isStale||last.draw) return true;
return false;
},
gameResult(){
const h=this.game.history; const last=h[h.length-1];
if(last.isCheckmate){
const winner = this.game.turn===WHITE?'Black':'White';
return {text:'Checkmate — '+winner+' wins!', sub:''};
}
if(last.draw){
return {text:'Draw — '+last.drawReason, sub:''};
}
return {text:'',sub:''};
},
showBanner(res){
const b=document.getElementById('banner');
document.getElementById('bannerText').textContent=res.text;
document.getElementById('bannerSub').textContent=res.sub||'';
b.classList.add('show');
},
hideBanner(){ document.getElementById('banner').classList.remove('show'); },
persist(){
localStorage.setItem(CFG.saveKey, JSON.stringify({
board:this.game.board.slice(), turn:this.game.turn,
castle:this.game.castle, ep:this.game.ep,
halfmove:this.game.halfmove, fullmove:this.game.fullmove,
history:this.game.history.map(s=>({
board:s.board, turn:s.turn, castle:s.castle, ep:s.ep,
halfmove:s.halfmove, fullmove:s.fullmove,
san:s.san, isCheck:s.isCheck, isCheckmate:s.isCheckmate,
isStale:s.isStale, draw:s.draw, drawReason:s.drawReason,
move:s.move, trayCapture:s.trayCapture
}))
}));
},
toggleMute(){
this.muted=!this.muted;
document.getElementById('muteBtn').textContent=this.muted?'🔇':'🔊';
},
/* ---- Drag and drop ---- */
dragInfo:null,
onDragStart(e,f,r){
if(this.aiThinking||this.gameover()||this.shouldAIMove()) { e.preventDefault(); return; }
const p=this.game.pieceAt(f,r);
if(!p||pieceColor(p)!==this.game.turn) return;
// set legal moves for this piece
this.selected={f,r};
this.legalForSel=this.game.legalMoves(this.game.turn).filter(m=>m.from.f===f&&m.from.r===r);
this.dragInfo={f,r};
e.dataTransfer.effectAllowed='move';
e.dataTransfer.setData('text/plain',f+','+r);
// ghost handled by browser; use custom ghost
const ghost=document.getElementById('dragGhost');
ghost.textContent=GLYPHS[p];
ghost.style.display='block';
e.dataTransfer.setDragImage(ghost,32,32);
this.render();
},
onDragEnd(e){
this.dragInfo=null;
},
onDrop(e,f,r){
if(!this.dragInfo) return;
const mv=this.legalForSel.find(m=>m.to.f===f&&m.to.r===r);
if(mv){
this.selected={f:this.dragInfo.f,r:this.dragInfo.r};
this.legalForSel=[mv];
this.tryMove(mv);
}
this.dragInfo=null;
},
/* ---- Sound ---- */
ensureAudio(){
if(!this.audioCtx){
try{ this.audioCtx=new (window.AudioContext||window.webkitAudioContext)(); }
catch(e){ this.audioCtx=null; }
}
return this.audioCtx;
},
beep(freq,dur,type,vol){
if(this.muted) return;
const ctx=this.ensureAudio(); if(!ctx) return;
const o=ctx.createOscillator(), g=ctx.createGain();
o.type=type||'sine'; o.frequency.value=freq;
g.gain.value=vol||0.15;
o.connect(g); g.connect(ctx.destination);
o.start();
g.gain.exponentialRampToValueAtTime(0.0001, ctx.currentTime+dur);
o.stop(ctx.currentTime+dur);
},
playSoundFor(snap){
if(this.muted) return;
if(snap.isCheckmate){ this.beep(300,0.3,'square',0.2); setTimeout(()=>this.beep(240,0.4,'square',0.2),150); }
else if(snap.isCheckmate) return;
else if(snap.isCheck) this.beep(660,0.18,'triangle',0.2);
else if(snap.captured||snap.ep) this.beep(200,0.12,'square',0.18);
else this.beep(320,0.08,'sine',0.12);
}
};
const GLYPHS={
'P':'♙','N':'♘','B':'♗','R':'♖','Q':'♕','K':'♔',
'p':'♟','n':'♞','b':'♝','r':'♜','q':'♛','k':'♚'
};
/* ============================================================
DEBUG INTERFACE on window
============================================================ */
window.chessGame = {
get:()=>UI.game,
fen:()=>UI.game?UI.game.fen():null,
legalMoves:()=>UI.game?UI.game.legalMoves(UI.game.turn):[],
move:(sanOrUc)=>{
if(!UI.game) return null;
// accept like 'e2e4' or SAN
let m=null;
if(/^[a-h][1-8][a-h][1-8]/.test(sanOrUc)){
const from={f:sanOrUc.charCodeAt(0)-97,r:parseInt(sanOrUc[1],10)-1};
const to={f:sanOrUc.charCodeAt(2)-97,r:parseInt(sanOrUc[3],10)-1};
m=UI.game.legalMoves(UI.game.turn).find(x=>x.from.f===from.f&&x.from.r===from.r&&x.to.f===to.f&&x.to.r===to.r);
} else {
m=UI.game.legalMoves(UI.game.turn).find(x=>UI.game.san(x)===sanOrUc);
}
if(!m) return null;
UI.executeMove(m);
return UI.game.fen();
},
newGame:()=>UI.newGame(),
undo:()=>UI.undo(),
gameover:()=>UI.gameover(),
isDraw:()=>UI.game?UI.game.isDraw():{draw:false},
depth:(d)=>{ if(d!==undefined){CFG.ai.depth=d;} return CFG.ai.depth; },
setSide:(s)=>{ document.getElementById('sideSel').value=s; document.getElementById('sideSel').dispatchEvent(new Event('change')); },
aiMove:()=>UI.aiMove()
};
document.addEventListener('DOMContentLoaded',()=>UI.init());
</script>
</body>
</html>
<!-- agent-meta {"model":"bartowski/Ornith-1.5-35B-A3B","provider":"brigid","persona":"full","sessionId":"06f47a2f-7aba-4852-addf-3f0b148a8190","tokensIn":7585424,"tokensOut":110524,"tokensTotal":7695948,"cost":0,"turns":94,"toolCalls":93,"failedToolCalls":5,"timestamp":"2026-08-23T18:56:37.947Z"} -->