Chess
muse spark 1.2 contributor · 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,869 bytes · SHA-256 d857b99856aa
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — Grandmaster Board</title>
<style>
/* ============ TUNABLE CONSTANTS (CSS) ============ */
:root{
--light:#f0d9b5;
--dark:#b58863;
--light-hi:#f7e8c9;
--dark-hi:#c9a27a;
--select:#f6e58d;
--legal-dot:#2f3542;
--capture-ring:#e74c3c;
--last-move:#a8d08d;
--check-glow:#ff4757;
--bg:#1a1a1e;
--panel:#26262b;
--panel2:#2f2f35;
--text:#eee;
--muted:#9a9aa0;
--accent:#d4a574;
--border:#3a3a42;
}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:'Segoe UI',system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);min-height:100vh;display:flex;flex-direction:column}
header{padding:14px 20px;display:flex;align-items:center;justify-content:space-between;border-bottom:1px solid var(--border);background:#1e1e22;flex-wrap:wrap;gap:10px}
header h1{font-size:18px;letter-spacing:0.12em;text-transform:uppercase;font-weight:700}
header h1 span{color:var(--accent)}
.hdr-right{display:flex;gap:8px;align-items:center;flex-wrap:wrap}
.btn{appearance:none;border:1px solid var(--border);background:var(--panel2);color:var(--text);padding:7px 12px;border-radius:8px;cursor:pointer;font-size:13px;font-weight:600;transition:all .15s}
.btn:hover{background:#3a3a42;transform:translateY(-1px)}
.btn.primary{background:var(--accent);color:#1a1a1e;border-color:var(--accent)}
.btn.small{padding:5px 9px;font-size:12px}
select.btn{padding:6px 8px}
.layout{flex:1;display:flex;gap:20px;padding:18px;max-width:1280px;margin:0 auto;width:100%;align-items:flex-start;justify-content:center}
.board-wrap{position:relative;flex-shrink:0}
.board{width:min(68vh, 560px);height:min(68vh,560px);display:grid;grid-template-columns:repeat(8,1fr);grid-template-rows:repeat(8,1fr);border:6px solid #3e2f1f;border-radius:6px;overflow:hidden;box-shadow:0 20px 50px rgba(0,0,0,.5), 0 0 0 1px rgba(255,255,255,.06);background:#3e2f1f;user-select:none;touch-action:none}
.square{position:relative;display:flex;align-items:center;justify-content:center;font-size:42px;cursor:pointer;transition:background .08s}
.square.light{background:var(--light)}
.square.dark{background:var(--dark)}
.square.last-move.light{background:#c8e6a8}
.square.last-move.dark{background:#a3c48a}
.square.selected.light{background:var(--select)!important}
.square.selected.dark{background:#e6c86a!important}
.square.check{box-shadow:inset 0 0 0 4px var(--check-glow), inset 0 0 18px rgba(255,71,87,.6)}
.square.legal::after{content:'';width:16px;height:16px;background:rgba(47,53,66,.72);border-radius:50%;position:absolute;pointer-events:none}
.square.legal.capture::after{width:92%;height:92%;background:transparent;border:4px solid rgba(231,76,60,.85);border-radius:6px}
.coord{position:absolute;font-size:10px;font-weight:700;opacity:.55;pointer-events:none}
.coord.file{bottom:2px;right:4px}
.coord.rank{top:2px;left:3px}
.square.dark .coord{color:var(--light)}
.square.light .coord{color:var(--dark)}
.piece{width:100%;height:100%;display:flex;align-items:center;justify-content:center;z-index:2;cursor:grab;transition:transform .08s;filter:drop-shadow(0 1px 2px rgba(0,0,0,.4))}
.piece.dragging{position:absolute;z-index:100;pointer-events:none;transform:scale(1.08);filter:drop-shadow(0 8px 16px rgba(0,0,0,.5))}
.piece.w{color:#f5f5f0;text-shadow:-1.2px -1.2px 0 #1a1a1a, 1.2px -1.2px 0 #1a1a1a, -1.2px 1.2px 0 #1a1a1a, 1.2px 1.2px 0 #1a1a1a, 0 0 3px #000, 0 2px 4px rgba(0,0,0,.9);filter:drop-shadow(0 1px 1px rgba(0,0,0,.8))}
.piece.b{color:#0d0d0d;text-shadow:-0.8px -0.8px 0 #e8dcc8, 0.8px -0.8px 0 #e8dcc8, -0.8px 0.8px 0 #e8dcc8, 0.8px 0.8px 0 #e8dcc8, 0 1px 3px rgba(0,0,0,.7)}
.side{width:340px;flex-shrink:0;display:flex;flex-direction:column;gap:14px}
.card{background:var(--panel);border:1px solid var(--border);border-radius:12px;padding:14px}
.card h3{font-size:11px;letter-spacing:.12em;text-transform:uppercase;color:var(--muted);margin-bottom:10px;display:flex;justify-content:space-between;align-items:center}
.status{display:flex;align-items:center;gap:10px;padding:10px 12px;background:var(--panel2);border-radius:8px;border:1px solid var(--border)}
.turn-dot{width:12px;height:12px;border-radius:50%;background:#fff;box-shadow:0 0 0 2px var(--border);flex-shrink:0}
.turn-dot.b{background:#111;border:1px solid #555}
.captured{display:flex;flex-wrap:wrap;gap:2px;min-height:22px;font-size:18px;line-height:1}
.move-list{max-height:220px;overflow-y:auto;display:flex;flex-direction:column;gap:1px;background:var(--bg);border-radius:8px;padding:6px;border:1px solid var(--border)}
.move-row{display:grid;grid-template-columns:28px 1fr 1fr;gap:4px;padding:4px 6px;border-radius:6px;font-size:13px;font-family:'Consolas','Courier New',monospace}
.move-row:nth-child(odd){background:rgba(255,255,255,.03)}
.move-row span.num{color:var(--muted)}
.controls{display:flex;flex-wrap:wrap;gap:8px}
.banner{padding:12px;text-align:center;border-radius:8px;font-weight:700;letter-spacing:.04em;display:none}
.banner.show{display:block}
.banner.check{background:rgba(255,71,87,.15);border:1px solid rgba(255,71,87,.4);color:#ff6b81}
.banner.mate{background:#d4a574;color:#1a1a1e}
.banner.draw{background:#57606f;color:#fff}
.promotion-overlay{position:absolute;inset:0;background:rgba(0,0,0,.55);display:none;align-items:center;justify-content:center;z-index:50;border-radius:6px}
.promotion-overlay.show{display:flex}
.promo-box{background:#fff;border-radius:10px;padding:10px;display:flex;gap:6px;box-shadow:0 10px 30px rgba(0,0,0,.4)}
.promo-box button{width:56px;height:56px;font-size:36px;border:2px solid #ddd;border-radius:8px;background:#f8f8f8;cursor:pointer}
.promo-box button:hover{border-color:var(--accent);background:#fff}
.flip-label{font-size:11px;color:var(--muted)}
@media(max-width:900px){
.layout{flex-direction:column;align-items:center}
.side{width:100%;max-width:560px}
.board{width:min(92vw,560px);height:min(92vw,560px)}
}
</style>
</head>
<body>
<header>
<h1>♔ Chess <span>Grandmaster</span></h1>
<div class="hdr-right">
<label class="flip-label">Play as <select id="colorSel" class="btn small"><option value="w">White</option><option value="b">Black</option><option value="both">Both / Analysis</option></select></label>
<label class="flip-label">AI <select id="diffSel" class="btn small"><option value="1">Easy (1)</option><option value="2" selected>Medium (2)</option><option value="3">Hard (3)</option></select></label>
<button class="btn small" id="flipBtn">⇅ Flip Board</button>
<button class="btn small" id="muteBtn">🔊 Sound</button>
</div>
</header>
<div class="layout">
<div class="board-wrap" id="boardWrap">
<div class="board" id="board"></div>
<div class="promotion-overlay" id="promoOverlay"><div class="promo-box" id="promoBox"></div></div>
</div>
<div class="side">
<div class="card">
<div class="status" id="statusBox"><div class="turn-dot" id="turnDot"></div><div><div id="turnText" style="font-weight:700;font-size:14px">White to move</div><div id="subText" style="font-size:12px;color:var(--muted)">Game in progress</div></div></div>
<div class="banner" id="banner"></div>
</div>
<div class="card">
<h3>Captured by White <span id="matW" style="color:var(--accent);font-size:12px"></span></h3>
<div class="captured" id="capWhite"></div>
<h3 style="margin-top:10px">Captured by Black <span id="matB" style="color:var(--accent);font-size:12px"></span></h3>
<div class="captured" id="capBlack"></div>
</div>
<div class="card">
<h3>Moves <span id="moveCount" style="text-transform:none;letter-spacing:0;color:var(--muted)"></span></h3>
<div class="move-list" id="moveList"><div style="padding:20px;text-align:center;color:var(--muted);font-size:13px">No moves yet</div></div>
</div>
<div class="card">
<div class="controls">
<button class="btn" id="undoBtn">↩ Undo</button>
<button class="btn" id="hintBtn">💡 Hint</button>
<button class="btn primary" id="newBtn">+ New Game</button>
</div>
</div>
</div>
</div>
<script>
/* ============================================================
TUNABLE CONSTANTS
============================================================ */
const CONFIG = {
AI_DEPTH_EASY: 1,
AI_DEPTH_MED: 2,
AI_DEPTH_HARD: 3,
PIECE_VALUES: {p:100, n:320, b:330, r:500, q:900, k:20000},
// PSTs are from white perspective; will be mirrored for black
};
// Unicode pieces
const GLYPH = {
'wK':'♔','wQ':'♕','wR':'♖','wB':'♗','wN':'♘','wP':'♙',
'bK':'♚','bQ':'♛','bR':'♜','bB':'♝','bN':'♞','bP':'♟'
};
const PIECE_LETTER = {p:'', n:'N', b:'B', r:'R', q:'Q', k:'K'};
const FILES = ['a','b','c','d','e','f','g','h'];
/* ============================================================
BOARD REPRESENTATION
============================================================ */
// board[0]=a8 ... board[63]=h1
// piece = 'wP','bK' etc or null
let state = null;
let history = [];
let selected = null; // index
let legalTargets = []; // array of move objects for selected
let flipped = false;
let muted = false;
let pendingPromo = null;
let aiThinking = false;
function initialState(){
const b = Array(64).fill(null);
const back = ['R','N','B','Q','K','B','N','R'];
for(let f=0;f<8;f++){
b[f] = 'b'+back[f];
b[8+f] = 'bP';
b[48+f] = 'wP';
b[56+f] = 'w'+back[f];
}
return {
board:b,
turn:'w',
castling:{wK:true,wQ:true,bK:true,bQ:true},
enPassant:null,
halfmove:0,
fullmove:1,
lastMove:null
};
}
function cloneState(s){
return {
board:s.board.slice(),
turn:s.turn,
castling:{...s.castling},
enPassant:s.enPassant,
halfmove:s.halfmove,
fullmove:s.fullmove,
lastMove:s.lastMove ? {...s.lastMove} : null
};
}
function idxToRC(i){ return [Math.floor(i/8), i%8]; }
function rcToIdx(r,c){ return r*8+c; }
function idxToAlg(i){ const [r,c]=idxToRC(i); return FILES[c]+(8-r); }
function algToIdx(a){ const f=FILES.indexOf(a[0]); const r=8-parseInt(a[1]); return r*8+f; }
function pieceColor(p){ return p ? p[0] : null; }
function pieceType(p){ return p ? p[1].toLowerCase() : null; }
function opposite(c){ return c==='w'?'b':'w'; }
/* ============================================================
ATTACK DETECTION & KING FIND
============================================================ */
function findKing(board, color){
const k = color+'K';
for(let i=0;i<64;i++) if(board[i]===k) return i;
return -1;
}
function isSquareAttacked(board, sq, byColor){
const [r,c]=idxToRC(sq);
// pawn attacks
const dir = byColor==='w' ? -1 : 1;
// pawns attack diagonally forward (from pawn perspective). So to attack sq, pawn is one rank behind.
// If byColor is white, white pawns are below sq (higher row number) because white moves up (decreasing r)
// White pawn at (r+1,c-1) or (r+1,c+1) attacks sq
// Black pawn at (r-1,c-1) or (r-1,c+1) attacks sq
const pr = r + (byColor==='w' ? 1 : -1);
if(pr>=0 && pr<8){
if(c>0 && board[rcToIdx(pr,c-1)]===byColor+'P') return true;
if(c<7 && board[rcToIdx(pr,c+1)]===byColor+'P') return true;
}
// knights
const kn=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
for(const [dr,dc] of kn){
const nr=r+dr,nc=c+dc;
if(nr>=0&&nr<8&&nc>=0&&nc<8 && board[rcToIdx(nr,nc)]===byColor+'N') return true;
}
// king
for(let dr=-1;dr<=1;dr++) for(let dc=-1;dc<=1;dc++) if(dr||dc){
const nr=r+dr,nc=c+dc;
if(nr>=0&&nr<8&&nc>=0&&nc<8 && board[rcToIdx(nr,nc)]===byColor+'K') return true;
}
// sliding pieces
const dirs = [[-1,0],[1,0],[0,-1],[0,1],[-1,-1],[-1,1],[1,-1],[1,1]];
for(let d=0;d<8;d++){
const [dr,dc]=dirs[d];
let nr=r+dr,nc=c+dc;
while(nr>=0&&nr<8&&nc>=0&&nc<8){
const p=board[rcToIdx(nr,nc)];
if(p){
if(pieceColor(p)===byColor){
const t=pieceType(p);
if(d<4 && (t==='r'||t==='q')) return true;
if(d>=4 && (t==='b'||t==='q')) return true;
}
break;
}
nr+=dr; nc+=dc;
}
}
return false;
}
function isInCheck(board, color){
const k=findKing(board,color);
if(k===-1) return false;
return isSquareAttacked(board,k,opposite(color));
}
/* ============================================================
MOVE GENERATION
============================================================ */
function generatePseudoMoves(s){
const moves=[];
const board=s.board;
const us=s.turn, them=opposite(us);
for(let from=0;from<64;from++){
const p=board[from];
if(!p || pieceColor(p)!==us) continue;
const type=pieceType(p);
const [r,c]=idxToRC(from);
if(type==='p'){
const dir = us==='w' ? -1 : 1;
const startRank = us==='w' ? 6 : 1;
const promoRank = us==='w' ? 0 : 7;
// single push
const nr=r+dir;
if(nr>=0&&nr<8){
const to=rcToIdx(nr,c);
if(!board[to]){
if(nr===promoRank){
for(const promo of ['q','r','b','n']) moves.push({from,to,promotion:promo,capture:null,isEnPassant:false,isCastle:false,isDouble:false});
} else {
moves.push({from,to,promotion:null,capture:null,isEnPassant:false,isCastle:false,isDouble:false});
// double push
if(r===startRank){
const nr2=r+dir*2;
const to2=rcToIdx(nr2,c);
if(!board[to2]) moves.push({from,to:to2,promotion:null,capture:null,isEnPassant:false,isCastle:false,isDouble:true});
}
}
}
// captures
for(const dc of [-1,1]){
const nc=c+dc;
if(nc<0||nc>7) continue;
const capIdx=rcToIdx(nr,nc);
const target=board[capIdx];
if(target && pieceColor(target)===them){
if(nr===promoRank){
for(const promo of ['q','r','b','n']) moves.push({from,to:capIdx,promotion:promo,capture:target,isEnPassant:false,isCastle:false,isDouble:false});
} else {
moves.push({from,to:capIdx,promotion:null,capture:target,isEnPassant:false,isCastle:false,isDouble:false});
}
}
// en passant
if(s.enPassant!==null && capIdx===s.enPassant){
moves.push({from,to:capIdx,promotion:null,capture:them+'P',isEnPassant:true,isCastle:false,isDouble:false});
}
}
}
} else if(type==='n'){
const del=[[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]];
for(const [dr,dc] of del){
const nr=r+dr,nc=c+dc;
if(nr<0||nr>7||nc<0||nc>7) continue;
const to=rcToIdx(nr,nc);
const t=board[to];
if(!t || pieceColor(t)===them) moves.push({from,to,promotion:null,capture:t||null,isEnPassant:false,isCastle:false,isDouble:false});
}
} else if(type==='b' || type==='r' || type==='q'){
const dirs=[];
if(type==='b'||type==='q') dirs.push([-1,-1],[-1,1],[1,-1],[1,1]);
if(type==='r'||type==='q') dirs.push([-1,0],[1,0],[0,-1],[0,1]);
for(const [dr,dc] of dirs){
let nr=r+dr,nc=c+dc;
while(nr>=0&&nr<8&&nc>=0&&nc<8){
const to=rcToIdx(nr,nc);
const t=board[to];
if(!t) moves.push({from,to,promotion:null,capture:null,isEnPassant:false,isCastle:false,isDouble:false});
else {
if(pieceColor(t)===them) moves.push({from,to,promotion:null,capture:t,isEnPassant:false,isCastle:false,isDouble:false});
break;
}
nr+=dr; nc+=dc;
}
}
} else if(type==='k'){
for(let dr=-1;dr<=1;dr++) for(let dc=-1;dc<=1;dc++) if(dr||dc){
const nr=r+dr,nc=c+dc;
if(nr<0||nr>7||nc<0||nc>7) continue;
const to=rcToIdx(nr,nc);
const t=board[to];
if(!t || pieceColor(t)===them) moves.push({from,to,promotion:null,capture:t||null,isEnPassant:false,isCastle:false,isDouble:false});
}
// castling
const kingStart = us==='w' ? 60 : 4;
if(from===kingStart && !isInCheck(board,us)){
// kingside
const canK = us==='w' ? s.castling.wK : s.castling.bK;
if(canK){
const f1=from+1, f2=from+2;
const rookFrom=from+3;
if(!board[f1] && !board[f2] && board[rookFrom]===us+'R'){
if(!isSquareAttacked(board,f1,them) && !isSquareAttacked(board,f2,them)){
moves.push({from,to:f2,promotion:null,capture:null,isEnPassant:false,isCastle:'K',isDouble:false});
}
}
}
const canQ = us==='w' ? s.castling.wQ : s.castling.bQ;
if(canQ){
const f1=from-1,f2=from-2,f3=from-3;
const rookFrom=from-4;
if(!board[f1] && !board[f2] && !board[f3] && board[rookFrom]===us+'R'){
if(!isSquareAttacked(board,f1,them) && !isSquareAttacked(board,f2,them)){
moves.push({from,to:f2,promotion:null,capture:null,isEnPassant:false,isCastle:'Q',isDouble:false});
}
}
}
}
}
}
return moves;
}
function generateLegalMoves(s){
const pseudo=generatePseudoMoves(s);
const legal=[];
for(const m of pseudo){
const ns=applyMove(s,m);
// after move, the moving side's king must not be in check
if(!isInCheck(ns.board, s.turn)){
legal.push(m);
}
}
return legal;
}
function applyMove(s,m){
const ns=cloneState(s);
const board=ns.board;
const us=s.turn, them=opposite(us);
const piece=board[m.from];
// handle en passant capture
if(m.isEnPassant){
const [r,c]=idxToRC(m.to);
const capRow = r + (us==='w' ? 1 : -1);
const capIdx=rcToIdx(capRow,c);
board[capIdx]=null;
}
// handle castling rook move
if(m.isCastle){
if(m.isCastle==='K'){
const rookFrom=m.from+3, rookTo=m.from+1;
board[rookTo]=board[rookFrom];
board[rookFrom]=null;
} else {
const rookFrom=m.from-4, rookTo=m.from-1;
board[rookTo]=board[rookFrom];
board[rookFrom]=null;
}
}
// move piece (with promotion)
board[m.to]= m.promotion ? us + m.promotion.toUpperCase() : piece;
board[m.from]=null;
// update castling rights
if(pieceType(piece)==='k'){
if(us==='w'){ ns.castling.wK=false; ns.castling.wQ=false; }
else { ns.castling.bK=false; ns.castling.bQ=false; }
}
if(pieceType(piece)==='r'){
if(us==='w'){
if(m.from===56) ns.castling.wQ=false;
if(m.from===63) ns.castling.wK=false;
} else {
if(m.from===0) ns.castling.bQ=false;
if(m.from===7) ns.castling.bK=false;
}
}
// if capture removed rook on starting square, remove castling right
if(m.capture && pieceType(m.capture)==='r'){
const capSq = m.isEnPassant ? (()=>{const [r,c]=idxToRC(m.to); return rcToIdx(r+(us==='w'?1:-1),c)})() : m.to;
// Actually for en passant capture is pawn, not rook, so ignore
if(!m.isEnPassant){
if(capSq===56) ns.castling.wQ=false;
if(capSq===63) ns.castling.wK=false;
if(capSq===0) ns.castling.bQ=false;
if(capSq===7) ns.castling.bK=false;
}
}
// en passant square
if(m.isDouble){
const [r,c]=idxToRC(m.from);
ns.enPassant = rcToIdx(r + (us==='w'?-1:1), c);
} else {
ns.enPassant=null;
}
// halfmove clock
if(pieceType(piece)==='p' || m.capture) ns.halfmove=0;
else ns.halfmove = s.halfmove+1;
ns.turn = them;
if(us==='b') ns.fullmove = s.fullmove+1;
ns.lastMove = {...m, piece, san:null};
return ns;
}
/* ============================================================
GAME STATUS
============================================================ */
function isInsufficientMaterial(board){
const pieces=[];
for(let i=0;i<64;i++) if(board[i]) pieces.push(board[i]);
if(pieces.length===2) return true; // K vs K
if(pieces.length===3){
const types=pieces.map(p=>pieceType(p)).sort().join('');
if(types==='kn' || types==='kb') return true; // K+N vs K or K+B vs K (k is king)
// actually pieces include two kings + one minor: check
const minors=pieces.filter(p=>pieceType(p)!=='k');
if(minors.length===1 && (pieceType(minors[0])==='b' || pieceType(minors[0])==='n')) return true;
}
return false;
}
function gameStatus(s){
const legal=generateLegalMoves(s);
const inCheck=isInCheck(s.board,s.turn);
if(legal.length===0){
if(inCheck) return {over:true, result: s.turn==='w' ? '0-1' : '1-0', reason: 'Checkmate'};
else return {over:true, result:'1/2-1/2', reason:'Stalemate'};
}
if(isInsufficientMaterial(s.board)) return {over:true, result:'1/2-1/2', reason:'Insufficient material'};
if(s.halfmove>=100) return {over:true, result:'1/2-1/2', reason:'50-move rule'};
if(inCheck) return {over:false, inCheck:true};
return {over:false, inCheck:false};
}
/* ============================================================
SAN GENERATION
============================================================ */
function moveToSAN(s, m){
if(m.isCastle) return m.isCastle==='K' ? 'O-O' : 'O-O-O';
const board=s.board;
const piece=board[m.from];
const type=pieceType(piece);
const isCap = !!m.capture || m.isEnPassant;
let san='';
if(type==='p'){
if(isCap) san+=FILES[m.from%8];
} else {
san+=PIECE_LETTER[type];
// disambiguation
const pseudo=generatePseudoMoves(s).filter(x=> {
const p2=board[x.from];
return p2 && pieceType(p2)===type && pieceColor(p2)===s.turn && x.to===m.to && x.from!==m.from;
});
// need to check if any other same piece could also move to same square legally? use pseudo for simplicity
if(pseudo.length>0){
const sameFile=pseudo.some(x=> x.from%8===m.from%8);
const sameRank=pseudo.some(x=> Math.floor(x.from/8)===Math.floor(m.from/8));
if(!sameFile) san+=FILES[m.from%8];
else if(!sameRank) san+= (8-Math.floor(m.from/8));
else san+=FILES[m.from%8]+(8-Math.floor(m.from/8));
}
}
if(isCap) san+='x';
san+=idxToAlg(m.to);
if(m.promotion) san+='='+m.promotion.toUpperCase();
if(m.isEnPassant) san+=' e.p.';
// check / mate suffix
const ns=applyMove(s,m);
const oppInCheck=isInCheck(ns.board, ns.turn);
if(oppInCheck){
const oppLegal=generateLegalMoves(ns);
san+= oppLegal.length===0 ? '#' : '+';
}
return san;
}
/* ============================================================
EVALUATION & AI
============================================================ */
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,10,10,10,10,0,-10, -10,5,5,10,12,5,5,-10, -10,0,5,10,10,5,0,-10, -10,5,5,5,5,5,5,-10, -10,0,5,0,0,5,0,-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],
};
function evaluateBoard(board){
let score=0;
for(let i=0;i<64;i++){
const p=board[i];
if(!p) continue;
const type=pieceType(p);
const val=CONFIG.PIECE_VALUES[type];
// PST index: board index 0=a8 corresponds to PST index 0
const pstVal = PST[type] ? PST[type][i] : 0;
if(pieceColor(p)==='w') score += val + pstVal;
else score -= val + (PST[type] ? PST[type][63-i] : 0); // mirror for black
}
return score;
}
function orderMoves(moves, board){
// MVV-LVA: captures first
return moves.slice().sort((a,b)=>{
const ca = a.capture ? CONFIG.PIECE_VALUES[pieceType(a.capture)] : 0;
const cb = b.capture ? CONFIG.PIECE_VALUES[pieceType(b.capture)] : 0;
if(cb!==ca) return cb-ca;
if(a.promotion && !b.promotion) return -1;
if(!a.promotion && b.promotion) return 1;
return 0;
});
}
function minimax(s, depth, alpha, beta, maximizing){
const status=gameStatus(s);
if(status.over){
if(status.reason==='Checkmate'){
// side to move is mated => bad for side to move
return s.turn==='w' ? -99999 : 99999;
}
return 0;
}
if(depth===0){
const ev=evaluateBoard(s.board);
return ev;
}
const moves=orderMoves(generateLegalMoves(s), s.board);
if(moves.length===0) return evaluateBoard(s.board);
if(maximizing){
let maxEval=-Infinity;
for(const m of moves){
const ns=applyMove(s,m);
const ev=minimax(ns, depth-1, alpha, beta, false);
maxEval=Math.max(maxEval, ev);
alpha=Math.max(alpha, ev);
if(beta<=alpha) break;
}
return maxEval;
} else {
let minEval=Infinity;
for(const m of moves){
const ns=applyMove(s,m);
const ev=minimax(ns, depth-1, alpha, beta, true);
minEval=Math.min(minEval, ev);
beta=Math.min(beta, ev);
if(beta<=alpha) break;
}
return minEval;
}
}
function findBestMove(s, depth){
const moves=orderMoves(generateLegalMoves(s), s.board);
if(moves.length===0) return null;
let best=null;
let bestScore = s.turn==='w' ? -Infinity : Infinity;
const maximizing = s.turn==='w';
for(const m of moves){
const ns=applyMove(s,m);
const score=minimax(ns, depth-1, -Infinity, Infinity, !maximizing);
if(maximizing){
if(score>bestScore){ bestScore=score; best=m; }
} else {
if(score<bestScore){ bestScore=score; best=m; }
}
}
return best;
}
// Chunked AI to avoid freezing UI
function findBestMoveAsync(s, depth, callback){
const moves=orderMoves(generateLegalMoves(s), s.board);
if(moves.length===0){ callback(null); return; }
let best=null;
let bestScore = s.turn==='w' ? -Infinity : Infinity;
const maximizing = s.turn==='w';
let i=0;
function step(){
if(i>=moves.length){ callback(best); return; }
const m=moves[i];
const ns=applyMove(s,m);
const score=minimax(ns, depth-1, -Infinity, Infinity, !maximizing);
if(maximizing){ if(score>bestScore){bestScore=score; best=m;}}
else { if(score<bestScore){bestScore=score; best=m;}}
i++;
if(i%2===0) setTimeout(step, 0);
else step();
}
setTimeout(step, 10);
}
/* ============================================================
PERSISTENCE
============================================================ */
function saveState(){
const data={state, history, flipped, humanColor: document.getElementById('colorSel').value, diff: document.getElementById('diffSel').value};
localStorage.setItem('chess_save', JSON.stringify(data));
}
function loadState(){
try{
const raw=localStorage.getItem('chess_save');
if(!raw) return false;
const d=JSON.parse(raw);
if(d.state && d.state.board && d.state.board.length===64){
state=d.state;
history=d.history||[];
flipped=!!d.flipped;
if(d.humanColor) document.getElementById('colorSel').value=d.humanColor;
if(d.diff) document.getElementById('diffSel').value=d.diff;
return true;
}
}catch(e){}
return false;
}
/* ============================================================
FEN & DEBUG INTERFACE
============================================================ */
function boardToFEN(s){
let fen='';
for(let r=0;r<8;r++){
let empty=0;
for(let c=0;c<8;c++){
const p=s.board[r*8+c];
if(!p) empty++;
else {
if(empty){ fen+=empty; empty=0; }
const ch=pieceType(p);
fen+= pieceColor(p)==='w' ? ch.toUpperCase() : ch;
}
}
if(empty) fen+=empty;
if(r<7) fen+='/';
}
fen+=' '+(s.turn)+' ';
let cs='';
if(s.castling.wK) cs+='K';
if(s.castling.wQ) cs+='Q';
if(s.castling.bK) cs+='k';
if(s.castling.bQ) cs+='q';
fen+= cs||'-';
fen+=' '+(s.enPassant!==null ? idxToAlg(s.enPassant) : '-');
fen+=' '+s.halfmove+' '+s.fullmove;
return fen;
}
window.game = {
fen: ()=> boardToFEN(state),
board: ()=> state.board.slice(),
state: ()=> cloneState(state),
legalMoves: ()=> generateLegalMoves(state).map(m=> ({from:idxToAlg(m.from), to:idxToAlg(m.to), promotion:m.promotion, san: moveToSAN(state,m), raw:m})),
move: (uci)=>{
// uci like e2e4 or e7e8q
if(!uci || uci.length<4) return false;
const from=algToIdx(uci.slice(0,2));
const to=algToIdx(uci.slice(2,4));
const promo=uci.length>4 ? uci[4].toLowerCase() : null;
const legal=generateLegalMoves(state);
const found=legal.find(m=> m.from===from && m.to===to && (m.promotion||null)===(promo||null));
if(!found){
// try without promo spec if pawn promotion and only one promo? allow any
const alt=legal.find(m=> m.from===from && m.to===to);
if(!alt) return false;
if(alt.promotion && !promo) return false;
doMove(alt);
return true;
}
doMove(found);
return true;
},
setFEN: (fen)=>{ /* simple */ },
perft: (depth)=>{ /* count nodes */ let cnt=0; function perftRec(s,d){ if(d===0){cnt++; return;} const ms=generateLegalMoves(s); for(const m of ms) perftRec(applyMove(s,m), d-1);} perftRec(state, depth); return cnt; }
};
/* ============================================================
SOUND
============================================================ */
let audioCtx=null;
function playSound(type){
if(muted) return;
try{
if(!audioCtx) audioCtx=new (window.AudioContext||window.webkitAudioContext)();
const o=audioCtx.createOscillator();
const g=audioCtx.createGain();
o.connect(g); g.connect(audioCtx.destination);
const now=audioCtx.currentTime;
if(type==='move'){ o.frequency.value=420; g.gain.setValueAtTime(.18,now); g.gain.exponentialRampToValueAtTime(.001, now+.12); o.start(now); o.stop(now+.12); }
else if(type==='capture'){ o.frequency.value=220; g.gain.setValueAtTime(.22,now); g.gain.exponentialRampToValueAtTime(.001, now+.18); o.start(now); o.stop(now+.18); const o2=audioCtx.createOscillator(); const g2=audioCtx.createGain(); o2.connect(g2); g2.connect(audioCtx.destination); o2.frequency.value=330; g2.gain.setValueAtTime(.12,now); g2.gain.exponentialRampToValueAtTime(.001, now+.15); o2.start(now+.02); o2.stop(now+.17); }
else if(type==='check'){ o.frequency.value=880; g.gain.setValueAtTime(.2,now); g.gain.exponentialRampToValueAtTime(.001, now+.35); o.start(now); o.stop(now+.35); }
}catch(e){}
}
/* ============================================================
RENDERING
============================================================ */
const boardEl=document.getElementById('board');
const turnDot=document.getElementById('turnDot');
const turnText=document.getElementById('turnText');
const subText=document.getElementById('subText');
const banner=document.getElementById('banner');
const moveListEl=document.getElementById('moveList');
const capWhiteEl=document.getElementById('capWhite');
const capBlackEl=document.getElementById('capBlack');
function render(){
boardEl.innerHTML='';
const lastFrom=state.lastMove ? state.lastMove.from : -1;
const lastTo=state.lastMove ? state.lastMove.to : -1;
const kingInCheck = isInCheck(state.board, state.turn) ? findKing(state.board, state.turn) : -1;
for(let i=0;i<64;i++){
const dispIdx = flipped ? 63-i : i;
const sqIdx = dispIdx; // actual board index to display at this cell
// But we iterate visual order: visual position v = i, board index b = flipped?63-i:i
const bIdx = flipped ? 63 - i : i;
const [r,c]=idxToRC(bIdx);
const isLight = (r+c)%2===0;
const div=document.createElement('div');
div.className='square '+(isLight?'light':'dark');
div.dataset.idx=bIdx;
if(bIdx===lastFrom || bIdx===lastTo) div.classList.add('last-move');
if(bIdx===kingInCheck) div.classList.add('check');
if(bIdx===selected) div.classList.add('selected');
if(legalTargets.some(m=> m.to===bIdx)){
div.classList.add('legal');
const mv=legalTargets.find(m=> m.to===bIdx);
if(mv.capture) div.classList.add('capture');
}
// coords
if((flipped? r===0 : r===7) ) {
const f=document.createElement('span'); f.className='coord file'; f.textContent=FILES[c]; div.appendChild(f);
}
if((flipped? c===7 : c===0)){
const rk=document.createElement('span'); rk.className='coord rank'; rk.textContent=8-r; div.appendChild(rk);
}
const p=state.board[bIdx];
if(p){
const pe=document.createElement('div');
pe.className='piece '+(pieceColor(p)==='w'?'w':'b');
pe.textContent=GLYPH[p];
pe.draggable=true;
pe.dataset.idx=bIdx;
// drag events
pe.addEventListener('dragstart', onDragStart);
pe.addEventListener('dragend', onDragEnd);
div.appendChild(pe);
}
div.addEventListener('click', ()=> onSquareClick(bIdx));
div.addEventListener('dragover', e=> e.preventDefault());
div.addEventListener('drop', e=>{ e.preventDefault(); onDrop(bIdx); });
boardEl.appendChild(div);
}
// status
turnDot.className='turn-dot '+(state.turn==='w'?'w':'b');
const status=gameStatus(state);
if(status.over){
if(status.reason==='Checkmate'){
turnText.textContent = status.result==='1-0' ? 'White wins!' : 'Black wins!';
subText.textContent = 'Checkmate';
banner.textContent = `♔ Checkmate — ${status.result==='1-0'?'White':'Black'} wins`;
banner.className='banner show mate';
} else {
turnText.textContent='Draw';
subText.textContent=status.reason;
banner.textContent=`½–½ Draw — ${status.reason}`;
banner.className='banner show draw';
}
} else {
banner.className='banner';
if(status.inCheck){
turnText.textContent=(state.turn==='w'?'White':'Black')+' in check!';
subText.textContent='Must get out of check';
banner.textContent='Check!';
banner.className='banner show check';
} else {
turnText.textContent=(state.turn==='w'?'White':'Black')+' to move';
subText.textContent= state.turn==='w' ? 'White pieces' : 'Black pieces';
}
}
renderCaptured();
renderMoveList();
saveState();
maybeTriggerAI();
}
function renderCaptured(){
// compute captured by diff from initial material
const count = {w:{p:0,n:0,b:0,r:0,q:0}, b:{p:0,n:0,b:0,r:0,q:0}};
const initCounts = {w:{p:8,n:2,b:2,r:2,q:1,k:1}, b:{p:8,n:2,b:2,r:2,q:1,k:1}};
const cur={w:{p:0,n:0,b:0,r:0,q:0,k:0}, b:{p:0,n:0,b:0,r:0,q:0,k:0}};
for(const p of state.board) if(p){ cur[pieceColor(p)][pieceType(p)]++; }
// captured by white = black pieces missing
function listFor(capturer){
const victim = capturer==='w' ? 'b' : 'w';
let html='';
let val=0;
for(const t of ['p','n','b','r','q']){
const missing = initCounts[victim][t] - (cur[victim][t]||0);
// promotions can create extra pieces, but ignore for display
const n=Math.max(0, missing);
for(let i=0;i<n;i++) html+= GLYPH[victim+t]+' ';
val+= n*CONFIG.PIECE_VALUES[t];
}
return {html, val};
}
const wCap=listFor('w');
const bCap=listFor('b');
capWhiteEl.innerHTML = wCap.html || '<span style="color:var(--muted);font-size:13px">—</span>';
capBlackEl.innerHTML = bCap.html || '<span style="color:var(--muted);font-size:13px">—</span>';
document.getElementById('matW').textContent = wCap.val? '+'+wCap.val : '';
document.getElementById('matB').textContent = bCap.val? '+'+bCap.val : '';
}
function renderMoveList(){
if(history.length===0){ moveListEl.innerHTML='<div style="padding:20px;text-align:center;color:var(--muted);font-size:13px">No moves yet<br><span style="font-size:11px">White moves first</span></div>'; document.getElementById('moveCount').textContent=''; return; }
let html='';
for(let i=0;i<history.length;i+=2){
const n=Math.floor(i/2)+1;
const w=history[i]?.san||'';
const b=history[i+1]?.san||'';
html+=`<div class="move-row"><span class="num">${n}.</span><span>${w}</span><span>${b}</span></div>`;
}
moveListEl.innerHTML=html;
moveListEl.scrollTop=moveListEl.scrollHeight;
document.getElementById('moveCount').textContent=history.length+' moves';
}
/* ============================================================
INTERACTION
============================================================ */
let dragFrom=null;
function onSquareClick(idx){
if(aiThinking) return;
const status=gameStatus(state);
if(status.over) return;
// if pending promotion, ignore
if(pendingPromo) return;
const p=state.board[idx];
// if selected and clicked legal target -> move
if(selected!==null && legalTargets.some(m=> m.to===idx)){
const mv=legalTargets.find(m=> m.to===idx);
attemptMove(mv);
return;
}
// select own piece
if(p && pieceColor(p)===state.turn){
// check if human is allowed to move this color
const human=document.getElementById('colorSel').value;
if(human!=='both' && human!==state.turn){
// not human's turn, ignore select
return;
}
selected=idx;
const all=generateLegalMoves(state);
legalTargets=all.filter(m=> m.from===idx);
render();
} else {
selected=null; legalTargets=[];
render();
}
}
function onDragStart(e){
const idx=parseInt(e.target.dataset.idx);
const p=state.board[idx];
if(!p || pieceColor(p)!==state.turn) { e.preventDefault(); return; }
const human=document.getElementById('colorSel').value;
if(human!=='both' && human!==state.turn){ e.preventDefault(); return; }
dragFrom=idx;
selected=idx;
legalTargets=generateLegalMoves(state).filter(m=> m.from===idx);
render();
setTimeout(()=> e.target.classList.add('dragging'),0);
}
function onDragEnd(e){ e.target.classList.remove('dragging'); }
function onDrop(toIdx){
if(dragFrom===null) return;
const mv=legalTargets.find(m=> m.to===toIdx);
if(mv) attemptMove(mv);
dragFrom=null;
}
function attemptMove(m){
// check promotion needed? If pawn to last rank but no promotion specified, show picker
const piece=state.board[m.from];
if(pieceType(piece)==='p'){
const [r]=idxToRC(m.to);
const promoRank = pieceColor(piece)==='w' ? 0 : 7;
if(r===promoRank && !m.promotion){
// need promotion picker
pendingPromo=m;
showPromotionPicker(m.to, pieceColor(piece));
return;
}
}
doMove(m);
}
function showPromotionPicker(sq, color){
const overlay=document.getElementById('promoOverlay');
const box=document.getElementById('promoBox');
box.innerHTML='';
for(const t of ['q','r','b','n']){
const btn=document.createElement('button');
btn.textContent=GLYPH[color+t.toUpperCase()];
btn.onclick=()=>{
overlay.classList.remove('show');
const mv={...pendingPromo, promotion:t};
// need to set capture correctly if capture move
const target=state.board[pendingPromo.to];
if(target) mv.capture=target;
pendingPromo=null;
doMove(mv);
};
box.appendChild(btn);
}
overlay.classList.add('show');
}
document.getElementById('promoOverlay').addEventListener('click', e=>{
if(e.target.id==='promoOverlay'){ e.target.classList.remove('show'); pendingPromo=null; }
});
function doMove(m){
const san=moveToSAN(state,m);
const ns=applyMove(state,m);
// sound
const isCap = !!m.capture || m.isEnPassant;
const givesCheck=isInCheck(ns.board, ns.turn);
if(givesCheck) playSound('check');
else if(isCap) playSound('capture');
else playSound('move');
// push history
history.push({from:m.from,to:m.to,san, uci: idxToAlg(m.from)+idxToAlg(m.to)+(m.promotion||''), fenBefore: boardToFEN(state)});
state=ns;
selected=null; legalTargets=[];
render();
}
function undo(){
if(history.length===0) return;
// undo up to 2 moves if AI involved, but spec says undo takes back both player and AI move
// We'll undo 2 if possible, else 1
const human=document.getElementById('colorSel').value;
let steps = 1;
if(human!=='both' && history.length>=2) steps=2;
// reconstruct from initial by replaying history minus steps
const remaining=history.slice(0, history.length-steps);
let s=initialState();
const newHist=[];
for(const h of remaining){
const legal=generateLegalMoves(s);
const mv=legal.find(m=> m.from===h.from && m.to===h.to && (m.promotion||null)===(h.uci.length>4?h.uci[4]:null) || (m.from===h.from && m.to===h.to && !m.promotion && h.uci.length===4));
// fallback: find by from/to
let found=legal.find(m=> m.from===h.from && m.to===h.to);
if(h.uci.length>4) found=legal.find(m=> m.from===h.from && m.to===h.to && m.promotion===h.uci[4]);
if(!found) found=legal.find(m=> m.from===h.from && m.to===h.to);
if(found){
const san=moveToSAN(s,found);
s=applyMove(s,found);
newHist.push({from:found.from,to:found.to,san, uci: idxToAlg(found.from)+idxToAlg(found.to)+(found.promotion||'')});
}
}
state=s;
history=newHist;
selected=null; legalTargets=[]; pendingPromo=null;
document.getElementById('promoOverlay').classList.remove('show');
render();
}
function newGame(){
state=initialState();
history=[];
selected=null; legalTargets=[];
document.getElementById('promoOverlay').classList.remove('show');
render();
}
function getAIDepth(){
const v=document.getElementById('diffSel').value;
if(v==='1') return CONFIG.AI_DEPTH_EASY;
if(v==='2') return CONFIG.AI_DEPTH_MED;
return CONFIG.AI_DEPTH_HARD;
}
function maybeTriggerAI(){
if(aiThinking || pendingPromo) return;
const status=gameStatus(state);
if(status.over) return;
const human=document.getElementById('colorSel').value;
if(human==='both') return;
if(state.turn===human) return;
// AI turn
aiThinking=true;
subText.textContent='AI thinking...';
const depth=getAIDepth();
findBestMoveAsync(cloneState(state), depth, (best)=>{
aiThinking=false;
if(!best) { render(); return; }
// ensure still AI turn (user may have changed settings)
if(state.turn!==human && human!=='both'){
doMove(best);
} else render();
});
}
/* ============================================================
INIT
============================================================ */
document.getElementById('flipBtn').onclick=()=>{ flipped=!flipped; render(); };
document.getElementById('muteBtn').onclick=(e)=>{ muted=!muted; e.target.textContent= muted ? '🔇 Muted' : '🔊 Sound'; };
document.getElementById('undoBtn').onclick=undo;
document.getElementById('newBtn').onclick=newGame;
document.getElementById('hintBtn').onclick=()=>{
const moves=generateLegalMoves(state);
if(moves.length===0) return;
const best=findBestMove(state, 2);
if(best){
selected=best.from;
legalTargets=[best];
render();
setTimeout(()=>{ selected=null; legalTargets=[]; render(); }, 1200);
}
};
document.getElementById('colorSel').onchange=()=>{ saveState(); render(); };
document.getElementById('diffSel').onchange=saveState;
if(!loadState()){
state=initialState();
history=[];
}
render();
// expose for probe
window._state=()=>state;
window._history=()=>history;
</script>
</body>
</html>
<!-- agent-meta {"model":"meta/muse-spark-1.2-contributor","provider":"openrouter","persona":"full","sessionId":"91c5090c-4a14-4384-8dc0-4eada87b7d98","tokensIn":314801,"tokensOut":19524,"tokensTotal":334325,"cost":0.008381294,"turns":12,"toolCalls":11,"failedToolCalls":0,"timestamp":"2026-08-23T00:31:33.077Z"} -->