Chess
Ornith 1.5 9B GGUF · 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 44,473 bytes · SHA-256 2a568cbce18c
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chess — AI Opponent</title>
<style>
:root{
--sq:72px;
--light:#e8d0a9;
--dark:#a57c50;
--panel:#2b2620;
--panel2:#3a332b;
--ink:#f4ecd8;
--accent:#d9a441;
--move:#26607a;
--capture:#7a2638;
--last:#b8d06e;
--check:#cf3a3a;
}
*{box-sizing:border-box;}
html,body{margin:0;height:100%;}
body{
background:radial-gradient(circle at 50% 0%, #3a3326, #1c1813);
color:var(--ink);
font-family:"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
display:flex;flex-direction:column;align-items:center;
min-height:100vh;padding:16px;gap:14px;
}
h1{font-weight:600;letter-spacing:1px;margin:4px 0 0;color:var(--accent);font-size:20px;}
#app{display:flex;gap:20px;align-items:flex-start;flex-wrap:wrap;justify-content:center;}
#board-wrap{display:flex;flex-direction:column;align-items:center;gap:8px;}
#turn-indicator{
font-size:14px;letter-spacing:.5px;padding:4px 12px;border-radius:20px;
background:var(--panel2);border:1px solid #555044;min-height:28px;
}
#board{
display:grid;grid-template-columns:repeat(8,var(--sq));grid-template-rows:repeat(8,var(--sq));
border:6px solid #6b4f2a;border-radius:4px;box-shadow:0 10px 30px rgba(0,0,0,.5);
user-select:none;-webkit-user-select:none;
}
.sq{
position:relative;width:var(--sq);height:var(--sq);
display:flex;align-items:center;justify-content:center;
cursor:default;overflow:hidden;
}
.sq.light{background:var(--light);}
.sq.dark{background:var(--dark);}
.sq.light.check,.sq.dark.check{box-shadow:inset 0 0 0 4px var(--check);}
.sq.selected{box-shadow:inset 0 0 0 4px var(--accent);}
.sq.last{box-shadow:inset 0 0 0 4px var(--last);}
.sq .piece{font-size:calc(var(--sq)*0.82);line-height:1;pointer-events:none;
filter:drop-shadow(0 1px 1px rgba(0,0,0,.4));}
.piece-white{color:#fff;}
.piece-black{color:#201a12;}
.piece-black{transform:scaleX(-1);}
.sq.move::after{content:"";position:absolute;width:30%;height:30%;margin:35%;
background:rgba(38,96,122,.72);border-radius:50%;}
.sq.capture{box-shadow:inset 0 0 0 5px var(--capture);}
.sq.capture::after{content:"";position:absolute;inset:6%;border:5px solid rgba(122,38,56,.6);border-radius:50%;}
#panel{
width:300px;background:var(--panel);border:1px solid #555044;border-radius:8px;
padding:14px;display:flex;flex-direction:column;gap:12px;
box-shadow:0 10px 30px rgba(0,0,0,.4);
}
#banner{
text-align:center;font-weight:700;padding:10px;border-radius:6px;
background:rgba(217,164,65,.15);border:1px solid var(--accent);color:var(--accent);
min-height:20px;display:none;line-height:1.3;
}
#captured-black{min-height:26px;color:#e8d0a9;font-size:26px;line-height:1.2;
padding:4px 6px;border-radius:4px;background:var(--panel2);letter-spacing:1px;}
#moves-wrap{flex:1;display:flex;flex-direction:column;min-height:120px;}
#moves-title{font-size:12px;text-transform:uppercase;letter-spacing:1px;color:#c9bfa8;margin:2px 0;}
#move-list{flex:1;overflow-y:auto;font-family:"Cascadia Code","Consolas",monospace;font-size:13px;
line-height:1.5;display:grid;grid-template-columns:36px 1fr 1fr;gap:2px 8px;align-content:start;}
#move-list .mv{color:#8f8674;padding:2px 4px;border-radius:3px;cursor:default;}
#move-list .mv.hl{background:rgba(184,208,110,.25);color:var(--ink);}
#move-list .mv .n{color:#7a7160;}
#captured-white{color:#f4ecd8;font-size:26px;line-height:1.2;padding:4px 6px;
border-radius:4px;background:var(--panel2);letter-spacing:1px;min-height:26px;}
#controls{display:flex;flex-wrap:wrap;gap:8px;align-items:center;}
button{background:var(--panel2);color:var(--ink);border:1px solid #555044;border-radius:6px;
padding:8px 12px;cursor:pointer;font-size:14px;transition:.15s;}
button:hover{background:#4a4236;border-color:var(--accent);}
button.active{background:var(--accent);color:#201a12;}
select,label{font-size:13px;}
select{background:var(--panel2);color:var(--ink);border:1px solid #555044;border-radius:6px;padding:6px;}
label{display:flex;align-items:center;gap:6px;cursor:pointer;}
#promo-overlay{
position:fixed;inset:0;background:rgba(0,0,0,.6);display:none;align-items:center;justify-content:center;z-index:50;
}
#promo-overlay.show{display:flex;}
#promo-box{background:var(--panel);border:2px solid var(--accent);border-radius:10px;padding:16px 18px;
display:flex;gap:10px;box-shadow:0 20px 50px rgba(0,0,0,.6);}
#promo-box .promot{
width:76px;height:76px;display:flex;align-items:center;justify-content:center;
font-size:52px;background:var(--panel2);border:2px solid #555044;border-radius:8px;cursor:pointer;
color:#201a12;filter:drop-shadow(0 1px 1px rgba(0,0,0,.4));
}
#promo-box .promot:hover{border-color:var(--accent);transform:translateY(-2px);}
#promo-box .promot.piece-white{color:#fff;}
#promo-box .promot.piece-black{transform:scaleX(-1);color:#201a12;}
#panel::-webkit-scrollbar{width:8px;}
#panel::-webkit-scrollbar-thumb{background:#555044;border-radius:4px;}
@media (max-width:820px){:root{--sq:11vw;}#panel{width:80vw;}}
</style>
</head>
<body>
<h1>♞ CHESS vs AI</h1>
<div id="app">
<div id="board-wrap">
<div id="turn-indicator">White to move</div>
<div id="board" data-orientation="normal"></div>
</div>
<div id="panel">
<div id="banner"></div>
<div id="captured-black" title="Pieces captured by White"></div>
<div id="moves-wrap">
<div id="moves-title">Moves</div>
<div id="move-list"></div>
</div>
<div id="captured-white" title="Pieces captured by Black"></div>
<div id="controls">
<button id="btn-flip" title="Flip board">⇅ Flip</button>
<button id="btn-undo" title="Undo (player + AI)">↶ Undo</button>
<button id="btn-new" title="New game">+ New</button>
<select id="difficulty" title="AI difficulty">
<option value="1">Easy (Depth 1)</option>
<option value="2">Medium (Depth 2)</option>
<option value="3" selected>Hard (Depth 3)</option>
</select>
<label title="Mute sound effects"><input type="checkbox" id="mute"> Mute</label>
</div>
</div>
</div>
<div id="promo-overlay"><div id="promo-box"></div></div>
<script>
/* =========================================================================
CONSTANTS
========================================================================= */
const WHITE = 0, BLACK = 1;
const PAWN = 0, KNIGHT = 1, BISHOP = 2, ROOK = 3, QUEEN = 4, KING = 5;
const TYPES = [PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING];
const GLYPHS = ['♙','♟','♘','♞','♗','♝','♕','♛','♔','♚'];
const FILES = 'abcdefgh';
const START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
// Tunable eval weights (centipawns)
const EVAL = {
PAWN: 100, KNIGHT: 320, BISHOP: 330, ROOK: 500, QUEEN: 900,
KING: 20000
};
// Piece-square tables (white perspective). Index = rank from white's side (0 = a1).
const PST = [
// Pawn
[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]
];
const PST_KING = [
[-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]
];
// PST_KING[5] used in middlegame, PST_KING[7] in endgame.
/* =========================================================================
BOARD REPRESENTATION
Pieces stored as single bytes: [color<<4 | type]. 0 = empty.
board[0] = a1 (rank1, white's back rank), board[63] = h8.
index = rank*8 + file, rank 0 = rank1.
========================================================================= */
/* =========================================================================
GAME STATE
========================================================================= */
const State = {
board: new Uint8Array(64),
turn: WHITE,
castling: 0, // bit0 K,wK, bit1 Q,wQ, bit2 k,bK, bit3 q,bQ
ep: -1, // en-passant target square (-1 = none)
halfmove: 0, // for 50-move rule
fullmove: 1,
history: [], // stack of applied moves (for undo)
moved: new Set() // squares that have moved (a piece's origin/destination)
};
function clearState(){
State.board.fill(0);
State.turn = WHITE;
State.castling = 0b1111;
State.ep = -1;
State.halfmove = 0;
State.fullmove = 1;
State.history.length = 0;
State.moved.clear();
setupStart();
}
function setupStart(){
const back = [ROOK,KNIGHT,BISHOP,QUEEN,KING,BISHOP,KNIGHT,ROOK];
for(let f=0;f<8;f++){
State.board[f] = WHITE<<4|PAWN;
State.board[8+f] = WHITE<<4|back[f];
State.board[47+f] = BLACK<<4|back[f];
State.board[56+f] = BLACK<<4|PAWN;
}
}
// color of a piece byte
function color(b){return b>>4;}
function type(b){return b&15;}
function isWhite(b){return color(b)===WHITE;}
function isBlack(b){return color(b)===BLACK;}
function empty(b){return b===0;}
function clr(bit){return State.castling & bit; }
function setc(bit){State.castling |= bit;}
function clrb(bit){State.castling &= ~bit;}
function hasCastling(){return State.castling!==0;}
// square helpers
function sq(f,r){return r*8+f;}
function fileOf(s){return s&7;}
function rankOf(s){return s>>3;}
function opp(c){return c===WHITE?BLACK:WHITE;}
// is the given color in check?
function inCheck(c, b){
if(!b) b = State.board;
const ksq = findKing(c, b);
if(ksq<0) return false;
return attacked(opp(c), ksq, b);
}
function findKing(c, b){
const t = c<<4|KING;
for(let i=0;i<64;i++) if(b[i]===t) return i;
return -1;
}
/* =========================================================================
ATTACK GENERATION (does `color` attack `sq` on board `b`?)
Uses pseudo-legality (ignores own pieces for sliders, but blockers matter).
========================================================================= */
function attacked(byColor, sq, b){
if(!b) b = State.board;
const f = sq&7, r = sq>>3;
// Pawn attacks
const pT = byColor===WHITE?PAWN:BLACK; // pawns moving toward rank0 for white
// A white pawn on (f-1,r+1) or (f+1,r+1) attacks sq at (f,r)
for(const df of [-1,1]){
const pf = f+df, pr = r+1;
if(pf>=0&&pf<8&&pr<8){
const pb = b[pr*8+pf];
if(color(pb)===byColor&&type(pb)===PAWN) return true;
}
}
// Knight
const kMoves=[[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
for(const[df,dr] of kMoves){
const nf=f+df, nr=r+dr;
if(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(color(nb)===byColor&&type(nb)===KNIGHT) 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(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(color(nb)===byColor&&type(nb)===KING) return true;
}
}
// Bishop/Queen (diagonals)
for(const[df,dr] of [[1,1],[1,-1],[-1,1],[-1,-1]]){
let nf=f+df, nr=r+dr, ok=true;
while(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(nb!==0){ if(color(nb)===byColor&&type(nb)!==BISHOP&&type(nb)!==QUEEN) break; return true; }
nf+=df; nr+=dr;
}
}
// Rook/Queen (orthogonals)
for(const[df,dr] of [[1,0],[-1,0],[0,1],[0,-1]]){
let nf=f+df, nr=r+dr, ok=true;
while(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(nb!==0){ if(color(nb)===byColor&&type(nb)!==ROOK&&type(nb)!==QUEEN) break; return true; }
nf+=df; nr+=dr;
}
}
return false;
}
/* =========================================================================
MOVE GENERATION
A move: { from, to, piece, color, flags, promotion }
flags: castleKing, castleQueen, ep, promo
========================================================================= */
// Pseudo-legal moves for a color (ignores whether own king is left in check).
function genPseudoMoves(c, b){
const moves = [];
const enemy = opp(c);
const push = (from,to,extra)=>moves.push({from,to,piece:b[from],color:c,flags:0, promotion:extra||null});
for(let s=0;s<64;s++){
const p=b[s]; if(!p) continue; if(color(p)!==c) continue;
const t=type(p);
if(t===PAWN){
const fr=rankOf(s);
const dir = c===WHITE?1:-1;
const startR = c===WHITE?1:6;
const endR = c===WHITE?6:1;
const f=fileOf(s);
// forward push
const one = sq(f, fr+dir);
if(empty(b[one])){
let promoR = c===WHITE?7:0;
if(fr+dir!==promoR){ push(s,one,0); }
else { for(const pr of TYPES.slice(0,4)) push(s,one,pr); }
// double push
if(fr===startR){
const two=sq(f, fr+2*dir);
if(empty(b[two])) push(s,two,0);
}
}
// captures
for(const df of [-1,1]){
const cf=f+df; if(cf<0||cf>7) continue;
const cs=sq(cf,fr+dir);
if(color(b[cs])===enemy){
let promoR=promoR; promoR=c===WHITE?7:0;
if(fr+dir===promoR) for(const pr of TYPES.slice(0,4)) push(s,cs,pr);
else push(s,cs,0);
} else if(empty(b[cs])){
// en passant
if(s===State.ep){ push(s,cs,0); }
}
}
} else if(t===KNIGHT){
const f=fileOf(s);
const offs=[[1,2],[2,1],[-1,2],[-2,1],[1,-2],[2,-1],[-1,-2],[-2,-1]];
for(const[df,dr] of offs){
const nf=f+df,nr=rankOf(s)+dr;
if(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(nb===0||color(nb)===enemy) push(s,nr*8+nf,0);
}
}
} else if(t===KING){
const f=fileOf(s);
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=rankOf(s)+dr;
if(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(nb===0||color(nb)===enemy) push(s,nr*8+nf,0);
}
}
// castling
if(inCheck(c,b)) {} // can't castle out of check
const backRank = c===WHITE?0:7;
const rookHome = c===WHITE?sq(0,backRank):sq(7,backRank);
const kDir = c===WHITE?1:-1;
const wK = clr(0b1), wQ = clr(0b10), bK = clr(0b100), bQ = clr(0b100|0b10);
const hasKside = (c===WHITE)? wK && b[rookHome]===c<<4|ROOK : bK && b[rookHome]===c<<4|ROOK;
const hasQside = (c===WHITE)? wQ && b[rookHome]===c<<4|ROOK : bQ && b[rookHome]===c<<4|ROOK;
if(hasKside){
const ksq=findKing(c,b);
const between=[sq(3,backRank),sq(2,backRank)];
let safe=true;
for(const bs of between){ if(!empty(b[bs])||attacked(opp(c),bs,b)){safe=false;break;} }
if(ksq===sq(4,backRank) && safe && !attacked(opp(c),sq(5,backRank),b) && !attacked(opp(c),sq(6,backRank),b)){
push(sq(4,backRank),sq(6,backRank),c<<4|KING,0);
}
}
if(hasQside){
const ksq=findKing(c,b);
const between=[sq(5,backRank),sq(4,backRank),sq(3,backRank)];
let safe=true;
for(const bs of between){ if(!empty(b[bs])||attacked(opp(c),bs,b)){safe=false;break;} }
if(ksq===sq(4,backRank) && safe && !attacked(opp(c),sq(3,backRank),b) && !attacked(opp(c),sq(2,backRank),b)){
push(sq(4,backRank),sq(2,backRank),c<<4|KING,0);
}
}
} else {
// sliders: bishop/rook/queen
const f=fileOf(s);
const dirs=[];
if(t===BISHOP||t===QUEEN) dirs.push([1,1],[1,-1],[-1,1],[-1,-1]);
if(t===ROOK||t===QUEEN) dirs.push([1,0],[-1,0],[0,1],[0,-1]);
for(const[df,dr] of dirs){
let nf=f+df,nr=rankOf(s)+dr;
while(nf>=0&&nf<8&&nr>=0&&nr<8){
const nb=b[nr*8+nf];
if(nb===0){ push(s,nr*8+nf,0); }
else { if(color(nb)===enemy) push(s,nr*8+nf,0); break; }
nf+=df; nr+=dr;
}
}
}
}
return moves;
}
// Full legal move generation: filter out moves leaving own king in check,
// and add castling if not already added by pseudo-gen (we handle castle flags here).
function legalMoves(c){
if(c===undefined) c=State.turn;
const b=State.board;
let moves=genPseudoMoves(c,b);
const legal=[];
const enemy=opp(c);
for(const m of moves){
// apply to a scratch board
const nb=applyMoveTo(m,b,State.castling,State.ep,c,true);
if(!inCheck(c,nb)) legal.push(m);
}
return legal;
}
/* =========================================================================
applyMoveTo: apply a move to a scratch board `b`, returning a new state
object {board, castling, ep, turn, halfmove, fullmove} (or the state object
itself when `own` is false — used by the real game).
========================================================================= */
function applyMoveTo(m, b, castling, ep, c, own){
const nb = new Uint8Array(b);
nb[m.to] = 0;
const t=type(m.piece);
// handle en passant capture (pawn removed from its square)
let captured = false;
if(t===PAWN && Math.abs(fileOf(m.from)-fileOf(m.to))===1 && empty(nb[m.to]) && m.to===ep){
captured = true;
nb[ep]=0;
}
// promotion
const promoType = m.promotion!==null? m.promotion : (c===WHITE?QUEEN:QUEEN);
nb[m.to] = c<<4|promoType;
// castling rook movement
if(t===KING && Math.abs(fileOf(m.from)-fileOf(m.to))===2){
const backRank=rankOf(m.from);
if(m.to===sq(6,backRank)){ // king side
nb[rookSquare(c,backRank)]=0;
nb[sq(5,backRank)] = c<<4|ROOK;
} else if(m.to===sq(2,backRank)){ // queen side
nb[rookSquare(c,backRank)]=0;
nb[sq(3,backRank)] = c<<4|ROOK;
}
}
const nCast = updateCastling(castling, m.from, m.to, t);
// en-passant target for next move
let nEp=-1;
if(t===PAWN && Math.abs(rankOf(m.from)-rankOf(m.to))===2){
nEp = (rankOf(m.from)+rankOf(m.to))/2*8+fileOf(m.from);
}
if(own){
return { board:nb, castling:nCast, ep:nEp, turn:opp(c), halfmove:State.halfmove+1, fullmove:State.fullmove+1 };
}
// mutate real state
b[m.to]=nb[m.to]; b[m.from]=0;
castling=nCast;
if(captured){ b[ep]=0; }
ep=nEp;
State.turn=opp(c);
State.halfmove++; State.fullmove++;
return State;
}
function rookSquare(c,backRank){
return c===WHITE? sq(0,backRank): sq(7,backRank);
}
// update castling rights after a move
function updateCastling(castling, from, to, t){
let n = castling;
const backRank=rankOf(from);
const isKing = t===KING;
if(isKing){
if(castling===WHITE){ clrb(0b1); clrb(0b10); }
else { clrb(0b100); clrb(0b100|0b10); }
// bK bit is 0b10(2), bQ is 0b100(4)
}
// rook moved OR rook's home square vacated
const f=fileOf(from);
if(castling===WHITE){
if(from===sq(0,0)||to===sq(0,0)) clrb(0b1);
if(from===sq(7,0)||to===sq(7,0)) clrb(0b10);
} else {
if(from===sq(0,7)||to===sq(0,7)) clrb(0b100);
if(from===sq(7,7)||to===sq(7,7)) clrb(0b100|0b10);
}
return n;
}
/* =========================================================================
GAME LOGIC (applies a legal move to the real state, tracks history, AI turn)
========================================================================= */
const Game = {
// apply a legal move from state to the live board
applyMove(m){
const from = m.from, to = m.to;
const prev = {
board: State.board.slice(),
turn: State.turn,
castling: State.castling,
ep: State.ep,
halfmove: State.halfmove,
fullmove: State.fullmove
};
applyMoveTo(m, State.board, State.castling, State.ep, State.turn, true);
State.moved.add(from);
State.moved.add(to);
State.history.push({ move:m, prev });
},
// undo everything back to the initial position
newGame(){
clearState();
},
// take back one ply (player or AI move)
undo(){
if(State.history.length===0) return;
const h = State.history.pop();
State.board = h.prev.board;
State.turn = h.prev.turn;
State.castling = h.prev.castling;
State.ep = h.prev.ep;
State.halfmove = h.prev.halfmove;
State.fullmove = h.prev.fullmove;
State.moved.clear();
},
// ---- game status ----
gameOver(){
const legal = legalMoves(State.turn);
if(legal.length===0){
if(inCheck(State.turn)) return { over:true, type:'checkmate', winner: opp(State.turn) };
return { over:true, type:'stalemate' };
}
// insufficient material (excluding kings)
if(insufficient()) return { over:true, type:'insufficient' };
// 50-move rule
if(State.halfmove>=100) return { over:true, type:'fifty' };
// threefold handled in FEN check
return { over:false };
},
// ---- AI ----
runAI(){
const depth = parseInt(document.getElementById('difficulty').value,10);
const legal = legalMoves(State.turn);
if(legal.length===0) return;
const mv = searchBest(legal, depth);
if(mv) this.applyMove(mv);
}
};
function epSq(){ return State.ep; }
function insufficient(){
const pieces=[];
for(let i=0;i<64;i++){
const b=State.board[i]; if(b===0) continue;
const t=type(b);
if(t===KING) continue;
// K+B vs K or K+N vs K
if(pieces.length===0){ pieces.push(t); continue; }
const hasBishop=t===BISHOP;
const hasKnight=t===KNIGHT;
if(pieces[0]===BISHOP && (t===BISHOP||t===KNIGHT)) return true;
if(pieces[0]===KNIGHT && (t===KNIGHT||t===BISHOP)) return true;
return false;
}
return false;
}
/* =========================================================================
EVALUATION
Returns centipawns from WHITE's perspective (positive = good for White).
Uses material + piece-square tables. Adjusts king table for endgame.
========================================================================= */
function evaluate(){
let score=0;
let wCount=0, bCount=0;
for(let i=0;i<64;i++){
const b=State.board[i];
if(b===0) continue;
const c=color(b);
if(c===WHITE){ score+=EVAL[type(b)] + PST[type(b)][i]; wCount++; }
else { score-=EVAL[type(b)] + PST[type(b)][63-i]; bCount++; }
}
// endgame king adjustment when few pieces remain
const phase = (wCount+bCount<=8)?7:5;
score += (isWhitePos(score)>0? PST_KING[phase][findKing(WHITE)] : 0);
// (king PST already added for whites above; subtract blacks symmetrically)
const bk=findKing(BLACK);
score -= PST_KING[phase][63-bk];
return score;
}
function isWhitePos(s){ return s>=0; }
/* =========================================================================
AI SEARCH — minimax with alpha-beta pruning.
Move ordering: captures first (improves pruning), then others.
Depth chosen for ~<=1s per move on typical hardware.
========================================================================= */
/* =========================================================================
AI SEARCH — minimax with alpha-beta pruning.
Move ordering: captures first (improves pruning), then promotions, others.
Depth chosen for ~<=1s per move on typical hardware.
========================================================================= */
function searchBest(legalMovesArr, depth){
let best=null, bestScore=-Infinity;
const ordered = orderMoves(legalMovesArr);
let alpha=-Infinity, beta=Infinity;
for(const m of ordered){
const info = nodeSearch(ordered, depth-1, alpha, beta, m, 0);
if(info && info.score>bestScore){
bestScore=info.score; best=info.move;
if(info.score>=beta) break; // beta cutoff
}
}
return best;
}
// nodeSearch: search `moves` for the side to move (State.turn), going `depth`
// plies deep. Returns best move found + score.
function nodeSearch(moves, depth, alpha, beta, root, ply){
if(depth===0){ return { score: evaluate() }; }
// if opponent has no legal moves, we are in checkmate
const oppMoves = legalMoves(opp(State.turn));
if(oppMoves.length===0){
return { score: inCheck(State.turn)? -200000-ply : 200000+ply };
}
let bestScore=-Infinity;
for(const m of moves){
const info = searchOne(oppMoves, depth-1, alpha, beta, m, ply+1);
if(info && info.score>bestScore){
bestScore=info.score;
if(info.score>=beta) break; // beta cutoff
}
if(info && info.score>alpha) alpha=info.score;
}
return { score: bestScore };
}
// searchOne: apply move `root` for current side, search opponent's reply.
function searchOne(moves, depth, alpha, beta, root, ply){
const m=root;
const nb=State.board;
const res=applyScratch(m,nb);
State.board=res.board; State.castling=res.castling; State.ep=res.ep;
const score=nodeSearch(moves, depth, alpha, beta, null, ply+1);
State.board=nb; State.castling=State.castling; State.ep=State.ep;
return score;
}
function applyScratch(m,b){
const nb=new Uint8Array(b);
nb[m.to]=0;
// en passant capture
if(type(m.piece)===PAWN && Math.abs(fileOf(m.from)-fileOf(m.to))===1 && empty(nb[m.to]) && m.to===State.ep){
nb[epSq()]=0;
}
// promotion
const promoType=type(m.piece)===PAWN?m.promotion||QUEEN:type(m.piece);
nb[m.to]=m.color<<4|promoType;
// castling rook movement
if(type(m.piece)===KING && Math.abs(fileOf(m.from)-fileOf(m.to))===2){
const br=rankOf(m.from);
if(m.to===sq(6,br)){ nb[rookSquare(m.color,br)]=0; nb[sq(5,br)]=m.color<<4|ROOK; }
else if(m.to===sq(2,br)){ nb[rookSquare(m.color,br)]=0; nb[sq(3,br)]=m.color<<4|ROOK; }
}
const nCast=updateCastling(State.castling,m.from,m.to,type(m.piece));
let nEp=-1;
if(type(m.piece)===PAWN && Math.abs(rankOf(m.from)-rankOf(m.to))===2){
nEp=(rankOf(m.from)+rankOf(m.to))/2*8+fileOf(m.from);
}
return { board:nb, castling:nCast, ep:nEp };
}
// order moves: captures first (descending material), then promotions, others
function orderMoves(moves){
const byMaterial={KNIGHT:320,BISHOP:330,ROOK:500,QUEEN:900,PAWN:100};
return moves.slice().sort((a,b)=>{
const am=a.to!==a.from? byMaterial[type(a.piece)] : (type(a.piece)===PAWN?10:0);
const bm=b.to!==b.from? byMaterial[type(b.piece)] : (type(b.piece)===PAWN?10:0);
return bm-am;
});
}
/* =========================================================================
FEN SERIALIZATION
========================================================================= */
function toFen(){
let rows='';
for(let r=0;r<8;r++){
let empty=0;
for(let f=0;f<8;f++){
const b=State.board[r*8+f];
if(b===0) empty++; else { if(empty) { rows+=empty; empty=0; } rows+=GLYPHS[b>>4]; }
}
if(empty) rows+=empty;
rows+=' /';
}
rows=rows.slice(0,-2);
const legal=legalMoves(State.turn);
let cr='KQkq';
if(State.castling&0b1) cr+='K'; if(State.castling&0b10) cr+='Q';
if(State.castling&0b100) cr+='k'; if(State.castling&0b110) cr+='q';
if(cr==='KQkq') cr='-';
let ep='-'; if(State.ep>=0) ep=FILES[fileOf(State.ep)]+(8-rankOf(State.ep));
return `${rows} ${State.turn===WHITE?'w':'b'} ${cr} ${ep} ${State.halfmove} ${State.fullmove}`;
}
function fromFen(fen){
const parts=fen.split(' ');
const boardArr=parts[0].split(' ').map(row=>{
const u=new Uint8Array(8); let f=0;
for(const ch of row){ if(ch==='/') continue; if(ch>= '1'&&ch<='8') f+=+ch; else f=u[f++]=GLYPH_INDEX[ch]; }
return u;
});
const board=new Uint8Array(64);
for(let r=0;r<8;r++) for(let f=0;f<8;f++) board[r*8+f]=boardArr[r][f];
State.board=board;
State.turn=parts[1]==='b'?BLACK:WHITE;
State.castling=parseCastling(parts[2]);
State.ep=parts[3]==='-'?-1:epSquare(parts[3]);
State.halfmove=+parts[4];
State.fullmove=+parts[5];
State.history.length=0;
State.moved.clear();
}
function parseCastling(s){
let n=0;
if(s.includes('K')) n|=0b1; if(s.includes('Q')) n|=0b10;
if(s.includes('k')) n|=0b100; if(s.includes('q')) n|=0b110;
return n;
}
function glyphToType(g){
const idx=TYPES.indexOf(g);
if(idx>=0) return TYPES[idx];
return 0;
}
function epSquare(s){
const f=FILES.indexOf(s[0]), r=8-(+s[1]);
return sq(f,r);
}
/* =========================================================================
DEBUG INTERFACE (exposed on window)
game.fen(), game.move('e2e4'), game.legalMoves()
========================================================================= */
window.game = {
fen: toFen,
legalMoves: ()=>legalMoves(State.turn),
move: function(san){
const m = parseSan(san);
if(!m){ console.error('illegal/unknown move:', san); return false; }
Game.applyMove(m);
onGameChange();
return true;
},
_board: State.board,
_turn: State.turn,
_castling: State.castling,
_ep: State.ep,
_halfmove: State.halfmove,
_fullmove: State.fullmove,
reset: ()=>Game.newGame()
};
/* =========================================================================
SAN NOTATION
========================================================================= */
function moveToSan(m, prevBoard){
const t=type(m.piece), c=State.turn;
const file=FILES[fileOf(m.to)], rank=8-rankOf(m.to);
let san='';
if(t===KING) san='K';
else if(t===KNIGHT) san='N';
else if(t===PAWN){ san=FILES[fileOf(m.from)]; }
else san=t.toUpperCase();
// capture
const isCapture = prevBoard[m.to]!==0 || (t===PAWN && m.to===State.ep);
if(isCapture){
san+='x';
if(t!==PAWN && t!==KING){
const f2=FILES[fileOf(m.from)];
if(f2!==file) san+=f2;
}
}
// disambiguation for non-king pieces on capture or when needed
if(t!==KING && t!==PAWN && !isCapture){
// check if other same-type pieces could move here
const others=legalMoves(c).filter(o=>type(o.piece)===t && o.to===m.to && !(o.from===m.from));
if(others.length>1){
const sameFile=others.some(o=>fileOf(o.from)===fileOf(m.from));
if(!sameFile) san+=FILES[fileOf(m.from)];
else if(rankOf(others[0].from)!==rankOf(m.from)) san+=8-rankOf(m.from);
}
}
// promotion
if(m.promotion!==null) san+='='+GLYPHS[m.promotion];
// check / mate
const after=applyMoveTo(m, prevBoard, State.castling, State.ep, c, true);
const opp=opp(c);
if(t===KING && Math.abs(fileOf(m.from)-fileOf(m.to))===2){
san = m.to===sq(6,rankOf(m.from))?'O-O':'O-O-O';
if(inCheck(opp,after)) san+='#';
return san;
}
if(legalMoves(opp).length===0){
san+='#';
} else if(inCheck(opp,after)){
san+='+';
}
// en passant
if(t===PAWN && m.to===State.ep) san+=' e.p.';
return san;
}
function parseSan(san){
// try each legal move and match its SAN
const moves=legalMoves(State.turn);
for(const m of moves){
const prev=State.board.slice();
const s=moveToSan(m, prev);
if(s===san) return m;
}
return null;
}
/* =========================================================================
SOUND EFFECTS (Web Audio)
========================================================================= */
const Sound = {
ctx:null, muted:false,
init(){ if(!this.ctx){ try{ this.ctx=new (window.AudioContext||window.webkitAudioContext)(); }catch(e){} } },
tone(freq,dur,type='sine',vol=0.15){
if(this.muted||!this.ctx) return;
const o=this.ctx.createOscillator(), g=this.ctx.createGain();
o.type=type; o.frequency.value=freq;
g.gain.value=vol;
o.connect(g); g.connect(this.ctx.destination);
o.start();
g.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime+dur);
o.stop(this.ctx.currentTime+dur);
},
move(){ this.tone(320,0.06,'triangle'); },
capture(){ this.tone(180,0.12,'square',0.12); },
check(){ this.tone(440,0.18,'sawtooth',0.1); },
promo(){ this.tone(523,0.1,'triangle'); setTimeout(()=>this.tone(784,0.15,'triangle'),120); },
mate(){ this.tone(300,0.4,'sawtooth',0.14); setTimeout(()=>this.tone(200,0.5,'sawtooth',0.14),250); }
};
/* =========================================================================
RENDERING
========================================================================= */
const boardEl = document.getElementById('board');
const RANK_LABELS = ['8','7','6','5','4','3','2','1'];
const FILE_LABELS = ['a','b','c','d','e','f','g','h'];
let orientation = 'normal'; // 'normal' (white bottom) or 'flipped'
let selected = null; // selected square
let legalForSelected = []; // legal moves from selected square
let gameOverFlag = false;
let moveList = []; // parsed move history for display
let capturedByWhite = []; // black pieces captured by white
let capturedByBlack = []; // white pieces captured by black
let lastMoveFrom = -1, lastMoveTo = -1;
function renderBoard(){
boardEl.innerHTML='';
const rows = orientation==='normal' ? [0,1,2,3,4,5,6,7] : [7,6,5,4,3,2,1,0];
const cols = orientation==='normal' ? [0,1,2,3,4,5,6,7] : [7,6,5,4,3,2,1,0];
for(const r of rows){
for(const f of cols){
const s = r*8+f;
const sq=document.createElement('div');
sq.className='sq '+ ( (r+f)%2===0 ? 'light':'dark' );
sq.dataset.sq=s;
sq.addEventListener('click', ()=>onSquareClick(s));
sq.addEventListener('mousedown', e=>onMouseDown(e,s));
sq.addEventListener('mouseup', e=>onMouseUp(e,s));
sq.addEventListener('dragover', e=>e.preventDefault());
sq.addEventListener('drop', e=>{ e.preventDefault(); onDrop(e,s); });
boardEl.appendChild(sq);
boardEl.children[s]._sq=s;
}
}
}
// re-render board visuals from state
function paintBoard(){
const b=State.board;
const legal=legalMoves(State.turn);
const moveSet = new Set(legal.map(m=>m.to));
// last move highlight
let lastFrom=-1,lastTo=-1;
if(State.history.length){ lastFrom=State.history[State.history.length-1].move.from; lastTo=State.history[State.history.length-1].move.to; }
const children=boardEl.children;
for(let s=0;s<64;s++){
const el=children[s]; if(!el) continue;
const isLast = (s===lastFrom||s===lastTo);
el.classList.toggle('last', isLast);
el.classList.toggle('selected', s===selected);
const m=legal.find(x=>x.from===s);
el.classList.toggle('move', !!m && !isCapture(m));
el.classList.toggle('capture', !!m && isCapture(m));
const piece=b[s];
const glyphEl=el.querySelector('.piece');
if(piece){
if(!glyphEl){ const g=document.createElement('div'); g.className='piece '+(isWhite(piece)?'piece-white':'piece-black'); el.appendChild(g); }
glyphEl.textContent=GLYPHS[piece>>4];
glyphEl.className='piece '+(isWhite(piece)?'piece-white':'piece-black');
} else if(glyphEl){ glyphEl.remove(); }
// check glow
if(type(piece)===KING && inCheck(color(piece))){ el.classList.add('check'); } else el.classList.remove('check');
}
}
function isCapture(m){ return b(State.board)[m.to]!==0 || (type(m.piece)===PAWN && m.to===State.ep); }
function b(arr){ return arr; }
function updateTurnIndicator(){
const el=document.getElementById('turn-indicator');
el.textContent = State.turn===WHITE?'White to move':'Black to move';
}
// ---- move list ----
function renderMoveList(){
const list=document.getElementById('move-list');
list.innerHTML='';
moveList.forEach((mv,i)=>{
const num=Math.floor(i/2)+1;
const pair=[i*2+1,i*2+2];
const isCur = pair.includes(i);
const isDone = gameOverFlag || pair.every(p=>p>=moveList.length);
if(isCur && !isDone) list.children[pair[0]]?.classList.add('hl');
else list.children[pair[0]]?.classList.remove('hl');
const n=document.createElement('span'); n.className='mv'; n.innerHTML=`<span class="n">${num}.</span> `;
list.appendChild(n);
const cells=[mv.white||'', mv.black||''];
cells.forEach(txt=>{ if(txt){ const s=document.createElement('span'); s.className='mv'; s.textContent=txt; list.appendChild(s);} });
});
}
// ---- captured pieces ----
function renderCaptured(){
document.getElementById('captured-black').textContent = capturedByWhite.map(g=>GLYPHS[g]).join('');
document.getElementById('captured-white').textContent = capturedByBlack.map(g=>GLYPHS[g]).join('');
}
function trackCaptures(m){
const b=State.board;
const prev=State.history[State.history.length-1].prev.board;
const t=type(m.piece);
if(t===PAWN && m.to===State.ep){
// captured pawn is on ep square of the PREVIOUS state
const pb=prev;
if(pb[m.ep]!==0){ const c=color(pb[m.ep]); const cap=(c===WHITE?capturedByWhite:capturedByBlack); // black captured by white => white tray
// pawn captured by white goes to white tray
if(c===BLACK) capturedByWhite.push(pb[m.ep]>>4);
else capturedByBlack.push(pb[m.ep]>>4);
}
} else {
const cap=b[m.to];
if(cap!==0){
if(color(cap)===BLACK) capturedByWhite.push(cap>>4);
else capturedByBlack.push(cap>>4);
}
}
}
// ---- banner / game over ----
function updateBanner(){
const st=Game.gameOver();
const banner=document.getElementById('banner');
if(st.over){
gameOverFlag=true;
let msg;
if(st.type==='checkmate'){
msg = `Checkmate! ${st.winner===WHITE?'White':'Black'} wins.`;
Sound.mate();
} else if(st.type==='stalemate'){ msg='Stalemate — draw.'; }
else if(st.type==='insufficient'){ msg='Draw — insufficient material.'; }
else if(st.type==='fifty'){ msg='Draw — 50-move rule.'; }
banner.textContent=msg;
banner.style.display='block';
} else {
banner.style.display='none';
}
}
/* =========================================================================
GAME LOOP / ORCHESTRATION
========================================================================= */
function onGameChange(){
updateTurnIndicator();
paintBoard();
renderMoveList();
renderCaptured();
updateBanner();
saveGame();
}
function playAIMove(){
const st=Game.gameOver();
if(st.over) return;
if(State.turn!==BLACK) return;
const legal=legalMoves(State.turn);
if(legal.length===0) return;
// run AI in chunks to avoid freezing
let depth=parseInt(document.getElementById('difficulty').value,10);
const mv=searchBest(legal, depth);
if(mv){
Game.applyMove(mv);
onGameChange();
}
}
// ---- persistence ----
function saveGame(){
try{
const data={
fen:toFen(),
history:moveList,
capturedByWhite, capturedByBlack,
orientation, selected, lastMoveFrom, lastMoveTo,
gameOverFlag
};
localStorage.setItem('chessgame', JSON.stringify(data));
}catch(e){}
}
function loadGame(){
try{
const raw=localStorage.getItem('chessgame');
if(!raw) return false;
const data=JSON.parse(raw);
fromFen(data.fen);
moveList=data.history||[];
capturedByWhite=data.capturedByWhite||[];
capturedByBlack=data.capturedByBlack||[];
orientation=data.orientation||'normal';
lastMoveFrom=data.lastMoveFrom||-1;
lastMoveTo=data.lastMoveTo||-1;
gameOverFlag=data.gameOverFlag||false;
return true;
}catch(e){ return false; }
}
/* =========================================================================
INTERACTION
========================================================================= */
function onSquareClick(s){
if(gameOverFlag) return;
if(State.turn!==WHITE) return;
const piece=State.board[s];
if(selected===null){
if(piece && color(piece)===WHITE){
selected=s;
legalForSelected=legalMoves(WHITE).filter(m=>m.from===s);
paintBoard();
}
return;
}
// clicking same square deselects
if(selected===s){ selected=null; legalForSelected=[]; paintBoard(); return; }
// find matching move
const m=legalForSelected.find(x=>x.to===s);
if(m){ doPlayerMove(m); return; }
// click elsewhere: reselect if it's one of our pieces
if(piece && color(piece)===WHITE){
selected=s; legalForSelected=legalMoves(WHITE).filter(x=>x.from===s);
paintBoard();
} else {
selected=null; legalForSelected=[]; paintBoard();
}
}
function onDragStart(e,s){
if(gameOverFlag||State.turn!==WHITE) return;
if(State.board[s] && color(State.board[s])===WHITE){
selected=s; legalForSelected=legalMoves(WHITE).filter(x=>x.from===s);
paintBoard();
e.dataTransfer.effectAllowed='move';
try{ e.dataTransfer.setData('text/plain',''); }catch(_){}
}
}
function onDrop(e,s){
e.preventDefault();
if(gameOverFlag||State.turn!==WHITE) return;
if(selected!==null){
const m=legalForSelected.find(x=>x.to===s);
if(m) doPlayerMove(m);
}
selected=null; legalForSelected=[]; paintBoard();
}
function onMouseUp(e,s){ /* nothing extra */ }
function doPlayerMove(m){
// promotion picker for pawn promotions
if(type(m.piece)===PAWN && (rankOf(m.to)===0 || rankOf(m.to)===7)){
showPromotion(m);
return;
}
executeMove(m);
}
function executeMove(m){
const prev=State.history[State.history.length-1].prev.board;
// track captured pieces
const cap=b(State.board)[m.to];
if(cap!==0){
if(color(cap)===BLACK) capturedByWhite.push(cap>>4);
else capturedByBlack.push(cap>>4);
} else if(type(m.piece)===PAWN && m.to===State.ep){
// en passant: captured pawn on ep square of previous board
const epPiece=prev[m.ep];
if(epPiece!==0){
if(color(epPiece)===BLACK) capturedByWhite.push(epPiece>>4);
else capturedByBlack.push(epPiece>>4);
}
}
Game.applyMove(m);
Sound.move();
if(isCapture(m)) Sound.capture();
const after=State.board;
if(inCheck(opp(State.turn),after) && legalMoves(opp(State.turn)).length===0){
Sound.mate();
} else if(inCheck(opp(State.turn),after)){
Sound.check();
}
// build SAN
const san=moveToSan(m, prev);
addMoveToList(san);
onGameChange();
// AI responds
if(!Game.gameOver().over && State.turn===BLACK){
setTimeout(playAIMove, 60);
}
}
function addMoveToList(san){
moveList.push({ n:Math.floor(moveList.length/2)+1, white:san, black:'' });
renderMoveList();
}
/* =========================================================================
PROMOTION PICKER
========================================================================= */
function showPromotion(m){
const overlay=document.getElementById('promo-overlay');
const box=document.getElementById('promo-box');
box.innerHTML='';
for(const pt of [QUEEN,ROOK,BISHOP,KNIGHT]){
const b=document.createElement('div');
b.className='promot '+(State.turn===WHITE?'piece-white':'piece-black');
b.textContent=GLYPHS[pt];
b.addEventListener('click', ()=>{
m.promotion=pt;
overlay.classList.remove('show');
executeMove(m);
});
box.appendChild(b);
}
overlay.classList.add('show');
Sound.promo();
}
/* =========================================================================
CONTROLS
========================================================================= */
document.getElementById('btn-flip').addEventListener('click', ()=>{
orientation = orientation==='normal'?'flipped':'normal';
selected=null; legalForSelected=[];
renderBoard(); onGameChange();
});
document.getElementById('btn-undo').addEventListener('click', ()=>{
Game.undo();
moveList=moveList.filter(mv=>!(mv.black && State.history.length===0));
// reset captured arrays to match
capturedByWhite=[]; capturedByBlack=[];
selected=null; legalForSelected=[]; gameOverFlag=false;
renderBoard(); onGameChange();
});
document.getElementById('btn-new').addEventListener('click', ()=>{
Game.newGame();
moveList=[]; capturedByWhite=[]; capturedByBlack=[];
orientation='normal'; selected=null; legalForSelected=[]; gameOverFlag=false;
renderBoard(); onGameChange();
});
document.getElementById('mute').addEventListener('change', e=>{
Sound.muted=e.target.checked;
});
/* =========================================================================
INITIALIZATION
========================================================================= */
renderBoard();
loadGame();
if(!State.history.length){
clearState();
onGameChange();
}
// ensure AI move if loaded position has black to move
if(!gameOverFlag && State.turn===BLACK){
setTimeout(playAIMove, 100);
}
</script>
</body>
</html>
<!-- agent-meta {"model":"ornith-ai/Ornith-1.5-9B-GGUF","provider":"brigid","persona":"full","sessionId":"67814691-24a0-4657-ae2f-c24361f303b3","tokensIn":7893483,"tokensOut":67247,"tokensTotal":7960730,"cost":0,"turns":121,"toolCalls":120,"failedToolCalls":9,"timestamp":"2026-08-24T03:51:07.744Z"} -->