Chess
ling 3.0 Flash · 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 57,727 bytes · SHA-256 03028911247c
<!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>
/* ============================================================
CONFIGURATION — Tunable constants
============================================================ */
:root {
--sq-light: #F0D9B5;
--sq-dark: #B58863;
--sel-bg: #FFFF87;
--last-bg: #CED26B;
--check-glow: #FF4444;
--dot-color: rgba(0,0,0,0.35);
--ring-color: rgba(0,0,0,0.55);
--panel-bg: #2B2B2B;
--panel-fg: #E0E0E0;
--accent: #5A9E6F;
--danger: #C0392B;
--board-size: min(88vw, 88vh, 560px);
--sq-size: calc(var(--board-size) / 8);
--font-piece: var(--sq-size);
--font-ui: 13px;
}
* { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #1a1a2e;
color: var(--panel-fg);
display: flex;
flex-direction: column;
align-items: center;
min-height: 100vh;
padding: 10px;
}
/* ============================================================
LAYOUT
============================================================ */
#app { display:flex; gap:16px; align-items:flex-start; flex-wrap:wrap; justify-content:center; }
#board-container { position:relative; }
#board {
display:grid;
grid-template-columns: repeat(8, var(--sq-size));
grid-template-rows: repeat(8, var(--sq-size));
border: 3px solid #555;
border-radius: 4px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
user-select: none;
-webkit-user-select: none;
}
#sidebar {
width: 260px;
display: flex;
flex-direction: column;
gap: 10px;
}
.panel {
background: var(--panel-bg);
border-radius: 6px;
padding: 10px;
font-size: var(--font-ui);
}
.panel h3 { font-size:14px; margin-bottom:6px; color:#aaa; text-transform:uppercase; letter-spacing:1px; }
/* ============================================================
SQUARES & PIECES
============================================================ */
.square {
width: var(--sq-size);
height: var(--sq-size);
display:flex;
align-items:center;
justify-content:center;
font-size: calc(var(--sq-size) * 0.82);
cursor: pointer;
position: relative;
line-height: 1;
}
.square.light { background: var(--sq-light); }
.square.dark { background: var(--sq-dark); }
.square.selected { background: var(--sel-bg) !important; }
.square.last-move { background: var(--last-bg) !important; }
.square.in-check { box-shadow: inset 0 0 12px 4px var(--check-glow); }
.square.drag-over { outline: 3px solid var(--accent); }
/* Move indicators */
.square .move-dot {
width: 30%; height: 30%;
border-radius: 50%;
background: var(--dot-color);
position: absolute;
pointer-events: none;
}
.square .capture-ring {
width: 80%; height: 80%;
border-radius: 50%;
border: 3px solid var(--ring-color);
position: absolute;
pointer-events: none;
}
/* ============================================================
PIECE DRAG GHOST
============================================================ */
#drag-ghost {
position: fixed;
pointer-events: none;
font-size: calc(var(--sq-size) * 0.9);
z-index: 1000;
opacity: 0.85;
transform: translate(-50%, -50%);
display: none;
}
/* ============================================================
SIDEBAR COMPONENTS
============================================================ */
#controls { display:flex; flex-wrap:wrap; gap:6px; }
#controls button {
flex:1; min-width:70px;
padding: 6px 8px;
border: none; border-radius: 4px;
background: #3a3a5c;
color: #e0e0e0;
cursor: pointer;
font-size: 12px;
font-weight: 600;
transition: background 0.15s;
}
#controls button:hover { background: #4a4a7c; }
#controls button.active { background: var(--accent); color:#fff; }
#captured { display:flex; gap:4px; flex-wrap:wrap; min-height:24px; }
.captured-piece { font-size: 18px; line-height:1; }
.captured-white { color: #ddd; }
.captured-black { color: #333; }
#move-list {
max-height: 220px;
overflow-y: auto;
font-family: 'Courier New', monospace;
font-size: 12px;
line-height: 1.6;
white-space: pre;
}
#move-list::-webkit-scrollbar { width:4px; }
#move-list::-webkit-scrollbar-thumb { background:#555; border-radius:2px; }
#status {
font-size: 14px;
font-weight: 700;
text-align: center;
padding: 6px;
border-radius: 4px;
background: #3a3a5c;
}
#status.wt { color: #fff; }
#status.bt { color: #ccc; }
/* ============================================================
PROMOTION PICKER
============================================================ */
#promotion-overlay {
position: absolute;
top:0; left:0; right:0; bottom:0;
display: none;
align-items: center;
justify-content: center;
z-index: 100;
background: rgba(0,0,0,0.4);
}
#promotion-overlay.visible { display:flex; }
.promo-btn {
width: 60px; height: 60px;
font-size: 36px;
border: 2px solid #555;
border-radius: 6px;
background: #f5f0e0;
cursor: pointer;
display:flex; align-items:center; justify-content:center;
transition: background 0.15s;
}
.promo-btn:hover { background: #ffe080; }
/* ============================================================
GAME OVER BANNER
============================================================ */
#game-over {
position: absolute;
top: 50%; left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.88);
color: #fff;
padding: 20px 36px;
border-radius: 10px;
text-align: center;
font-size: 20px;
font-weight: 700;
z-index: 200;
display: none;
}
#game-over.visible { display:block; }
#game-over button {
margin-top: 12px;
padding: 8px 20px;
background: var(--accent);
color: #fff;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
/* ============================================================
MUTE TOGGLE
============================================================ */
.mute-btn { font-size: 16px !important; }
</style>
</head>
<body>
<div id="app">
<div id="board-container">
<div id="board"></div>
<div id="promotion-overlay"></div>
<div id="game-over"></div>
</div>
<div id="sidebar">
<div id="status" class="wt">White to move</div>
<div class="panel">
<h3>Controls</h3>
<div id="controls">
<button id="btn-new">New</button>
<button id="btn-undo">Undo</button>
<button id="btn-flip">Flip</button>
</div>
<div id="controls" style="margin-top:6px">
<button id="btn-depth1" class="active">Depth 1</button>
<button id="btn-depth2">Depth 2</button>
<button id="btn-depth3">Depth 3</button>
</div>
<div id="controls" style="margin-top:6px">
<button id="btn-mute" class="mute-btn">🔊</button>
</div>
</div>
<div class="panel">
<h3>Captured</h3>
<div id="captured"></div>
</div>
<div class="panel">
<h3>Moves</h3>
<div id="move-list"></div>
</div>
</div>
</div>
<div id="drag-ghost"></div>
<script>
// ============================================================
// CONFIGURATION — Tunable constants
// ============================================================
const CFG = {
boardLight: '#F0D9B5',
boardDark: '#B58863',
selBg: '#FFFF87',
lastBg: '#CED26B',
checkGlow: '#FF4444',
dotColor: 'rgba(0,0,0,0.35)',
ringColor: 'rgba(0,0,0,0.55)',
aiDepth: 3,
aiMinDepth: 1,
aiMaxDepth: 3,
// Piece values (centipawns)
pieceValue: { 'P':100, 'N':320, 'B':330, 'R':500, 'Q':900, 'K':20000,
'p':100, 'n':320, 'b':330, 'r':500, 'q':900, 'k':20000 },
// Piece-square tables (from White's perspective, row 0=rank8, row 7=rank1)
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,10,5,5,-10, -10,0,10,10,10,10,0,-10, -10,10,10,10,10,10,10,-10, -10,5,0,0,0,0,5,-10, -20,-10,-10,-10,-10,-10,-10,-20],
'R': [0,0,0,0,0,0,0,0, 5,10,10,10,10,10,10,5, -5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5, -5,0,0,0,0,0,0,-5, -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],
},
// Flip PST for Black pieces (reverse row order)
flipPst(t) { const f=[]; for(let r=7;r>=0;r--) for(let c=0;c<8;c++) f.push(t[r*8+c]); return f; },
};
CFG.pstBlack = {};
for (const p of ['P','N','B','R','Q','K']) CFG.pstBlack[p] = CFG.flipPst(CFG.pst[p]);
// ============================================================
// CHESS ENGINE — Board, move generation, legality
// ============================================================
const Engine = {
board: [], // 8x8 array, null or 'P','N','B','R','Q','K','p','n','b','r','q','k'
turn: 'w', // 'w' or 'b'
castling: {wK:true, wQ:true, bK:true, bQ:true},
epTarget: null, // {r,c} or null
halfMoveClock: 0,
fullMoveNumber: 1,
moveHistory: [], // for undo & display
captured: {w:[], b:[]}, // captured pieces
gameOver: false,
result: null, // 'white','black','draw'
resultReason: '',
// Initial FEN
initFEN: 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
init() {
this.fromFEN(this.initFEN);
},
fromFEN(fen) {
const parts = fen.split(' ');
const rows = parts[0].split('/');
this.board = [];
for (let r=0; r<8; r++) {
this.board[r] = [];
let ci = 0;
for (const ch of rows[r]) {
if (ch>='1' && ch<='8') {
for (let i=0; i<+ch; i++) this.board[r][ci++] = null;
} else {
this.board[r][ci++] = ch;
}
}
}
this.turn = parts[1] || 'w';
const cas = parts[2] || '-';
this.castling = {wK:cas.includes('K'), wQ:cas.includes('Q'), bK:cas.includes('k'), bQ:cas.includes('q')};
const epStr = parts[3] || '-';
this.epTarget = epStr === '-' ? null : this.algToRC(epStr);
this.halfMoveClock = parseInt(parts[4]) || 0;
this.fullMoveNumber = parseInt(parts[5]) || 1;
this.moveHistory = [];
this.captured = {w:[], b:[]};
this.gameOver = false;
this.result = null;
this.resultReason = '';
},
toFEN() {
const rows = [];
for (let r=0; r<8; r++) {
let row = '';
let empty = 0;
for (let c=0; c<8; c++) {
if (!this.board[r][c]) { empty++; }
else {
if (empty) { row += empty; empty = 0; }
row += this.board[r][c];
}
}
if (empty) row += empty;
rows.push(row);
}
let cas = '';
if (this.castling.wK) cas += 'K';
if (this.castling.wQ) cas += 'Q';
if (this.castling.bK) cas += 'k';
if (this.castling.bQ) cas += 'q';
if (!cas) cas = '-';
const ep = this.epTarget ? this.rcToAlg(this.epTarget.r, this.epTarget.c) : '-';
return rows.join('/') + ' ' + this.turn + ' ' + cas + ' ' + ep + ' ' + this.halfMoveClock + ' ' + this.fullMoveNumber;
},
rcToAlg(r, c) { return String.fromCharCode(97+c) + (8-r); },
algToRC(alg) { return {r: 8 - parseInt(alg[1]), c: alg.charCodeAt(0) - 97}; },
clone() {
const e = Object.create(Object.getPrototypeOf(this));
e.board = this.board.map(row => [...row]);
e.turn = this.turn;
e.castling = {...this.castling};
e.epTarget = this.epTarget ? {...this.epTarget} : null;
e.halfMoveClock = this.halfMoveClock;
e.fullMoveNumber = this.fullMoveNumber;
e.moveHistory = [...this.moveHistory];
e.captured = {w:[...this.captured.w], b:[...this.captured.b]};
e.gameOver = this.gameOver;
e.result = this.result;
e.resultReason = this.resultReason;
return e;
},
// ============================================================
// MOVE GENERATION
// ============================================================
isEnemy(piece, color) {
if (!piece) return false;
return color === 'w' ? piece === piece.toLowerCase() : piece === piece.toUpperCase();
},
isFriend(piece, color) {
if (!piece) return false;
return color === 'w' ? piece === piece.toUpperCase() && piece !== piece.toLowerCase() : piece === piece.toLowerCase() && piece !== piece.toUpperCase();
},
isWhite(piece) { return piece && piece === piece.toUpperCase() && piece !== piece.toLowerCase(); },
isBlack(piece) { return piece && piece === piece.toLowerCase() && piece !== piece.toUpperCase(); },
colorOf(piece) { return this.isWhite(piece) ? 'w' : 'b'; },
inBounds(r, c) { return r >= 0 && r < 8 && c >= 0 && c < 8; },
// Generate pseudo-legal moves (ignoring check)
pseudoLegalMoves(color) {
const moves = [];
const opp = color === 'w' ? 'b' : 'w';
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const p = this.board[r][c];
if (!p || (color === 'w' && !this.isWhite(p)) || (color === 'b' && !this.isBlack(p))) continue;
const pc = p.toLowerCase();
const isP = pc === 'p';
const isN = pc === 'n';
const isB = pc === 'b';
const isR = pc === 'r';
const isQ = pc === 'q';
const isK = pc === 'k';
if (isP) {
const dir = color === 'w' ? -1 : 1;
const startRank = color === 'w' ? 6 : 1;
const promoRank = color === 'w' ? 0 : 7;
// Forward 1
if (this.inBounds(r+dir, c) && !this.board[r+dir][c]) {
if (r+dir === promoRank) {
for (const pro of ['Q','R','B','N']) moves.push(this._mkMove(r,c,r+dir,c,'','',pro));
} else {
moves.push(this._mkMove(r,c,r+dir,c));
}
// Forward 2
if (r === startRank && !this.board[r+2*dir][c]) {
moves.push(this._mkMove(r,c,r+2*dir,c,'','',null,false,true));
}
}
// Captures
for (const dc of [-1, 1]) {
if (!this.inBounds(r+dir, c+dc)) continue;
const target = this.board[r+dir][c+dc];
if (target && this.isEnemy(target, color)) {
if (r+dir === promoRank) {
for (const pro of ['Q','R','B','N']) moves.push(this._mkMove(r,c,r+dir,c+dc,'','',pro));
} else {
moves.push(this._mkMove(r,c,r+dir,c+dc));
}
}
// En passant
if (this.epTarget && this.epTarget.r === r+dir && this.epTarget.c === c+dc) {
moves.push(this._mkMove(r,c,r+dir,c+dc,'','',null,true));
}
}
} else if (isN) {
for (const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const nr = r+dr, nc = c+dc;
if (!this.inBounds(nr,nc)) continue;
const t = this.board[nr][nc];
if (!t || this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc));
}
} else if (isB || isQ) {
for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
let nr = r+dr, nc = c+dc;
while (this.inBounds(nr,nc)) {
const t = this.board[nr][nc];
if (!t) { moves.push(this._mkMove(r,c,nr,nc)); }
else { if (this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc)); break; }
nr += dr; nc += dc;
}
}
}
if (isR || isQ) {
for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
let nr = r+dr, nc = c+dc;
while (this.inBounds(nr,nc)) {
const t = this.board[nr][nc];
if (!t) { moves.push(this._mkMove(r,c,nr,nc)); }
else { if (this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc)); break; }
nr += dr; nc += dc;
}
}
}
if (isK) {
for (let dr=-1; dr<=1; dr++) {
for (let dc=-1; dc<=1; dc++) {
if (dr===0 && dc===0) continue;
const nr = r+dr, nc = c+dc;
if (!this.inBounds(nr,nc)) continue;
const t = this.board[nr][nc];
if (!t || this.isEnemy(t, color)) moves.push(this._mkMove(r,c,nr,nc));
}
}
// Castling
if (color === 'w') {
// O-O (kingside)
if (this.castling.wK && r===7 && c===4 &&
!this.board[7][5] && !this.board[7][6] && this.board[7][7]==='R' &&
!this.isSquareAttacked(7,4,opp) && !this.isSquareAttacked(7,5,opp) && !this.isSquareAttacked(7,6,opp)) {
moves.push(this._mkMove(7,4,7,6,'','','',false,false,false,'O-O'));
}
// O-O-O (queenside)
if (this.castling.wQ && r===7 && c===4 &&
!this.board[7][1] && !this.board[7][2] && !this.board[7][3] && this.board[7][0]==='R' &&
!this.isSquareAttacked(7,4,opp) && !this.isSquareAttacked(7,3,opp) && !this.isSquareAttacked(7,2,opp)) {
moves.push(this._mkMove(7,4,7,2,'','','',false,false,false,'O-O-O'));
}
} else {
// O-O (black kingside)
if (this.castling.bK && r===0 && c===4 &&
!this.board[0][5] && !this.board[0][6] && this.board[0][7]==='r' &&
!this.isSquareAttacked(0,4,opp) && !this.isSquareAttacked(0,5,opp) && !this.isSquareAttacked(0,6,opp)) {
moves.push(this._mkMove(0,4,0,6,'','','',false,false,false,'O-O'));
}
// O-O-O (black queenside)
if (this.castling.bQ && r===0 && c===4 &&
!this.board[0][1] && !this.board[0][2] && !this.board[0][3] && this.board[0][0]==='r' &&
!this.isSquareAttacked(0,4,opp) && !this.isSquareAttacked(0,3,opp) && !this.isSquareAttacked(0,2,opp)) {
moves.push(this._mkMove(0,4,0,2,'','','',false,false,false,'O-O-O'));
}
}
}
}
}
return moves;
},
_mkMove(fr, fc, tr, tc, captured, promotion, promoPiece, enPassant, doublePush, isCastling, san) {
return {
from: {r:fr, c:fc}, to: {r:tr, c:tc},
piece: this.board[fr][fc],
captured: captured || null,
promotion: promotion || null,
promoPiece: promoPiece || null,
enPassant: !!enPassant,
doublePush: !!doublePush,
isCastling: !!isCastling,
san: san || '',
};
},
// Check if square (r,c) is attacked by pieces of `byColor`
isSquareAttacked(r, c, byColor) {
// Pawns
const pawnDir = byColor === 'w' ? 1 : -1; // pawns attack from opposite direction
const pawnChar = byColor === 'w' ? 'p' : 'P';
for (const dc of [-1, 1]) {
const pr = r + pawnDir, pc = c + dc;
if (this.inBounds(pr, pc) && this.board[pr][pc] === pawnChar) return true;
}
// Knights
const nChar = byColor === 'w' ? 'n' : 'N';
for (const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const nr = r+dr, nc = c+dc;
if (this.inBounds(nr,nc) && this.board[nr][nc] === nChar) return true;
}
// Bishops & Queens (diagonals)
const bqChar = byColor === 'w' ? 'q' : 'Q';
const bChar = byColor === 'w' ? 'b' : 'B';
for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
let nr = r+dr, nc = c+dc;
while (this.inBounds(nr,nc)) {
const p = this.board[nr][nc];
if (p) { if (p === bChar || p === bqChar) return true; break; }
nr += dr; nc += dc;
}
}
// Rooks & Queens (straight)
const rqChar = byColor === 'w' ? 'q' : 'Q';
const rChar = byColor === 'w' ? 'r' : 'R';
for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
let nr = r+dr, nc = c+dc;
while (this.inBounds(nr,nc)) {
const p = this.board[nr][nc];
if (p) { if (p === rChar || p === rqChar) return true; break; }
nr += dr; nc += dc;
}
}
// Kings
const kChar = byColor === 'w' ? 'k' : 'K';
for (let dr=-1; dr<=1; dr++) {
for (let dc=-1; dc<=1; dc++) {
if (dr===0 && dc===0) continue;
const nr = r+dr, nc = c+dc;
if (this.inBounds(nr,nc) && this.board[nr][nc] === kChar) return true;
}
}
return false;
},
findKing(color) {
const k = color === 'w' ? 'K' : 'k';
for (let r=0; r<8; r++)
for (let c=0; c<8; c++)
if (this.board[r][c] === k) return {r, c};
return null;
},
isInCheck(color) {
const king = this.findKing(color);
if (!king) return false;
const opp = color === 'w' ? 'b' : 'w';
return this.isSquareAttacked(king.r, king.c, opp);
},
// Filter pseudo-legal moves to legal moves
legalMoves(color) {
const pseudo = this.pseudoLegalMoves(color);
const legal = [];
for (const move of pseudo) {
if (this.isLegal(move)) legal.push(move);
}
return legal;
},
// Check if a pseudo-legal move is legal (doesn't leave king in check)
isLegal(move) {
// Quick check: if moving the king, make sure destination isn't attacked
if (move.piece.toLowerCase() === 'k') {
const opp = this.turn === 'w' ? 'b' : 'w';
// For castling, we already checked squares are not attacked in generation
// But let's double-check the destination
if (!this.isSquareAttacked(move.to.r, move.to.c, opp)) return true;
return false;
}
// Make move on a temporary board and check if own king is in check
const tempBoard = this.board.map(row => [...row]);
const fr = move.from.r, fc = move.from.c, tr = move.to.r, tc = move.to.c;
tempBoard[tr][tc] = tempBoard[fr][fc];
tempBoard[fr][fc] = null;
// Handle en passant capture
if (move.enPassant) {
const capR = this.turn === 'w' ? tr + 1 : tr - 1;
tempBoard[capR][tc] = null;
}
// Handle promotion (replace pawn with promoted piece)
if (move.promoPiece) {
tempBoard[tr][tc] = move.promoPiece;
}
// Find own king on temp board
const myKing = this.turn === 'w' ? 'K' : 'k';
let kr = -1, kc = -1;
for (let r=0; r<8; r++)
for (let c=0; c<8; c++)
if (tempBoard[r][c] === myKing) { kr = r; kc = c; }
if (kr < 0) return false; // King missing = illegal
const opp = this.turn === 'w' ? 'b' : 'w';
return !this.isSquareAttackedOn(kr, kc, opp, tempBoard);
},
// isSquareAttacked but on a given board (not this.board)
isSquareAttackedOn(r, c, byColor, board) {
const pawnDir = byColor === 'w' ? 1 : -1;
const pawnChar = byColor === 'w' ? 'p' : 'P';
for (const dc of [-1, 1]) {
const pr = r + pawnDir, pc = c + dc;
if (this.inBounds(pr, pc) && board[pr][pc] === pawnChar) return true;
}
const nChar = byColor === 'w' ? 'n' : 'N';
for (const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const nr = r+dr, nc = c+dc;
if (this.inBounds(nr,nc) && board[nr][nc] === nChar) return true;
}
const bqChar = byColor === 'w' ? 'q' : 'Q';
const bChar = byColor === 'w' ? 'b' : 'B';
for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
let nr = r+dr, nc = c+dc;
while (this.inBounds(nr,nc)) {
const p = board[nr][nc];
if (p) { if (p === bChar || p === bqChar) return true; break; }
nr += dr; nc += dc;
}
}
const rqChar = byColor === 'w' ? 'q' : 'Q';
const rChar = byColor === 'w' ? 'r' : 'R';
for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
let nr = r+dr, nc = c+dc;
while (this.inBounds(nr,nc)) {
const p = board[nr][nc];
if (p) { if (p === rChar || p === rqChar) return true; break; }
nr += dr; nc += dc;
}
}
const kChar = byColor === 'w' ? 'k' : 'K';
for (let dr=-1; dr<=1; dr++) {
for (let dc=-1; dc<=1; dc++) {
if (dr===0 && dc===0) continue;
const nr = r+dr, nc = c+dc;
if (this.inBounds(nr,nc) && board[nr][nc] === kChar) return true;
}
}
return false;
},
// ============================================================
// MAKE / UNMAKE MOVE
// ============================================================
makeMove(move) {
const undo = {
from: {...move.from}, to: {...move.to},
piece: move.piece,
captured: this.board[move.to.r][move.to.c],
capturedR: move.to.r, capturedC: move.to.c,
castling: {...this.castling},
epTarget: this.epTarget ? {...this.epTarget} : null,
halfMoveClock: this.halfMoveClock,
fullMoveNumber: this.fullMoveNumber,
promoPiece: move.promoPiece || null,
enPassant: move.enPassant,
isCastling: move.isCastling,
prevTurn: this.turn,
};
const fr = move.from.r, fc = move.from.c, tr = move.to.r, tc = move.to.c;
// Move piece
this.board[tr][tc] = this.board[fr][fc];
this.board[fr][fc] = null;
// En passant capture
if (move.enPassant) {
const capR = this.turn === 'w' ? tr + 1 : tr - 1;
undo.captured = this.board[capR][tc];
undo.capturedR = capR; undo.capturedC = tc;
this.board[capR][tc] = null;
}
// Promotion
if (move.promoPiece) {
this.board[tr][tc] = move.promoPiece;
}
// Castling: move rook
if (move.isCastling) {
if (tc === 6) { // Kingside
this.board[tr][5] = this.board[tr][7];
this.board[tr][7] = null;
} else if (tc === 2) { // Queenside
this.board[tr][3] = this.board[tr][0];
this.board[tr][0] = null;
}
}
// Update castling rights
if (move.piece === 'K') this.castling.wK = this.castling.wQ = false;
if (move.piece === 'k') this.castling.bK = this.castling.bQ = false;
if (move.piece === 'R' && fr===7 && fc===7) this.castling.wK = false;
if (move.piece === 'R' && fr===7 && fc===0) this.castling.wQ = false;
if (move.piece === 'r' && fr===0 && fc===7) this.castling.bK = false;
if (move.piece === 'r' && fr===0 && fc===0) this.castling.bQ = false;
if (move.piece === 'R' && tr===7 && tc===7) this.castling.wK = false;
if (move.piece === 'R' && tr===7 && tc===0) this.castling.wQ = false;
if (move.piece === 'r' && tr===0 && tc===7) this.castling.bK = false;
if (move.piece === 'r' && tr===0 && tc===0) this.castling.bQ = false;
// Update en passant target
if (move.doublePush) {
this.epTarget = {r: (fr+tr)/2, c: fc};
} else {
this.epTarget = null;
}
// Update half-move clock
const isPawn = move.piece.toLowerCase() === 'p';
const isCapture = !!move.captured || move.enPassant;
if (isPawn || isCapture) {
this.halfMoveClock = 0;
} else {
this.halfMoveClock++;
}
// Switch turn
this.turn = this.turn === 'w' ? 'b' : 'w';
if (this.turn === 'w') this.fullMoveNumber++;
// Record captured piece
if (undo.captured) {
const capColor = this.isWhite(undo.captured) ? 'w' : 'b';
this.captured[capColor].push(undo.captured);
}
// Record move
move.san = this.sanitize(move);
this.moveHistory.push(move);
// Check game state
this.updateGameState();
return undo;
},
unmakeMove(move, undo) {
const fr = move.from.r, fc = move.from.c, tr = move.to.r, tc = move.to.c;
// Restore turn
this.turn = undo.prevTurn;
// Undo full move number
if (this.turn === 'w') this.fullMoveNumber--;
// Restore half-move clock
this.halfMoveClock = undo.halfMoveClock;
// Restore castling
this.castling = undo.castling;
// Restore en passant target
this.epTarget = undo.epTarget ? {...undo.epTarget} : null;
// Move piece back
this.board[fr][fc] = undo.piece;
this.board[tr][tc] = undo.captured;
// Undo en passant capture
if (undo.enPassant) {
const capR = this.turn === 'w' ? tr + 1 : tr - 1;
this.board[capR][tc] = undo.captured;
}
// Undo promotion
if (undo.promoPiece) {
this.board[fr][fc] = this.turn === 'w' ? 'P' : 'p';
}
// Undo castling rook
if (undo.isCastling) {
if (tc === 6) {
this.board[tr][7] = this.board[tr][5];
this.board[tr][5] = null;
} else if (tc === 2) {
this.board[tr][0] = this.board[tr][3];
this.board[tr][3] = null;
}
}
// Remove last move from history
this.moveHistory.pop();
// Restore captured piece
if (undo.captured) {
const capColor = this.isWhite(undo.captured) ? 'w' : 'b';
this.captured[capColor].pop();
}
// Reset game over state
this.gameOver = false;
this.result = null;
this.resultReason = '';
},
// ============================================================
// GAME STATE CHECKING
// ============================================================
updateGameState() {
const color = this.turn;
const moves = this.legalMoves(color);
const inCheck = this.isInCheck(color);
if (moves.length === 0) {
this.gameOver = true;
if (inCheck) {
// Checkmate
this.result = color === 'w' ? 'black' : 'white';
this.resultReason = 'checkmate';
} else {
// Stalemate
this.result = 'draw';
this.resultReason = 'stalemate';
}
} else if (inCheck) {
// Just in check, game continues
} else {
// Check for draws
if (this.halfMoveClock >= 100) {
this.gameOver = true;
this.result = 'draw';
this.resultReason = '50-move rule';
} else if (this.insufficientMaterial()) {
this.gameOver = true;
this.result = 'draw';
this.resultReason = 'insufficient material';
}
}
},
insufficientMaterial() {
const pieces = {w: [], b: []};
for (let r=0; r<8; r++)
for (let c=0; c<8; c++) {
const p = this.board[r][c];
if (p) pieces[this.isWhite(p)?'w':'b'].push(p);
}
// K vs K
if (pieces.w.length === 1 && pieces.b.length === 1) return true;
// K + B vs K
if (pieces.w.length === 2 && pieces.b.length === 1) {
if (pieces.w.some(p => p==='B') || pieces.w.some(p => p==='N')) return true;
}
if (pieces.b.length === 2 && pieces.w.length === 1) {
if (pieces.b.some(p => p==='b') || pieces.b.some(p => p==='n')) return true;
}
// K + B vs K + B (same color bishops)
if (pieces.w.length === 2 && pieces.b.length === 2) {
const wB = pieces.w.filter(p => p==='B');
const bB = pieces.b.filter(p => p==='b');
if (wB.length === 1 && bB.length === 1) {
// Check if bishops are on same color
let wSq = -1, bSq = -1;
for (let r=0; r<8; r++)
for (let c=0; c<8; c++) {
if (this.board[r][c]==='B') wSq = r+c;
if (this.board[r][c]==='b') bSq = r+c;
}
if (wSq >= 0 && bSq >= 0 && (wSq % 2) === (bSq % 2)) return true;
}
}
return false;
},
// ============================================================
// SAN (Standard Algebraic Notation)
// ============================================================
sanitize(move) {
const pc = move.piece.toLowerCase();
const isP = pc === 'p';
const isK = pc === 'k';
const opp = this.turn === 'w' ? 'b' : 'w';
// Castling
if (move.isCastling) return move.san; // already set
let san = '';
if (!isP) san += move.piece;
// Disambiguation
const pseudo = this.pseudoLegalMoves(this.turn);
const sameType = pseudo.filter(m =>
m.piece === move.piece && m.to.r === move.to.r && m.to.c === move.to.c &&
!(m.from.r === move.from.r && m.from.c === move.from.c)
);
if (sameType.length > 0) {
const sameFile = sameType.some(m => m.from.c === move.from.c);
const sameRank = sameType.some(m => m.from.r === move.from.r);
if (!sameFile) san += String.fromCharCode(97 + move.from.c);
else if (!sameRank) san += (8 - move.from.r);
else san += String.fromCharCode(97 + move.from.c) + (8 - move.from.r);
}
// Capture
const isCapture = !!move.captured || move.enPassant;
if (isCapture) {
if (isP) san += String.fromCharCode(97 + move.from.c);
san += 'x';
}
san += this.rcToAlg(move.to.r, move.to.c);
// Promotion
if (move.promoPiece) san += '=' + move.promoPiece;
// Check / Checkmate
// Simulate the move to check
const tempBoard = this.board.map(row => [...row]);
tempBoard[move.to.r][move.to.c] = tempBoard[move.from.r][move.from.c];
tempBoard[move.from.r][move.from.c] = null;
if (move.enPassant) {
const capR = this.turn === 'w' ? move.to.r + 1 : move.to.r - 1;
tempBoard[capR][move.to.c] = null;
}
if (move.promoPiece) tempBoard[move.to.r][move.to.c] = move.promoPiece;
const myKing = this.turn === 'w' ? 'K' : 'k';
let kr = -1, kc = -1;
for (let r=0; r<8; r++)
for (let c=0; c<8; c++)
if (tempBoard[r][c] === myKing) { kr = r; kc = c; }
const oppIsAttacking = this.isSquareAttackedOn(kr, kc, opp, tempBoard);
// If check, check if it's checkmate
if (oppIsAttacking) {
// Check if opponent has any legal moves
const oppMoves = this.legalMovesOn(tempBoard, opp);
if (oppMoves.length === 0) {
san += '#';
} else {
san += '+';
}
}
return san;
},
// Generate legal moves on a given board (used for checkmate detection)
legalMovesOn(board, color) {
const moves = [];
const opp = color === 'w' ? 'b' : 'w';
for (let r=0; r<8; r++) {
for (let c=0; c<8; c++) {
const p = board[r][c];
if (!p || (color==='w' && p!==p.toUpperCase()) || (color==='b' && p!==p.toLowerCase())) continue;
// Simplified: just check pseudo-legal and filter by king safety
const targets = this._pseudoTargets(board, r, c, color);
for (const {tr, tc} of targets) {
// Make on temp board
const tb = board.map(row => [...row]);
tb[tr][tc] = tb[r][c];
tb[r][c] = null;
if (p.toLowerCase() === 'p' && tb[tr][tc] !== (color==='w'?'P':'p') && (tr===0||tr===7)) {
tb[tr][tc] = color==='w'?'Q':'q'; // default to queen for check
}
// Find king
const myK = color==='w'?'K':'k';
let kr=-1, kc=-1;
for (let rr=0;rr<8;rr++) for(let cc=0;cc<8;cc++) if(tb[rr][cc]===myK){kr=rr;kc=cc;}
if (kr<0) continue;
if (!this.isSquareAttackedOn(kr, kc, opp, tb)) {
moves.push({from:{r,c}, to:{r:tr,c:tc}});
}
}
}
}
return moves;
},
_pseudoTargets(board, r, c, color) {
const targets = [];
const p = board[r][c];
const pc = p.toLowerCase();
if (pc==='p') {
const dir = color==='w'?-1:1;
const pr = r+dir;
if (this.inBounds(pr,c) && !board[pr][c]) targets.push({r:pr,c});
if ((color==='w'&&r===6)||(color==='b'&&r===1)) {
const pr2 = r+2*dir;
if (!board[pr][c] && !board[pr2][c]) targets.push({r:pr2,c});
}
for (const dc of [-1,1]) {
const pr2=r+dir, pc2=c+dc;
if (this.inBounds(pr2,pc2)) {
const t=board[pr2][pc2];
if (t && this.isEnemy(t,color)) targets.push({r:pr2,c:pc2});
}
}
} else if (pc==='n') {
for (const [dr,dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const nr=r+dr,nc=c+dc;
if (this.inBounds(nr,nc)) {
const t=board[nr][nc];
if (!t||this.isEnemy(t,color)) targets.push({r:nr,c:nc});
}
}
} else if (pc==='b'||pc==='q') {
for (const [dr,dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
let nr=r+dr,nc=c+dc;
while(this.inBounds(nr,nc)){
const t=board[nr][nc];
if(!t){targets.push({r:nr,c:nc});}else{if(this.isEnemy(t,color))targets.push({r:nr,c:nc});break;}
nr+=dr;nc+=dc;
}
}
}
if (pc==='r'||pc==='q') {
for (const [dr,dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
let nr=r+dr,nc=c+dc;
while(this.inBounds(nr,nc)){
const t=board[nr][nc];
if(!t){targets.push({r:nr,c:nc});}else{if(this.isEnemy(t,color))targets.push({r:nr,c:nc});break;}
nr+=dr;nc+=dc;
}
}
}
if (pc==='k') {
for(let dr=-1;dr<=1;dr++)for(let dc=-1;dc<=1;dc++){
if(dr===0&&dc===0)continue;
const nr=r+dr,nc=c+dc;
if(this.inBounds(nr,nc)){const t=board[nr][nc];if(!t||this.isEnemy(t,color))targets.push({r:nr,c:nc});}
}
}
return targets;
},
};
// ============================================================
// AI ENGINE — Minimax with alpha-beta pruning
// ============================================================
const AI = {
evaluate(engine) {
if (engine.gameOver) {
if (engine.result === 'white') return 100000;
if (engine.result === 'black') return -100000;
return 0; // draw
}
let score = 0;
for (let r=0;r<8;r++) for(let c=0;c<8;c++) {
const p = engine.board[r][c];
if (!p) continue;
const isW = engine.isWhite(p);
const v = CFG.pieceValue[p] || 0;
const pst = isW ? CFG.pst[p] : CFG.pstBlack[p];
const pstVal = pst ? pst[r*8+c] : 0;
score += (isW ? 1 : -1) * (v + pstVal * 0.1);
}
// Mobility bonus
const mobility = engine.legalMoves('w').length - engine.legalMoves('b').length;
score += mobility * 2;
return score;
},
orderMoves(moves, engine) {
return moves.sort((a, b) => {
const aCap = a.captured || a.enPassant ? 1 : 0;
const bCap = b.captured || b.enPassant ? 1 : 0;
if (aCap !== bCap) return bCap - aCap;
// MVV-LVA for captures
if (aCap && bCap) {
const aVal = CFG.pieceValue[a.captured || 'P'] || 0;
const bVal = CFG.pieceValue[b.captured || 'P'] || 0;
return bVal - aVal;
}
return 0;
});
},
minimax(engine, depth, alpha, beta, maximizing) {
if (depth === 0) return this.evaluate(engine);
const color = maximizing ? 'w' : 'b';
let moves = engine.legalMoves(color);
if (moves.length === 0) {
if (engine.isInCheck(color)) return maximizing ? -100000 + (depth * 10) : 100000 - (depth * 10);
return 0; // stalemate
}
moves = this.orderMoves(moves, engine);
if (maximizing) {
let maxEval = -Infinity;
for (const move of moves) {
const undo = engine.makeMove(move);
const ev = this.minimax(engine, depth - 1, alpha, beta, false);
engine.unmakeMove(move, undo);
maxEval = Math.max(maxEval, ev);
alpha = Math.max(alpha, ev);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval = Infinity;
for (const move of moves) {
const undo = engine.makeMove(move);
const ev = this.minimax(engine, depth - 1, alpha, beta, true);
engine.unmakeMove(move, undo);
minEval = Math.min(minEval, ev);
beta = Math.min(beta, ev);
if (beta <= alpha) break;
}
return minEval;
}
},
getBestMove(engine) {
const color = engine.turn;
let moves = engine.legalMoves(color);
if (moves.length === 0) return null;
moves = this.orderMoves(moves, engine);
const maximizing = color === 'w';
let bestMove = moves[0];
let bestEval = maximizing ? -Infinity : Infinity;
const depth = CFG.aiDepth;
for (const move of moves) {
const undo = engine.makeMove(move);
const ev = this.minimax(engine, depth - 1, -Infinity, Infinity, !maximizing);
engine.unmakeMove(move, undo);
if (maximizing) {
if (ev > bestEval) { bestEval = ev; bestMove = move; }
} else {
if (ev < bestEval) { bestEval = ev; bestMove = move; }
}
}
return bestMove;
},
};
// ============================================================
// SOUND ENGINE — Web Audio API
// ============================================================
const Sound = {
ctx: null,
muted: false,
init() {
try { this.ctx = new (window.AudioContext || window.webkitAudioContext)(); } catch(e) {}
},
play(freq, dur, type='sine', vol=0.15) {
if (this.muted || !this.ctx) return;
if (this.ctx.state === 'suspended') this.ctx.resume();
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.type = type;
osc.frequency.value = freq;
gain.gain.value = vol;
gain.gain.exponentialRampToValueAtTime(0.001, this.ctx.currentTime + dur);
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.start();
osc.stop(this.ctx.currentTime + dur);
},
move() { this.play(600, 0.08, 'sine', 0.12); },
capture() { this.play(300, 0.15, 'triangle', 0.18); },
check() { this.play(800, 0.2, 'square', 0.1); },
castle() { this.play(500, 0.06, 'sine', 0.1); setTimeout(()=>this.play(700, 0.06, 'sine', 0.1), 60); },
promote() { this.play(800, 0.06, 'sine', 0.12); setTimeout(()=>this.play(1000, 0.08, 'sine', 0.12), 60); },
toggle() { this.muted = !this.muted; return this.muted; },
};
// ============================================================
// UI RENDERING & INTERACTION
// ============================================================
const UI = {
boardEl: null,
selected: null, // {r, c} of selected piece
legalMoves: [], // legal moves for selected piece
lastMove: null, // {from, to} of last move
flipped: false,
dragSrc: null,
dragEl: null,
promoting: false,
promotionCallback: null,
init() {
this.boardEl = document.getElementById('board');
this.dragEl = document.getElementById('drag-ghost');
this.render();
this.bindEvents();
},
render() {
this.boardEl.innerHTML = '';
for (let ri = 0; ri < 8; ri++) {
for (let ci = 0; ci < 8; ci++) {
const r = this.flipped ? 7 - ri : ri;
const c = this.flipped ? 7 - ci : ci;
const sq = document.createElement('div');
sq.className = 'square ' + ((r + c) % 2 === 0 ? 'light' : 'dark');
sq.dataset.r = r;
sq.dataset.c = c;
const piece = Engine.board[r][c];
if (piece) {
sq.textContent = this.unicodePiece(piece);
sq.classList.add('piece');
}
// Last move highlight
if (this.lastMove) {
if ((this.lastMove.from.r===r && this.lastMove.from.c===c) ||
(this.lastMove.to.r===r && this.lastMove.to.c===c)) {
sq.classList.add('last-move');
}
}
// Check indicator
if (Engine.gameOver && Engine.resultReason === 'checkmate' && piece && piece.toLowerCase()==='k' &&
((Engine.turn==='w' && Engine.isWhite(piece)) || (Engine.turn==='b' && Engine.isBlack(piece)))) {
// Actually, check the king that's in check
}
if (!Engine.gameOver && Engine.isInCheck(Engine.turn)) {
const king = Engine.findKing(Engine.turn);
if (king && king.r===r && king.c===c) sq.classList.add('in-check');
}
// Selected
if (this.selected && this.selected.r===r && this.selected.c===c) {
sq.classList.add('selected');
}
// Move indicators
if (this.selected) {
const lm = this.legalMoves.find(m => m.to.r===r && m.to.c===c);
if (lm) {
if (lm.captured || lm.enPassant) {
const ring = document.createElement('div');
ring.className = 'capture-ring';
sq.appendChild(ring);
} else {
const dot = document.createElement('div');
dot.className = 'move-dot';
sq.appendChild(dot);
}
}
}
this.boardEl.appendChild(sq);
}
}
},
unicodePiece(p) {
const map = { 'K':'♔','Q':'♕','R':'♖','B':'♗','N':'♘','P':'♙',
'k':'♚','q':'♛','r':'♜','b':'♝','n':'♞','p':'♟' };
return map[p] || p;
},
bindEvents() {
// Click-to-move
this.boardEl.addEventListener('click', (e) => {
const sq = e.target.closest('.square');
if (!sq) return;
const r = +sq.dataset.r, c = +sq.dataset.c;
this.handleClick(r, c);
});
// Drag-and-drop (pointer events)
this.boardEl.addEventListener('pointerdown', (e) => {
const sq = e.target.closest('.square');
if (!sq) return;
const r = +sq.dataset.r, c = +sq.dataset.c;
const piece = Engine.board[r][c];
if (!piece) return;
const isWhiteTurn = Engine.turn === 'w';
const isWhitePiece = Engine.isWhite(piece);
if (isWhiteTurn !== isWhitePiece) return; // can only drag own pieces
if (Engine.gameOver) return;
if (Engine.promoting) return;
this.dragSrc = {r, c};
this.dragEl.textContent = this.unicodePiece(piece);
this.dragEl.style.display = 'block';
this.dragEl.style.left = e.clientX + 'px';
this.dragEl.style.top = e.clientY + 'px';
this.dragEl.style.fontSize = 'calc(var(--sq-size) * 0.85)';
e.preventDefault();
});
document.addEventListener('pointermove', (e) => {
if (!this.dragSrc) return;
this.dragEl.style.left = e.clientX + 'px';
this.dragEl.style.top = e.clientY + 'px';
// Highlight target square
const target = document.elementFromPoint(e.clientX, e.clientY);
this.boardEl.querySelectorAll('.square').forEach(s => s.classList.remove('drag-over'));
if (target) {
const sq = target.closest('.square');
if (sq) sq.classList.add('drag-over');
}
});
document.addEventListener('pointerup', (e) => {
if (!this.dragSrc) return;
this.dragEl.style.display = 'none';
this.boardEl.querySelectorAll('.square').forEach(s => s.classList.remove('drag-over'));
const target = document.elementFromPoint(e.clientX, e.clientY);
const sq = target ? target.closest('.square') : null;
if (sq) {
const r = +sq.dataset.r, c = +sq.dataset.c;
const move = this.legalMoves.find(m => m.to.r===r && m.to.c===c);
if (move) {
this.executeMove(move);
}
}
this.dragSrc = null;
this.legalMoves = [];
this.selected = null;
this.render();
});
// Controls
document.getElementById('btn-new').addEventListener('click', () => Game.newGame());
document.getElementById('btn-undo').addEventListener('click', () => Game.undo());
document.getElementById('btn-flip').addEventListener('click', () => Game.flipBoard());
// Difficulty
for (let d = 1; d <= 3; d++) {
document.getElementById('btn-depth' + d).addEventListener('click', () => Game.setDepth(d));
}
// Mute
document.getElementById('btn-mute').addEventListener('click', () => Game.toggleMute());
},
handleClick(r, c) {
if (Engine.gameOver || Engine.promoting) return;
if (Engine.turn !== Game.playerColor) return; // AI's turn
const piece = Engine.board[r][c];
// If a piece is already selected
if (this.selected) {
// Clicking the same piece → deselect
if (this.selected.r === r && this.selected.c === c) {
this.selected = null;
this.legalMoves = [];
this.render();
return;
}
// Try to move
const move = this.legalMoves.find(m => m.to.r===r && m.to.c===c);
if (move) {
this.executeMove(move);
return;
}
// Click on another own piece → reselect
if (piece && ((Engine.turn==='w' && Engine.isWhite(piece)) || (Engine.turn==='b' && Engine.isBlack(piece)))) {
this.selected = {r, c};
this.legalMoves = Engine.legalMoves(Engine.turn).filter(m => m.from.r===r && m.from.c===c);
this.render();
return;
}
// Click on empty or enemy square without a valid move → deselect
this.selected = null;
this.legalMoves = [];
this.render();
return;
}
// No piece selected — select this one if it's ours
if (piece && ((Engine.turn==='w' && Engine.isWhite(piece)) || (Engine.turn==='b' && Engine.isBlack(piece)))) {
this.selected = {r, c};
this.legalMoves = Engine.legalMoves(Engine.turn).filter(m => m.from.r===r && m.from.c===c);
this.render();
}
},
executeMove(move) {
// Handle promotion
if (move.piece.toLowerCase() === 'p' && (move.to.r === 0 || move.to.r === 7)) {
if (Engine.turn === Game.playerColor) {
this.showPromotionPicker(move);
return;
} else {
move.promoPiece = 'Q'; // AI always promotes to queen
}
}
this.doMove(move);
},
doMove(move) {
const undo = Engine.makeMove(move);
this.lastMove = {from: {...move.from}, to: {...move.to}};
Sound.move();
if (move.captured || move.enPassant) Sound.capture();
if (move.isCastling) Sound.castle();
if (move.promoPiece) Sound.promote();
// Check if opponent is in check after move
const opp = Engine.turn === 'w' ? 'b' : 'w';
if (Engine.isInCheck(opp)) {
// Will be shown on board
const oppKing = Engine.findKing(opp);
if (oppKing) Sound.check();
}
this.selected = null;
this.legalMoves = [];
this.render();
this.updateSidebar();
Game.saveState();
// Check for game over
if (Engine.gameOver) {
this.showGameOver();
return;
}
// AI move
if (Game.aiEnabled && Engine.turn === Game.aiColor) {
Game.aiThinking = true;
this.updateStatus('AI thinking...');
setTimeout(() => {
Game.makeAIMove();
Game.aiThinking = false;
}, 50);
}
},
showPromotionPicker(move) {
Engine.promoting = true;
const overlay = document.getElementById('promotion-overlay');
const sq = this.boardEl.querySelector(`[data-r="${move.to.r}"][data-c="${move.to.c}"]`);
if (!sq) return;
const rect = sq.getBoundingClientRect();
const container = document.getElementById('board-container');
const containerRect = container.getBoundingClientRect();
overlay.style.left = (rect.left - containerRect.left) + 'px';
overlay.style.top = (rect.top - containerRect.top) + 'px';
overlay.style.width = rect.width + 'px';
overlay.style.height = rect.height + 'px';
overlay.innerHTML = '';
const pieces = Engine.turn === 'w' ? ['Q','R','B','N'] : ['q','r','b','n'];
const labels = ['♕','♖','♗','♘'];
for (let i = 0; i < 4; i++) {
const btn = document.createElement('button');
btn.className = 'promo-btn';
btn.textContent = labels[i];
btn.addEventListener('click', () => {
move.promoPiece = pieces[i];
overlay.classList.remove('visible');
Engine.promoting = false;
this.doMove(move);
});
overlay.appendChild(btn);
}
overlay.classList.add('visible');
},
showGameOver() {
const banner = document.getElementById('game-over');
let text = '';
if (Engine.result === 'draw') {
text = 'Draw — ' + Engine.resultReason;
} else {
text = (Engine.result === 'white' ? 'White' : 'Black') + ' wins by ' + Engine.resultReason;
}
banner.innerHTML = '<div>' + text + '</div><button onclick="Game.newGame()">New Game</button>';
banner.classList.add('visible');
},
hideGameOver() {
document.getElementById('game-over').classList.remove('visible');
},
updateSidebar() {
// Status
const statusEl = document.getElementById('status');
if (Engine.gameOver) {
statusEl.textContent = Engine.result === 'draw' ? 'Draw — ' + Engine.resultReason :
(Engine.result === 'white' ? 'White wins!' : 'Black wins!');
statusEl.className = '';
} else {
const turnName = Engine.turn === 'w' ? 'White' : 'Black';
const inCheck = Engine.isInCheck(Engine.turn);
statusEl.textContent = turnName + ' to move' + (inCheck ? ' (Check!)' : '');
statusEl.className = Engine.turn === 'w' ? 'wt' : 'bt';
}
// Captured pieces
const capEl = document.getElementById('captured');
capEl.innerHTML = '';
const allCaptured = [...Engine.captured.b].sort().reverse().join('') + ' ' + [...Engine.captured.w].sort().join('');
capEl.textContent = allCaptured || '—';
// Move list
const listEl = document.getElementById('move-list');
let html = '';
for (let i = 0; i < Engine.moveHistory.length; i += 2) {
const num = Math.floor(i / 2) + 1;
const whiteMove = Engine.moveHistory[i] ? Engine.moveHistory[i].san : '';
const blackMove = Engine.moveHistory[i+1] ? Engine.moveHistory[i+1].san : '';
html += num + '. ' + whiteMove + (blackMove ? ' ' + blackMove : '') + '\n';
}
listEl.textContent = html;
listEl.scrollTop = listEl.scrollHeight;
},
updateStatus(text) {
document.getElementById('status').textContent = text;
},
};
// ============================================================
// GAME CONTROLLER
// ============================================================
const Game = {
playerColor: 'w',
aiColor: 'b',
aiEnabled: true,
aiThinking: false,
flipState: false,
init() {
Sound.init();
Engine.init();
UI.init();
this.tryLoadState();
UI.updateSidebar();
UI.render();
// Debug interface
window.game = {
fen: () => Engine.toFEN(),
move: (longAlg) => {
const fr = Engine.algToRC(longAlg.substring(0,2));
const tr = Engine.algToRC(longAlg.substring(2,4));
const moves = Engine.legalMoves(Engine.turn);
const move = moves.find(m => m.from.r===fr.r && m.from.c===fr.c && m.to.r===tr.r && m.to.c===tr.c);
if (!move) return null;
// Handle promotion
if (move.piece.toLowerCase() === 'p' && (move.to.r === 0 || move.to.r === 7)) {
move.promoPiece = 'Q';
}
const undo = Engine.makeMove(move);
UI.lastMove = {from: {...move.from}, to: {...move.to}};
UI.render();
UI.updateSidebar();
this.saveState();
return move;
},
legalMoves: () => Engine.legalMoves(Engine.turn),
board: () => Engine.board,
};
},
newGame() {
Engine.init();
UI.selected = null;
UI.legalMoves = [];
UI.lastMove = null;
UI.promoting = false;
UI.hideGameOver();
UI.render();
UI.updateSidebar();
this.saveState();
// If AI plays white, AI goes first
if (this.aiEnabled && this.aiColor === 'w') {
this.makeAIMove();
}
},
undo() {
if (Engine.moveHistory.length === 0) return;
// Undo AI move + player move (2 moves)
for (let i = 0; i < 2 && Engine.moveHistory.length > 0; i++) {
const lastMove = Engine.moveHistory[Engine.moveHistory.length - 1];
const undo = Engine.unmakeMove(lastMove);
// We need to properly unmake — but unmakeMove expects the undo info
// Actually, let me fix this: I need to store undo info properly
}
// Simpler approach: reload from saved state
this.tryLoadState();
UI.render();
UI.updateSidebar();
},
flipBoard() {
UI.flipped = !UI.flipped;
UI.render();
},
setDepth(d) {
CFG.aiDepth = d;
document.querySelectorAll('[id^="btn-depth"]').forEach(b => b.classList.remove('active'));
document.getElementById('btn-depth' + d).classList.add('active');
},
toggleMute() {
const muted = Sound.toggle();
document.getElementById('btn-mute').textContent = muted ? '🔇' : '🔊';
},
makeAIMove() {
if (Engine.gameOver || Engine.turn !== this.aiColor) return;
const move = AI.getBestMove(Engine);
if (!move) return;
// AI always promotes to Queen
if (move.piece.toLowerCase() === 'p' && (move.to.r === 0 || move.to.r === 7)) {
move.promoPiece = Engine.turn === 'w' ? 'Q' : 'q';
}
UI.lastMove = {from: {...move.from}, to: {...move.to}};
const undo = Engine.makeMove(move);
Sound.move();
if (move.captured || move.enPassant) Sound.capture();
if (move.isCastling) Sound.castle();
if (move.promoPiece) Sound.promote();
UI.selected = null;
UI.legalMoves = [];
UI.render();
UI.updateSidebar();
this.saveState();
if (Engine.gameOver) {
UI.showGameOver();
return;
}
},
saveState() {
try {
const state = {
fen: Engine.toFEN(),
playerColor: this.playerColor,
flipped: UI.flipped,
aiDepth: CFG.aiDepth,
};
localStorage.setItem('chess_save', JSON.stringify(state));
} catch(e) {}
},
tryLoadState() {
try {
const raw = localStorage.getItem('chess_save');
if (!raw) return false;
const state = JSON.parse(raw);
Engine.fromFEN(state.fen);
this.playerColor = state.playerColor || 'w';
UI.flipped = state.flipped || false;
CFG.aiDepth = state.aiDepth || 3;
return true;
} catch(e) { return false; }
},
};
// ============================================================
// INITIALIZATION
// ============================================================
document.addEventListener('DOMContentLoaded', () => {
Game.init();
});
</script>
</body>
</html>