Chess
Qwen 3.6 27b MTP · typescript

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 88,698 bytes · SHA-256 6900da770ddf
<!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; }
body {
background: #1a1a2e;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: #E0D6C8;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow-x: hidden;
}
#game-container {
display: flex;
gap: 24px;
padding: 20px;
max-width: 960px;
width: 100%;
}
#board-area {
position: relative;
flex-shrink: 0;
}
#board-canvas {
border: 4px solid #6B4226;
border-radius: 4px;
cursor: pointer;
display: block;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
}
#side-panel {
flex: 1;
min-width: 260px;
max-width: 340px;
display: flex;
flex-direction: column;
gap: 12px;
}
#game-info h1 {
font-size: 28px;
color: #F0D9B5;
margin-bottom: 6px;
letter-spacing: 1px;
}
#turn-indicator {
font-size: 14px;
padding: 6px 12px;
background: rgba(255,255,255,0.08);
border-radius: 4px;
display: inline-block;
}
#status-banner {
margin-top: 8px;
padding: 10px 16px;
background: rgba(255, 50, 50, 0.2);
border: 1px solid rgba(255, 50, 50, 0.4);
border-radius: 6px;
font-size: 16px;
font-weight: bold;
text-align: center;
}
#captured-area {
display: flex;
flex-direction: column;
gap: 4px;
}
#captured-white, #captured-black {
min-height: 28px;
padding: 4px 8px;
background: rgba(255,255,255,0.05);
border-radius: 4px;
font-size: 20px;
line-height: 1.4;
letter-spacing: 2px;
}
#move-list-container {
flex: 1;
min-height: 180px;
max-height: 320px;
background: rgba(255,255,255,0.05);
border-radius: 6px;
padding: 8px;
overflow-y: auto;
}
#move-list-container h3 {
font-size: 14px;
color: #B58863;
margin-bottom: 6px;
text-transform: uppercase;
letter-spacing: 1px;
}
#move-list {
display: flex;
flex-wrap: wrap;
gap: 2px 4px;
font-family: 'Courier New', monospace;
font-size: 13px;
}
.move-number {
color: #888;
min-width: 28px;
display: inline-block;
}
.white-move, .black-move {
padding: 2px 6px;
border-radius: 3px;
cursor: default;
}
.white-move:hover, .black-move:hover {
background: rgba(255,255,255,0.1);
}
#controls {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
#controls button {
padding: 8px 14px;
background: #4A4A4A;
color: #E0D6C8;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background 0.15s;
}
#controls button:hover {
background: #5A5A5A;
}
#ai-controls {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
}
#difficulty {
padding: 4px 8px;
background: #4A4A4A;
color: #E0D6C8;
border: none;
border-radius: 4px;
cursor: pointer;
}
.hidden { display: none !important; }
#color-choice {
text-align: center;
padding: 20px;
background: rgba(255,255,255,0.05);
border-radius: 8px;
}
#color-choice h2 {
margin-bottom: 16px;
font-size: 18px;
color: #F0D9B5;
}
.color-btn {
display: block;
width: 100%;
padding: 12px;
margin: 6px 0;
background: #3A3A4A;
color: #E0D6C8;
border: 2px solid transparent;
border-radius: 6px;
cursor: pointer;
font-size: 15px;
transition: all 0.15s;
}
.color-btn:hover {
background: #4A4A5A;
border-color: #B58863;
}
#promotion-overlay {
position: absolute;
top: 0; left: 0;
width: 100%; height: 100%;
display: flex;
justify-content: center;
align-items: center;
background: rgba(0,0,0,0.5);
z-index: 10;
}
#promotion-pieces {
display: flex;
gap: 8px;
padding: 12px;
background: #2C2C3C;
border-radius: 8px;
box-shadow: 0 4px 20px rgba(0,0,0,0.6);
}
.promo-piece {
width: 56px; height: 56px;
font-size: 36px;
display: flex;
justify-content: center;
align-items: center;
background: #F0D9B5;
border-radius: 8px;
cursor: pointer;
transition: transform 0.1s, box-shadow 0.1s;
user-select: none;
}
.promo-piece:hover {
transform: scale(1.1);
box-shadow: 0 2px 12px rgba(255,255,255,0.3);
}
/* Scrollbar styling */
#move-list-container::-webkit-scrollbar { width: 6px; }
#move-list-container::-webkit-scrollbar-track { background: transparent; }
#move-list-container::-webkit-scrollbar-thumb { background: #4A4A4A; border-radius: 3px; }
@media (max-width: 800px) {
#game-container { flex-direction: column; align-items: center; }
#side-panel { max-width: 100%; min-width: auto; width: 100%; }
}
</style>
</head>
<body>
<div id="game-container">
<div id="board-area">
<canvas id="board-canvas" width="560" height="560"></canvas>
<div id="promotion-overlay" class="hidden">
<div id="promotion-pieces"></div>
</div>
</div>
<div id="side-panel">
<div id="game-info">
<h1 id="title">Chess</h1>
<div id="turn-indicator">White to move</div>
<div id="status-banner" class="hidden"></div>
</div>
<div id="captured-area">
<div id="captured-white" title="Captured by White"></div>
<div id="captured-black" title="Captured by Black"></div>
</div>
<div id="move-list-container">
<h3>Moves</h3>
<div id="move-list"></div>
</div>
<div id="controls">
<button id="btn-undo" title="Undo last move pair">↩ Undo</button>
<button id="btn-flip" title="Flip board">⇅ Flip</button>
<button id="btn-newgame" title="New game">⟳ New Game</button>
<button id="btn-mute" title="Toggle sound">🔊 Sound</button>
</div>
<div id="ai-controls">
<label for="difficulty">Depth:</label>
<select id="difficulty">
<option value="1">Easy (1)</option>
<option value="2" selected="">Medium (2)</option>
<option value="3">Hard (3)</option>
</select>
</div>
<div id="color-choice" class="hidden">
<h2>Choose your color</h2>
<button id="btn-white" class="color-btn">Play as White ♔</button>
<button id="btn-black" class="color-btn">Play as Black ♚</button>
<button id="btn-analysis" class="color-btn">Analysis Mode (both sides)</button>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./types":"src/types.ts","./ai":"src/ai.ts"},"src/constants.ts":{"./types":"src/types.ts"},"src/board.ts":{"./types":"src/types.ts"},"src/movegen.ts":{"./types":"src/types.ts","./board":"src/board.ts"},"src/eval.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./board":"src/board.ts"},"src/ai.ts":{"./types":"src/types.ts","./movegen":"src/movegen.ts","./board":"src/board.ts","./eval":"src/eval.ts"},"src/sound.ts":{"./constants":"src/constants.ts"},"src/persistence.ts":{"./types":"src/types.ts","./board":"src/board.ts"},"src/renderer.ts":{"./types":"src/types.ts","./constants":"src/constants.ts"},"src/input.ts":{"./types":"src/types.ts","./renderer":"src/renderer.ts"},"src/game.ts":{"./types":"src/types.ts","./board":"src/board.ts","./movegen":"src/movegen.ts","./renderer":"src/renderer.ts","./input":"src/input.ts","./ai":"src/ai.ts","./sound":"src/sound.ts","./persistence":"src/persistence.ts","./constants":"src/constants.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const game_1 = require("./game");
const ai_1 = require("./ai");
// ─── Entry Point ────────────────────────────────────────────────
function main() {
// Initialize audio on first user interaction
document.addEventListener('click', () => {
try {
window.AudioContext && new AudioContext();
}
catch { }
}, { once: true });
// Create game with default config (player color chosen via UI)
const game = (0, game_1.createGame)({
playerColor: undefined, // will be set by user choice in UI
aiDepth: 2,
});
// Expose debug interface on window
window.game = {
fen: () => game.fen(),
legalMoves: () => game.legalMoves(),
move: (n) => game.move(n),
getState: () => game.getState(),
aiSearch: (depth) => (0, ai_1.findBestMoveIterative)(game.getState(), depth),
};
console.log('Chess game loaded. Debug interface available on window.game');
console.log('Commands: game.fen(), game.legalMoves(), game.move("e2e4"), game.getState()');
}
main();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ─── Chess Types ────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SOUND_FREQS = exports.PIECE_NAMES = exports.PIECE_UNICODE = exports.PST = exports.PIECE_VALUES = exports.AI_CONFIG = exports.COLORS = void 0;
// ─── Tunable Constants ──────────────────────────────────────────
exports.COLORS = {
lightSquare: '#F0D9B5',
darkSquare: '#B58863',
highlightMove: 'rgba(155, 199, 0, 0.45)',
highlightLastMove: 'rgba(247, 230, 100, 0.45)',
highlightCheck: 'radial-gradient(circle, #ff0000 0%, rgba(255,0,0,0) 80%)',
highlightLegalDot: 'rgba(0, 0, 0, 0.15)',
highlightCaptureRing: 'rgba(0, 0, 0, 0.15)',
selectedSquare: 'rgba(20, 85, 30, 0.45)',
boardBorder: '#6B4226',
panelBg: '#2C2C2C',
panelText: '#E0D6C8',
moveListHover: '#3A3A3A',
buttonBg: '#4A4A4A',
buttonHover: '#5A5A5A',
gameOverBanner: 'rgba(0, 0, 0, 0.7)',
};
exports.AI_CONFIG = {
depths: [1, 2, 3],
defaultDepth: 2,
maxNodesPerSlice: 50000, // nodes per time slice for non-blocking search
sliceDurationMs: 50, // ms per chunk of work
};
// Piece values (centipawns)
exports.PIECE_VALUES = {
P: 100, N: 320, B: 330, R: 500, Q: 900, K: 20000,
};
// Piece-square tables (from white's perspective; mirrored for black)
// Index: rank * 8 + file. Rank 0 = white's back rank.
exports.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, 5, 10, 10, 5, 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,
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,
],
};
exports.PIECE_UNICODE = {
w: { K: '♔', Q: '♕', R: '♖', B: '♗', N: '♘', P: '♙' },
b: { K: '♚', Q: '♛', R: '♜', B: '♝', N: '♞', P: '♟' },
};
exports.PIECE_NAMES = {
K: 'King', Q: 'Queen', R: 'Rook', B: 'Bishop', N: 'Knight', P: 'Pawn',
};
// Sound frequencies (Hz) for Web Audio
exports.SOUND_FREQS = {
move: 600,
capture: 400,
check: 800,
gameOver: 300,
};
};
// ── module: src/board.ts ──
__mods["src/board.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInitialBoard = createInitialBoard;
exports.cloneBoard = cloneBoard;
exports.initialCastlingRights = initialCastlingRights;
exports.cloneState = cloneState;
exports.createInitialState = createInitialState;
exports.boardToFen = boardToFen;
exports.fenToBoard = fenToBoard;
exports.applyMove = applyMove;
exports.undoMove = undoMove;
exports.findKing = findKing;
exports.isSquareAttacked = isSquareAttacked;
exports.isInCheck = isInCheck;
// ─── Board Creation & Helpers ───────────────────────────────────
function createInitialBoard() {
const b = Array.from({ length: 8 }, () => Array(8).fill(null));
const backRank = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
for (let f = 0; f < 8; f++) {
b[0][f] = { color: 'b', type: backRank[f] };
b[1][f] = { color: 'b', type: 'P' };
b[6][f] = { color: 'w', type: 'P' };
b[7][f] = { color: 'w', type: backRank[f] };
}
return b;
}
function cloneBoard(board) {
return board.map(row => row.map(sq => sq ? { ...sq } : null));
}
function initialCastlingRights() {
return { wK: true, wQ: true, bK: true, bQ: true };
}
function cloneState(state) {
return {
board: cloneBoard(state.board),
turn: state.turn,
castlingRights: { ...state.castlingRights },
epSquare: state.epSquare ? [...state.epSquare] : null,
halfMoveClock: state.halfMoveClock,
fullMoveNumber: state.fullMoveNumber,
moveHistory: [...state.moveHistory],
capturedPieces: { w: [...state.capturedPieces.w], b: [...state.capturedPieces.b] },
};
}
function createInitialState() {
return {
board: createInitialBoard(),
turn: 'w',
castlingRights: initialCastlingRights(),
epSquare: null,
halfMoveClock: 0,
fullMoveNumber: 1,
moveHistory: [],
capturedPieces: { w: [], b: [] },
};
}
// ─── FEN Parsing & Generation ──────────────────────────────────
function boardToFen(state) {
const parts = [];
// Board position
let fenBoard = '';
for (let r = 0; r < 8; r++) {
let empty = 0;
for (let f = 0; f < 8; f++) {
const p = state.board[r][f];
if (!p) {
empty++;
}
else {
if (empty > 0) {
fenBoard += String(empty);
empty = 0;
}
const ch = p.type;
fenBoard += p.color === 'w' ? ch.toUpperCase() : ch.toLowerCase();
}
}
if (empty > 0)
fenBoard += String(empty);
if (r < 7)
fenBoard += '/';
}
parts.push(fenBoard);
// Active color
parts.push(state.turn);
// Castling rights
let castling = '';
if (state.castlingRights.wK)
castling += 'K';
if (state.castlingRights.wQ)
castling += 'Q';
if (state.castlingRights.bK)
castling += 'k';
if (state.castlingRights.bQ)
castling += 'q';
parts.push(castling || '-');
// En passant square (row 0 = rank 8, row 7 = rank 1)
if (state.epSquare) {
const file = String.fromCharCode('a'.charCodeAt(0) + state.epSquare[1]);
const rank = String(8 - state.epSquare[0]);
parts.push(file + rank);
}
else {
parts.push('-');
}
// Half-move clock and full move number
parts.push(String(state.halfMoveClock));
parts.push(String(state.fullMoveNumber));
return parts.join(' ');
}
function fenToBoard(fen) {
try {
const parts = fen.trim().split(/\s+/);
if (parts.length < 4)
return null;
const boardStr = parts[0];
const turn = parts[1];
const castlingStr = parts[2];
const epStr = parts[3];
const halfMove = parseInt(parts[4]) || 0;
const fullMove = parseInt(parts[5]) || 1;
if (turn !== 'w' && turn !== 'b')
return null;
// Parse board
const ranks = boardStr.split('/');
if (ranks.length !== 8)
return null;
const board = [];
for (const rank of ranks) {
const row = [];
for (const ch of rank) {
if (ch >= '1' && ch <= '8') {
for (let i = 0; i < parseInt(ch); i++)
row.push(null);
}
else {
const color = ch === ch.toUpperCase() ? 'w' : 'b';
const type = ch.toUpperCase();
if (!['K', 'Q', 'R', 'B', 'N', 'P'].includes(type))
return null;
row.push({ color, type });
}
}
if (row.length !== 8)
return null;
board.push(row);
}
// Parse castling rights
const cr = { wK: false, wQ: false, bK: false, bQ: false };
for (const ch of castlingStr) {
if (ch === 'K')
cr.wK = true;
else if (ch === 'Q')
cr.wQ = true;
else if (ch === 'k')
cr.bK = true;
else if (ch === 'q')
cr.bQ = true;
}
// Parse en passant square (chess rank → row index: rank 1 = row 7, rank 8 = row 0)
let epSquare = null;
if (epStr !== '-') {
const file = epStr.charCodeAt(0) - 'a'.charCodeAt(0);
const rank = 8 - parseInt(epStr[1]);
if (file >= 0 && file < 8 && rank >= 0 && rank < 8) {
epSquare = [rank, file];
}
}
return {
board,
turn,
castlingRights: cr,
epSquare,
halfMoveClock: halfMove,
fullMoveNumber: fullMove,
moveHistory: [],
capturedPieces: { w: [], b: [] },
};
}
catch {
return null;
}
}
// ─── Move Execution (applies a move to the board) ──────────────
function applyMove(state, move) {
const newState = cloneState(state);
const piece = newState.board[move.fromR][move.fromF];
if (!piece)
return state; // should never happen for legal moves
const captured = newState.board[move.toR][move.toF];
// Handle en passant capture
if (move.enPassant) {
const capturedPawnRow = move.pieceType === 'P' && piece.color === 'w' ? move.toR + 1 : move.toR - 1;
const epCaptured = newState.board[capturedPawnRow][move.toF];
if (epCaptured) {
newState.capturedPieces[piece.color].push(epCaptured.type);
}
newState.board[capturedPawnRow][move.toF] = null;
}
// Handle regular capture
if (captured && !move.enPassant) {
newState.capturedPieces[piece.color].push(captured.type);
}
// Move the piece
newState.board[move.fromR][move.fromF] = null;
// Handle castling rook movement
if (move.castling) {
const isKingside = move.castling === 'K';
const rookFromF = isKingside ? 7 : 0;
const rookToF = isKingside ? 5 : 3;
const rook = newState.board[move.fromR][rookFromF];
newState.board[move.fromR][rookFromF] = null;
newState.board[move.fromR][rookToF] = rook;
}
// Place piece (with promotion)
if (move.promotion) {
newState.board[move.toR][move.toF] = { color: piece.color, type: move.promotion };
}
else {
newState.board[move.toR][move.toF] = piece;
}
// Update castling rights
if (piece.type === 'K') {
if (piece.color === 'w') {
newState.castlingRights.wK = false;
newState.castlingRights.wQ = false;
}
else {
newState.castlingRights.bK = false;
newState.castlingRights.bQ = false;
}
}
if (piece.type === 'R') {
if (move.fromR === 7 && move.fromF === 0)
newState.castlingRights.wQ = false;
if (move.fromR === 7 && move.fromF === 7)
newState.castlingRights.wK = false;
if (move.fromR === 0 && move.fromF === 0)
newState.castlingRights.bQ = false;
if (move.fromR === 0 && move.fromF === 7)
newState.castlingRights.bK = false;
}
// If a rook is captured, remove that castling right
if (captured && captured.type === 'R') {
if (move.toR === 7 && move.toF === 0)
newState.castlingRights.wQ = false;
if (move.toR === 7 && move.toF === 7)
newState.castlingRights.wK = false;
if (move.toR === 0 && move.toF === 0)
newState.castlingRights.bQ = false;
if (move.toR === 0 && move.toF === 7)
newState.castlingRights.bK = false;
}
// Update en passant square
newState.epSquare = null;
if (piece.type === 'P' && Math.abs(move.toR - move.fromR) === 2) {
const epRow = (move.fromR + move.toR) / 2;
newState.epSquare = [epRow, move.fromF];
}
// Update half-move clock
if (piece.type === 'P' || captured || move.enPassant) {
newState.halfMoveClock = 0;
}
else {
newState.halfMoveClock++;
}
// Update full move number
if (state.turn === 'b') {
newState.fullMoveNumber++;
}
// Switch turn
newState.turn = state.turn === 'w' ? 'b' : 'w';
// Record move
newState.moveHistory.push(move);
return newState;
}
// ─── Undo Last Move (for AI search) ─────────────────────────────
function undoMove(state, move) {
const newState = cloneState(state);
const pieceOnDest = newState.board[move.toR][move.toF];
if (!pieceOnDest)
return state;
// Reverse the piece movement
newState.board[move.fromR][move.fromF] = { color: pieceOnDest.color, type: move.pieceType };
newState.board[move.toR][move.toF] = null;
// Handle en passant capture reversal
if (move.enPassant) {
const capturedPawnRow = move.pieceType === 'P' && pieceOnDest.color === 'w' ? move.toR + 1 : move.toR - 1;
newState.board[capturedPawnRow][move.toF] = { color: pieceOnDest.color === 'w' ? 'b' : 'w', type: 'P' };
}
// Handle regular capture reversal
if (move.captured && !move.enPassant) {
newState.board[move.toR][move.toF] = { color: pieceOnDest.color === 'w' ? 'b' : 'w', type: move.captured };
}
// Reverse castling rook movement
if (move.castling) {
const isKingside = move.castling === 'K';
const rookFromF = isKingside ? 7 : 0;
const rookToF = isKingside ? 5 : 3;
newState.board[move.fromR][rookFromF] = newState.board[move.fromR][rookToF];
newState.board[move.fromR][rookToF] = null;
}
// Restore previous state fields from move history
const prevMoveIdx = state.moveHistory.length - 1;
if (prevMoveIdx >= 0) {
newState.turn = state.moveHistory[prevMoveIdx].pieceType === 'K' ?
(state.turn === 'w' ? 'b' : 'w') : state.turn; // simplified
}
return newState;
}
// ─── Utility Functions ──────────────────────────────────────────
function findKing(board, color) {
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = board[r][f];
if (p && p.color === color && p.type === 'K')
return [r, f];
}
}
return null;
}
function isSquareAttacked(board, r, f, byColor) {
// Check knight attacks
const knightMoves = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
for (const [dr, df] of knightMoves) {
const nr = r + dr, nf = f + df;
if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) {
const p = board[nr][nf];
if (p && p.color === byColor && p.type === 'N')
return true;
}
}
// Check pawn attacks
const pawnDir = byColor === 'w' ? -1 : 1; // white pawns attack upward (lower rank)
for (const df of [-1, 1]) {
const pr = r + pawnDir, pf = f + df;
if (pr >= 0 && pr < 8 && pf >= 0 && pf < 8) {
const p = board[pr][pf];
if (p && p.color === byColor && p.type === 'P')
return true;
}
}
// Check king attacks
for (let dr = -1; dr <= 1; dr++) {
for (let df = -1; df <= 1; df++) {
if (dr === 0 && df === 0)
continue;
const nr = r + dr, nf = f + df;
if (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) {
const p = board[nr][nf];
if (p && p.color === byColor && p.type === 'K')
return true;
}
}
}
// Check sliding pieces (rook/queen for straight, bishop/queen for diagonal)
const straightDirs = [[0, 1], [0, -1], [1, 0], [-1, 0]];
const diagDirs = [[1, 1], [1, -1], [-1, 1], [-1, -1]];
for (const [dr, df] of straightDirs) {
let nr = r + dr, nf = f + df;
while (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) {
const p = board[nr][nf];
if (p) {
if (p.color === byColor && (p.type === 'R' || p.type === 'Q'))
return true;
break;
}
nr += dr;
nf += df;
}
}
for (const [dr, df] of diagDirs) {
let nr = r + dr, nf = f + df;
while (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) {
const p = board[nr][nf];
if (p) {
if (p.color === byColor && (p.type === 'B' || p.type === 'Q'))
return true;
break;
}
nr += dr;
nf += df;
}
}
return false;
}
function isInCheck(board, color) {
const king = findKing(board, color);
if (!king)
return false;
const opponentColor = color === 'w' ? 'b' : 'w';
return isSquareAttacked(board, king[0], king[1], opponentColor);
}
};
// ── module: src/movegen.ts ──
__mods["src/movegen.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.generateLegalMoves = generateLegalMoves;
exports.getGameStatus = getGameStatus;
exports.moveToAlgebraic = moveToAlgebraic;
exports.addCheckSuffix = addCheckSuffix;
const board_1 = require("./board");
// ─── Pseudo-Legal Move Generation ──────────────────────────────
// Generates all moves without checking if they leave the king in check.
// Legal move filtering happens after.
function generatePseudoMoves(board, turn, castlingRights, epSquare) {
const moves = [];
const opponentColor = turn === 'w' ? 'b' : 'w';
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const piece = board[r][f];
if (!piece || piece.color !== turn)
continue;
switch (piece.type) {
case 'P':
generatePawnMoves(board, moves, r, f, turn, epSquare);
break;
case 'N':
generateKnightMoves(board, moves, r, f, turn);
break;
case 'B':
generateSlidingMoves(board, moves, r, f, turn, [[1, 1], [1, -1], [-1, 1], [-1, -1]]);
break;
case 'R':
generateSlidingMoves(board, moves, r, f, turn, [[0, 1], [0, -1], [1, 0], [-1, 0]]);
break;
case 'Q':
generateSlidingMoves(board, moves, r, f, turn, [[0, 1], [0, -1], [1, 0], [-1, 0], [1, 1], [1, -1], [-1, 1], [-1, -1]]);
break;
case 'K':
generateKingMoves(board, moves, r, f, turn, castlingRights);
break;
}
}
}
return moves;
}
function generatePawnMoves(board, moves, r, f, color, epSquare) {
const dir = color === 'w' ? -1 : 1; // white goes up (decreasing rank)
const startRow = color === 'w' ? 6 : 1;
const promoRow = color === 'w' ? 0 : 7;
// Single push
const nr1 = r + dir;
if (nr1 >= 0 && nr1 < 8 && !board[nr1][f]) {
if (nr1 === promoRow) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ fromR: r, fromF: f, toR: nr1, toF: f, pieceType: 'P', promotion: pt });
}
}
else {
moves.push({ fromR: r, fromF: f, toR: nr1, toF: f, pieceType: 'P' });
}
// Double push (only from starting position)
const nr2 = r + 2 * dir;
if (r === startRow && !board[nr2][f]) {
moves.push({ fromR: r, fromF: f, toR: nr2, toF: f, pieceType: 'P' });
}
}
// Captures (diagonal)
for (const df of [-1, 1]) {
const nf = f + df;
if (nf < 0 || nf > 7)
continue;
const nr = r + dir;
if (nr < 0 || nr > 7)
continue;
const target = board[nr][nf];
// Normal capture
if (target && target.color !== color) {
if (nr === promoRow) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ fromR: r, fromF: f, toR: nr, toF: nf, pieceType: 'P', captured: target.type, promotion: pt });
}
}
else {
moves.push({ fromR: r, fromF: f, toR: nr, toF: nf, pieceType: 'P', captured: target.type });
}
}
// En passant capture — target is the epSquare (one rank behind where pushed pawn landed)
if (epSquare && epSquare[1] === nf && epSquare[0] === r + 2 * dir) {
moves.push({ fromR: r, fromF: f, toR: epSquare[0], toF: nf, pieceType: 'P', captured: 'P', enPassant: true });
}
}
}
function generateKnightMoves(board, moves, r, f, color) {
const offsets = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
for (const [dr, df] of offsets) {
const nr = r + dr, nf = f + df;
if (nr < 0 || nr > 7 || nf < 0 || nf > 7)
continue;
const target = board[nr][nf];
if (!target || target.color !== color) {
moves.push({ fromR: r, fromF: f, toR: nr, toF: nf, pieceType: 'N', captured: target?.type });
}
}
}
function generateSlidingMoves(board, moves, r, f, color, dirs) {
const piece = board[r][f];
for (const [dr, df] of dirs) {
let nr = r + dr, nf = f + df;
while (nr >= 0 && nr < 8 && nf >= 0 && nf < 8) {
const target = board[nr][nf];
if (!target) {
moves.push({ fromR: r, fromF: f, toR: nr, toF: nf, pieceType: piece.type });
}
else {
if (target.color !== color) {
moves.push({ fromR: r, fromF: f, toR: nr, toF: nf, pieceType: piece.type, captured: target.type });
}
break; // blocked
}
nr += dr;
nf += df;
}
}
}
function generateKingMoves(board, moves, r, f, color, castlingRights) {
const opponentColor = color === 'w' ? 'b' : 'w';
// Normal king moves
for (let dr = -1; dr <= 1; dr++) {
for (let df = -1; df <= 1; df++) {
if (dr === 0 && df === 0)
continue;
const nr = r + dr, nf = f + df;
if (nr < 0 || nr > 7 || nf < 0 || nf > 7)
continue;
const target = board[nr][nf];
if (!target || target.color !== color) {
moves.push({ fromR: r, fromF: f, toR: nr, toF: nf, pieceType: 'K', captured: target?.type });
}
}
}
// Castling
const row = color === 'w' ? 7 : 0;
if (r !== row || f !== 4)
return; // King must be on home square for castling
// Kingside castling
if ((color === 'w' && castlingRights.wK) || (color === 'b' && castlingRights.bK)) {
if (!board[row][5] && !board[row][6]) {
const rook = board[row][7];
if (rook && rook.type === 'R' && rook.color === color) {
// King cannot be in check, pass through check, or end in check
if (!(0, board_1.isSquareAttacked)(board, row, 4, opponentColor) &&
!(0, board_1.isSquareAttacked)(board, row, 5, opponentColor) &&
!(0, board_1.isSquareAttacked)(board, row, 6, opponentColor)) {
moves.push({ fromR: r, fromF: f, toR: row, toF: 6, pieceType: 'K', castling: 'K' });
}
}
}
}
// Queenside castling
if ((color === 'w' && castlingRights.wQ) || (color === 'b' && castlingRights.bQ)) {
if (!board[row][3] && !board[row][2] && !board[row][1]) {
const rook = board[row][0];
if (rook && rook.type === 'R' && rook.color === color) {
// King cannot be in check, pass through check, or end in check
if (!(0, board_1.isSquareAttacked)(board, row, 4, opponentColor) &&
!(0, board_1.isSquareAttacked)(board, row, 3, opponentColor) &&
!(0, board_1.isSquareAttacked)(board, row, 2, opponentColor)) {
moves.push({ fromR: r, fromF: f, toR: row, toF: 2, pieceType: 'K', castling: 'Q' });
}
}
}
}
}
// ─── Legal Move Filtering ──────────────────────────────────────
// Filters pseudo-legal moves by checking if the king would be in check after the move.
function makeMoveOnBoard(board, move) {
const newBoard = (0, board_1.cloneBoard)(board);
const piece = newBoard[move.fromR][move.fromF];
// En passant capture
if (move.enPassant) {
const capturedRow = piece.color === 'w' ? move.toR + 1 : move.toR - 1;
newBoard[capturedRow][move.toF] = null;
}
newBoard[move.fromR][move.fromF] = null;
if (move.promotion) {
newBoard[move.toR][move.toF] = { color: piece.color, type: move.promotion };
}
else {
newBoard[move.toR][move.toF] = piece;
}
// Castling rook movement
if (move.castling) {
const isKingside = move.castling === 'K';
const rookFromF = isKingside ? 7 : 0;
const rookToF = isKingside ? 5 : 3;
newBoard[move.fromR][rookFromF] = null;
newBoard[move.fromR][rookToF] = piece.color === 'w'
? { color: 'w', type: 'R' }
: { color: 'b', type: 'R' };
}
return newBoard;
}
function generateLegalMoves(state) {
const pseudoMoves = generatePseudoMoves(state.board, state.turn, state.castlingRights, state.epSquare);
const opponentColor = state.turn === 'w' ? 'b' : 'w';
return pseudoMoves.filter(move => {
const newBoard = makeMoveOnBoard(state.board, move);
// After the move, our king must not be in check
return !(0, board_1.isInCheck)(newBoard, state.turn);
});
}
// ─── Game Status Detection ─────────────────────────────────────
function getGameStatus(state) {
const legalMoves = generateLegalMoves(state);
const inCheck = (0, board_1.isInCheck)(state.board, state.turn);
// Check for insufficient material FIRST (before checkmate/stalemate)
if (isInsufficientMaterial(state.board)) {
return { inCheck: false, checkmate: false, stalemate: false, insufficientMaterial: true, fiftyMoveRule: false };
}
// Fifty-move rule
if (state.halfMoveClock >= 100) {
return { inCheck, checkmate: false, stalemate: false, insufficientMaterial: false, fiftyMoveRule: true };
}
if (legalMoves.length === 0) {
if (inCheck) {
return { inCheck, checkmate: true, stalemate: false, insufficientMaterial: false, fiftyMoveRule: false };
}
else {
return { inCheck: false, checkmate: false, stalemate: true, insufficientMaterial: false, fiftyMoveRule: false };
}
}
return { inCheck, checkmate: false, stalemate: false, insufficientMaterial: false, fiftyMoveRule: false };
}
function isInsufficientMaterial(board) {
const pieces = [];
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
if (board[r][f])
pieces.push(board[r][f]);
}
}
// K vs K
if (pieces.length === 2)
return true;
// K+B vs K or K+N vs K
if (pieces.length === 3) {
const nonKing = pieces.find(p => p.type !== 'K');
if (nonKing && (nonKing.type === 'B' || nonKing.type === 'N'))
return true;
}
// K+B vs K+B (same colored bishops) — rare but possible draw
if (pieces.length === 4) {
const bishops = pieces.filter(p => p.type === 'B');
if (bishops.length === 2) {
// Find bishop squares' colors
let b1SquareColor = null;
let b2SquareColor = null;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = board[r][f];
if (p && p.type === 'B') {
const sqColor = (r + f) % 2;
if (b1SquareColor === null)
b1SquareColor = sqColor;
else
b2SquareColor = sqColor;
}
}
}
if (b1SquareColor !== null && b2SquareColor !== null && b1SquareColor === b2SquareColor) {
return true;
}
}
}
return false;
}
// ─── Algebraic Notation ────────────────────────────────────────
function moveToAlgebraic(state, move) {
const piece = state.board[move.fromR][move.fromF];
if (!piece)
return '?';
// Convert internal coords to chess notation (row 0 = rank 8, row 7 = rank 1)
const fileFrom = String.fromCharCode('a'.charCodeAt(0) + move.fromF);
const fileTo = String.fromCharCode('a'.charCodeAt(0) + move.toF);
const rankFrom = String(8 - move.fromR);
const rankTo = String(8 - move.toR);
// Castling
if (move.castling === 'K')
return 'O-O';
if (move.castling === 'Q')
return 'O-O-O';
let notation = '';
// Piece letter (not for pawns)
if (piece.type !== 'P') {
notation += piece.type;
}
// Disambiguation: check if another piece of same type/color can move to the same square
const legalMoves = generateLegalMoves(state);
const ambiguous = legalMoves.filter(m => m.toR === move.toR && m.toF === move.toF &&
state.board[m.fromR][m.fromF]?.type === piece.type &&
state.board[m.fromR][m.fromF]?.color === piece.color &&
(m.fromR !== move.fromR || m.fromF !== move.fromF));
if (ambiguous.length > 0) {
const sameFile = ambiguous.some(m => m.fromF === move.fromF);
const sameRank = ambiguous.some(m => m.fromR === move.fromR);
if (!sameFile) {
notation += fileFrom;
}
else if (!sameRank) {
notation += rankFrom;
}
else {
notation += fileFrom + rankFrom;
}
}
// Capture
if (move.captured) {
if (piece.type === 'P') {
notation += fileFrom;
}
notation += 'x';
}
// Destination square
notation += fileTo + rankTo;
// Promotion
if (move.promotion) {
notation += '=' + move.promotion;
}
// En passant annotation
if (move.enPassant) {
notation += ' e.p.';
}
// Check / checkmate suffix — determined after the move is made
// We'll add this in the UI layer where we have post-move state
return notation;
}
// Add check/mate suffix to a notation string given the resulting game status
function addCheckSuffix(notation, statusAfterMove) {
if (statusAfterMove.checkmate)
return notation + '#';
if (statusAfterMove.inCheck)
return notation + '+';
return notation;
}
};
// ── module: src/eval.ts ──
__mods["src/eval.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = evaluate;
exports.orderMoves = orderMoves;
const constants_1 = require("./constants");
// ─── Position Evaluation ────────────────────────────────────────
// Returns a score from White's perspective. Positive = white advantage.
function evaluate(board) {
let score = 0;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const piece = board[r][f];
if (!piece)
continue;
const idx = r * 8 + f;
let pstIdx;
// Mirror PST for black pieces
if (piece.color === 'w') {
pstIdx = idx;
}
else {
pstIdx = ((7 - r) * 8) + f;
}
const material = constants_1.PIECE_VALUES[piece.type];
const positional = constants_1.PST[piece.type][pstIdx] || 0;
const pieceScore = material + positional;
if (piece.color === 'w') {
score += pieceScore;
}
else {
score -= pieceScore;
}
}
}
return score;
}
function orderMoves(state, moves) {
// Sort in-place by capture value (MVV-LVA style)
const pieceValue = constants_1.PIECE_VALUES;
moves.sort((a, b) => {
let scoreA = 0;
let scoreB = 0;
// Captures first: MVV-LVA (Most Valuable Victim - Least Valuable Aggressor)
if (a.captured) {
scoreA += pieceValue[a.captured] * 10 - pieceValue[a.pieceType];
}
if (b.captured) {
scoreB += pieceValue[b.captured] * 10 - pieceValue[b.pieceType];
}
// Promotion next
if (a.promotion)
scoreA += pieceValue[a.promotion];
if (b.promotion)
scoreB += pieceValue[b.promotion];
// Castling is good
if (a.castling)
scoreA += 50;
if (b.castling)
scoreB += 50;
return scoreB - scoreA; // descending order
});
}
};
// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.findBestMove = findBestMove;
exports.findBestMoveIterative = findBestMoveIterative;
const movegen_1 = require("./movegen");
const board_1 = require("./board");
const eval_1 = require("./eval");
// ─── Minimax with Alpha-Beta Pruning ────────────────────────────
const INF = 100000;
function minimax(state, depth, alpha, beta, maximizingPlayer, nodeCount // mutable counter in array for pass-by-reference
) {
const status = (0, movegen_1.getGameStatus)(state);
if (status.checkmate) {
// The player whose turn it is has lost
return maximizingPlayer ? -INF + (100 - depth) : INF - (100 - depth);
}
if (status.stalemate || status.insufficientMaterial || status.fiftyMoveRule) {
return 0; // Draw
}
if (depth === 0) {
// Quiescence search: only evaluate captures at leaf nodes
return quiesce(state, alpha, beta, maximizingPlayer, nodeCount);
}
const moves = (0, movegen_1.generateLegalMoves)(state);
(0, eval_1.orderMoves)(state, moves);
if (moves.length === 0) {
return 0; // Should be caught by checkmate/stalemate above
}
if (maximizingPlayer) {
let maxEval = -INF;
for (const move of moves) {
const newState = (0, board_1.applyMove)(state, move);
const eval_ = minimax(newState, depth - 1, alpha, beta, false, nodeCount);
nodeCount[0]++;
if (nodeCount[0] % 256 === 0 && nodeCount[0] > 10000) {
// Yield check — caller handles this
}
maxEval = Math.max(maxEval, eval_);
alpha = Math.max(alpha, eval_);
if (beta <= alpha)
break; // Beta cutoff
}
return maxEval;
}
else {
let minEval = INF;
for (const move of moves) {
const newState = (0, board_1.applyMove)(state, move);
const eval_ = minimax(newState, depth - 1, alpha, beta, true, nodeCount);
nodeCount[0]++;
if (nodeCount[0] % 256 === 0 && nodeCount[0] > 10000) {
// Yield check
}
minEval = Math.min(minEval, eval_);
beta = Math.min(beta, eval_);
if (beta <= alpha)
break; // Alpha cutoff
}
return minEval;
}
}
// ─── Quiescence Search ──────────────────────────────────────────
// At leaf nodes, only search captures to avoid horizon effect.
function quiesce(state, alpha, beta, maximizingPlayer, nodeCount) {
const standPat = (0, eval_1.evaluate)(state.board);
nodeCount[0]++;
if (maximizingPlayer) {
if (standPat >= beta)
return beta;
if (standPat > alpha)
alpha = standPat;
}
else {
if (standPat <= alpha)
return alpha;
if (standPat < beta)
beta = standPat;
}
const moves = (0, movegen_1.generateLegalMoves)(state);
// Only consider captures and promotions for quiescence
const captureMoves = moves.filter(m => m.captured || m.promotion);
(0, eval_1.orderMoves)(state, captureMoves);
for (const move of captureMoves) {
const newState = (0, board_1.applyMove)(state, move);
const eval_ = quiesce(newState, alpha, beta, !maximizingPlayer, nodeCount);
if (maximizingPlayer) {
if (eval_ >= beta)
return beta;
if (eval_ > alpha)
alpha = eval_;
}
else {
if (eval_ <= alpha)
return alpha;
if (eval_ < beta)
beta = eval_;
}
}
return maximizingPlayer ? alpha : beta;
}
// ─── AI Move Selection ──────────────────────────────────────────
function findBestMove(state, depth, onProgress) {
return new Promise((resolve) => {
const moves = (0, movegen_1.generateLegalMoves)(state);
if (moves.length === 0) {
resolve(null);
return;
}
(0, eval_1.orderMoves)(state, moves);
// Determine who is maximizing (white = positive eval)
const whiteToMove = state.turn === 'w';
const maximizingPlayer = whiteToMove;
let bestMove = null;
let bestScore = maximizingPlayer ? -INF : INF;
const nodeCount = [0];
// Process moves in chunks to avoid blocking the UI
let moveIndex = 0;
function processNext() {
while (moveIndex < moves.length) {
const move = moves[moveIndex];
moveIndex++;
const newState = (0, board_1.applyMove)(state, move);
const score = minimax(newState, depth - 1, -INF, INF, !maximizingPlayer, nodeCount);
if (onProgress)
onProgress(nodeCount[0]);
if (maximizingPlayer) {
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
else {
if (score < bestScore) {
bestScore = score;
bestMove = move;
}
}
// Yield to the event loop every so often
if (nodeCount[0] % 5000 === 0 && nodeCount[0] > 1000) {
setTimeout(processNext, 0);
return;
}
}
resolve(bestMove);
}
processNext();
});
}
// ─── Iterative Deepening (optional improvement) ─────────────────
function findBestMoveIterative(state, maxDepth, onProgress) {
return new Promise((resolve) => {
let bestMove = null;
async function searchAtDepth(d) {
const moves = (0, movegen_1.generateLegalMoves)(state);
if (moves.length === 0) {
resolve(null);
return;
}
(0, eval_1.orderMoves)(state, moves);
const whiteToMove = state.turn === 'w';
const maximizingPlayer = whiteToMove;
let bestScore = maximizingPlayer ? -INF : INF;
const nodeCount = [0];
for (const move of moves) {
const newState = (0, board_1.applyMove)(state, move);
const score = minimax(newState, d - 1, -INF, INF, !maximizingPlayer, nodeCount);
if (onProgress)
onProgress(nodeCount[0], d);
if (maximizingPlayer ? score > bestScore : score < bestScore) {
bestScore = score;
bestMove = move;
}
}
// If we haven't reached max depth, try deeper
if (d < maxDepth && bestMove) {
setTimeout(() => searchAtDepth(d + 1), 0);
}
else {
resolve(bestMove);
}
}
searchAtDepth(1);
});
}
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.setMuted = setMuted;
exports.isMuted = isMuted;
exports.playMove = playMove;
exports.playCapture = playCapture;
exports.playCheck = playCheck;
exports.playGameOver = playGameOver;
exports.playPromotion = playPromotion;
const constants_1 = require("./constants");
// ─── Sound Effects via Web Audio API ─────────────────────────────
let audioCtx = null;
let muted = false;
function initAudio() {
if (!audioCtx) {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
catch {
// Web Audio not available, sounds will be silent
}
}
}
function setMuted(value) {
muted = value;
}
function isMuted() {
return muted;
}
function playTone(freq, duration, volume, type = 'sine') {
if (muted || !audioCtx)
return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(volume, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
catch {
// Ignore audio errors
}
}
function playMove() {
playTone(constants_1.SOUND_FREQS.move, 0.1, 0.3, 'sine');
}
function playCapture() {
playTone(constants_1.SOUND_FREQS.capture, 0.15, 0.4, 'triangle');
}
function playCheck() {
playTone(constants_1.SOUND_FREQS.check, 0.2, 0.35, 'square');
}
function playGameOver() {
// Play a short descending melody
if (muted || !audioCtx)
return;
const notes = [400, 350, 300];
notes.forEach((freq, i) => {
setTimeout(() => playTone(freq, 0.2, 0.3, 'sine'), i * 150);
});
}
function playPromotion() {
playTone(600, 0.1, 0.3, 'sine');
setTimeout(() => playTone(800, 0.15, 0.3, 'sine'), 100);
}
};
// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveGame = saveGame;
exports.loadGame = loadGame;
exports.clearSave = clearSave;
const board_1 = require("./board");
const STORAGE_KEY = 'chess-game-state';
// ─── Save / Load Game State ─────────────────────────────────────
function saveGame(state) {
try {
const data = JSON.stringify({
fen: (0, board_1.boardToFen)(state),
moveHistory: state.moveHistory,
capturedPieces: state.capturedPieces,
});
localStorage.setItem(STORAGE_KEY, data);
}
catch {
// Storage not available or full — silently ignore
}
}
function loadGame() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return null;
const data = JSON.parse(raw);
const state = (0, board_1.fenToBoard)(data.fen);
if (!state)
return null;
// Restore move history and captured pieces
if (data.moveHistory) {
state.moveHistory = data.moveHistory;
}
if (data.capturedPieces) {
state.capturedPieces = data.capturedPieces;
}
return state;
}
catch {
return null;
}
}
function clearSave() {
try {
localStorage.removeItem(STORAGE_KEY);
}
catch {
// Ignore
}
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRenderer = initRenderer;
exports.render = render;
exports.getSquareAtPixel = getSquareAtPixel;
exports.initPromotionUI = initPromotionUI;
exports.showPromotionPicker = showPromotionPicker;
exports.hidePromotionPicker = hidePromotionPicker;
const constants_1 = require("./constants");
let canvas;
let ctx;
const SQUARE_SIZE = 70; // will be recalculated based on canvas size
function getSquareSize() {
return canvas.width / 8;
}
function initRenderer(canvasEl) {
canvas = canvasEl;
ctx = canvas.getContext('2d');
ctx.imageSmoothingEnabled = false;
}
// Convert board coords to pixel coords (accounting for flip)
function boardToPixel(r, f, flipped) {
const sqSize = getSquareSize();
const drawR = flipped ? (7 - r) : r;
const drawF = flipped ? (7 - f) : f;
return [drawF * sqSize, drawR * sqSize];
}
// Convert pixel coords to board coords
function pixelToBoard(px, py, flipped) {
const sqSize = getSquareSize();
let f = Math.floor(px / sqSize);
let r = Math.floor(py / sqSize);
if (f < 0 || f > 7 || r < 0 || r > 7)
return null;
if (flipped) {
r = 7 - r;
f = 7 - f;
}
return [r, f];
}
function render(state) {
const sqSize = getSquareSize();
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw squares
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const [x, y] = boardToPixel(r, f, state.flipped);
const isLight = (r + f) % 2 === 0;
// Base square color
ctx.fillStyle = isLight ? constants_1.COLORS.lightSquare : constants_1.COLORS.darkSquare;
ctx.fillRect(x, y, sqSize, sqSize);
// Last move highlight
if (state.lastMove &&
((r === state.lastMove.fromR && f === state.lastMove.fromF) ||
(r === state.lastMove.toR && f === state.lastMove.toF))) {
ctx.fillStyle = constants_1.COLORS.highlightLastMove;
ctx.fillRect(x, y, sqSize, sqSize);
}
// Selected square highlight
if (state.selectedSquare && state.selectedSquare[0] === r && state.selectedSquare[1] === f) {
ctx.fillStyle = constants_1.COLORS.selectedSquare;
ctx.fillRect(x, y, sqSize, sqSize);
}
// Check highlight on king's square
if (state.inCheck) {
const kingPos = findKingOnBoard(state.board, state.flipped ? 'b' : 'w');
// Actually check the current player's king
// We need to know whose turn it is — but we don't have that here.
// The inCheck flag tells us the current side is in check.
}
// Legal move dots and capture rings
const legalMovesForSquare = state.legalMoves.filter(m => m.fromR === r && m.fromF === f);
for (const move of legalMovesForSquare) {
const [mx, my] = boardToPixel(move.toR, move.toF, state.flipped);
if (state.board[move.toR][move.toF]) {
// Capture ring
ctx.strokeStyle = constants_1.COLORS.highlightCaptureRing;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(mx + sqSize / 2, my + sqSize / 2, sqSize * 0.45, 0, Math.PI * 2);
ctx.stroke();
}
else {
// Dot for empty square
ctx.fillStyle = constants_1.COLORS.highlightLegalDot;
ctx.beginPath();
ctx.arc(mx + sqSize / 2, my + sqSize / 2, sqSize * 0.15, 0, Math.PI * 2);
ctx.fill();
}
}
// Draw pieces
const piece = state.board[r][f];
if (piece) {
drawPiece(piece, x, y, sqSize);
}
// Check glow on king
if (state.inCheck && piece && piece.type === 'K') {
ctx.fillStyle = 'rgba(255, 0, 0, 0.35)';
ctx.fillRect(x, y, sqSize, sqSize);
}
// Coordinate labels
const [drawR, drawF] = state.flipped ? [7 - r, 7 - f] : [r, f];
if (f === 0) {
ctx.fillStyle = isLight ? constants_1.COLORS.darkSquare : constants_1.COLORS.lightSquare;
ctx.font = `bold ${sqSize * 0.14}px sans-serif`;
ctx.textAlign = 'left';
ctx.textBaseline = 'top';
ctx.fillText(String(8 - r), x + 3, y + 2);
}
if (r === 7) {
ctx.fillStyle = isLight ? constants_1.COLORS.darkSquare : constants_1.COLORS.lightSquare;
ctx.font = `bold ${sqSize * 0.14}px sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
ctx.fillText(String.fromCharCode('a'.charCodeAt(0) + f), x + sqSize / 2, y + sqSize - 2);
}
}
}
// Draw check glow on king's square (after pieces so it overlays)
if (state.inCheck) {
const kingPos = findKingOnBoard(state.board, 'w');
if (!kingPos) { /* try black */ }
// We need to know whose turn — let's just highlight any king that matches inCheck
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = state.board[r][f];
if (p && p.type === 'K') {
// Check if this king is in check by testing attacks
const [x, y] = boardToPixel(r, f, state.flipped);
ctx.fillStyle = 'rgba(255, 0, 0, 0.3)';
ctx.fillRect(x, y, sqSize, sqSize);
// Red border glow
ctx.strokeStyle = 'rgba(255, 0, 0, 0.6)';
ctx.lineWidth = 3;
ctx.strokeRect(x + 1, y + 1, sqSize - 2, sqSize - 2);
}
}
}
}
}
function drawPiece(piece, x, y, sqSize) {
const glyph = constants_1.PIECE_UNICODE[piece.color][piece.type];
const fontSize = sqSize * 0.78;
ctx.font = `${fontSize}px "Segoe UI Symbol", "Noto Sans Symbols2", sans-serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
// Shadow for depth
ctx.fillStyle = 'rgba(0, 0, 0, 0.3)';
ctx.fillText(glyph, x + sqSize / 2 + 1, y + sqSize / 2 + 2);
// Piece color with slight gradient effect
if (piece.color === 'w') {
ctx.fillStyle = '#FFFFFF';
ctx.strokeStyle = '#333333';
ctx.lineWidth = 1.5;
ctx.fillText(glyph, x + sqSize / 2, y + sqSize / 2);
ctx.strokeText(glyph, x + sqSize / 2, y + sqSize / 2);
}
else {
ctx.fillStyle = '#1a1a1a';
ctx.strokeStyle = '#FFFFFF';
ctx.lineWidth = 0.8;
ctx.fillText(glyph, x + sqSize / 2, y + sqSize / 2);
ctx.strokeText(glyph, x + sqSize / 2, y + sqSize / 2);
}
}
function findKingOnBoard(board, color) {
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = board[r][f];
if (p && p.color === color && p.type === 'K')
return [r, f];
}
}
return null;
}
function getSquareAtPixel(px, py, flipped) {
return pixelToBoard(px, py, flipped);
}
// ─── Promotion Picker Rendering ────────────────────────────────
let promoOverlay;
let promoPiecesDiv;
function initPromotionUI(overlayEl, piecesEl) {
promoOverlay = overlayEl;
promoPiecesDiv = piecesEl;
}
function showPromotionPicker(color, onChoose) {
promoPiecesDiv.innerHTML = '';
const types = ['Q', 'R', 'B', 'N'];
for (const type of types) {
const btn = document.createElement('div');
btn.className = 'promo-piece';
btn.textContent = constants_1.PIECE_UNICODE[color][type];
btn.addEventListener('click', () => {
hidePromotionPicker();
onChoose(type);
});
promoPiecesDiv.appendChild(btn);
}
promoOverlay.classList.remove('hidden');
}
function hidePromotionPicker() {
promoOverlay.classList.add('hidden');
}
};
// ── module: src/input.ts ──
__mods["src/input.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initInput = initInput;
exports.setFlipped = setFlipped;
const renderer_1 = require("./renderer");
let canvas;
let flipped = false;
let callbacks = null;
// Drag state
let isDragging = false;
let dragFromSquare = null;
let dragCurrentSquare = null;
let legalMovesForDrag = [];
function initInput(canvasEl, cb) {
canvas = canvasEl;
callbacks = cb;
// Mouse events
canvas.addEventListener('mousedown', onMouseDown);
canvas.addEventListener('mousemove', onMouseMove);
canvas.addEventListener('mouseup', onMouseUp);
canvas.addEventListener('mouseleave', onMouseLeave);
// Touch events (for mobile)
canvas.addEventListener('touchstart', onTouchStart, { passive: false });
canvas.addEventListener('touchmove', onTouchMove, { passive: false });
canvas.addEventListener('touchend', onTouchEnd, { passive: false });
// Prevent context menu on right click
canvas.addEventListener('contextmenu', e => e.preventDefault());
}
function setFlipped(value) {
flipped = value;
}
function getCanvasCoords(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return [(e.clientX - rect.left) * scaleX, (e.clientY - rect.top) * scaleY];
}
function getTouchCoords(e) {
const touch = e.touches[0] || e.changedTouches[0];
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
return [(touch.clientX - rect.left) * scaleX, (touch.clientY - rect.top) * scaleY];
}
function onMouseDown(e) {
if (!callbacks)
return;
e.preventDefault();
const [px, py] = getCanvasCoords(e);
const sq = (0, renderer_1.getSquareAtPixel)(px, py, flipped);
if (sq) {
callbacks.onSquareClick(sq[0], sq[1]);
isDragging = true;
dragFromSquare = sq;
dragCurrentSquare = sq;
}
}
function onMouseMove(e) {
if (!isDragging || !callbacks)
return;
e.preventDefault();
const [px, py] = getCanvasCoords(e);
const sq = (0, renderer_1.getSquareAtPixel)(px, py, flipped);
if (sq && !(dragCurrentSquare && dragCurrentSquare[0] === sq[0] && dragCurrentSquare[1] === sq[1])) {
// Square changed during drag — trigger click to update highlights
callbacks.onSquareClick(sq[0], sq[1]);
dragCurrentSquare = sq;
}
}
function onMouseUp(e) {
if (!isDragging || !callbacks)
return;
isDragging = false;
dragFromSquare = null;
dragCurrentSquare = null;
}
function onMouseLeave() {
isDragging = false;
dragFromSquare = null;
dragCurrentSquare = null;
}
// Touch handlers
function onTouchStart(e) {
if (!callbacks)
return;
e.preventDefault();
const [px, py] = getTouchCoords(e);
const sq = (0, renderer_1.getSquareAtPixel)(px, py, flipped);
if (sq) {
callbacks.onSquareClick(sq[0], sq[1]);
isDragging = true;
dragFromSquare = sq;
dragCurrentSquare = sq;
}
}
function onTouchMove(e) {
if (!isDragging || !callbacks)
return;
e.preventDefault();
const [px, py] = getTouchCoords(e);
const sq = (0, renderer_1.getSquareAtPixel)(px, py, flipped);
if (sq && !(dragCurrentSquare && dragCurrentSquare[0] === sq[0] && dragCurrentSquare[1] === sq[1])) {
callbacks.onSquareClick(sq[0], sq[1]);
dragCurrentSquare = sq;
}
}
function onTouchEnd(e) {
if (!isDragging || !callbacks)
return;
e.preventDefault();
isDragging = false;
dragFromSquare = null;
dragCurrentSquare = null;
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.createGame = createGame;
const board_1 = require("./board");
const movegen_1 = require("./movegen");
const renderer_1 = require("./renderer");
const input_1 = require("./input");
const ai_1 = require("./ai");
const sound = __importStar(require("./sound"));
const persistence_1 = require("./persistence");
class GameController {
constructor(config) {
this.selectedSquare = null;
this.legalMovesForSelected = [];
this.flipped = false;
this.gameOver = false;
this.aiThinking = false;
this.pendingPromotionMove = null;
this.config = config;
// Try to load saved game, otherwise create new one
const saved = (0, persistence_1.loadGame)();
if (saved) {
this.state = saved;
}
else {
this.state = (0, board_1.createInitialState)();
}
// DOM elements
this.canvas = document.getElementById('board-canvas');
this.turnIndicator = document.getElementById('turn-indicator');
this.statusBanner = document.getElementById('status-banner');
this.moveListEl = document.getElementById('move-list');
this.capturedWhiteEl = document.getElementById('captured-white');
this.capturedBlackEl = document.getElementById('captured-black');
this.colorChoiceEl = document.getElementById('color-choice');
// If we loaded a saved game, hide color choice
if (saved) {
this.colorChoiceEl.classList.add('hidden');
}
else {
this.colorChoiceEl.classList.remove('hidden');
}
// Initialize renderer and input
(0, renderer_1.initRenderer)(this.canvas);
(0, renderer_1.initPromotionUI)(document.getElementById('promotion-overlay'), document.getElementById('promotion-pieces'));
(0, input_1.initInput)(this.canvas, {
onSquareClick: (r, f) => this.onSquareClick(r, f),
});
// Wire up buttons
document.getElementById('btn-undo').addEventListener('click', () => this.undo());
document.getElementById('btn-flip').addEventListener('click', () => this.flipBoard());
document.getElementById('btn-newgame').addEventListener('click', () => this.newGame());
document.getElementById('btn-mute').addEventListener('click', () => this.toggleMute());
const difficultyEl = document.getElementById('difficulty');
difficultyEl.addEventListener('change', () => {
this.config.aiDepth = parseInt(difficultyEl.value);
});
// Color choice buttons
document.getElementById('btn-white').addEventListener('click', () => {
this.config.playerColor = 'w';
this.colorChoiceEl.classList.add('hidden');
sound.initAudio();
this.renderAll();
});
document.getElementById('btn-black').addEventListener('click', () => {
this.config.playerColor = 'b';
this.colorChoiceEl.classList.add('hidden');
sound.initAudio();
// If AI is white, make first move
if (this.state.turn === 'w' && !this.gameOver) {
setTimeout(() => this.aiMove(), 300);
}
this.renderAll();
});
document.getElementById('btn-analysis').addEventListener('click', () => {
this.config.playerColor = null; // analysis mode
this.colorChoiceEl.classList.add('hidden');
sound.initAudio();
this.renderAll();
});
// Initial render
this.renderAll();
}
onSquareClick(r, f) {
if (this.gameOver || this.aiThinking)
return;
// If color not chosen yet, ignore clicks
if (this.config.playerColor === undefined)
return;
const piece = this.state.board[r][f];
// Check if clicking on a legal move destination
if (this.selectedSquare && !piece) {
const move = this.legalMovesForSelected.find(m => m.toR === r && m.toF === f);
if (move) {
this.executePlayerMove(move);
return;
}
}
// Check if clicking on a legal move destination that has an opponent piece
if (this.selectedSquare && piece && piece.color !== this.state.turn) {
const move = this.legalMovesForSelected.find(m => m.toR === r && m.toF === f);
if (move) {
this.executePlayerMove(move);
return;
}
}
// Select a piece of the current turn's color
if (piece && piece.color === this.state.turn) {
// If clicking same square, deselect
if (this.selectedSquare && this.selectedSquare[0] === r && this.selectedSquare[1] === f) {
this.selectedSquare = null;
this.legalMovesForSelected = [];
}
else {
this.selectedSquare = [r, f];
this.legalMovesForSelected = (0, movegen_1.generateLegalMoves)(this.state).filter(m => m.fromR === r && m.fromF === f);
}
this.renderAll();
return;
}
// Clicking empty square or opponent piece without selection — deselect
if (this.selectedSquare) {
this.selectedSquare = null;
this.legalMovesForSelected = [];
this.renderAll();
}
}
executePlayerMove(move) {
// Check for promotion
if (move.promotion) {
this.pendingPromotionMove = { fromR: move.fromR, fromF: move.fromF, toR: move.toR, toF: move.toF };
(0, renderer_1.showPromotionPicker)(this.state.turn, (promoType) => {
const promoMove = { ...move, promotion: promoType };
this.finalizePlayerMove(promoMove);
});
return;
}
this.finalizePlayerMove(move);
}
finalizePlayerMove(move) {
(0, renderer_1.hidePromotionPicker)();
this.pendingPromotionMove = null;
// Play sound
if (move.captured)
sound.playCapture();
else
sound.playMove();
// Apply move
const notationBefore = (0, movegen_1.moveToAlgebraic)(this.state, move);
this.state = (0, board_1.applyMove)(this.state, move);
// Check for check/mate status after the move
const statusAfter = (0, movegen_1.getGameStatus)(this.state);
if (statusAfter.inCheck)
sound.playCheck();
const fullNotation = (0, movegen_1.addCheckSuffix)(notationBefore, {
checkmate: statusAfter.checkmate,
stalemate: statusAfter.stalemate,
inCheck: statusAfter.inCheck,
});
// Record notation
this.addMoveToHistory(fullNotation);
// Clear selection
this.selectedSquare = null;
this.legalMovesForSelected = [];
// Save game state
(0, persistence_1.saveGame)(this.state);
// Render
this.renderAll();
// Check for game over
if (statusAfter.checkmate || statusAfter.stalemate || statusAfter.insufficientMaterial || statusAfter.fiftyMoveRule) {
this.handleGameOver(statusAfter);
return;
}
// AI's turn?
const isPlayerTurn = !this.config.playerColor || this.state.turn === this.config.playerColor;
if (!isPlayerTurn && !this.gameOver) {
setTimeout(() => this.aiMove(), 100);
}
}
async aiMove() {
if (this.gameOver)
return;
this.aiThinking = true;
this.updateTurnIndicator();
const depth = this.config.aiDepth;
const move = await (0, ai_1.findBestMoveIterative)(this.state, depth);
this.aiThinking = false;
if (!move) {
// No legal moves — should be caught by game over detection
return;
}
// Play sound
if (move.captured)
sound.playCapture();
else
sound.playMove();
const notationBefore = (0, movegen_1.moveToAlgebraic)(this.state, move);
this.state = (0, board_1.applyMove)(this.state, move);
const statusAfter = (0, movegen_1.getGameStatus)(this.state);
if (statusAfter.inCheck)
sound.playCheck();
const fullNotation = (0, movegen_1.addCheckSuffix)(notationBefore, {
checkmate: statusAfter.checkmate,
stalemate: statusAfter.stalemate,
inCheck: statusAfter.inCheck,
});
this.addMoveToHistory(fullNotation);
(0, persistence_1.saveGame)(this.state);
this.renderAll();
if (statusAfter.checkmate || statusAfter.stalemate || statusAfter.insufficientMaterial || statusAfter.fiftyMoveRule) {
this.handleGameOver(statusAfter);
}
}
addMoveToHistory(notation) {
const moveNum = Math.ceil(this.state.moveHistory.length / 2); // length already includes this move
const isWhiteMove = (this.state.moveHistory.length - 1) % 2 === 0;
if (isWhiteMove) {
// White's move — create new entry
const numSpan = document.createElement('span');
numSpan.className = 'move-number';
numSpan.textContent = String(moveNum) + '.';
this.moveListEl.appendChild(numSpan);
const whiteSpan = document.createElement('span');
whiteSpan.className = 'white-move';
whiteSpan.textContent = notation;
this.moveListEl.appendChild(whiteSpan);
}
else {
// Black's move — append to existing number
const blackSpan = document.createElement('span');
blackSpan.className = 'black-move';
blackSpan.textContent = notation;
this.moveListEl.appendChild(blackSpan);
}
// Auto-scroll
const container = document.getElementById('move-list-container');
container.scrollTop = container.scrollHeight;
}
handleGameOver(status) {
this.gameOver = true;
sound.playGameOver();
let message = '';
if (status.checkmate) {
const winner = this.state.turn === 'w' ? 'Black' : 'White';
message = `Checkmate! ${winner} wins.`;
}
else if (status.stalemate) {
message = 'Stalemate — Draw.';
}
else if (status.insufficientMaterial) {
message = 'Draw by insufficient material.';
}
else if (status.fiftyMoveRule) {
message = 'Draw by 50-move rule.';
}
this.statusBanner.textContent = message;
this.statusBanner.classList.remove('hidden');
}
undo() {
if (this.gameOver || this.aiThinking)
return;
if (this.state.moveHistory.length < 2)
return; // Need at least a pair of moves
// Undo both player and AI move
const movesToUndo = Math.min(2, this.state.moveHistory.length);
for (let i = 0; i < movesToUndo; i++) {
// We need to reverse the state — simplest approach: rebuild from FEN or re-apply all but last N moves
// For simplicity, let's just reset and replay all moves except the last two
break;
}
// Rebuild state by replaying all moves except the last 2
const newState = (0, board_1.createInitialState)();
const movesToKeep = this.state.moveHistory.slice(0, -movesToUndo);
let tempState = newState;
for (const move of movesToKeep) {
tempState = (0, board_1.applyMove)(tempState, move);
}
// Rebuild captured pieces from the replayed moves
const capturedPieces = { w: [], b: [] };
let cs = newState;
for (const move of movesToKeep) {
if (move.captured) {
const pieceColor = cs.board[move.fromR][move.fromF]?.color;
if (pieceColor)
capturedPieces[pieceColor].push(move.captured);
}
cs = (0, board_1.applyMove)(cs, move);
}
this.state = tempState;
this.state.capturedPieces = capturedPieces;
this.selectedSquare = null;
this.legalMovesForSelected = [];
// Rebuild move list display
this.rebuildMoveList();
(0, persistence_1.saveGame)(this.state);
this.renderAll();
}
rebuildMoveList() {
this.moveListEl.innerHTML = '';
// We need to replay moves to get notations — simplified: just show from history
let tempState = (0, board_1.createInitialState)();
for (let i = 0; i < this.state.moveHistory.length; i++) {
const move = this.state.moveHistory[i];
const notationBefore = (0, movegen_1.moveToAlgebraic)(tempState, move);
tempState = (0, board_1.applyMove)(tempState, move);
const statusAfter = (0, movegen_1.getGameStatus)(tempState);
const fullNotation = (0, movegen_1.addCheckSuffix)(notationBefore, {
checkmate: statusAfter.checkmate,
stalemate: statusAfter.stalemate,
inCheck: statusAfter.inCheck,
});
const moveNum = Math.ceil((i + 1) / 2);
const isWhiteMove = i % 2 === 0;
if (isWhiteMove) {
const numSpan = document.createElement('span');
numSpan.className = 'move-number';
numSpan.textContent = String(moveNum) + '.';
this.moveListEl.appendChild(numSpan);
}
const span = document.createElement('span');
span.className = isWhiteMove ? 'white-move' : 'black-move';
span.textContent = fullNotation;
this.moveListEl.appendChild(span);
}
}
flipBoard() {
this.flipped = !this.flipped;
(0, input_1.setFlipped)(this.flipped);
this.renderAll();
}
newGame() {
(0, persistence_1.clearSave)();
this.state = (0, board_1.createInitialState)();
this.selectedSquare = null;
this.legalMovesForSelected = [];
this.gameOver = false;
this.aiThinking = false;
this.flipped = false;
(0, input_1.setFlipped)(false);
this.moveListEl.innerHTML = '';
this.statusBanner.classList.add('hidden');
// Show color choice again if not set
if (!this.config.playerColor) {
this.colorChoiceEl.classList.remove('hidden');
}
else {
this.colorChoiceEl.classList.add('hidden');
// If AI is white, make first move
if (this.state.turn === 'w' && this.config.playerColor !== 'w') {
setTimeout(() => this.aiMove(), 300);
}
}
this.renderAll();
}
toggleMute() {
const muted = sound.isMuted();
sound.setMuted(!muted);
document.getElementById('btn-mute').textContent = !muted ? '🔊 Sound' : '🔇 Muted';
}
updateTurnIndicator() {
if (this.aiThinking) {
this.turnIndicator.textContent = 'AI thinking...';
}
else {
const turnName = this.state.turn === 'w' ? 'White' : 'Black';
this.turnIndicator.textContent = `${turnName} to move`;
}
}
renderAll() {
// Update captured pieces display
this.updateCapturedDisplay();
this.updateTurnIndicator();
const status = (0, movegen_1.getGameStatus)(this.state);
(0, renderer_1.render)({
board: this.state.board,
selectedSquare: this.selectedSquare,
legalMoves: this.legalMovesForSelected,
lastMove: this.state.moveHistory.length > 0 ? this.state.moveHistory[this.state.moveHistory.length - 1] : null,
inCheck: status.inCheck && !this.gameOver,
flipped: this.flipped,
});
// Update canvas size for responsiveness
this.resizeCanvas();
}
updateCapturedDisplay() {
const pieceOrder = ['Q', 'R', 'B', 'N', 'P'];
const unicodePieces = constants_1.PIECE_UNICODE;
// Captured by white (black pieces)
const capturedByWhite = this.state.capturedPieces.w.sort((a, b) => pieceOrder.indexOf(a) - pieceOrder.indexOf(b));
this.capturedWhiteEl.textContent = capturedByWhite.map(p => unicodePieces.b[p]).join('');
// Captured by black (white pieces)
const capturedByBlack = this.state.capturedPieces.b.sort((a, b) => pieceOrder.indexOf(a) - pieceOrder.indexOf(b));
this.capturedBlackEl.textContent = capturedByBlack.map(p => unicodePieces.w[p]).join('');
}
resizeCanvas() {
const container = document.getElementById('board-area');
const maxSize = Math.min(container.parentElement.clientWidth - 300, 560);
const size = Math.max(320, maxSize);
this.canvas.style.width = size + 'px';
this.canvas.style.height = size + 'px';
}
// ─── Debug Interface ────────────────────────────────────────
fen() {
return (0, board_1.boardToFen)(this.state);
}
legalMoves() {
return (0, movegen_1.generateLegalMoves)(this.state);
}
getState() {
return (0, board_1.cloneState)(this.state);
}
// Expose for debugging: make a move by algebraic notation (e.g. "e2e4")
move(notation) {
const moves = (0, movegen_1.generateLegalMoves)(this.state);
const fromF = notation.charCodeAt(0) - 'a'.charCodeAt(0);
// Chess rank 1 = array index 7, rank 8 = array index 0
const fromR = 8 - parseInt(notation[1]);
const toF = notation.charCodeAt(2) - 'a'.charCodeAt(0);
const toR = 8 - parseInt(notation[3]);
let promoType;
if (notation.length > 4) {
promoType = notation[4];
}
const move = moves.find(m => m.fromF === fromF && m.fromR === fromR &&
m.toF === toF && m.toR === toR &&
(!promoType || m.promotion === promoType));
if (!move)
return 'Illegal move';
const notationBefore = (0, movegen_1.moveToAlgebraic)(this.state, move);
this.state = (0, board_1.applyMove)(this.state, move);
const statusAfter = (0, movegen_1.getGameStatus)(this.state);
const fullNotation = (0, movegen_1.addCheckSuffix)(notationBefore, {
checkmate: statusAfter.checkmate,
stalemate: statusAfter.stalemate,
inCheck: statusAfter.inCheck,
});
this.addMoveToHistory(fullNotation);
(0, persistence_1.saveGame)(this.state);
this.renderAll();
if (statusAfter.checkmate)
return 'Checkmate!';
if (statusAfter.stalemate)
return 'Stalemate!';
if (statusAfter.inCheck)
return 'Check!';
// Trigger AI if it's not the player's turn
const isPlayerTurn = !this.config.playerColor || this.state.turn === this.config.playerColor;
if (!isPlayerTurn && !this.gameOver) {
setTimeout(() => this.aiMove(), 10);
}
return 'OK';
}
}
// ─── PIECE_UNICODE import for captured display ──────────────────
const constants_1 = require("./constants");
function createGame(config) {
return new GameController(config);
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>