Chess
Qwen 3.6 27b MTP · composer

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 60,327 bytes · SHA-256 96c981d5a237
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — Play Against AI</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body { height: 100%; overflow: hidden; }
body {
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
background: #2c2c34;
color: #e8e6e3;
}
#app { width: 100%; height: 100%; }
#game-container {
display: flex;
justify-content: center;
align-items: stretch;
gap: 24px;
padding: 16px;
height: 100vh;
max-width: 1200px;
margin: 0 auto;
}
#board-area {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
#side-panel {
width: 260px;
min-width: 220px;
display: flex;
flex-direction: column;
gap: 12px;
padding: 8px;
overflow-y: auto;
}
#status-bar {
background: #3a3a45;
border-radius: 8px;
padding: 10px 14px;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 14px;
}
#turn-indicator {
font-weight: 600;
color: #f0e6d3;
}
#game-status {
color: #ff9800;
font-weight: 500;
}
#controls {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
#controls button, #btn-new-game-overlay {
background: #4a4a58;
color: #e8e6e3;
border: 1px solid #5a5a68;
border-radius: 6px;
padding: 8px 12px;
cursor: pointer;
font-size: 13px;
transition: background 0.15s;
}
#controls button:hover, #btn-new-game-overlay:hover {
background: #5a5a68;
}
#difficulty, #side-choice {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
#difficulty select, #side-choice select {
background: #4a4a58;
color: #e8e6e3;
border: 1px solid #5a5a68;
border-radius: 4px;
padding: 4px 8px;
font-size: 12px;
}
#move-list-container {
flex: 1;
background: #3a3a45;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
min-height: 0;
}
#move-list-container h3 {
padding: 8px 12px;
font-size: 13px;
color: #b0a89a;
border-bottom: 1px solid #4a4a58;
}
#move-list {
flex: 1;
overflow-y: auto;
padding: 6px 10px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.7;
}
.move-row {
display: flex;
gap: 4px;
}
.move-num {
color: #808090;
min-width: 28px;
text-align: right;
}
.move-white, .move-black {
min-width: 56px;
cursor: default;
}
.move-white:hover, .move-black:hover {
background: #4a4a58;
border-radius: 2px;
}
.hidden { display: none !important; }
/* ── css: board ── */
#board {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
width: min(calc((100vh - 60px) * 0.9), calc((1200px - 300px) * 0.55));
height: min(calc((100vh - 60px) * 0.9), calc((1200px - 300px) * 0.55));
max-width: 560px;
max-height: 560px;
border: 3px solid #5a4a3a;
border-radius: 4px;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
position: relative;
}
.square {
display: flex;
justify-content: center;
align-items: center;
font-size: min(calc((100vh - 60px) * 0.9 / 8 * 0.7), calc((1200px - 300px) * 0.55 / 8 * 0.7));
cursor: pointer;
user-select: none;
position: relative;
transition: background-color 0.1s;
}
.square.light { background: #f0d9b5; }
.square.dark { background: #b58863; }
.square.selected { background: #7fc97f !important; }
.square.last-move { background: rgba(255, 255, 100, 0.4) !important; }
.square.check-glow {
background: radial-gradient(circle, #ff0000 0%, #cc0000 30%, transparent 70%) !important;
}
.piece {
line-height: 1;
pointer-events: none;
filter: drop-shadow(1px 1px 1px rgba(0,0,0,0.3));
}
.piece.white-piece { color: #fff; text-shadow: 0 0 2px rgba(0,0,0,0.8); }
.piece.black-piece { color: #222; text-shadow: 0 0 1px rgba(255,255,255,0.3); }
/* Move indicators */
.move-dot::after {
content: '';
position: absolute;
width: 28%;
height: 28%;
border-radius: 50%;
background: rgba(0, 0, 0, 0.2);
pointer-events: none;
}
.capture-ring::after {
content: '';
position: absolute;
width: 90%;
height: 90%;
border-radius: 50%;
border: 4px solid rgba(0, 0, 0, 0.2);
pointer-events: none;
}
/* Coordinate labels */
.coord-label {
position: absolute;
font-size: 10px;
font-weight: 700;
pointer-events: none;
opacity: 0.6;
}
.file-label { bottom: 1px; right: 3px; }
.rank-label { top: 1px; left: 3px; }
.square.light .coord-label { color: #b58863; }
.square.dark .coord-label { color: #f0d9b5; }
/* Captured pieces */
#captured-top, #captured-bottom {
min-height: 28px;
display: flex;
align-items: center;
font-size: 18px;
gap: 1px;
padding: 0 4px;
width: 100%;
}
/* Promotion overlay */
#promotion-overlay {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.6);
display: flex;
justify-content: center;
align-items: center;
z-index: 100;
}
#promotion-picker {
background: #3a3a45;
border-radius: 12px;
padding: 16px;
display: flex;
gap: 8px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
.promo-piece {
width: 64px;
height: 64px;
display: flex;
justify-content: center;
align-items: center;
font-size: 42px;
background: #f0d9b5;
border-radius: 8px;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.1s;
}
.promo-piece:hover {
transform: scale(1.1);
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
}
/* Game over banner */
#game-over-banner {
position: fixed;
top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.7);
display: flex;
justify-content: center;
align-items: center;
z-index: 200;
}
#game-over-content {
background: #3a3a45;
border-radius: 16px;
padding: 32px 48px;
text-align: center;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
#game-over-title { font-size: 28px; margin-bottom: 8px; }
#game-over-detail { color: #b0a89a; margin-bottom: 20px; }
/* Dragging */
.square.drag-over { background: rgba(100, 200, 100, 0.5) !important; }
</style>
</head>
<body>
<div id="game-container">
<div id="board-area">
<div id="captured-top"></div>
<div id="board"></div>
<div id="captured-bottom"></div>
</div>
<div id="side-panel">
<div id="status-bar"><span id="turn-indicator"></span><span id="game-status"></span></div>
<div id="controls">
<button id="btn-new-game" title="New Game">⟳ New</button>
<button id="btn-undo" title="Undo (both moves)">↩ Undo</button>
<button id="btn-flip" title="Flip Board">⇅ Flip</button>
<button id="btn-mute" title="Toggle Sound">🔊</button>
</div>
<div id="difficulty">
<label for="depth-select">AI Depth:</label>
<select id="depth-select">
<option value="1">1 — Easy</option>
<option value="2" selected="">2 — Medium</option>
<option value="3">3 — Hard</option>
</select>
</div>
<div id="side-choice">
<label>Play as:</label>
<select id="color-select">
<option value="white" selected="">White</option>
<option value="black">Black</option>
<option value="both">Both (Analysis)</option>
</select>
</div>
<div id="move-list-container">
<h3>Moves</h3>
<div id="move-list"></div>
</div>
</div>
</div>
<div id="promotion-overlay" class="hidden">
<div id="promotion-picker"></div>
</div>
<div id="game-over-banner" class="hidden">
<div id="game-over-content">
<h2 id="game-over-title"></h2>
<p id="game-over-detail"></p>
<button id="btn-new-game-overlay">New Game</button>
</div>
</div>
<script>
'use strict';
// ── const: CFG ──
const CFG = {
lightSquare: '#f0d9b5',
darkSquare: '#b58863',
selectedColor: '#7fc97f',
lastMoveColor: 'rgba(255,255,100,0.4)',
aiDepths: { 1: 1, 2: 2, 3: 3 },
maxSearchTime: 800,
soundVolume: 0.3,
};
// ── const: EMPTY ──
const EMPTY = 0;
// ── const: WP ──
const WP = 1;
// ── const: WN ──
const WN = 2;
// ── const: WB ──
const WB = 3;
// ── const: WR ──
const WR = 4;
// ── const: WQ ──
const WQ = 5;
// ── const: WK ──
const WK = 6;
// ── const: BP ──
const BP = -1;
// ── const: BN ──
const BN = -2;
// ── const: BB ──
const BB = -3;
// ── const: BR ──
const BR = -4;
// ── const: BQ ──
const BQ = -5;
// ── const: BK ──
const BK = -6;
// ── const: PAWN ──
const PAWN = 1;
// ── const: KNIGHT ──
const KNIGHT = 2;
// ── const: BISHOP ──
const BISHOP = 3;
// ── const: ROOK ──
const ROOK = 4;
// ── const: QUEEN ──
const QUEEN = 5;
// ── const: KING ──
const KING = 6;
// ── const: PIECE_UNICODE ──
const PIECE_UNICODE = {
[WK]: '♔', [WN]: '♘', [WB]: '♗', [WR]: '♖', [WQ]: '♕', [WP]: '♙',
[BK]: '♚', [BN]: '♞', [BB]: '♝', [BR]: '♜', [BQ]: '♛', [BP]: '♟',
};
// ── const: PIECE_LETTER ──
const PIECE_LETTER = {
[KNIGHT]: 'N', [BISHOP]: 'B', [ROOK]: 'R', [QUEEN]: 'Q', [KING]: 'K',
};
// ── const: PIECE_VALUE ──
const PIECE_VALUE = {
[PAWN]: 100, [KNIGHT]: 320, [BISHOP]: 330, [ROOK]: 500, [QUEEN]: 900, [KING]: 20000,
};
// ── const: FILES ──
const FILES = 'abcdefgh';
// ── const: RANKS ──
const RANKS = '12345678';
// ── const: PST ──
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],
[KNIGHT]: [-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],
[BISHOP]: [-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],
[ROOK]: [0,0,0,0,0,0,0,0,5,10,10,10,10,10,10,5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,-5,0,0,0,0,0,0,-5,0,0,0,5,5,0,0,0],
[QUEEN]: [-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],
[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],
};
// ── const: INITIAL_CASTLING ──
const INITIAL_CASTLING = { K: true, Q: true, k: true, q: true };
// ── function: PIECE_LETTERInverse ──
function PIECE_LETTERInverse(letter) {
const map = { 'Q': QUEEN, 'R': ROOK, 'B': BISHOP, 'N': KNIGHT };
return map[letter] || QUEEN;
}
// ── class: ChessAI ──
// ============================================================
// AI ENGINE — Minimax with Alpha-Beta Pruning
// ============================================================
class ChessAI {
constructor(game) {
this.game = game;
this.nodesSearched = 0;
this.startTime = 0;
this.maxTime = CFG.maxSearchTime;
}
// Evaluate the current position from White's perspective
evaluate() {
let score = 0;
const board = this.game.board;
for (let i = 0; i < 64; i++) {
const p = board[i];
if (!p) continue;
const t = Math.abs(p);
const isWhite = p > 0;
const [r, c] = this.game.rc(i);
// Material
score += PIECE_VALUE[t];
// Piece-square tables (mirrored for black)
let pstIdx;
if (isWhite) {
pstIdx = r * 8 + c;
} else {
pstIdx = (7 - r) * 8 + c;
}
score += PST[t][pstIdx];
}
// Mobility bonus (simplified)
const wMoves = this.game.generatePseudoMoves('w').length;
const bMoves = this.game.generatePseudoMoves('b').length;
score += (wMoves - bMoves) * 3;
return score;
}
// Order moves for better alpha-beta pruning
orderMoves(moves) {
return [...moves].sort((a, b) => this.moveScore(b) - this.moveScore(a));
}
moveScore(move) {
let score = 0;
const captured = this.game.board[move.to];
if (captured) {
// MVV-LVA: value of victim minus value of attacker
score += 10 * PIECE_VALUE[Math.abs(captured)] - PIECE_VALUE[Math.abs(this.game.board[move.from])];
}
if (move.enPassant) score += 10 * PIECE_VALUE[PAWN];
if (move.promotion) score += PIECE_VALUE[Math.abs(move.promotion)];
// Prefer center moves slightly
const [fr, fc] = this.game.rc(move.from);
const [tr, tc] = this.game.rc(move.to);
const centerDist = Math.abs(tr - 3.5) + Math.abs(tc - 3.5);
score += (7 - centerDist) * 2;
return score;
}
// Minimax with alpha-beta pruning and quiescence search
minimax(depth, alpha, beta, isMaximizing) {
this.nodesSearched++;
// Time check
if (Date.now() - this.startTime > this.maxTime) {
return null; // Signal timeout
}
if (depth === 0) {
return this.quiescence(alpha, beta, isMaximizing);
}
const moves = this.game.generateLegalMoves(isMaximizing ? 'w' : 'b');
if (moves.length === 0) {
if (this.game.isInCheck(isMaximizing ? 'w' : 'b')) {
return isMaximizing ? -99999 + (3 - depth) : 99999 - (3 - depth); // Checkmate, prefer quicker mates
}
return 0; // Stalemate
}
const ordered = this.orderMoves(moves);
if (isMaximizing) {
let maxEval = -Infinity;
for (const move of ordered) {
const notation = this.game.makeMove(move);
const eval_ = this.minimax(depth - 1, alpha, beta, false);
this.game.undoMove();
if (eval_ === null) return null; // Timeout propagated
maxEval = Math.max(maxEval, eval_);
alpha = Math.max(alpha, eval_);
if (beta <= alpha) break;
}
return maxEval;
} else {
let minEval = Infinity;
for (const move of ordered) {
const notation = this.game.makeMove(move);
const eval_ = this.minimax(depth - 1, alpha, beta, true);
this.game.undoMove();
if (eval_ === null) return null; // Timeout propagated
minEval = Math.min(minEval, eval_);
beta = Math.min(beta, eval_);
if (beta <= alpha) break;
}
return minEval;
}
}
// Quiescence search to avoid horizon effect
quiescence(alpha, beta, isMaximizing) {
const standPat = this.evaluate();
if (isMaximizing) {
if (standPat >= beta) return beta;
if (alpha < standPat) alpha = standPat;
} else {
if (standPat <= alpha) return alpha;
if (beta > standPat) beta = standPat;
}
const color = isMaximizing ? 'w' : 'b';
// Only consider captures and promotions for quiescence
const moves = this.game.generateLegalMoves(color).filter(m => {
return this.game.board[m.to] || m.enPassant || m.promotion;
});
const ordered = this.orderMoves(moves);
if (isMaximizing) {
for (const move of ordered) {
this.game.makeMove(move);
const eval_ = this.quiescence(alpha, beta, false);
this.game.undoMove();
if (eval_ === null) return standPat; // Timeout
if (eval_ >= beta) return beta;
if (eval_ > alpha) alpha = eval_;
}
return alpha;
} else {
for (const move of ordered) {
this.game.makeMove(move);
const eval_ = this.quiescence(alpha, beta, true);
this.game.undoMove();
if (eval_ === null) return standPat; // Timeout
if (eval_ <= alpha) return alpha;
if (eval_ < beta) beta = eval_;
}
return beta;
}
}
// Find the best move at given depth, returns {move, score} or null on timeout
findBestMove(depth) {
this.nodesSearched = 0;
this.startTime = Date.now();
const color = this.game.turn;
const isMaximizing = color === 'w';
const moves = this.game.generateLegalMoves(color);
if (moves.length === 0) return null;
let bestMove = null;
let bestScore = isMaximizing ? -Infinity : Infinity;
let alpha = -Infinity;
let beta = Infinity;
// Iterative deepening for time control
const ordered = this.orderMoves(moves);
for (const move of ordered) {
this.game.makeMove(move);
const score = this.minimax(depth - 1, alpha, beta, !isMaximizing);
this.game.undoMove();
if (score === null) break; // Timeout
if (isMaximizing) {
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
alpha = Math.max(alpha, score);
} else {
if (score < bestScore) {
bestScore = score;
bestMove = move;
}
beta = Math.min(beta, score);
}
}
return bestMove ? { move: bestMove, score: bestScore, nodes: this.nodesSearched } : null;
}
}
// ── class: SoundEngine ──
// ============================================================
// SOUND ENGINE — Web Audio API
// ============================================================
class SoundEngine {
constructor() {
this.ctx = null;
this.muted = false;
this.initialized = false;
}
init() {
if (this.initialized) return;
try {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.initialized = true;
} catch(e) {
console.warn('Web Audio not available');
}
}
play(type) {
if (!this.ctx || this.muted) return;
if (this.ctx.state === 'suspended') this.ctx.resume();
const now = this.ctx.currentTime;
const vol = CFG.soundVolume;
switch(type) {
case 'move': this._playMove(now, vol); break;
case 'capture': this._playCapture(now, vol); break;
case 'check': this._playCheck(now, vol); break;
case 'castle': this._playCastle(now, vol); break;
}
}
_playMove(t, v) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(600, t);
osc.frequency.exponentialRampToValueAtTime(400, t + 0.08);
gain.gain.setValueAtTime(v * 0.5, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.1);
osc.start(t);
osc.stop(t + 0.1);
}
_playCapture(t, v) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'sawtooth';
osc.frequency.setValueAtTime(300, t);
osc.frequency.exponentialRampToValueAtTime(150, t + 0.12);
gain.gain.setValueAtTime(v * 0.4, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.15);
osc.start(t);
osc.stop(t + 0.15);
}
_playCheck(t, v) {
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'square';
osc.frequency.setValueAtTime(800, t);
osc.frequency.setValueAtTime(600, t + 0.1);
gain.gain.setValueAtTime(v * 0.3, t);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.25);
osc.start(t);
osc.stop(t + 0.25);
}
_playCastle(t, v) {
this._playMove(t, v);
const osc = this.ctx.createOscillator();
const gain = this.ctx.createGain();
osc.connect(gain);
gain.connect(this.ctx.destination);
osc.type = 'sine';
osc.frequency.setValueAtTime(500, t + 0.06);
osc.frequency.exponentialRampToValueAtTime(350, t + 0.14);
gain.gain.setValueAtTime(v * 0.4, t + 0.06);
gain.gain.exponentialRampToValueAtTime(0.001, t + 0.18);
osc.start(t + 0.06);
osc.stop(t + 0.18);
}
toggleMute() {
this.muted = !this.muted;
return this.muted;
}
}
// ── class: ChessUI ──
// ============================================================
// UI RENDERER — Board display, interaction, move list
// ============================================================
class ChessUI {
constructor(game) {
this.game = game;
this.selectedSquare = null;
this.legalMovesForSelected = [];
this.flipped = false;
this.dragging = null; // {from, piece}
this.promotionPending = null; // {from, to, moves} waiting for user choice
}
render() {
const boardEl = document.getElementById('board');
boardEl.innerHTML = '';
const inCheck = this.game.isInCheck(this.game.turn);
const kingPos = inCheck ? this.game.findKing(this.game.turn) : -1;
for (let displayRow = 0; displayRow < 8; displayRow++) {
for (let displayCol = 0; displayCol < 8; displayCol++) {
const r = this.flipped ? 7 - displayRow : displayRow;
const c = this.flipped ? 7 - displayCol : displayCol;
const idx = r * 8 + c;
const piece = this.game.board[idx];
const sq = document.createElement('div');
sq.className = 'square';
sq.dataset.idx = idx;
sq.dataset.row = r;
sq.dataset.col = c;
// Square color
if ((r + c) % 2 === 0) sq.classList.add('light');
else sq.classList.add('dark');
// Last move highlight
if (this.game.lastMove) {
if (idx === this.game.lastMove.from || idx === this.game.lastMove.to) {
sq.classList.add('last-move');
}
}
// Selected square
if (this.selectedSquare === idx) {
sq.classList.add('selected');
}
// Check glow on king
if (idx === kingPos) {
sq.classList.add('check-glow');
}
// Legal move indicators
const legalMove = this.legalMovesForSelected.find(m => m.to === idx);
if (legalMove) {
if (piece || legalMove.enPassant) {
sq.classList.add('capture-ring');
} else {
sq.classList.add('move-dot');
}
}
// Piece rendering
if (piece) {
const pieceEl = document.createElement('span');
pieceEl.className = 'piece';
pieceEl.textContent = PIECE_UNICODE[piece];
if (piece > 0) pieceEl.classList.add('white-piece');
else pieceEl.classList.add('black-piece');
sq.appendChild(pieceEl);
}
// Coordinate labels
if (displayCol === 0) {
const label = document.createElement('span');
label.className = 'coord-label rank-label';
label.textContent = RANKS[r];
sq.appendChild(label);
}
if (displayRow === 7) {
const label = document.createElement('span');
label.className = 'coord-label file-label';
label.textContent = FILES[c];
sq.appendChild(label);
}
boardEl.appendChild(sq);
}
}
this.renderCaptured();
this.renderMoveList();
this.renderStatus();
}
renderCaptured() {
const topEl = document.getElementById('captured-top');
const bottomEl = document.getElementById('captured-bottom');
// Top shows pieces captured by the side at the bottom of the board
// Bottom shows pieces captured by the side at the top of the board
if (this.flipped) {
this._renderCapturedRow(topEl, 'w', true);
this._renderCapturedRow(bottomEl, 'b', false);
} else {
this._renderCapturedRow(topEl, 'b', true);
this._renderCapturedRow(bottomEl, 'w', false);
}
}
_renderCapturedRow(el, color, isTop) {
const pieces = this.game.capturedPieces[color];
// Sort by value: Q > R > B/N > P
const order = [QUEEN, ROOK, BISHOP, KNIGHT, PAWN];
const sorted = [...pieces].sort((a, b) => order.indexOf(a) - order.indexOf(b));
el.innerHTML = '';
for (const pt of sorted) {
const span = document.createElement('span');
span.className = 'piece';
// Show the captured piece in its original color
const pieceVal = isTop ? (-pt) : pt;
span.textContent = PIECE_UNICODE[pieceVal];
if (isTop) span.classList.add('black-piece');
else span.classList.add('white-piece');
el.appendChild(span);
}
}
renderMoveList() {
const listEl = document.getElementById('move-list');
listEl.innerHTML = '';
for (let i = 0; i < this.game.moveHistory.length; i += 2) {
const moveNum = Math.floor(i / 2) + 1;
const row = document.createElement('div');
row.className = 'move-row';
const numSpan = document.createElement('span');
numSpan.className = 'move-num';
numSpan.textContent = moveNum + '.';
row.appendChild(numSpan);
const whiteMove = this.game.moveHistory[i];
if (whiteMove) {
const wSpan = document.createElement('span');
wSpan.className = 'move-white';
wSpan.textContent = whiteMove.notation;
row.appendChild(wSpan);
}
const blackMove = this.game.moveHistory[i + 1];
if (blackMove) {
const bSpan = document.createElement('span');
bSpan.className = 'move-black';
bSpan.textContent = blackMove.notation;
row.appendChild(bSpan);
}
listEl.appendChild(row);
}
// Auto-scroll to bottom
listEl.scrollTop = listEl.scrollHeight;
}
renderStatus() {
const turnEl = document.getElementById('turn-indicator');
const statusEl = document.getElementById('game-status');
if (this.game.gameOver) {
let resultText = '';
switch(this.game.result) {
case 'checkmate':
const winner = this.game.turn === 'w' ? 'Black' : 'White';
resultText = `Checkmate! ${winner} wins.`;
break;
case 'stalemate':
resultText = 'Stalemate — Draw.';
break;
case 'draw-50move':
resultText = 'Draw by 50-move rule.';
break;
case 'draw-material':
resultText = 'Draw — Insufficient material.';
break;
}
turnEl.textContent = '';
statusEl.textContent = resultText;
} else {
const turnName = this.game.turn === 'w' ? 'White' : 'Black';
turnEl.textContent = `${turnName}'s turn`;
if (this.game.isInCheck(this.game.turn)) {
statusEl.textContent = '⚠ Check!';
} else {
statusEl.textContent = '';
}
}
}
// ---- Interaction handlers ----
handleSquareClick(idx) {
if (this.game.gameOver || this.promotionPending) return;
const piece = this.game.board[idx];
// If we have a selected square and click on a legal move target
if (this.selectedSquare !== null) {
const move = this.legalMovesForSelected.find(m => m.to === idx);
if (move) {
this.executeMove(move);
return;
}
}
// Select a piece of the current side
if (piece && this.game.colorOf(piece) === this.game.turn) {
this.selectedSquare = idx;
this.legalMovesForSelected = this.game.generateLegalMoves(this.game.turn).filter(m => m.from === idx);
this.render();
} else {
// Deselect
this.selectedSquare = null;
this.legalMovesForSelected = [];
this.render();
}
}
handleDragStart(idx) {
if (this.game.gameOver || this.promotionPending) return;
const piece = this.game.board[idx];
if (!piece || this.game.colorOf(piece) !== this.game.turn) return;
this.dragging = { from: idx, piece };
this.selectedSquare = idx;
this.legalMovesForSelected = this.game.generateLegalMoves(this.game.turn).filter(m => m.from === idx);
this.render();
}
handleDragOver(idx) {
if (!this.dragging) return;
const sqEl = document.querySelector(`.square[data-idx="${idx}"]`);
if (sqEl) {
const isLegal = this.legalMovesForSelected.some(m => m.to === idx);
if (isLegal) sqEl.classList.add('drag-over');
else sqEl.classList.remove('drag-over');
}
}
handleDragEnd() {
// Clear drag highlights
document.querySelectorAll('.square.drag-over').forEach(el => el.classList.remove('drag-over'));
this.dragging = null;
}
handleDrop(idx) {
if (!this.dragging) return;
const move = this.legalMovesForSelected.find(m => m.to === idx);
if (move) {
this.executeMove(move);
}
this.handleDragEnd();
}
executeMove(move) {
// Check for promotion — need user input
const piece = this.game.board[move.from];
const [tr] = this.game.rc(move.to);
const isPromotion = Math.abs(piece) === PAWN && (tr === 0 || tr === 7);
if (isPromotion && move.promotion) {
// If it's the player's turn and we need to pick promotion piece
const playerColor = this.getPlayerColor();
if (this.game.turn === playerColor || playerColor === 'both') {
this.showPromotionPicker(move);
return;
}
}
this.selectedSquare = null;
this.legalMovesForSelected = [];
const notation = this.game.makeMove(move);
this.playSoundForMove(move, notation);
this.render();
this.saveState();
// Check game over
if (this.game.gameOver) {
this.showGameOver();
return;
}
// Trigger AI move if needed
const playerColor = this.getPlayerColor();
if (this.game.turn !== playerColor && playerColor !== 'both') {
setTimeout(() => this.triggerAIMove(), 100);
}
}
showPromotionPicker(baseMove) {
const overlay = document.getElementById('promotion-overlay');
const picker = document.getElementById('promotion-picker');
picker.innerHTML = '';
const color = this.game.turn;
const promoPieces = [QUEEN, ROOK, BISHOP, KNIGHT];
for (const pt of promoPieces) {
const pieceVal = color === 'w' ? pt : -pt;
const btn = document.createElement('div');
btn.className = 'promo-piece';
btn.textContent = PIECE_UNICODE[pieceVal];
if (color === 'w') btn.classList.add('white-piece');
else btn.classList.add('black-piece');
btn.addEventListener('click', () => {
// Find the matching move with this promotion type
const allMoves = this.game.generateLegalMoves(this.game.turn);
const promoMove = allMoves.find(m => m.from === baseMove.from && m.to === baseMove.to && Math.abs(m.promotion) === pt);
if (promoMove) {
overlay.classList.add('hidden');
this.selectedSquare = null;
this.legalMovesForSelected = [];
const notation = this.game.makeMove(promoMove);
this.playSoundForMove(promoMove, notation);
this.render();
this.saveState();
if (this.game.gameOver) {
this.showGameOver();
return;
}
const playerColor = this.getPlayerColor();
if (this.game.turn !== playerColor && playerColor !== 'both') {
setTimeout(() => this.triggerAIMove(), 100);
}
}
});
picker.appendChild(btn);
}
overlay.classList.remove('hidden');
}
playSoundForMove(move, notation) {
const sound = window._soundEngine;
if (!sound) return;
if (move.castle) {
sound.play('castle');
} else if (this.game.board[move.to] || move.enPassant) {
sound.play('capture');
} else {
sound.play('move');
}
// Check sound
const enemy = this.game.turn === 'w' ? 'b' : 'w';
if (this.game.isInCheck(enemy)) {
setTimeout(() => sound.play('check'), 150);
}
}
showGameOver() {
const banner = document.getElementById('game-over-banner');
const titleEl = document.getElementById('game-over-title');
const detailEl = document.getElementById('game-over-detail');
switch(this.game.result) {
case 'checkmate':
const winner = this.game.turn === 'w' ? 'Black' : 'White';
titleEl.textContent = `${winner} Wins!`;
detailEl.textContent = 'Checkmate';
break;
case 'stalemate':
titleEl.textContent = 'Draw';
detailEl.textContent = 'Stalemate — no legal moves, king not in check.';
break;
case 'draw-50move':
titleEl.textContent = 'Draw';
detailEl.textContent = '50-move rule — no captures or pawn moves for 100 half-moves.';
break;
case 'draw-material':
titleEl.textContent = 'Draw';
detailEl.textContent = 'Insufficient material to deliver checkmate.';
break;
}
banner.classList.remove('hidden');
}
triggerAIMove() {
const depthSelect = document.getElementById('depth-select');
const depth = parseInt(depthSelect.value);
const ai = new ChessAI(this.game);
const result = ai.findBestMove(depth);
if (result && result.move) {
this.selectedSquare = null;
this.legalMovesForSelected = [];
const notation = this.game.makeMove(result.move);
this.playSoundForMove(result.move, notation);
this.render();
this.saveState();
if (this.game.gameOver) {
this.showGameOver();
}
}
}
getPlayerColor() {
const sel = document.getElementById('color-select');
return sel.value;
}
// ---- Persistence ----
saveState() {
try {
const state = {
board: this.game.board,
turn: this.game.turn,
castling: this.game.castling,
epSquare: this.game.epSquare,
halfMoveClock: this.game.halfMoveClock,
fullMoveNumber: this.game.fullMoveNumber,
moveHistory: this.game.moveHistory.map(m => ({...m})),
capturedPieces: { w: [...this.game.capturedPieces.w], b: [...this.game.capturedPieces.b] },
gameOver: this.game.gameOver,
result: this.game.result,
lastMove: this.game.lastMove ? {...this.game.lastMove} : null,
};
localStorage.setItem('chessGameState', JSON.stringify(state));
} catch(e) { /* ignore */ }
}
loadState() {
try {
const raw = localStorage.getItem('chessGameState');
if (!raw) return false;
const state = JSON.parse(raw);
this.game.board = state.board;
this.game.turn = state.turn;
this.game.castling = state.castling;
this.game.epSquare = state.epSquare;
this.game.halfMoveClock = state.halfMoveClock;
this.game.fullMoveNumber = state.fullMoveNumber;
this.game.moveHistory = (state.moveHistory || []).map(m => ({...m}));
this.game.capturedPieces = { w: [...(state.capturedPieces?.w || [])], b: [...(state.capturedPieces?.b || [])] };
this.game.gameOver = state.gameOver;
this.game.result = state.result;
this.game.lastMove = state.lastMove ? {...state.lastMove} : null;
return true;
} catch(e) { return false; }
}
flipBoard() {
this.flipped = !this.flipped;
this.render();
}
undoBothMoves() {
// Undo AI move then player move (or just one if it's the first move of a pair)
const playerColor = this.getPlayerColor();
if (playerColor === 'both') {
// In analysis mode, undo just one move per click
if (this.game.moveHistory.length > 0) {
this.game.undoMove();
this.render();
this.saveState();
}
} else {
// Undo up to two moves (AI + player), but only if it's the player's turn now
if (this.game.turn === playerColor && this.game.moveHistory.length >= 2) {
this.game.undoMove(); // undo AI move
this.game.undoMove(); // undo player move
this.render();
this.saveState();
} else if (this.game.turn !== playerColor && this.game.moveHistory.length >= 1) {
// Player moved but AI hasn't responded yet — just undo one
this.game.undoMove();
this.render();
this.saveState();
}
}
}
newGame() {
document.getElementById('game-over-banner').classList.add('hidden');
document.getElementById('promotion-overlay').classList.add('hidden');
this.game.reset();
this.selectedSquare = null;
this.legalMovesForSelected = [];
this.render();
this.saveState();
// If playing as black, AI (white) moves first
const playerColor = this.getPlayerColor();
if (playerColor === 'black') {
setTimeout(() => this.triggerAIMove(), 200);
}
}
}
// ── const: INITIAL_BOARD ──
const INITIAL_BOARD = [
BR, BN, BB, BQ, BK, BB, BN, BR, // rank 8: Black back row
BP, BP, BP, BP, BP, BP, BP, BP, // rank 7: Black pawns
EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 6: empty
EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 5: empty
EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 4: empty
EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY,EMPTY, // rank 3: empty
WP, WP, WP, WP, WP, WP, WP, WP, // rank 2: White pawns
WR, WN, WB, WQ, WK, WB, WN, WR, // rank 1: White back row
];
// ── class: ChessGame ──
class ChessGame {
constructor() { this.reset(); }
reset() {
this.board = [...INITIAL_BOARD];
this.turn = 'w';
this.castling = { ...INITIAL_CASTLING };
this.epSquare = null;
this.halfMoveClock = 0;
this.fullMoveNumber = 1;
this.moveHistory = [];
this.capturedPieces = { w: [], b: [] };
this.gameOver = false;
this.result = null;
this.lastMove = null;
}
idx(r, c) { return r * 8 + c; }
rc(i) { return [Math.floor(i / 8), i % 8]; }
colorOf(p) { return p > 0 ? 'w' : p < 0 ? 'b' : null; }
typeOf(p) { return Math.abs(p); }
clone() {
const g = new ChessGame();
g.board = [...this.board];
g.turn = this.turn;
g.castling = { ...this.castling };
g.epSquare = this.epSquare;
g.halfMoveClock = this.halfMoveClock;
g.fullMoveNumber = this.fullMoveNumber;
g.lastMove = this.lastMove ? { ...this.lastMove } : null;
return g;
}
generatePseudoMoves(color) {
const moves = [];
for (let i = 0; i < 64; i++) {
const p = this.board[i];
if (!p || this.colorOf(p) !== color) continue;
const t = Math.abs(p);
const [r, c] = this.rc(i);
if (t === PAWN) {
const dir = color === 'w' ? -1 : 1;
const startRow = color === 'w' ? 6 : 1;
const promoRow = color === 'w' ? 0 : 7;
// Forward one
const f1r = r + dir;
if (f1r >= 0 && f1r < 8) {
const f1 = this.idx(f1r, c);
if (!this.board[f1]) {
if (f1r === promoRow) {
for (const pt of [QUEEN, ROOK, BISHOP, KNIGHT]) moves.push({ from: i, to: f1, promotion: color === 'w' ? pt : -pt });
} else {
moves.push({ from: i, to: f1 });
// Forward two
const f2r = r + 2 * dir;
if (r === startRow && f2r >= 0 && f2r < 8) {
const f2 = this.idx(f2r, c);
if (!this.board[f2]) moves.push({ from: i, to: f2 });
}
}
}
}
// Captures (regular + en passant)
for (const dc of [-1, 1]) {
const nc = c + dc;
if (nc < 0 || nc > 7) continue;
const capR = r + dir;
if (capR < 0 || capR > 7) continue;
const ti = this.idx(capR, nc);
const target = this.board[ti];
// Regular capture
if (target && this.colorOf(target) !== color) {
if (capR === promoRow) {
for (const pt of [QUEEN, ROOK, BISHOP, KNIGHT]) moves.push({ from: i, to: ti, promotion: color === 'w' ? pt : -pt });
} else {
moves.push({ from: i, to: ti });
}
}
}
// En passant: check if epSquare is on a diagonal from this pawn
// The captured pawn sits one step forward from the epSquare (in capturing direction)
if (this.epSquare !== null) {
const [epR, epC] = this.rc(this.epSquare);
const capPawnRow = epR - dir; // where the captured pawn actually is
if (capPawnRow >= 0 && capPawnRow < 8 && Math.abs(capPawnRow - r) === 1 && Math.abs(epC - c) === 1) {
moves.push({ from: i, to: this.epSquare, enPassant: true });
}
}
} else if (t === KNIGHT) {
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 (nr < 0 || nr > 7 || nc < 0 || nc > 7) continue;
const ti = this.idx(nr, nc);
if (!this.board[ti] || this.colorOf(this.board[ti]) !== color) moves.push({ from: i, to: ti });
}
} else if (t === BISHOP || t === ROOK || t === QUEEN) {
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 [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
const ti = this.idx(nr, nc);
if (!this.board[ti]) { moves.push({ from: i, to: ti }); }
else {
if (this.colorOf(this.board[ti]) !== color) moves.push({ from: i, to: ti });
break;
}
nr += dr; nc += dc;
}
}
} else if (t === KING) {
for (const [dr, dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
const nr = r + dr, nc = c + dc;
if (nr < 0 || nr > 7 || nc < 0 || nc > 7) continue;
const ti = this.idx(nr, nc);
if (!this.board[ti] || this.colorOf(this.board[ti]) !== color) moves.push({ from: i, to: ti });
}
// Castling
const row = color === 'w' ? 7 : 0;
const kSide = color === 'w' ? 'K' : 'k';
const qSide = color === 'w' ? 'Q' : 'q';
if (this.castling[kSide] && !this.board[this.idx(row,5)] && !this.board[this.idx(row,6)]) {
if (!this.isSquareAttacked(this.idx(row,4), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,5), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,6), color==='b'?'w':'b')) {
moves.push({ from: this.idx(row,4), to: this.idx(row,6), castle: 'k' });
}
}
if (this.castling[qSide] && !this.board[this.idx(row,3)] && !this.board[this.idx(row,2)] && !this.board[this.idx(row,1)]) {
if (!this.isSquareAttacked(this.idx(row,4), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,3), color==='b'?'w':'b') && !this.isSquareAttacked(this.idx(row,2), color==='b'?'w':'b')) {
moves.push({ from: this.idx(row,4), to: this.idx(row,2), castle: 'q' });
}
}
}
}
return moves;
}
isSquareAttacked(sq, byColor) {
const [sr, sc] = this.rc(sq);
// Pawn attacks (pawns attack "down" from their perspective in array terms)
const pawnDir = byColor === 'w' ? 1 : -1;
for (const dc of [-1, 1]) {
const pr = sr + pawnDir, pc = sc + dc;
if (pr >= 0 && pr < 8 && pc >= 0 && pc < 8) {
const p = this.board[this.idx(pr, pc)];
if (p && Math.abs(p) === PAWN && this.colorOf(p) === byColor) return true;
}
}
// Knight attacks
for (const [dr, dc] of [[-2,-1],[-2,1],[-1,-2],[-1,2],[1,-2],[1,2],[2,-1],[2,1]]) {
const nr = sr + dr, nc = sc + dc;
if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
const p = this.board[this.idx(nr, nc)];
if (p && Math.abs(p) === KNIGHT && this.colorOf(p) === byColor) return true;
}
}
// King attacks
for (const [dr, dc] of [[-1,-1],[-1,0],[-1,1],[0,-1],[0,1],[1,-1],[1,0],[1,1]]) {
const nr = sr + dr, nc = sc + dc;
if (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
const p = this.board[this.idx(nr, nc)];
if (p && Math.abs(p) === KING && this.colorOf(p) === byColor) return true;
}
}
// Bishop/Queen diagonals
for (const [dr, dc] of [[-1,-1],[-1,1],[1,-1],[1,1]]) {
let nr = sr + dr, nc = sc + dc;
while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
const p = this.board[this.idx(nr, nc)];
if (p) {
if (this.colorOf(p) === byColor && (Math.abs(p) === BISHOP || Math.abs(p) === QUEEN)) return true;
break;
}
nr += dr; nc += dc;
}
}
// Rook/Queen straights
for (const [dr, dc] of [[-1,0],[1,0],[0,-1],[0,1]]) {
let nr = sr + dr, nc = sc + dc;
while (nr >= 0 && nr < 8 && nc >= 0 && nc < 8) {
const p = this.board[this.idx(nr, nc)];
if (p) {
if (this.colorOf(p) === byColor && (Math.abs(p) === ROOK || Math.abs(p) === QUEEN)) return true;
break;
}
nr += dr; nc += dc;
}
}
return false;
}
findKing(color) {
const k = color === 'w' ? WK : BK;
for (let i = 0; i < 64; i++) if (this.board[i] === k) return i;
return -1;
}
makeMoveInternal(move) {
const captured = this.board[move.to];
let epCaptured = null;
// En passant capture
if (move.enPassant) {
const [pr] = this.rc(move.from);
const dir = this.turn === 'w' ? -1 : 1;
const capRow = pr + dir;
epCaptured = this.board[this.idx(capRow, move.to % 8)];
this.board[this.idx(capRow, move.to % 8)] = EMPTY;
}
// Move piece
this.board[move.to] = this.board[move.from];
this.board[move.from] = EMPTY;
if (move.promotion) this.board[move.to] = move.promotion;
// Castling rook movement
if (move.castle) {
const row = Math.floor(move.from / 8);
if (move.castle === 'k') { this.board[this.idx(row,5)] = this.board[this.idx(row,7)]; this.board[this.idx(row,7)] = EMPTY; }
else { this.board[this.idx(row,3)] = this.board[this.idx(row,0)]; this.board[this.idx(row,0)] = EMPTY; }
}
return { captured, epCaptured };
}
unmakeMoveInternal(move, info) {
this.board[move.from] = this.board[move.to];
if (info.epCaptured !== null && move.enPassant) {
const [pr] = this.rc(move.from);
const dir = this.turn === 'w' ? -1 : 1; // turn already flipped back
const capRow = pr + dir;
this.board[this.idx(capRow, move.to % 8)] = info.epCaptured;
} else {
this.board[move.to] = info.captured || EMPTY;
}
if (move.castle) {
const row = Math.floor(move.from / 8);
if (move.castle === 'k') { this.board[this.idx(row,7)] = this.board[this.idx(row,5)]; this.board[this.idx(row,5)] = EMPTY; }
else { this.board[this.idx(row,0)] = this.board[this.idx(row,3)]; this.board[this.idx(row,3)] = EMPTY; }
}
}
generateLegalMoves(color) {
const c = color || this.turn;
const pseudo = this.generatePseudoMoves(c);
const enemy = c === 'w' ? 'b' : 'w';
return pseudo.filter(m => {
const info = this.makeMoveInternal(m);
const kingPos = this.findKing(c);
const inCheck = this.isSquareAttacked(kingPos, enemy);
this.unmakeMoveInternal(m, info);
return !inCheck;
});
}
isInCheck(color) {
const c = color || this.turn;
const kingPos = this.findKing(c);
const enemy = c === 'w' ? 'b' : 'w';
return this.isSquareAttacked(kingPos, enemy);
}
makeMove(move) {
const piece = this.board[move.from];
const pType = Math.abs(piece);
const capturedPiece = move.enPassant ? null : this.board[move.to];
let epCaptured = null;
const record = {
from: move.from, to: move.to, piece: piece,
captured: capturedPiece, enPassant: !!move.enPassant,
castle: move.castle || null, promotion: move.promotion || null,
prevCastling: { ...this.castling }, prevEpSquare: this.epSquare,
prevHalfMoveClock: this.halfMoveClock, prevFullMoveNumber: this.fullMoveNumber,
};
// En passant capture
if (move.enPassant) {
const [pr] = this.rc(move.from);
const dir = this.turn === 'w' ? -1 : 1;
const capRow = pr + dir;
epCaptured = this.board[this.idx(capRow, move.to % 8)];
record.epCapturedSquare = this.idx(capRow, move.to % 8);
this.board[this.idx(capRow, move.to % 8)] = EMPTY;
}
// Move piece
this.board[move.to] = this.board[move.from];
this.board[move.from] = EMPTY;
if (move.promotion) this.board[move.to] = move.promotion;
// Castling rook movement
if (move.castle) {
const row = Math.floor(move.from / 8);
if (move.castle === 'k') { this.board[this.idx(row,5)] = this.board[this.idx(row,7)]; this.board[this.idx(row,7)] = EMPTY; }
else { this.board[this.idx(row,3)] = this.board[this.idx(row,0)]; this.board[this.idx(row,0)] = EMPTY; }
}
// Update castling rights
if (pType === KING) {
if (this.turn === 'w') { this.castling.K = false; this.castling.Q = false; }
else { this.castling.k = false; this.castling.q = false; }
}
if (pType === ROOK) {
if (move.from === 63) this.castling.K = false;
if (move.from === 56) this.castling.Q = false;
if (move.from === 7) this.castling.k = false;
if (move.from === 0) this.castling.q = false;
}
if (move.to === 63) this.castling.K = false;
if (move.to === 56) this.castling.Q = false;
if (move.to === 7) this.castling.k = false;
if (move.to === 0) this.castling.q = false;
// Update en passant square
this.epSquare = null;
if (pType === PAWN && Math.abs(move.from - move.to) === 16) {
const epRow = (Math.floor(move.from / 8) + Math.floor(move.to / 8)) / 2;
this.epSquare = this.idx(epRow, move.to % 8);
}
// Half-move clock
if (pType === PAWN || capturedPiece || epCaptured) this.halfMoveClock = 0;
else this.halfMoveClock++;
if (this.turn === 'b') this.fullMoveNumber++;
const capPiece = epCaptured || capturedPiece;
if (capPiece) {
const capColor = this.colorOf(capPiece);
this.capturedPieces[capColor].push(Math.abs(capPiece));
}
record.notation = this.toAlgebraic(record, move);
// Switch turn
this.turn = this.turn === 'w' ? 'b' : 'w';
this.lastMove = { from: move.from, to: move.to };
// Check game state
const legalMoves = this.generateLegalMoves(this.turn);
if (legalMoves.length === 0) {
this.gameOver = true;
if (this.isInCheck(this.turn)) { this.result = 'checkmate'; record.notation += '#'; }
else this.result = 'stalemate';
} else {
if (this.halfMoveClock >= 100) { this.gameOver = true; this.result = 'draw-50move'; }
else if (this.isInsufficientMaterial()) { this.gameOver = true; this.result = 'draw-material'; }
}
// Add check symbol
if (!record.notation.includes('#') && !this.gameOver) {
if (this.isInCheck(this.turn)) record.notation += '+';
}
this.moveHistory.push(record);
return record.notation;
}
undoMove() {
if (this.moveHistory.length === 0) return false;
const rec = this.moveHistory.pop();
this.turn = this.turn === 'w' ? 'b' : 'w';
this.board[rec.from] = rec.piece;
if (rec.enPassant) {
this.board[rec.to] = EMPTY;
this.board[rec.epCapturedSquare] = this.capturedPieces[this.turn === 'w' ? 'b' : 'w'].pop();
} else {
const cap = rec.captured;
if (cap) { const arr = this.capturedPieces[this.colorOf(cap)]; arr.pop(); }
this.board[rec.to] = cap || EMPTY;
}
if (rec.castle) {
const row = Math.floor(rec.from / 8);
if (rec.castle === 'k') { this.board[this.idx(row,7)] = this.board[this.idx(row,5)]; this.board[this.idx(row,5)] = EMPTY; }
else { this.board[this.idx(row,0)] = this.board[this.idx(row,3)]; this.board[this.idx(row,3)] = EMPTY; }
}
this.castling = rec.prevCastling;
this.epSquare = rec.prevEpSquare;
this.halfMoveClock = rec.prevHalfMoveClock;
this.fullMoveNumber = rec.prevFullMoveNumber;
this.gameOver = false;
this.result = null;
if (this.moveHistory.length > 0) { const last = this.moveHistory[this.moveHistory.length - 1]; this.lastMove = { from: last.from, to: last.to }; }
else this.lastMove = null;
return true;
}
toAlgebraic(rec, move) {
const pType = Math.abs(rec.piece);
const [fr, fc] = this.rc(move.from);
const [tr, tc] = this.rc(move.to);
let s = '';
if (rec.castle === 'k') return 'O-O';
if (rec.castle === 'q') return 'O-O-O';
if (pType !== PAWN) {
s += PIECE_LETTER[pType];
// Disambiguation: check for other pieces of same type that could move to the same square
const color = this.colorOf(rec.piece);
let needFile = false, needRank = false;
for (let i = 0; i < 64; i++) {
if (i === move.from) continue;
const p = this.board[i];
if (!p || Math.abs(p) !== pType || this.colorOf(p) !== color) continue;
const [or, oc] = this.rc(i);
// Check if this piece could also reach the destination (simplified check)
if ((oc === tc && or !== fr) || (or === tr && oc !== fc)) needFile = true;
if ((or === tr && oc !== fc) || (oc === tc && or !== fr)) needRank = true;
}
// More precise: check actual legal moves for disambiguation
const sameMoves = this.generatePseudoMoves(color).filter(m => m.to === move.to && Math.abs(this.board[m.from]) === pType && m.from !== move.from);
if (sameMoves.length > 0) {
const sameFile = sameMoves.some(m => m.from % 8 === fc);
const sameRank = sameMoves.some(m => Math.floor(m.from / 8) === fr);
if (!sameFile) s += FILES[fc];
else if (!sameRank) s += RANKS[7 - fr];
else { s += FILES[fc] + RANKS[7 - fr]; }
}
}
const isCapture = rec.captured || rec.enPassant;
if (isCapture) { if (pType === PAWN) s += FILES[fc]; s += 'x'; }
s += FILES[tc] + RANKS[7 - tr];
if (rec.promotion) { const pt = Math.abs(rec.promotion); s += '=' + PIECE_LETTER[pt]; }
if (rec.enPassant) s += ' e.p.';
return s;
}
isInsufficientMaterial() {
const pieces = [];
for (let i = 0; i < 64; i++) { if (this.board[i]) pieces.push({ type: Math.abs(this.board[i]), color: this.colorOf(this.board[i]) }); }
if (pieces.length === 2) return true;
if (pieces.length === 3) { const nk = pieces.find(p => p.type !== KING); if (nk && (nk.type === BISHOP || nk.type === KNIGHT)) return true; }
return false;
}
fen() {
let fen = '';
for (let r = 0; r < 8; r++) {
let empty = 0;
for (let c = 0; c < 8; c++) {
const p = this.board[this.idx(r, c)];
if (!p) { empty++; }
else {
if (empty > 0) { fen += empty; empty = 0; }
const t = Math.abs(p);
const letter = PIECE_LETTER[t] || 'P';
fen += p > 0 ? letter : letter.toLowerCase();
}
}
if (empty > 0) fen += empty;
if (r < 7) fen += '/';
}
fen += ' ' + this.turn;
let c = '';
if (this.castling.K) c += 'K';
if (this.castling.Q) c += 'Q';
if (this.castling.k) c += 'k';
if (this.castling.q) c += 'q';
if (!c) c = '-';
fen += ' ' + c;
if (this.epSquare !== null) {
const [er, ec] = this.rc(this.epSquare);
fen += ' ' + FILES[ec] + RANKS[7 - er];
} else { fen += ' -'; }
fen += ' ' + this.halfMoveClock;
fen += ' ' + this.fullMoveNumber;
return fen;
}
legalMoves() {
const moves = this.generateLegalMoves(this.turn);
return moves.map(m => FILES[m.from % 8] + RANKS[7 - Math.floor(m.from / 8)] + FILES[m.to % 8] + RANKS[7 - Math.floor(m.to / 8)] + (m.promotion ? '=' + PIECE_LETTER[Math.abs(m.promotion)] : ''));
}
move(str) {
const match = str.match(/^([a-h])(\d)([a-h])(\d)(?:=([QRBN]))?$/);
if (!match) return null;
const fc = match[1].charCodeAt(0) - 97;
const fr = parseInt(match[2]) - 1;
const tc = match[3].charCodeAt(0) - 97;
const tr = parseInt(match[4]) - 1;
// Convert chess notation (rank 1-8 from White's side) to array rows (row 7=rank1, row 0=rank8)
const fromRow = 7 - fr;
const toRow = 7 - tr;
const from = this.idx(fromRow, fc);
const to = this.idx(toRow, tc);
const moves = this.generateLegalMoves(this.turn);
let found = null;
for (const m of moves) {
if (m.from === from && m.to === to) {
if (match[5]) {
const promoType = PIECE_LETTERInverse(match[5]);
if (m.promotion && Math.abs(m.promotion) === promoType) { found = m; break; }
} else {
if (!m.promotion || Math.abs(m.promotion) === QUEEN) { found = m; break; }
}
}
}
if (found) return this.makeMove(found);
return null;
}
}
// ── main ──
// ============================================================
// MAIN — Initialization and event wiring
// ============================================================
(function() {
// Create game and UI instances
const game = new ChessGame();
const ui = new ChessUI(game);
window._soundEngine = new SoundEngine();
// Load saved state or start fresh
if (!ui.loadState()) {
ui.saveState();
}
// Initial render
ui.render();
// ---- Board interaction (click) ----
document.getElementById('board').addEventListener('click', function(e) {
window._soundEngine.init();
const sq = e.target.closest('.square');
if (!sq) return;
const idx = parseInt(sq.dataset.idx);
ui.handleSquareClick(idx);
});
// ---- Board interaction (drag and drop) ----
let dragFromIdx = null;
document.getElementById('board').addEventListener('mousedown', function(e) {
window._soundEngine.init();
const sq = e.target.closest('.square');
if (!sq) return;
const idx = parseInt(sq.dataset.idx);
ui.handleDragStart(idx);
dragFromIdx = idx;
});
document.getElementById('board').addEventListener('mousemove', function(e) {
if (dragFromIdx === null) return;
const sq = e.target.closest('.square');
if (!sq) return;
const idx = parseInt(sq.dataset.idx);
ui.handleDragOver(idx);
});
document.addEventListener('mouseup', function(e) {
if (dragFromIdx === null) return;
const sq = e.target.closest('.square');
if (sq) {
const idx = parseInt(sq.dataset.idx);
ui.handleDrop(idx);
} else {
ui.handleDragEnd();
}
dragFromIdx = null;
});
// Touch support for mobile
let touchFromIdx = null;
document.getElementById('board').addEventListener('touchstart', function(e) {
window._soundEngine.init();
const touch = e.touches[0];
const sq = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.square');
if (!sq) return;
const idx = parseInt(sq.dataset.idx);
ui.handleDragStart(idx);
touchFromIdx = idx;
}, { passive: true });
document.getElementById('board').addEventListener('touchmove', function(e) {
if (touchFromIdx === null) return;
const touch = e.touches[0];
const sq = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.square');
if (!sq) return;
const idx = parseInt(sq.dataset.idx);
ui.handleDragOver(idx);
}, { passive: true });
document.getElementById('board').addEventListener('touchend', function(e) {
if (touchFromIdx === null) return;
// For touch, just treat as a click on the end position
const touch = e.changedTouches[0];
const sq = document.elementFromPoint(touch.clientX, touch.clientY)?.closest('.square');
if (sq) {
const idx = parseInt(sq.dataset.idx);
ui.handleDrop(idx);
} else {
ui.handleDragEnd();
}
touchFromIdx = null;
}, { passive: true });
// ---- Button handlers ----
document.getElementById('btn-new-game').addEventListener('click', function() {
window._soundEngine.init();
ui.newGame();
});
document.getElementById('btn-undo').addEventListener('click', function() {
window._soundEngine.init();
ui.undoBothMoves();
});
document.getElementById('btn-flip').addEventListener('click', function() {
window._soundEngine.init();
ui.flipBoard();
});
document.getElementById('btn-mute').addEventListener('click', function() {
window._soundEngine.init();
const muted = window._soundEngine.toggleMute();
this.textContent = muted ? '🔇' : '🔊';
});
document.getElementById('btn-new-game-overlay').addEventListener('click', function() {
ui.newGame();
});
// ---- Color selection change ----
document.getElementById('color-select').addEventListener('change', function() {
ui.newGame();
});
// ---- Debug interface on window ----
window.game = game;
window.ui = ui;
window.ai = ChessAI;
})();
</script>
</body>
</html>