Chess
GPT 6 Luna · full

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 30,151 bytes · SHA-256 bde92bc100e4
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Quiet Square — Chess</title>
<style>
:root{--ink:#292a28;--muted:#777970;--paper:#f4f0e7;--panel:#fffdf8;--line:#e7e1d6;--green:#52695b;--green2:#394d42;--light:#eadfc9;--dark:#927557;--gold:#c79548;--red:#bb594c;--shadow:0 18px 55px #352b1d18}*{box-sizing:border-box}body{margin:0;background:#f2eee5;color:var(--ink);font-family:ui-serif,Georgia,'Times New Roman',serif;min-height:100vh}.app{max-width:1280px;margin:0 auto;padding:25px 32px 32px}.top{display:flex;justify-content:space-between;align-items:center;margin-bottom:22px}.brand{display:flex;align-items:center;gap:13px}.brandmark{width:42px;height:42px;border-radius:50%;background:var(--green);color:#f6f0e3;display:grid;place-items:center;font-size:27px;box-shadow:inset 0 0 0 1px #ffffff36}.eyebrow{font:700 10px/1.2 system-ui,sans-serif;letter-spacing:.19em;text-transform:uppercase;color:var(--green);margin-bottom:4px}.brand h1{font-size:25px;line-height:1;margin:0;letter-spacing:-.03em;font-weight:600}.topnote{font:12px system-ui,sans-serif;color:var(--muted);letter-spacing:.02em}.layout{display:grid;grid-template-columns:minmax(420px, min(66vh,720px)) minmax(290px,365px);gap:30px;justify-content:center;align-items:start}.playarea{min-width:0}.turnbar{height:48px;display:flex;align-items:center;justify-content:space-between;padding:0 4px 12px}.turnlabel{display:flex;align-items:center;gap:10px;font-size:16px;font-weight:600}.turndot{width:10px;height:10px;border-radius:50%;background:#363936;box-shadow:0 0 0 3px #3639361c}.turndot.white{background:#e5d8bd;box-shadow:0 0 0 3px #aa977855}.subturn{font:11px system-ui,sans-serif;color:#89877f}.boardframe{background:#41392e;padding:12px;border-radius:6px;box-shadow:0 6px 0 #302a23, var(--shadow);position:relative}.board{width:100%;aspect-ratio:1;display:grid;grid-template:repeat(8,1fr)/repeat(8,1fr);overflow:hidden;border-radius:2px;touch-action:none;user-select:none}.square{position:relative;display:flex;align-items:center;justify-content:center;cursor:pointer;touch-action:none}.square.light{background:var(--light)}.square.dark{background:var(--dark)}.square.last.light{background:#ddc98c}.square.last.dark{background:#a78a57}.square.selected{background:#bbba67}.square.check{background:#cf6558!important;box-shadow:inset 0 0 22px #721e197a}.piece{position:relative;z-index:2;line-height:1;font-family:'Arial Unicode MS','DejaVu Sans',serif;font-size:clamp(32px,6.2vw,68px);cursor:grab;filter:drop-shadow(0 2px 1px #1f1a1430);transition:transform .12s}.piece.white{color:#fffaf0;text-shadow:0 1px 1px #453b2e,1px 0 #72634e,-1px 0 #72634e,0 -1px #72634e}.piece.black{color:#33332f;text-shadow:0 1px 1px #fff3d2a6}.square.drag-source .piece{opacity:.25}.square.legal:after{content:"";width:26%;height:26%;border-radius:50%;background:#20332d51;position:absolute;z-index:1}.square.capture:after{content:"";position:absolute;inset:4%;border:5px solid #334b3c83;border-radius:50%;z-index:1}.coord{position:absolute;font:700 10px/1 system-ui,sans-serif;z-index:3;opacity:.83}.coord.file{right:4px;bottom:3px}.coord.rank{left:4px;top:3px}.light .coord{color:#866e50}.dark .coord{color:#f1e1c3}.boardfoot{display:flex;justify-content:space-between;padding:10px 2px 0;color:#e4d8c4;font:10px system-ui,sans-serif;letter-spacing:.1em;text-transform:uppercase}.promotion{position:absolute;z-index:9;display:flex;padding:5px;background:#fffdf8;border:1px solid #d4c8b4;border-radius:7px;box-shadow:0 8px 30px #20180f55}.promotion button{border:0;background:transparent;width:53px;height:55px;font-size:42px;cursor:pointer;border-radius:4px}.promotion button:hover{background:#eee5d6}.promotion.flip{flex-direction:column}.gameover{position:absolute;z-index:8;left:50%;top:50%;transform:translate(-50%,-50%);padding:16px 23px;background:#263a30ed;color:#fff9e9;border:1px solid #e5c88c;border-radius:4px;text-align:center;box-shadow:0 10px 45px #0006;white-space:nowrap}.gameover strong{display:block;font-size:21px}.gameover span{display:block;margin-top:4px;font:11px system-ui,sans-serif;color:#d5c9af}.side{display:flex;flex-direction:column;gap:14px}.card{background:var(--panel);border:1px solid var(--line);border-radius:5px;box-shadow:0 3px 15px #392c1710}.statuscard{padding:17px 18px 14px}.statushead{display:flex;justify-content:space-between;align-items:center}.statushead h2{font-size:19px;margin:0;font-weight:600}.pill{font:10px system-ui,sans-serif;background:#e9eee9;color:var(--green);padding:5px 8px;border-radius:20px;text-transform:uppercase;letter-spacing:.08em}.captured{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:15px}.capturebox{background:#f7f4ed;border-radius:4px;padding:7px 9px;min-height:50px}.capturetitle{font:9px system-ui,sans-serif;text-transform:uppercase;letter-spacing:.12em;color:#88867d;margin-bottom:4px}.capturepieces{min-height:24px;white-space:nowrap;font-size:20px;letter-spacing:-4px}.sectionhead{display:flex;justify-content:space-between;align-items:center;padding:14px 17px 10px;border-bottom:1px solid var(--line)}.sectionhead h3{font-size:15px;margin:0;font-weight:600}.move-count{font:11px system-ui,sans-serif;color:#98958a}.moves{height:252px;overflow:auto;padding:7px 10px;scrollbar-color:#c9c0b2 transparent;scrollbar-width:thin}.moverow{display:grid;grid-template-columns:31px 1fr 1fr;align-items:center;min-height:31px;border-radius:3px;font-size:14px}.moverow:nth-child(even){background:#f7f4ee}.movenum{font:10px system-ui,sans-serif;color:#999488;text-align:center}.san{padding:4px 7px}.san.latest{color:#345946;font-weight:700}.controls{padding:14px 16px 16px}.controlrow{display:flex;gap:8px;margin-bottom:11px}.btn{border:1px solid #d9d2c5;background:#fbf9f4;border-radius:4px;padding:9px 11px;font:12px system-ui,sans-serif;color:#44453f;cursor:pointer;transition:.15s}.btn:hover{border-color:#9eaa9e;background:#f1f4ee}.btn.primary{background:var(--green);color:#fff9ed;border-color:var(--green);flex:1}.btn.primary:hover{background:var(--green2)}.settings{display:grid;grid-template-columns:1fr 1fr;gap:10px 12px}.field label{display:block;font:9px system-ui,sans-serif;text-transform:uppercase;letter-spacing:.1em;color:#89877f;margin:0 0 5px}.field select{width:100%;height:32px;border:1px solid #ded8cd;background:#fffefa;border-radius:3px;padding:0 7px;color:#45463f;font:12px system-ui,sans-serif}.toggleline{display:flex;align-items:center;justify-content:space-between;margin-top:12px;padding-top:10px;border-top:1px solid #eee9e0;font:11px system-ui,sans-serif;color:#77776f}.switch{border:0;background:none;color:#53685b;font:700 11px system-ui,sans-serif;cursor:pointer;padding:5px}.hint{padding:11px 14px;font:11px/1.5 system-ui,sans-serif;color:#77766f;background:#f8f5ee;border-radius:4px}.hint b{color:#555950}.thinking{color:#b37c32!important}.footer{display:flex;justify-content:center;margin-top:19px;font:10px system-ui,sans-serif;color:#9a968b;letter-spacing:.08em;text-transform:uppercase}.sr{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
@media(max-width:900px){.app{padding:17px 20px}.layout{grid-template-columns:minmax(330px,1fr) minmax(260px,310px);gap:18px}.moves{height:220px}}@media(max-width:680px){.app{padding:13px 12px}.top{margin-bottom:12px}.topnote{display:none}.layout{display:flex;flex-direction:column;gap:13px;align-items:stretch}.playarea{width:min(100%,560px);margin:auto}.side{width:min(100%,560px);margin:auto;display:grid;grid-template-columns:1fr}.boardframe{padding:8px}.piece{font-size:clamp(35px,11vw,64px)}.turnbar{height:40px;padding-bottom:7px}.moves{height:185px}.footer{margin:12px 0}}
</style>
</head>
<body>
<div class="app">
<header class="top"><div class="brand"><div class="brandmark">♞</div><div><div class="eyebrow">A study in strategy</div><h1>Quiet Square</h1></div></div><div class="topnote">A thoughtful game of chess</div></header>
<main class="layout">
<section class="playarea"><div class="turnbar"><div class="turnlabel"><i class="turndot white" id="turnDot"></i><span id="turnText">White to move</span></div><div class="subturn" id="turnSub">Your move</div></div><div class="boardframe"><div class="board" id="board" role="grid" aria-label="Chess board"></div><div class="boardfoot"><span id="filelegend">A B C D E F G H</span><span>Quiet Square</span></div><div id="overlay"></div></div></section>
<aside class="side"><section class="card statuscard"><div class="statushead"><h2>Game at a glance</h2><span class="pill" id="gamePill">In progress</span></div><div class="captured"><div class="capturebox"><div class="capturetitle">White captured</div><div class="capturepieces" id="whiteCaptured"></div></div><div class="capturebox"><div class="capturetitle">Black captured</div><div class="capturepieces" id="blackCaptured"></div></div></div></section>
<section class="card"><div class="sectionhead"><h3>Moves</h3><span class="move-count" id="moveCount">0 plies</span></div><div class="moves" id="moves" aria-live="polite"></div></section>
<section class="card controls"><div class="controlrow"><button class="btn" id="undoBtn" title="Take back the last turn">↶ Undo</button><button class="btn primary" id="newBtn">New game</button><button class="btn" id="flipBtn" title="Flip board">⇅</button></div><div class="settings"><div class="field"><label for="sideSelect">Play as</label><select id="sideSelect"><option value="w">White</option><option value="b">Black</option><option value="both">Both sides</option></select></div><div class="field"><label for="depthSelect">AI strength</label><select id="depthSelect"><option value="1">Casual · depth 1</option><option value="2" selected>Club · depth 2</option><option value="3">Strong · depth 3</option></select></div></div><div class="toggleline"><span>Move sounds</span><button id="soundBtn" class="switch" aria-pressed="true">ON</button></div></section>
<div class="hint"><b>How to play</b> Select a piece, then choose a highlighted square. Dragging also works. Promotion choices appear on the board.</div></aside>
</main><div class="footer">Take your time · Every move matters</div></div>
<script>
'use strict';
// ── Tunable constants ──────────────────────────────────────────────────────
const CONFIG={AI_DEPTHS:[1,2,3],AI_DELAY:180,AI_BUDGET_MS:850,PIECE_VALUE:{p:100,n:320,b:330,r:500,q:900,k:20000},MATE:100000,STORAGE_KEY:'quiet-square-chess-v1',SOUND:true};
// ── Rules engine: 0x88 mailbox, state is independent of the DOM ─────────────
const W=0,B=1,START_FEN='rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
const glyph={K:'♔',Q:'♕',R:'♖',B:'♗',N:'♘',P:'♙',k:'♚',q:'♛',r:'♜',b:'♝',n:'♞',p:'♟'};
const dirs={N:[33,31,18,14,-14,-18,-31,-33],B:[15,17,-15,-17],R:[1,-1,16,-16],Q:[1,-1,16,-16,15,17,-15,-17],K:[1,-1,16,-16,15,17,-15,-17]};
const sqName=s=>String.fromCharCode(97+(s&7))+String(1+(s>>4));
const parseSq=s=>{if(!/^[a-h][1-8]$/.test(s||''))return -1;return s.charCodeAt(0)-97+(+s[1]-1)*16};
const sideOf=p=>p?(p===p.toUpperCase()?W:B):-1, typeOf=p=>p.toLowerCase();
function fresh(){return fromFen(START_FEN)}
function fromFen(fen){let a=fen.trim().split(/\s+/);if(a.length<4)throw Error('Invalid FEN');let board=Array(128).fill('');let ranks=a[0].split('/');if(ranks.length!==8)throw Error('Invalid FEN board');for(let r=0;r<8;r++){let f=0;for(const c of ranks[7-r]){if(/[1-8]/.test(c))f+=+c;else if(/[prnbqkPRNBQK]/.test(c)&&f<8)board[r*16+f++]=c;else throw Error('Invalid FEN piece')}if(f!==8)throw Error('Invalid FEN rank')}let rights=0;if(a[2].includes('K'))rights|=1;if(a[2].includes('Q'))rights|=2;if(a[2].includes('k'))rights|=4;if(a[2].includes('q'))rights|=8;return{board,turn:a[1]==='b'?B:W,castle:rights,ep:a[3]==='-'?-1:parseSq(a[3]),half:+a[4]||0,full:+a[5]||1,king:[board.indexOf('K'),board.indexOf('k')],records:[]}}
function fen(s){let rows=[];for(let r=7;r>=0;r--){let row='',empty=0;for(let f=0;f<8;f++){let p=s.board[r*16+f];if(!p)empty++;else{if(empty)row+=empty,empty=0;row+=p}}if(empty)row+=empty;rows.push(row)}return rows.join('/')+' '+(s.turn===W?'w':'b')+' '+((s.castle&1?'K':'')+(s.castle&2?'Q':'')+(s.castle&4?'k':'')+(s.castle&8?'q':'')||'-')+' '+(s.ep<0?'-':sqName(s.ep))+' '+s.half+' '+s.full}
function clone(s){return{board:s.board.slice(),turn:s.turn,castle:s.castle,ep:s.ep,half:s.half,full:s.full,king:s.king.slice(),records:s.records.slice()}}
function attacked(s,sq,by){const b=s.board;let pawnFrom=by===W?sq-15:sq+15, pawnFrom2=by===W?sq-17:sq+17;for(const p of [pawnFrom,pawnFrom2])if(!(p&0x88)&&b[p]===(by===W?'P':'p'))return true;for(const d of dirs.N){let q=sq+d;if(!(q&0x88)&&b[q]===(by===W?'N':'n'))return true}for(const d of dirs.K){let q=sq+d;if(!(q&0x88)&&b[q]===(by===W?'K':'k'))return true}for(const [ds,types] of [[dirs.B,'bq'],[dirs.R,'rq']])for(const d of ds){let q=sq+d;while(!(q&0x88)){let p=b[q];if(p){if(sideOf(p)===by&&types.includes(typeOf(p)))return true;break}q+=d}}return false}
function inCheck(s,side=s.turn){let k=s.king[side];return k>=0&&attacked(s,k,1-side)}
function pseudo(s){let out=[],b=s.board,us=s.turn;const push=(from,to,extra={})=>{if(b[to]&&typeOf(b[to])==='k')return;let m={from,to,...extra};if(typeOf(b[from])==='p'&&((us===W&&(to>>4)===7)||(us===B&&(to>>4)===0))){for(const promo of ['q','r','b','n'])out.push({...m,promo:us===W?promo.toUpperCase():promo})}else out.push(m)};
for(let from=0;from<128;from++){if(from&0x88){from+=7;continue}let p=b[from];if(!p||sideOf(p)!==us)continue;let t=typeOf(p),rank=from>>4;
if(t==='p'){let step=us===W?16:-16,one=from+step;if(!(one&0x88)&&!b[one]){push(from,one);let two=from+step*2;if((us===W?rank===1:rank===6)&&!b[two])push(from,two,{double:true})}for(const df of [-1,1]){let to=from+step+df;if(to&0x88)continue;if(b[to]&&sideOf(b[to])!==us)push(from,to);else if(to===s.ep)push(from,to,{ep:true})}continue}
let sliding=t==='b'||t==='r'||t==='q';for(const d of dirs[t.toUpperCase()]){let to=from+d;while(!(to&0x88)){if(!b[to])push(from,to);else{if(sideOf(b[to])!==us)push(from,to);break}if(!sliding)break;to+=d}}
if(t==='k'&&from===(us===W?4:116)&&!inCheck(s,us)){let base=us===W?4:116,enemy=1-us;if((s.castle&(us===W?1:4))&&b[base+1]===''&&b[base+2]===''&&b[base+3]===(us===W?'R':'r')&&!attacked(s,base+1,enemy)&&!attacked(s,base+2,enemy))push(from,base+2,{castle:'K'});if((s.castle&(us===W?2:8))&&b[base-1]===''&&b[base-2]===''&&b[base-3]===''&&b[base-4]===(us===W?'R':'r')&&!attacked(s,base-1,enemy)&&!attacked(s,base-2,enemy))push(from,base-2,{castle:'Q'})}
}return out}
function apply(s,m){let b=s.board,p=b[m.from],captured=m.ep?b[m.to+(s.turn===W?-16:16)]:b[m.to];b[m.from]='';if(m.ep)b[m.to+(s.turn===W?-16:16)]='';b[m.to]=m.promo||p;if(typeOf(p)==='k')s.king[s.turn]=m.to;if(m.castle){let rookFrom=m.castle==='K'?m.from+3:m.from-4,rookTo=m.castle==='K'?m.from+1:m.from-1;b[rookTo]=b[rookFrom];b[rookFrom]=''}if(p==='K')s.castle&=~3;if(p==='k')s.castle&=~12;if(m.from===0||m.to===0)s.castle&=~2;if(m.from===7||m.to===7)s.castle&=~1;if(m.from===112||m.to===112)s.castle&=~8;if(m.from===119||m.to===119)s.castle&=~4;s.ep=m.double?m.from+(s.turn===W?16:-16):-1;s.half=(typeOf(p)==='p'||captured)?0:s.half+1;if(s.turn===B)s.full++;s.turn=1-s.turn;return captured}
function legal(s){let us=s.turn,out=[];for(const m of pseudo(s)){let n=clone(s);apply(n,m);if(!inCheck(n,us))out.push(m)}return out}
function sameMove(a,b){return a.from===b.from&&a.to===b.to&&(!a.promo||a.promo===b.promo)}
function terminal(s,moves=legal(s)){if(moves.length===0)return inCheck(s)?(s.turn===W?'Black wins by checkmate':'White wins by checkmate'):'Draw by stalemate';if(s.half>=100)return'Draw by the 50-move rule';let pieces=[];for(let i=0;i<128;i++){if(i&0x88){i+=7;continue}let p=s.board[i];if(p&&typeOf(p)!=='k')pieces.push(typeOf(p))}if(pieces.length===0||(pieces.length===1&&['b','n'].includes(pieces[0])))return'Draw by insufficient material';return''}
function sanFor(s,m,moves){let p=s.board[m.from],t=typeOf(p),capture=!!s.board[m.to]||m.ep,str='';if(m.castle)str=m.castle==='K'?'O-O':'O-O-O';else{if(t!=='p')str=p.toUpperCase();if(t!=='p'){let rivals=moves.filter(x=>x.to===m.to&&x.from!==m.from&&typeOf(s.board[x.from])===t);if(rivals.length){let file=rivals.some(x=>(x.from&7)!==(m.from&7)),rank=rivals.some(x=>(x.from>>4)!==(m.from>>4));str+=file?String.fromCharCode(97+(m.from&7)):rank?String(1+(m.from>>4)):String.fromCharCode(97+(m.from&7))+String(1+(m.from>>4))}}else if(capture)str=String.fromCharCode(97+(m.from&7));if(capture)str+='x';str+=sqName(m.to);if(m.promo)str+='='+m.promo.toUpperCase();if(m.ep)str+=' e.p.'}let n=clone(s);apply(n,m);let next=legal(n);if(inCheck(n)){str+=next.length?' +':'#'}return str}
// ── AI: alpha-beta, capture ordering and bounded quiescence ─────────────────
const pst={p:[0,0,0,0,0,0,0,0,5,10,10,-20,-20,10,10,5,5,-5,-10,0,0,-10,-5,5,0,0,0,20,20,0,0,0,5,5,10,25,25,10,5,5,10,10,20,30,30,20,10,10,50,50,50,50,50,50,50,50,0,0,0,0,0,0,0,0],n:[-50,-40,-30,-30,-30,-30,-40,-50,-40,-20,0,5,5,0,-20,-40,-30,5,10,15,15,10,5,-30,-30,0,15,20,20,15,0,-30,-30,5,15,20,20,15,5,-30,-30,0,10,15,15,10,0,-30,-40,-20,0,0,0,0,-20,-40,-50,-40,-30,-30,-30,-30,-40,-50],b:[-20,-10,-10,-10,-10,-10,-10,-20,-10,5,0,0,0,0,5,-10,-10,10,10,10,10,10,10,-10,-10,0,10,10,10,10,0,-10,-10,5,5,10,10,5,5,-10,-10,0,5,10,10,5,0,-10,-10,0,0,0,0,0,0,-10,-20,-10,-10,-10,-10,-10,-10,-20]};
function evaluate(s){let score=0;for(let i=0;i<128;i++){if(i&0x88){i+=7;continue}let p=s.board[i];if(!p)continue;let side=sideOf(p),t=typeOf(p),sq=side===W?(i>>4)*8+(i&7):(7-(i>>4))*8+(i&7);let v=CONFIG.PIECE_VALUE[t];if(pst[t])v+=pst[t][sq]||0;if(t==='k'){let advancement=side===W?(i>>4):(7-(i>>4));v+=advancement<2?20:-advancement*3}score+=side===W?v:-v}return s.turn===W?score:-score}
function ordered(s,moves){return moves.sort((a,b)=>moveScore(s,b)-moveScore(s,a))}function moveScore(s,m){let victim=m.ep?'p':s.board[m.to]?typeOf(s.board[m.to]):'';return(victim?10*CONFIG.PIECE_VALUE[victim]-CONFIG.PIECE_VALUE[typeOf(s.board[m.from])]:0)+(m.promo?CONFIG.PIECE_VALUE[typeOf(m.promo)]:0)}
function qsearch(s,alpha,beta,ply,depth=0){let check=inCheck(s),stand=evaluate(s);if(!check){if(stand>=beta)return beta;if(stand>alpha)alpha=stand;if(depth>=4)return alpha}let moves=legal(s);if(!moves.length)return check?-CONFIG.MATE+ply:0;if(!check)moves=moves.filter(m=>s.board[m.to]||m.ep||m.promo);for(const m of ordered(s,moves)){let n=clone(s);apply(n,m);let v=-qsearch(n,-beta,-alpha,ply+1,depth+1);if(v>=beta)return beta;if(v>alpha)alpha=v}return alpha}
function searchRoot(s,depth,deadline){let moves=ordered(s,legal(s)),best=moves[0],alpha=-Infinity,beta=Infinity;for(const m of moves){if(performance.now()>deadline)break;let n=clone(s);apply(n,m);let score=-negamax(n,depth-1,-beta,-alpha,1,deadline);if(score>alpha){alpha=score;best=m}}return best}
function negamax(s,depth,alpha,beta,ply,deadline){if(performance.now()>deadline)return evaluate(s);if(depth<=0)return qsearch(s,alpha,beta,ply);let moves=ordered(s,legal(s));if(!moves.length)return inCheck(s)?-CONFIG.MATE+ply:0;if(s.half>=100)return 0;for(const m of moves){let n=clone(s);apply(n,m);let v=-negamax(n,depth-1,-beta,-alpha,ply+1,deadline);if(v>=beta)return beta;if(v>alpha)alpha=v}return alpha}
function chooseAI(s,depth){let ms=legal(s);if(ms.length<2||depth<2)return ordered(s,ms)[0];let deadline=performance.now()+CONFIG.AI_BUDGET_MS,best=ms[0];for(let d=1;d<=depth;d++){if(performance.now()>deadline)break;best=searchRoot(s,d,deadline)}return best}
// ── UI, persistence, input and game orchestration ──────────────────────────
let state=fresh(),flipped=false,selected=-1,selectionMoves=[],promotionPending=null,aiBusy=false,soundOn=CONFIG.SOUND,audio=null,dragFrom=-1,gameMode='w',depth=2;
function serial(){return JSON.stringify({fen:fen(state),records:state.records.map(r=>({uci:r.uci,san:r.san,before:r.before,captured:r.captured||''})),flipped,gameMode,depth,soundOn})}
function save(){try{localStorage.setItem(CONFIG.STORAGE_KEY,serial())}catch(e){}}
function restore(){try{let data=JSON.parse(localStorage.getItem(CONFIG.STORAGE_KEY));if(data&&data.fen){state=fromFen(data.fen);state.records=(data.records||[]).map(r=>r);flipped=!!data.flipped;gameMode=data.gameMode||'w';depth=+data.depth||2;soundOn=data.soundOn!==false;document.querySelector('#sideSelect').value=gameMode;document.querySelector('#depthSelect').value=String(depth)}}catch(e){state=fresh()}}
function sqLabel(s){return String.fromCharCode(97+(s&7))+String(1+(s>>4))}
function uci(m){return sqLabel(m.from)+sqLabel(m.to)+(m.promo?typeOf(m.promo):'')}
function pieceHtml(p){return p?`<span class="piece ${sideOf(p)===W?'white':'black'}" draggable="false">${glyph[p]}</span>`:''}
function currentMoves(){return legal(state)}
function render(){let board=document.querySelector('#board'),moves=legal(state),end=terminal(state,moves),checked=inCheck(state);board.innerHTML='';let last=state.records.length?state.records[state.records.length-1].uci:null;for(let vr=0;vr<8;vr++)for(let vf=0;vf<8;vf++){let file=flipped?7-vf:vf,rank=flipped?vr:7-vr,sq=rank*16+file,p=state.board[sq],el=document.createElement('div');el.className='square '+((file+rank)%2?'dark':'light');el.dataset.sq=sq;el.setAttribute('role','gridcell');el.setAttribute('aria-label',`${sqLabel(sq)}${p?' '+p:''}`);if(last&&(last.slice(0,2)===sqLabel(sq)||last.slice(2,4)===sqLabel(sq)))el.classList.add('last');if(sq===selected)el.classList.add('selected');if(checked&&sq===state.king[state.turn])el.classList.add('check');let lm=selectionMoves.filter(m=>m.to===sq);if(lm.length)el.classList.add(state.board[sq]||lm[0].ep?'capture':'legal');if(sq===dragFrom)el.classList.add('drag-source');if(file===(flipped?7:0))el.insertAdjacentHTML('beforeend',`<span class="coord rank">${rank+1}</span>`);if(rank===(flipped?0:7))el.insertAdjacentHTML('beforeend',`<span class="coord file">${String.fromCharCode(97+file)}</span>`);if(p)el.insertAdjacentHTML('beforeend',pieceHtml(p));board.appendChild(el)}
let dot=document.querySelector('#turnDot');dot.className='turndot '+(state.turn===W?'white':'');document.querySelector('#turnText').textContent=end?'Game over':(state.turn===W?'White':'Black')+' to move';let playerTurn=gameMode==='both'||(gameMode==='w'?state.turn===W:state.turn===B);document.querySelector('#turnSub').textContent=end?'':aiBusy?'Computer is thinking…':playerTurn?'Your move':'Computer to move';document.querySelector('#turnSub').className='subturn'+(aiBusy?' thinking':'');document.querySelector('#gamePill').textContent=end?'Finished':checked?'Check':'In progress';document.querySelector('#undoBtn').disabled=!state.records.length;document.querySelector('#moveCount').textContent=`${state.records.length} ${state.records.length===1?'ply':'plies'}`;document.querySelector('#moves').innerHTML=state.records.map((r,i)=>`<div class="moverow"><span class="movenum">${i%2===0?Math.floor(i/2)+1+'.':''}</span><span class="san ${i===state.records.length-1?'latest':''}">${i%2===0?r.san:''}</span><span class="san ${i===state.records.length-1?'latest':''}">${i%2===1?r.san:''}</span></div>`).join('');document.querySelector('#moves').scrollTop=99999;renderCaptured();let ov=document.querySelector('#overlay');ov.innerHTML='';if(promotionPending)renderPromotion(ov,promotionPending);else if(end)ov.innerHTML=`<div class="gameover"><strong>${end}</strong><span>${checked?'The king has no escape.':'The position is drawn.'}</span></div>`;save()}
function renderCaptured(){let captured={w:[],b:[]};for(const r of state.records){if(r.captured){let c=sideOf(r.captured)===W?'b':'w';captured[c].push(r.captured)}}const sort='qrbnp';for(const side of ['w','b']){captured[side].sort((a,b)=>sort.indexOf(typeOf(a))-sort.indexOf(typeOf(b)));document.querySelector(side==='w'?'#whiteCaptured':'#blackCaptured').innerHTML=captured[side].map(p=>glyph[p]).join(' ')}}
function renderPromotion(ov,sq){let rect=document.querySelector(`[data-sq="${sq}"]`).getBoundingClientRect(),frame=document.querySelector('.boardframe').getBoundingClientRect(),color=state.turn===W?'w':'b',choices=['q','r','b','n'];let el=document.createElement('div');el.className='promotion'+(flipped?' flip':'');el.style.left=Math.max(0,Math.min(frame.width-220,rect.left-frame.left))+'px';el.style.top=Math.max(0,Math.min(frame.height-220,rect.top-frame.top))+'px';for(const ch of choices){let btn=document.createElement('button');btn.textContent=glyph[color==='w'?ch.toUpperCase():ch];btn.title=`Promote to ${ch.toUpperCase()}`;btn.onclick=()=>{let m=promotionPending.moves.find(x=>typeOf(x.promo)===ch);promotionPending=null;playMove(m)};el.appendChild(btn)}ov.appendChild(el)}
function canHuman(){return !terminal(state)&&!aiBusy&&(gameMode==='both'||(gameMode==='w'?state.turn===W:state.turn===B))}
function selectSquare(sq){if(!canHuman())return;let p=state.board[sq];if(selected>=0){let candidates=selectionMoves.filter(m=>m.to===sq);if(candidates.length){if(candidates.some(m=>m.promo)){promotionPending={sq,moves:candidates};render();return}playMove(candidates[0]);return}}if(p&&sideOf(p)===state.turn){selected=sq;selectionMoves=currentMoves().filter(m=>m.from===sq)}else{selected=-1;selectionMoves=[]}render()}
function playMove(m){if(!m||!canHuman())return;let moves=legal(state),actual=moves.find(x=>sameMove(x,m)&&(!m.promo||x.promo===m.promo));if(!actual)return;commit(actual);}
function commit(m){let moves=legal(state),san=sanFor(state,m,moves),before=fen(state),captured=apply(state,m);state.records.push({uci:uci(m),san,captured:captured||'',before});selected=-1;selectionMoves=[];promotionPending=null;render();playSound(captured||m.ep?'capture':inCheck(state)?'check':'move');if(!terminal(state)&&!isHumanTurn()){aiBusy=true;render();setTimeout(runAI,CONFIG.AI_DELAY)}}
function isHumanTurn(){return gameMode==='both'||(gameMode==='w'?state.turn===W:state.turn===B)}
function runAI(){if(terminal(state)){aiBusy=false;render();return}let m=chooseAI(state,depth);aiBusy=false;if(m)commit(m);else render()}
function undo(){if(!state.records.length||aiBusy)return;let history=state.records.slice(),count=gameMode==='both'?1:Math.min(2,history.length),target=history[history.length-count].before,kept=history.slice(0,history.length-count);state=fromFen(target);state.records=kept;selected=-1;selectionMoves=[];promotionPending=null;render()}
function freshGame(){state=fresh();selected=-1;selectionMoves=[];promotionPending=null;aiBusy=false;render();if(!isHumanTurn()){aiBusy=true;render();setTimeout(runAI,CONFIG.AI_DELAY)}}
function playUci(text){if(typeof text!=='string')return false;let match=text.trim().match(/^([a-h][1-8])([a-h][1-8])([qrbnQRBN])?$/);if(!match)return false;let from=parseSq(match[1]),to=parseSq(match[2]),promo=match[3]?.toLowerCase(),m=legal(state).find(x=>x.from===from&&x.to===to&&(!x.promo||typeOf(x.promo)===promo));if(!m)return false;commit(m);return true}
function playSound(kind){if(!soundOn)return;try{audio=audio||new(window.AudioContext||window.webkitAudioContext)();let osc=audio.createOscillator(),gain=audio.createGain();osc.type='sine';osc.frequency.value=kind==='capture'?260:kind==='check'?440:330;gain.gain.setValueAtTime(.0001,audio.currentTime);gain.gain.exponentialRampToValueAtTime(.11,audio.currentTime+.008);gain.gain.exponentialRampToValueAtTime(.0001,audio.currentTime+.12);osc.connect(gain);gain.connect(audio.destination);osc.start();osc.stop(audio.currentTime+.13)}catch(e){}}
// Board events: same legal destination logic powers clicks and drag/drop.
const boardEl=document.querySelector('#board');boardEl.addEventListener('pointerdown',e=>{let sq=+e.target.closest('.square')?.dataset.sq;if(Number.isInteger(sq)&&canHuman()&&state.board[sq]&&sideOf(state.board[sq])===state.turn){dragFrom=sq}});boardEl.addEventListener('pointerup',e=>{let dest=+e.target.closest('.square')?.dataset.sq,from=dragFrom;dragFrom=-1;if(Number.isInteger(dest)&&from>=0&&dest!==from){if(selected!==from){selected=from;selectionMoves=legal(state).filter(m=>m.from===from)}selectSquare(dest)}else if(Number.isInteger(dest))selectSquare(dest)});boardEl.addEventListener('click',e=>{if(dragFrom>=0)return;let sq=+e.target.closest('.square')?.dataset.sq;if(Number.isInteger(sq))selectSquare(sq)});
document.querySelector('#newBtn').onclick=()=>{if(confirm('Start a new game? The current game will be replaced.'))freshGame()};document.querySelector('#undoBtn').onclick=undo;document.querySelector('#flipBtn').onclick=()=>{flipped=!flipped;render()};document.querySelector('#sideSelect').onchange=e=>{gameMode=e.target.value;selected=-1;selectionMoves=[];freshGame()};document.querySelector('#depthSelect').onchange=e=>{depth=+e.target.value;save()};document.querySelector('#soundBtn').onclick=e=>{soundOn=!soundOn;e.target.textContent=soundOn?'ON':'OFF';e.target.setAttribute('aria-pressed',String(soundOn));save()};
// Restore exact persisted FEN + history. Expose a compact headless interface.
restore();document.querySelector('#soundBtn').textContent=soundOn?'ON':'OFF';document.querySelector('#soundBtn').setAttribute('aria-pressed',String(soundOn));render();if(!terminal(state)&&!isHumanTurn()){aiBusy=true;render();setTimeout(runAI,CONFIG.AI_DELAY)}
window.game={fen:()=>fen(state),legalMoves:()=>legal(state).map(m=>uci(m)),move:playUci,setFen:(f)=>{try{state=fromFen(f);selected=-1;selectionMoves=[];render();return true}catch(e){return false}},undo:()=>undo(),newGame:freshGame,board:()=>state.board.slice(),status:()=>terminal(state),turn:()=>state.turn===W?'w':'b'};
</script></body></html>
<!-- agent-meta {"model":"openai/gpt-6-luna","provider":"openrouter","persona":"full","sessionId":"1637eeaa-4979-4555-8458-ca7bdc72e66c","tokensIn":517101,"tokensOut":22594,"tokensTotal":539695,"cost":0.019706875000000002,"turns":26,"toolCalls":25,"failedToolCalls":2,"timestamp":"2026-09-22T23:49:33.569Z"} -->