Chess
thinkingcap qwen3.6 27b · 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 56,555 bytes · SHA-256 524cdbd4684d
<!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;
color: #e0e0e0;
font-family: 'Segoe UI', system-ui, sans-serif;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: auto;
}
#game-container {
display: flex;
gap: 24px;
padding: 20px;
align-items: flex-start;
}
#board-area { position: relative; }
#side-panel {
width: 260px;
display: flex;
flex-direction: column;
gap: 10px;
}
#status-bar {
background: #16213e;
padding: 12px 16px;
border-radius: 8px;
font-size: 15px;
font-weight: 600;
text-align: center;
}
#turn-indicator { color: #f0c040; }
.ctrl-btn, button, select {
background: #0f3460;
color: #e0e0e0;
border: none;
padding: 8px 14px;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
}
.ctrl-btn:hover, button:hover { background: #1a5276; }
select { appearance: auto; }
#controls { display: flex; gap: 6px; flex-wrap: wrap; align-items: center; }
#move-list-container {
background: #16213e;
border-radius: 8px;
padding: 10px;
flex: 1;
min-height: 120px;
max-height: 340px;
overflow-y: auto;
}
#move-list-container h3 { font-size: 13px; margin-bottom: 6px; color: #aaa; }
#move-list { font-family: 'Courier New', monospace; font-size: 13px; line-height: 1.7; }
.move-pair { display: flex; gap: 8px; }
.move-num { color: #666; min-width: 24px; text-align: right; }
.move-w, .move-b { min-width: 50px; }
#captured-white, #captured-black {
font-size: 20px;
min-height: 28px;
padding: 4px 8px;
background: #16213e;
border-radius: 6px;
letter-spacing: 2px;
}
#game-over-banner {
background: #0f3460;
padding: 14px;
border-radius: 8px;
text-align: center;
font-weight: bold;
font-size: 15px;
}
.hidden { display: none !important; }
#promotion-overlay {
position: absolute;
background: rgba(0,0,0,0.7);
border-radius: 8px;
padding: 6px;
display: flex;
gap: 4px;
z-index: 10;
}
.promo-piece {
width: 52px; height: 52px;
font-size: 36px;
text-align: center;
line-height: 52px;
cursor: pointer;
border-radius: 4px;
background: #16213e;
}
.promo-piece:hover { background: #0f3460; }
</style>
</head>
<body>
<div id="game-container">
<div id="board-area">
<canvas id="board" width="560" height="560"></canvas>
<div id="promotion-overlay" class="hidden"></div>
</div>
<div id="side-panel">
<div id="status-bar"><span id="turn-indicator">White to move</span></div>
<div id="captured-white"></div>
<div id="board-flip-btn" class="ctrl-btn">⟳ Flip Board</div>
<div id="move-list-container">
<h3>Moves</h3>
<div id="move-list"></div>
</div>
<div id="captured-black"></div>
<div id="controls">
<select id="difficulty"><option value="1">Easy (Depth 1)</option><option value="2" selected="">Medium (Depth 2)</option><option value="3">Hard (Depth 3)</option></select>
<button id="undo-btn">↩ Undo</button>
<button id="new-game-btn">New Game</button>
<button id="mute-btn">🔊 Mute</button>
</div>
<div id="game-over-banner" class="hidden"></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./sound":"src/sound.ts","./ui":"src/ui.ts","./board":"src/board.ts"},"src/board.ts":{"./types":"src/types.ts"},"src/evaluation.ts":{"./types":"src/types.ts"},"src/ai.ts":{"./types":"src/types.ts","./board":"src/board.ts","./evaluation":"src/evaluation.ts"},"src/ui.ts":{"./types":"src/types.ts","./board":"src/board.ts","./ai":"src/ai.ts","./sound":"src/sound.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";
// ── Entry Point ──────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
const sound_1 = require("./sound");
const ui_1 = require("./ui");
const board_1 = require("./board");
(0, sound_1.initSound)();
// Restore saved game or start fresh
const saved = (0, ui_1.loadFromStorage)();
if (saved) {
(0, ui_1.setGameState)(saved);
}
else {
(0, ui_1.setGameState)((0, board_1.newGameState)());
}
(0, ui_1.setupEvents)();
(0, ui_1.exposeDebug)();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Types & Constants for Chess Engine ───────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.CONFIG = exports.PIECE_NAMES = exports.PIECE_UNICODE = exports.PIECE_VALUES = exports.PST = void 0;
// Piece-square tables (from white's perspective, row 0 = rank 8)
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, 10, 10, 10, 10, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 5, 0, 0, 0, 0, 5, -10,
-20, -10, -10, -10, -10, -10, -10, -20
],
R: [
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
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, 10, 0, 0, 0, 0, 10, 20,
20, 30, 10, 0, 0, 10, 30, 20
]
};
exports.PIECE_VALUES = {
P: 100, N: 320, B: 330, R: 500, Q: 900, K: 20000
};
// Unicode piece glyphs
exports.PIECE_UNICODE = {
'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙',
'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟'
};
exports.PIECE_NAMES = {
K: 'K', Q: 'Q', R: 'R', B: 'B', N: 'N', P: ''
};
// Tunable constants
exports.CONFIG = {
squareSize: 70,
boardColors: { light: '#f0d9b5', dark: '#b58863' },
highlightColor: 'rgba(255, 255, 0, 0.4)',
lastMoveColor: 'rgba(155, 199, 0, 0.41)',
checkGlowColor: 'rgba(255, 0, 0, 0.6)',
legalDotRadius: 12,
captureRingWidth: 4,
};
};
// ── module: src/board.ts ──
__mods["src/board.ts"] = function (exports, require, module) {
"use strict";
// ── Chess Engine: Board representation & move generation ─────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialBoard = initialBoard;
exports.initialCastling = initialCastling;
exports.newGameState = newGameState;
exports.undoStateFrom = undoStateFrom;
exports.applyMove = applyMove;
exports.isInCheck = isInCheck;
exports.generateLegalMoves = generateLegalMoves;
exports.executeMove = executeMove;
exports.checkGameEnd = checkGameEnd;
exports.toAlgebraic = toAlgebraic;
exports.toFen = toFen;
exports.fromFen = fromFen;
const DIRS_N = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
const DIRS_B = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
const DIRS_R = [[-1, 0], [1, 0], [0, -1], [0, 1]];
const DIRS_Q = [[-1, -1], [-1, 1], [1, -1], [1, 1], [-1, 0], [1, 0], [0, -1], [0, 1]];
function sq(r, c) { return r * 8 + c; }
function row(s) { return Math.floor(s / 8); }
function col(s) { return s % 8; }
function inBounds(r, c) { return r >= 0 && r < 8 && c >= 0 && c < 8; }
function initialBoard() {
const b = new Array(64).fill(null);
const backRow = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
for (let c = 0; c < 8; c++) {
b[sq(0, c)] = { color: 'b', type: backRow[c] };
b[sq(1, c)] = { color: 'b', type: 'P' };
b[sq(6, c)] = { color: 'w', type: 'P' };
b[sq(7, c)] = { color: 'w', type: backRow[c] };
}
return b;
}
function initialCastling() {
return { wK: true, wQ: true, bK: true, bQ: true };
}
function newGameState() {
return {
board: initialBoard(),
turn: 'w',
castling: initialCastling(),
enPassant: null,
halfMoveClock: 0,
fullMoveNumber: 1,
moveHistory: [],
gameOver: false,
};
}
function cloneBoard(b) { return b.slice(); }
function cloneCastling(c) {
return { wK: c.wK, wQ: c.wQ, bK: c.bK, bQ: c.bQ };
}
function undoStateFrom(gs) {
return {
board: cloneBoard(gs.board),
turn: gs.turn,
castling: cloneCastling(gs.castling),
enPassant: gs.enPassant,
halfMoveClock: gs.halfMoveClock,
fullMoveNumber: gs.fullMoveNumber,
};
}
function applyMove(board, move) {
const nb = cloneBoard(board);
const piece = nb[move.from];
nb[move.to] = move.promotion ? { color: piece.color, type: move.promotion } : piece;
nb[move.from] = null;
if (move.enPassant) {
const capRow = row(move.from);
const capSq = sq(capRow, col(move.to));
nb[capSq] = null;
}
if (move.castling === 'K') {
const rFrom = sq(row(move.from), 7);
const rTo = sq(row(move.from), 5);
nb[rTo] = nb[rFrom];
nb[rFrom] = null;
}
else if (move.castling === 'Q') {
const rFrom = sq(row(move.from), 0);
const rTo = sq(row(move.from), 3);
nb[rTo] = nb[rFrom];
nb[rFrom] = null;
}
return nb;
}
function findKing(board, color) {
for (let i = 0; i < 64; i++) {
const p = board[i];
if (p && p.color === color && p.type === 'K')
return i;
}
return -1;
}
function isAttacked(board, sqIdx, byColor) {
const r = row(sqIdx), c = col(sqIdx);
for (const [dr, dc] of DIRS_N) {
const nr = r + dr, nc = c + dc;
if (inBounds(nr, nc)) {
const p = board[sq(nr, nc)];
if (p && p.color === byColor && p.type === 'N')
return true;
}
}
const pawnDir = byColor === 'w' ? 1 : -1;
for (const dc of [-1, 1]) {
const pr = r + pawnDir, pc = c + dc;
if (inBounds(pr, pc)) {
const p = board[sq(pr, pc)];
if (p && p.color === byColor && p.type === 'P')
return true;
}
}
for (const [dr, dc] of DIRS_Q) {
const nr = r + dr, nc = c + dc;
if (inBounds(nr, nc)) {
const p = board[sq(nr, nc)];
if (p && p.color === byColor && p.type === 'K')
return true;
}
}
for (const [dr, dc] of DIRS_B) {
let nr = r + dr, nc = c + dc;
while (inBounds(nr, nc)) {
const p = board[sq(nr, nc)];
if (p) {
if (p.color === byColor && (p.type === 'B' || p.type === 'Q'))
return true;
break;
}
nr += dr;
nc += dc;
}
}
for (const [dr, dc] of DIRS_R) {
let nr = r + dr, nc = c + dc;
while (inBounds(nr, nc)) {
const p = board[sq(nr, nc)];
if (p) {
if (p.color === byColor && (p.type === 'R' || p.type === 'Q'))
return true;
break;
}
nr += dr;
nc += dc;
}
}
return false;
}
function isInCheck(board, color) {
const kingSq = findKing(board, color);
if (kingSq === -1)
return false;
const enemy = color === 'w' ? 'b' : 'w';
return isAttacked(board, kingSq, enemy);
}
function pseudoMoves(board, fromSq, castling, enPassant) {
const piece = board[fromSq];
const moves = [];
const r = row(fromSq), c = col(fromSq);
function addMove(to, extra) {
moves.push({ from: fromSq, to, piece: piece.type, captured: board[to] ? board[to].type : null, ...extra });
}
switch (piece.type) {
case 'P': {
const dir = piece.color === 'w' ? -1 : 1;
const startRow = piece.color === 'w' ? 6 : 1;
const promoRow = piece.color === 'w' ? 0 : 7;
const f1r = r + dir, f1c = c;
if (inBounds(f1r, f1c) && !board[sq(f1r, f1c)]) {
if (f1r === promoRow) {
for (const pt of ['Q', 'R', 'B', 'N'])
addMove(sq(f1r, f1c), { promotion: pt });
}
else {
addMove(sq(f1r, f1c));
}
const f2r = r + 2 * dir;
if (r === startRow && !board[sq(f2r, f1c)]) {
addMove(sq(f2r, f1c));
}
}
for (const dc of [-1, 1]) {
const cr = r + dir, cc = c + dc;
if (!inBounds(cr, cc))
continue;
const csq = sq(cr, cc);
if (board[csq] && board[csq].color !== piece.color) {
if (cr === promoRow) {
for (const pt of ['Q', 'R', 'B', 'N'])
addMove(csq, { promotion: pt });
}
else {
addMove(csq);
}
}
}
// En passant - check if ep square is on adjacent file and in forward direction
if (enPassant !== null) {
const epR = row(enPassant), epC = col(enPassant);
for (const dc of [-1, 1]) {
if (epC === c + dc && (epR - r) * dir > 0) {
addMove(enPassant, { enPassant: true, captured: 'P' });
}
}
}
break;
}
case 'N': {
for (const [dr, dc] of DIRS_N) {
const nr = r + dr, nc = c + dc;
if (inBounds(nr, nc)) {
const nsq = sq(nr, nc);
if (!board[nsq] || board[nsq].color !== piece.color)
addMove(nsq);
}
}
break;
}
case 'B': {
for (const [dr, dc] of DIRS_B) {
let nr = r + dr, nc = c + dc;
while (inBounds(nr, nc)) {
const nsq = sq(nr, nc);
if (!board[nsq]) {
addMove(nsq);
}
else {
if (board[nsq].color !== piece.color)
addMove(nsq);
break;
}
nr += dr;
nc += dc;
}
}
break;
}
case 'R': {
for (const [dr, dc] of DIRS_R) {
let nr = r + dr, nc = c + dc;
while (inBounds(nr, nc)) {
const nsq = sq(nr, nc);
if (!board[nsq]) {
addMove(nsq);
}
else {
if (board[nsq].color !== piece.color)
addMove(nsq);
break;
}
nr += dr;
nc += dc;
}
}
break;
}
case 'Q': {
for (const [dr, dc] of DIRS_Q) {
let nr = r + dr, nc = c + dc;
while (inBounds(nr, nc)) {
const nsq = sq(nr, nc);
if (!board[nsq]) {
addMove(nsq);
}
else {
if (board[nsq].color !== piece.color)
addMove(nsq);
break;
}
nr += dr;
nc += dc;
}
}
break;
}
case 'K': {
for (const [dr, dc] of DIRS_Q) {
const nr = r + dr, nc = c + dc;
if (inBounds(nr, nc)) {
const nsq = sq(nr, nc);
if (!board[nsq] || board[nsq].color !== piece.color)
addMove(nsq);
}
}
const enemy = piece.color === 'w' ? 'b' : 'w';
const kRight = piece.color === 'w' ? castling.wK : castling.bK;
const qRight = piece.color === 'w' ? castling.wQ : castling.bQ;
if (kRight && !board[sq(r, 5)] && !board[sq(r, 6)]) {
if (!isAttacked(board, sq(r, 4), enemy) && !isAttacked(board, sq(r, 5), enemy) && !isAttacked(board, sq(r, 6), enemy)) {
addMove(sq(r, 6), { castling: 'K', captured: null });
}
}
if (qRight && !board[sq(r, 1)] && !board[sq(r, 2)] && !board[sq(r, 3)]) {
if (!isAttacked(board, sq(r, 4), enemy) && !isAttacked(board, sq(r, 3), enemy) && !isAttacked(board, sq(r, 2), enemy)) {
addMove(sq(r, 2), { castling: 'Q', captured: null });
}
}
break;
}
}
return moves;
}
function generateLegalMoves(board, turn, castling, enPassant) {
const legal = [];
for (let i = 0; i < 64; i++) {
const p = board[i];
if (!p || p.color !== turn)
continue;
const moves = pseudoMoves(board, i, castling, enPassant);
for (const m of moves) {
const nb = applyMove(board, m);
if (!isInCheck(nb, turn)) {
legal.push(m);
}
}
}
return legal;
}
function executeMove(gs, move) {
const ng = { ...gs };
ng.board = applyMove(gs.board, move);
ng.turn = gs.turn === 'w' ? 'b' : 'w';
const nc = cloneCastling(gs.castling);
if (move.piece === 'K') {
if (gs.turn === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
if (move.piece === 'R') {
const mr = row(move.from), mc = col(move.from);
if (gs.turn === 'w') {
if (mr === 7 && mc === 0)
nc.wQ = false;
if (mr === 7 && mc === 7)
nc.wK = false;
}
else {
if (mr === 0 && mc === 0)
nc.bQ = false;
if (mr === 0 && mc === 7)
nc.bK = false;
}
}
const rToR = row(move.to), rToC = col(move.to);
if (rToR === 0 && rToC === 0)
nc.bQ = false;
if (rToR === 0 && rToC === 7)
nc.bK = false;
if (rToR === 7 && rToC === 0)
nc.wQ = false;
if (rToR === 7 && rToC === 7)
nc.wK = false;
ng.castling = nc;
ng.enPassant = null;
if (move.piece === 'P' && Math.abs(row(move.to) - row(move.from)) === 2) {
ng.enPassant = sq((row(move.from) + row(move.to)) / 2, col(move.from));
}
if (move.piece === 'P' || move.captured !== null && move.captured !== undefined) {
ng.halfMoveClock = 0;
}
else {
ng.halfMoveClock = gs.halfMoveClock + 1;
}
if (gs.turn === 'b')
ng.fullMoveNumber = gs.fullMoveNumber + 1;
ng.moveHistory.push(move);
return ng;
}
function checkGameEnd(gs) {
const legalMoves = generateLegalMoves(gs.board, gs.turn, gs.castling, gs.enPassant);
if (legalMoves.length === 0) {
if (isInCheck(gs.board, gs.turn)) {
return { gameOver: true, result: gs.turn === 'w' ? 'Black wins by checkmate' : 'White wins by checkmate' };
}
else {
return { gameOver: true, result: 'Draw by stalemate' };
}
}
if (isInsufficientMaterial(gs.board)) {
return { gameOver: true, result: 'Draw by insufficient material' };
}
if (gs.halfMoveClock >= 100) {
return { gameOver: true, result: 'Draw by 50-move rule' };
}
return { gameOver: false };
}
function isInsufficientMaterial(board) {
const pieces = [];
for (const p of board) {
if (p)
pieces.push(p);
}
if (pieces.length === 2)
return true;
if (pieces.length === 3) {
const nonKing = pieces.find(p => p.type !== 'K');
if (nonKing && (nonKing.type === 'B' || nonKing.type === 'N'))
return true;
}
return false;
}
function toAlgebraic(gs, move) {
const fromR = row(move.from), fromC = col(move.from);
const toR = row(move.to), toC = col(move.to);
if (move.castling === 'K')
return 'O-O';
if (move.castling === 'Q')
return 'O-O-O';
let notation = '';
if (move.piece !== 'P') {
notation += move.piece;
const others = [];
for (let i = 0; i < 64; i++) {
if (i === move.from)
continue;
const p = gs.board[i];
if (!p || p.color !== gs.turn || p.type !== move.piece)
continue;
const pm = pseudoMoves(gs.board, i, gs.castling, gs.enPassant);
if (pm.some(m => m.to === move.to))
others.push(i);
}
if (others.length > 0) {
const sameCol = others.some(i => col(i) === fromC);
const sameRow = others.some(i => row(i) === fromR);
if (!sameCol)
notation += String.fromCharCode(97 + fromC);
else if (!sameRow)
notation += (8 - fromR);
else
notation += String.fromCharCode(97 + fromC) + (8 - fromR);
}
}
if (move.captured !== null && move.captured !== undefined) {
if (move.piece === 'P')
notation += String.fromCharCode(97 + fromC);
notation += 'x';
}
notation += String.fromCharCode(97 + toC) + (8 - toR);
if (move.promotion)
notation += '=' + move.promotion;
return notation;
}
function toFen(gs) {
let fen = '';
for (let r = 0; r < 8; r++) {
let empty = 0;
for (let c = 0; c < 8; c++) {
const p = gs.board[sq(r, c)];
if (!p) {
empty++;
}
else {
if (empty > 0) {
fen += empty;
empty = 0;
}
fen += (p.color === 'w' ? p.type : p.type.toLowerCase());
}
}
if (empty > 0)
fen += empty;
if (r < 7)
fen += '/';
}
fen += ' ' + gs.turn;
let cast = '';
if (gs.castling.wK)
cast += 'K';
if (gs.castling.wQ)
cast += 'Q';
if (gs.castling.bK)
cast += 'k';
if (gs.castling.bQ)
cast += 'q';
fen += ' ' + (cast || '-');
if (gs.enPassant !== null) {
fen += ' ' + String.fromCharCode(97 + col(gs.enPassant)) + (8 - row(gs.enPassant));
}
else {
fen += ' -';
}
fen += ' ' + gs.halfMoveClock + ' ' + gs.fullMoveNumber;
return fen;
}
function fromFen(fen) {
const parts = fen.split(' ');
const boardStr = parts[0];
const turn = parts[1];
const castlingStr = parts[2];
const epStr = parts[3];
const b = new Array(64).fill(null);
let sqIdx = 0;
for (const ch of boardStr) {
if (ch === '/')
continue;
if (ch >= '1' && ch <= '8') {
sqIdx += parseInt(ch);
}
else {
const color = ch === ch.toUpperCase() ? 'w' : 'b';
b[sqIdx] = { color, type: ch.toUpperCase() };
sqIdx++;
}
}
const cr = { wK: false, wQ: false, bK: false, bQ: false };
if (castlingStr.includes('K'))
cr.wK = true;
if (castlingStr.includes('Q'))
cr.wQ = true;
if (castlingStr.includes('k'))
cr.bK = true;
if (castlingStr.includes('q'))
cr.bQ = true;
let ep = null;
if (epStr !== '-') {
const c = epStr.charCodeAt(0) - 97;
const r = 8 - parseInt(epStr[1]);
ep = sq(r, c);
}
return {
board: b, turn, castling: cr, enPassant: ep,
halfMoveClock: parseInt(parts[4]) || 0,
fullMoveNumber: parseInt(parts[5]) || 1,
moveHistory: [], gameOver: false,
};
}
};
// ── module: src/evaluation.ts ──
__mods["src/evaluation.ts"] = function (exports, require, module) {
"use strict";
// ── Position Evaluation ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = evaluate;
exports.orderMoves = orderMoves;
const types_1 = require("./types");
function row(s) { return Math.floor(s / 8); }
function evaluate(board, turn) {
let score = 0;
for (let i = 0; i < 64; i++) {
const p = board[i];
if (!p)
continue;
const val = types_1.PIECE_VALUES[p.type];
const pstIdx = p.color === 'w' ? i : 63 - i; // mirror for black
const pstVal = types_1.PST[p.type][pstIdx];
if (p.color === turn) {
score += val + pstVal;
}
else {
score -= val + pstVal;
}
}
return score;
}
// Move ordering: captures first, then by SEE-like heuristic
function orderMoves(moves) {
const ordered = [...moves];
ordered.sort((a, b) => {
// Captures first
const aCap = a.captured ? types_1.PIECE_VALUES[a.captured] : 0;
const bCap = b.captured ? types_1.PIECE_VALUES[b.captured] : 0;
if (aCap !== bCap)
return bCap - aCap;
// Promotions next
if (a.promotion && !b.promotion)
return -1;
if (!a.promotion && b.promotion)
return 1;
return 0;
});
return ordered;
}
};
// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
// ── AI: Minimax with Alpha-Beta Pruning ──────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.findBestMove = findBestMove;
const board_1 = require("./board");
const evaluation_1 = require("./evaluation");
const INF = 100000;
function cloneCastling(c) {
return { wK: c.wK, wQ: c.wQ, bK: c.bK, bQ: c.bQ };
}
function searchStep(state, depth, alpha, beta, maximizing) {
if (depth === 0)
return quiesce(state, alpha, beta, maximizing, 4);
const moves = (0, board_1.generateLegalMoves)(state.board, state.turn, state.castling, state.enPassant);
if (moves.length === 0) {
if ((0, board_1.isInCheck)(state.board, state.turn)) {
return maximizing ? -INF + (100 - depth) : INF - (100 - depth); // mate closer is better/worse
}
return 0; // stalemate
}
const ordered = (0, evaluation_1.orderMoves)(moves);
if (maximizing) {
let best = -INF;
for (const move of ordered) {
const nb = (0, board_1.applyMove)(state.board, move);
const nc = cloneCastling(state.castling);
// Update castling rights
if (move.piece === 'K') {
if (state.turn === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
const val = searchStep({ board: nb, turn: state.turn === 'w' ? 'b' : 'w', castling: nc, enPassant: null }, depth - 1, alpha, beta, false);
best = Math.max(best, val);
alpha = Math.max(alpha, val);
if (beta <= alpha)
break;
}
return best;
}
else {
let best = INF;
for (const move of ordered) {
const nb = (0, board_1.applyMove)(state.board, move);
const nc = cloneCastling(state.castling);
if (move.piece === 'K') {
if (state.turn === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
const val = searchStep({ board: nb, turn: state.turn === 'w' ? 'b' : 'w', castling: nc, enPassant: null }, depth - 1, alpha, beta, true);
best = Math.min(best, val);
beta = Math.min(beta, val);
if (beta <= alpha)
break;
}
return best;
}
}
// Quiescence search to avoid horizon effect
function quiesce(state, alpha, beta, maximizing, depth) {
const standPat = (0, evaluation_1.evaluate)(state.board, state.turn);
if (depth === 0)
return standPat;
if (maximizing) {
if (standPat >= beta)
return beta;
if (standPat > alpha)
alpha = standPat;
// Only consider captures
const moves = (0, board_1.generateLegalMoves)(state.board, state.turn, state.castling, state.enPassant);
const captures = moves.filter(m => m.captured !== null && m.captured !== undefined);
const ordered = (0, evaluation_1.orderMoves)(captures);
for (const move of ordered) {
const nb = (0, board_1.applyMove)(state.board, move);
const nc = cloneCastling(state.castling);
if (move.piece === 'K') {
if (state.turn === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
const val = quiesce({ board: nb, turn: state.turn === 'w' ? 'b' : 'w', castling: nc, enPassant: null }, alpha, beta, false, depth - 1);
if (val >= beta)
return beta;
if (val > alpha)
alpha = val;
}
return alpha;
}
else {
if (standPat <= alpha)
return alpha;
if (standPat < beta)
beta = standPat;
const moves = (0, board_1.generateLegalMoves)(state.board, state.turn, state.castling, state.enPassant);
const captures = moves.filter(m => m.captured !== null && m.captured !== undefined);
const ordered = (0, evaluation_1.orderMoves)(captures);
for (const move of ordered) {
const nb = (0, board_1.applyMove)(state.board, move);
const nc = cloneCastling(state.castling);
if (move.piece === 'K') {
if (state.turn === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
const val = quiesce({ board: nb, turn: state.turn === 'w' ? 'b' : 'w', castling: nc, enPassant: null }, alpha, beta, true, depth - 1);
if (val <= alpha)
return alpha;
if (val < beta)
beta = val;
}
return beta;
}
}
function findBestMove(board, turn, castling, enPassant, depth) {
const moves = (0, board_1.generateLegalMoves)(board, turn, castling, enPassant);
if (moves.length === 0)
return null;
const ordered = (0, evaluation_1.orderMoves)(moves);
let bestMove = null;
let bestScore = -INF;
for (const move of ordered) {
const nb = (0, board_1.applyMove)(board, move);
const nc = cloneCastling(castling);
if (move.piece === 'K') {
if (turn === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
const val = searchStep({ board: nb, turn: turn === 'w' ? 'b' : 'w', castling: nc, enPassant: null }, depth - 1, -INF, INF, false);
if (val > bestScore) {
bestScore = val;
bestMove = move;
}
}
return bestMove;
}
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
// ── Sound Effects via Web Audio API ───────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initSound = initSound;
exports.playMove = playMove;
exports.playCapture = playCapture;
exports.playCheck = playCheck;
exports.playGameOver = playGameOver;
exports.setMute = setMute;
exports.isMuted = isMuted;
let audioCtx = null;
let muted = false;
function initSound() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
catch { /* no audio */ }
}
function playTone(freq, duration, vol, type = 'sine') {
if (!audioCtx || muted)
return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.value = freq;
gain.gain.setValueAtTime(vol, 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 */ }
}
function playMove() {
playTone(600, 0.08, 0.15);
}
function playCapture() {
playTone(300, 0.12, 0.2);
setTimeout(() => playTone(200, 0.1, 0.15), 40);
}
function playCheck() {
playTone(800, 0.15, 0.2, 'square');
setTimeout(() => playTone(600, 0.15, 0.15, 'square'), 80);
}
function playGameOver() {
playTone(440, 0.3, 0.2);
setTimeout(() => playTone(550, 0.3, 0.2), 150);
setTimeout(() => playTone(660, 0.4, 0.2), 300);
}
function setMute(m) { muted = m; }
function isMuted() { return muted; }
};
// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ── UI: Rendering & Interaction ──────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.render = render;
exports.updateMoveList = updateMoveList;
exports.setAIDepth = setAIDepth;
exports.hideGameOver = hideGameOver;
exports.loadFromStorage = loadFromStorage;
exports.setGameState = setGameState;
exports.getGameState = getGameState;
exports.setLastMove = setLastMove;
exports.setPlayerColor = setPlayerColor;
exports.setAnalysisMode = setAnalysisMode;
exports.setupEvents = setupEvents;
exports.exposeDebug = exposeDebug;
const types_1 = require("./types");
const board_1 = require("./board");
const ai_1 = require("./ai");
const sound_1 = require("./sound");
function sq(r, c) { return r * 8 + c; }
function row(s) { return Math.floor(s / 8); }
function col(s) { return s % 8; }
const canvas = document.getElementById('board');
const ctx = canvas.getContext('2d');
const SS = types_1.CONFIG.squareSize;
// ── State ────────────────────────────────────────────────────────────
let gs = null;
let selectedSquare = null;
let legalMovesForSelected = [];
let lastMove = null;
let flipped = false;
let pendingPromotion = null;
let playerColor = 'w';
let analysisMode = false;
let aiDepth = 2;
// ── Rendering ────────────────────────────────────────────────────────
function render() {
if (!gs)
return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const x = c * SS, y = r * SS;
ctx.fillStyle = (r + c) % 2 === 0 ? types_1.CONFIG.boardColors.light : types_1.CONFIG.boardColors.dark;
ctx.fillRect(x, y, SS, SS);
}
}
if (lastMove) {
ctx.fillStyle = types_1.CONFIG.lastMoveColor;
ctx.fillRect(col(lastMove.from) * SS, row(lastMove.from) * SS, SS, SS);
ctx.fillRect(col(lastMove.to) * SS, row(lastMove.to) * SS, SS, SS);
}
if ((0, board_1.isInCheck)(gs.board, gs.turn)) {
for (let i = 0; i < 64; i++) {
const p = gs.board[i];
if (p && p.color === gs.turn && p.type === 'K') {
ctx.fillStyle = types_1.CONFIG.checkGlowColor;
ctx.fillRect(col(i) * SS, row(i) * SS, SS, SS);
}
}
}
for (const m of legalMovesForSelected) {
const mr = row(m.to), mc = col(m.to);
if (gs.board[m.to]) {
ctx.strokeStyle = 'rgba(255,80,80,0.7)';
ctx.lineWidth = types_1.CONFIG.captureRingWidth;
ctx.beginPath();
ctx.arc(mc * SS + SS / 2, mr * SS + SS / 2, SS / 2 - 4, 0, Math.PI * 2);
ctx.stroke();
}
else {
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.beginPath();
ctx.arc(mc * SS + SS / 2, mr * SS + SS / 2, types_1.CONFIG.legalDotRadius, 0, Math.PI * 2);
ctx.fill();
}
}
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const p = gs.board[sq(r, c)];
if (!p)
continue;
const glyph = types_1.PIECE_UNICODE[p.color + p.type];
const x = c * SS + SS / 2, y = r * SS + SS / 2;
ctx.fillStyle = 'rgba(0,0,0,0.15)';
ctx.font = `${SS - 8}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(glyph, x + 2, y + 3);
ctx.fillStyle = p.color === 'w' ? '#fff' : '#1a1a1a';
ctx.strokeStyle = p.color === 'w' ? '#333' : '#ddd';
ctx.lineWidth = 0.5;
ctx.fillText(glyph, x, y);
ctx.strokeText(glyph, x, y);
}
}
if (selectedSquare !== null) {
ctx.fillStyle = types_1.CONFIG.highlightColor;
ctx.fillRect(col(selectedSquare) * SS, row(selectedSquare) * SS, SS, SS);
}
// Coordinate labels
ctx.font = '10px sans-serif';
for (let i = 0; i < 8; i++) {
ctx.fillStyle = i % 2 === 0 ? types_1.CONFIG.boardColors.dark : types_1.CONFIG.boardColors.light;
ctx.fillText(String.fromCharCode(97 + i), i * SS + SS / 2, canvas.height - 6);
ctx.fillText(String(8 - i), 8, i * SS + SS / 2);
}
updateStatus();
}
function updateStatus() {
if (!gs)
return;
const indicator = document.getElementById('turn-indicator');
if (gs.gameOver) {
indicator.textContent = gs.result || 'Game Over';
}
else {
const name = gs.turn === 'w' ? 'White' : 'Black';
const chk = (0, board_1.isInCheck)(gs.board, gs.turn);
indicator.textContent = `${name} to move${chk ? ' (check!)' : ''}`;
}
updateCapturedTrays();
}
function updateCapturedTrays() {
if (!gs)
return;
const wTray = document.getElementById('captured-white');
const bTray = document.getElementById('captured-black');
let capW = [];
let capB = [];
for (let i = 0; i < gs.moveHistory.length; i++) {
const m = gs.moveHistory[i];
if (m.captured !== null && m.captured !== undefined) {
// Even-indexed moves are white's, odd-indexed are black's
if (i % 2 === 0)
capW.push(m.captured);
else
capB.push(m.captured);
}
}
const order = ['Q', 'R', 'B', 'N', 'P'];
wTray.textContent = capW.sort((a, b) => order.indexOf(a) - order.indexOf(b))
.map(t => types_1.PIECE_UNICODE['b' + t]).join('');
bTray.textContent = capB.sort((a, b) => order.indexOf(a) - order.indexOf(b))
.map(t => types_1.PIECE_UNICODE['w' + t]).join('');
}
// ── Move List ────────────────────────────────────────────────────────
function updateMoveList() {
const container = document.getElementById('move-list');
container.innerHTML = '';
if (!gs)
return;
for (let i = 0; i < gs.moveHistory.length; i += 2) {
const pair = document.createElement('div');
pair.className = 'move-pair';
const numSpan = document.createElement('span');
numSpan.className = 'move-num';
numSpan.textContent = (Math.floor(i / 2) + 1) + '.';
pair.appendChild(numSpan);
// White move
const wm = gs.moveHistory[i];
let wNotation = (0, board_1.toAlgebraic)(gs, wm);
{
const afterW = replayMoves(i + 1);
if ((0, board_1.isInCheck)(afterW.board, 'b')) {
const bLegal = (0, board_1.generateLegalMoves)(afterW.board, 'b', afterW.castling, afterW.enPassant);
wNotation += bLegal.length === 0 ? '#' : '+';
}
}
const wSpan = document.createElement('span');
wSpan.className = 'move-w';
wSpan.textContent = wNotation;
pair.appendChild(wSpan);
// Black move
if (i + 1 < gs.moveHistory.length) {
const bm = gs.moveHistory[i + 1];
let bNotation = (0, board_1.toAlgebraic)(gs, bm);
{
const afterB = replayMoves(i + 2);
if ((0, board_1.isInCheck)(afterB.board, 'w')) {
const wLegal = (0, board_1.generateLegalMoves)(afterB.board, 'w', afterB.castling, afterB.enPassant);
bNotation += wLegal.length === 0 ? '#' : '+';
}
}
const bSpan = document.createElement('span');
bSpan.className = 'move-b';
bSpan.textContent = bNotation;
pair.appendChild(bSpan);
}
container.appendChild(pair);
}
container.scrollTop = container.scrollHeight;
}
function replayMoves(count) {
if (!gs)
return (0, board_1.newGameState)();
const state = (0, board_1.newGameState)();
for (let i = 0; i < count && i < gs.moveHistory.length; i++) {
state.board = (0, board_1.applyMove)(state.board, gs.moveHistory[i]);
state.turn = state.turn === 'w' ? 'b' : 'w';
}
return state;
}
// ── Interaction ──────────────────────────────────────────────────────
let dragState = null;
function getSquareFromEvent(e) {
const rect = canvas.getBoundingClientRect();
let cx, cy;
if ('touches' in e && 'changedTouches' in e) {
const te = e;
const touches = te.changedTouches.length > 0 ? te.changedTouches : te.touches;
if (touches.length === 0)
return null;
cx = touches[0].clientX;
cy = touches[0].clientY;
}
else {
cx = e.clientX;
cy = e.clientY;
}
const x = (cx - rect.left) / rect.width * canvas.width;
const y = (cy - rect.top) / rect.height * canvas.height;
const c = Math.floor(x / SS), r = Math.floor(y / SS);
if (r < 0 || r > 7 || c < 0 || c > 7)
return null;
return flipped ? sq(7 - r, 7 - c) : sq(r, c);
}
function handleSquareClick(square) {
if (!gs || gs.gameOver || pendingPromotion)
return;
if (gs.turn !== playerColor && !analysisMode)
return;
const piece = gs.board[square];
if (selectedSquare !== null) {
const matchingMove = legalMovesForSelected.find(m => m.to === square);
if (matchingMove) {
if (matchingMove.promotion) {
pendingPromotion = { from: selectedSquare, to: square };
showPromotionPicker(matchingMove, square);
return;
}
makePlayerMove(matchingMove);
return;
}
if (piece && piece.color === gs.turn) {
selectPiece(square);
return;
}
selectedSquare = null;
legalMovesForSelected = [];
render();
return;
}
if (piece && piece.color === gs.turn)
selectPiece(square);
}
function selectPiece(square) {
if (!gs)
return;
selectedSquare = square;
legalMovesForSelected = (0, board_1.generateLegalMoves)(gs.board, gs.turn, gs.castling, gs.enPassant)
.filter(m => m.from === square);
render();
}
function makePlayerMove(move) {
if (!gs)
return;
selectedSquare = null;
legalMovesForSelected = [];
lastMove = move;
if (move.captured !== null && move.captured !== undefined)
(0, sound_1.playCapture)();
else
(0, sound_1.playMove)();
gs = (0, board_1.executeMove)(gs, move);
const endResult = (0, board_1.checkGameEnd)(gs);
if (endResult.gameOver) {
gs.gameOver = true;
gs.result = endResult.result;
render();
updateMoveList();
showGameOver(endResult.result);
(0, sound_1.playGameOver)();
saveToStorage();
return;
}
if ((0, board_1.isInCheck)(gs.board, gs.turn))
(0, sound_1.playCheck)();
render();
updateMoveList();
saveToStorage();
if (!analysisMode && gs.turn !== playerColor) {
setTimeout(() => makeAIMove(), 50);
}
}
// ── Promotion Picker ─────────────────────────────────────────────────
function showPromotionPicker(move, toSquare) {
const overlay = document.getElementById('promotion-overlay');
overlay.innerHTML = '';
overlay.classList.remove('hidden');
const tr = row(toSquare), tc = col(toSquare);
const dr = flipped ? 7 - tr : tr;
const dc = flipped ? 7 - tc : tc;
overlay.style.left = (dc * SS) + 'px';
overlay.style.top = (dr * SS) + 'px';
for (const pt of ['Q', 'R', 'B', 'N']) {
const btn = document.createElement('div');
btn.className = 'promo-piece';
btn.textContent = types_1.PIECE_UNICODE[gs.turn + pt];
btn.addEventListener('click', () => {
overlay.classList.add('hidden');
pendingPromotion = null;
move.promotion = pt;
makePlayerMove(move);
});
overlay.appendChild(btn);
}
}
// ── AI Move ──────────────────────────────────────────────────────────
function setAIDepth(d) { aiDepth = d; }
function makeAIMove() {
if (!gs || gs.gameOver)
return;
const move = (0, ai_1.findBestMove)(gs.board, gs.turn, gs.castling, gs.enPassant, aiDepth);
if (!move)
return;
lastMove = move;
if (move.captured !== null && move.captured !== undefined)
(0, sound_1.playCapture)();
else
(0, sound_1.playMove)();
gs = (0, board_1.executeMove)(gs, move);
const endResult = (0, board_1.checkGameEnd)(gs);
if (endResult.gameOver) {
gs.gameOver = true;
gs.result = endResult.result;
render();
updateMoveList();
showGameOver(endResult.result);
(0, sound_1.playGameOver)();
saveToStorage();
return;
}
if ((0, board_1.isInCheck)(gs.board, gs.turn))
(0, sound_1.playCheck)();
render();
updateMoveList();
saveToStorage();
}
// ── Game Over Banner ─────────────────────────────────────────────────
function showGameOver(result) {
const banner = document.getElementById('game-over-banner');
banner.textContent = result;
banner.classList.remove('hidden');
}
function hideGameOver() {
document.getElementById('game-over-banner').classList.add('hidden');
}
// ── Persistence ──────────────────────────────────────────────────────
const STORAGE_KEY = 'chess-game-state';
function saveToStorage() {
if (!gs)
return;
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({
board: gs.board, turn: gs.turn, castling: gs.castling,
enPassant: gs.enPassant, halfMoveClock: gs.halfMoveClock,
fullMoveNumber: gs.fullMoveNumber, moveHistory: gs.moveHistory,
gameOver: gs.gameOver, result: gs.result,
}));
}
catch { /* ignore */ }
}
function loadFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return null;
const d = JSON.parse(raw);
return {
board: d.board, turn: d.turn, castling: d.castling, enPassant: d.enPassant,
halfMoveClock: d.halfMoveClock || 0, fullMoveNumber: d.fullMoveNumber || 1,
moveHistory: d.moveHistory || [], gameOver: !!d.gameOver, result: d.result,
};
}
catch {
return null;
}
}
// ── Public API ───────────────────────────────────────────────────────
function setGameState(state) {
gs = state;
selectedSquare = null;
legalMovesForSelected = [];
pendingPromotion = null;
render();
updateMoveList();
}
function getGameState() { return gs; }
function setLastMove(m) { lastMove = m; }
function setPlayerColor(c) { playerColor = c; }
function setAnalysisMode(m) { analysisMode = m; }
// ── Event Setup ──────────────────────────────────────────────────────
function setupEvents() {
canvas.addEventListener('click', (e) => { const s = getSquareFromEvent(e); if (s !== null)
handleSquareClick(s); });
canvas.addEventListener('mousedown', (e) => {
const s = getSquareFromEvent(e);
if (!gs || gs.gameOver || !s)
return;
if (gs.turn !== playerColor && !analysisMode)
return;
const p = gs.board[s];
if (!p || p.color !== gs.turn)
return;
dragState = { from: s };
selectPiece(s);
});
canvas.addEventListener('mouseup', (e) => {
if (!dragState || !gs) {
dragState = null;
return;
}
const s = getSquareFromEvent(e);
if (s !== null && s !== dragState.from) {
const m = legalMovesForSelected.find(mv => mv.to === s);
if (m) {
if (m.promotion) {
pendingPromotion = { from: dragState.from, to: s };
showPromotionPicker(m, s);
}
else
makePlayerMove(m);
}
}
dragState = null;
});
canvas.addEventListener('touchstart', (e) => {
e.preventDefault();
const s = getSquareFromEvent(e);
if (!gs || gs.gameOver || !s)
return;
if (gs.turn !== playerColor && !analysisMode)
return;
const p = gs.board[s];
if (!p || p.color !== gs.turn)
return;
dragState = { from: s };
selectPiece(s);
}, { passive: false });
canvas.addEventListener('touchend', (e) => {
e.preventDefault();
if (!dragState || !gs) {
dragState = null;
return;
}
const s = getSquareFromEvent(e);
if (s !== null && s !== dragState.from) {
const m = legalMovesForSelected.find(mv => mv.to === s);
if (m) {
if (m.promotion) {
pendingPromotion = { from: dragState.from, to: s };
showPromotionPicker(m, s);
}
else
makePlayerMove(m);
}
}
dragState = null;
}, { passive: false });
document.getElementById('board-flip-btn').addEventListener('click', () => { flipped = !flipped; render(); });
document.getElementById('undo-btn').addEventListener('click', () => {
if (!gs || gs.moveHistory.length < 2)
return;
const newHist = gs.moveHistory.slice(0, -2);
const fresh = (0, board_1.newGameState)();
for (const m of newHist) {
fresh.board = (0, board_1.applyMove)(fresh.board, m);
fresh.turn = fresh.turn === 'w' ? 'b' : 'w';
}
gs.moveHistory = newHist;
gs.board = fresh.board;
gs.turn = fresh.turn;
gs.gameOver = false;
delete gs.result;
lastMove = newHist.length > 0 ? newHist[newHist.length - 1] : null;
selectedSquare = null;
legalMovesForSelected = [];
hideGameOver();
render();
updateMoveList();
saveToStorage();
});
document.getElementById('new-game-btn').addEventListener('click', () => {
gs = (0, board_1.newGameState)();
lastMove = null;
selectedSquare = null;
legalMovesForSelected = [];
hideGameOver();
render();
updateMoveList();
saveToStorage();
if (!analysisMode && playerColor === 'b')
setTimeout(() => makeAIMove(), 100);
});
const muteBtn = document.getElementById('mute-btn');
muteBtn.addEventListener('click', () => {
(0, sound_1.setMute)(!(0, sound_1.isMuted)());
muteBtn.textContent = (0, sound_1.isMuted)() ? '🔇 Unmute' : '🔊 Mute';
});
document.getElementById('difficulty').addEventListener('change', (e) => {
setAIDepth(parseInt(e.target.value));
});
}
// ── Debug Interface ──────────────────────────────────────────────────
function exposeDebug() {
window.game = {
fen: () => gs ? (0, board_1.toFen)(gs) : null,
legalMoves: () => { if (!gs)
return []; return (0, board_1.generateLegalMoves)(gs.board, gs.turn, gs.castling, gs.enPassant); },
move: (notation) => {
const fc = notation.charCodeAt(0) - 97, fr = 8 - parseInt(notation[1]);
const tc = notation.charCodeAt(2) - 97, tr = 8 - parseInt(notation[3]);
if (!gs || gs.gameOver)
return false;
const moves = (0, board_1.generateLegalMoves)(gs.board, gs.turn, gs.castling, gs.enPassant);
const mv = moves.find(m => m.from === sq(fr, fc) && m.to === sq(tr, tc));
if (!mv)
return false;
makePlayerMove(mv);
return true;
},
state: () => gs,
};
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>