Chess
Qwen 3.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 61,560 bytes · SHA-256 8cc8c276fe10
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — Play Against AI</title>
<style>
/* ── css: layout ── */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #312e2b;
font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
color: #e0d6c8;
}
#game-container {
display: flex;
justify-content: center;
align-items: flex-start;
gap: 24px;
padding: 20px;
height: 100%;
overflow-y: auto;
}
#board-area {
position: relative;
flex-shrink: 0;
}
#board {
display: grid;
grid-template-columns: repeat(8, 1fr);
grid-template-rows: repeat(8, 1fr);
width: min(560px, calc(100vw - 300px));
height: min(560px, calc(100vw - 300px));
min-width: 280px;
min-height: 280px;
border: 3px solid #5c5652;
border-radius: 4px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
user-select: none;
-webkit-user-select: none;
}
.square {
display: flex;
justify-content: center;
align-items: center;
font-size: min(56px, calc((100vw - 300px) / 14));
cursor: pointer;
position: relative;
transition: background-color 0.1s;
}
.square.light {
background-color: #f0d9b5;
}
.square.dark {
background-color: #b58863;
}
.square.selected {
background-color: rgba(20, 85, 30, 0.5) !important;
}
.square.last-move {
background-color: rgba(155, 199, 0, 0.41) !important;
}
.square.in-check {
background: radial-gradient(ellipse at center, rgba(255,0,0,0.8) 0%, rgba(255,0,0,0) 70%);
}
.square .legal-dot {
position: absolute;
width: 28%;
height: 28%;
background: rgba(0, 0, 0, 0.15);
border-radius: 50%;
pointer-events: none;
}
.square .legal-ring {
position: absolute;
width: 90%;
height: 90%;
border: 4px solid rgba(0, 0, 0, 0.15);
border-radius: 50%;
pointer-events: none;
}
.piece {
z-index: 1;
line-height: 1;
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
transition: transform 0.05s;
}
.piece:hover {
transform: scale(1.08);
}
/* Coordinate labels */
.coord-label {
position: absolute;
font-size: 10px;
font-weight: bold;
pointer-events: none;
opacity: 0.7;
}
.coord-file {
bottom: 2px;
right: 4px;
}
.coord-rank {
top: 2px;
left: 4px;
}
.square.light .coord-label { color: #b58863; }
.square.dark .coord-label { color: #f0d9b5; }
/* ── css: side-panel ── */
#side-panel {
width: 220px;
display: flex;
flex-direction: column;
gap: 12px;
flex-shrink: 0;
max-height: min(560px, calc(100vw - 300px));
}
#game-info {
background: #44403c;
border-radius: 8px;
padding: 12px;
text-align: center;
}
#turn-indicator {
font-size: 16px;
font-weight: 600;
padding: 6px 0;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
}
#turn-indicator .turn-dot {
width: 14px;
height: 14px;
border-radius: 50%;
border: 2px solid #888;
}
#turn-indicator .turn-dot.white {
background: #fff;
}
#turn-indicator .turn-dot.black {
background: #333;
}
#game-status {
font-size: 13px;
color: #e0a45d;
min-height: 20px;
font-weight: 500;
}
#captured-trays {
background: #44403c;
border-radius: 8px;
padding: 8px;
display: flex;
flex-direction: column;
gap: 4px;
}
.captured-tray {
min-height: 28px;
font-size: 20px;
line-height: 1.4;
display: flex;
flex-wrap: wrap;
gap: 1px;
padding: 2px;
}
#move-list-container {
flex: 1;
background: #44403c;
border-radius: 8px;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 120px;
}
#move-list-header {
font-size: 13px;
font-weight: 600;
padding: 8px 12px;
border-bottom: 1px solid #555;
flex-shrink: 0;
}
#move-list {
flex: 1;
overflow-y: auto;
padding: 8px 12px;
font-family: 'Courier New', monospace;
font-size: 13px;
line-height: 1.6;
}
.move-entry {
display: flex;
gap: 8px;
}
.move-number {
color: #888;
min-width: 28px;
text-align: right;
}
.move-white, .move-black {
min-width: 55px;
cursor: default;
}
.move-white:hover, .move-black:hover {
text-decoration: underline;
}
#controls {
background: #44403c;
border-radius: 8px;
padding: 12px;
display: flex;
flex-direction: column;
gap: 8px;
}
#difficulty-selector, #side-selector {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 13px;
}
#difficulty-selector select, #side-selector select {
background: #333;
color: #e0d6c8;
border: 1px solid #666;
border-radius: 4px;
padding: 3px 6px;
font-size: 12px;
cursor: pointer;
}
#controls button {
background: #5c5652;
color: #e0d6c8;
border: 1px solid #777;
border-radius: 6px;
padding: 8px 12px;
font-size: 13px;
cursor: pointer;
transition: background 0.15s;
font-weight: 500;
}
#controls button:hover {
background: #7a726e;
}
#controls button:active {
background: #4a4540;
}
/* Promotion picker overlay */
#promotion-picker {
position: absolute;
background: #333;
border: 2px solid #888;
border-radius: 8px;
padding: 8px;
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
box-shadow: 0 8px 24px rgba(0,0,0,0.6);
}
#promotion-picker.hidden {
display: none;
}
.promo-title {
font-size: 12px;
color: #ccc;
margin-bottom: 2px;
}
.promo-pieces {
display: flex;
gap: 4px;
}
.promo-piece {
font-size: 36px;
width: 48px;
height: 48px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 6px;
background: #555;
transition: background 0.1s;
}
.promo-piece:hover {
background: #777;
}
/* Scrollbar styling */
#move-list::-webkit-scrollbar {
width: 6px;
}
#move-list::-webkit-scrollbar-track {
background: #333;
}
#move-list::-webkit-scrollbar-thumb {
background: #666;
border-radius: 3px;
}
/* Game over banner */
.game-over-banner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(0,0,0,0.85);
color: #f0d9b5;
padding: 20px 40px;
border-radius: 12px;
font-size: 22px;
font-weight: 700;
text-align: center;
z-index: 50;
pointer-events: none;
box-shadow: 0 4px 20px rgba(0,0,0,0.5);
border: 2px solid #e0a45d;
}
/* AI thinking indicator */
.ai-thinking {
font-size: 12px;
color: #e0a45d;
font-style: italic;
}
</style>
</head>
<body>
<div id="game-container">
<div id="board-area">
<div id="board"></div>
<div id="promotion-picker" class="hidden">
<div class="promo-title">Promote to:</div>
<div class="promo-pieces" id="promo-pieces"></div>
</div>
</div>
<div id="side-panel">
<div id="game-info">
<div id="turn-indicator">White to move</div>
<div id="game-status"></div>
</div>
<div id="captured-trays">
<div id="captured-white" class="captured-tray"></div>
<div id="captured-black" class="captured-tray"></div>
</div>
<div id="move-list-container">
<div id="move-list-header">Moves</div>
<div id="move-list"></div>
</div>
<div id="controls">
<div id="difficulty-selector">
<label>Difficulty:</label>
<select id="difficulty-select">
<option value="1">Easy</option>
<option value="2" selected="">Medium</option>
<option value="3">Hard</option>
</select>
</div>
<div id="side-selector">
<label>Play as:</label>
<select id="side-select">
<option value="w" selected="">White</option>
<option value="b">Black</option>
<option value="both">Both (Analysis)</option>
</select>
</div>
<button id="btn-new-game">New Game</button>
<button id="btn-undo">Undo</button>
<button id="btn-flip">Flip Board</button>
<button id="btn-mute">🔊 Sound</button>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/consts.ts":{"./types":"src/types.ts"},"src/board.ts":{"./types":"src/types.ts","./consts":"src/consts.ts"},"src/ai.ts":{"./types":"src/types.ts","./board":"src/board.ts"},"src/sound.ts":{"./consts":"src/consts.ts"},"src/persistence.ts":{"./types":"src/types.ts","./consts":"src/consts.ts"},"src/render.ts":{"./types":"src/types.ts","./consts":"src/consts.ts"},"src/game.ts":{"./types":"src/types.ts","./board":"src/board.ts","./ai":"src/ai.ts","./render":"src/render.ts","./sound":"src/sound.ts","./persistence":"src/persistence.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. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
function start() {
// Replace this with the real startup sequence.
}
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Core Chess Types ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/consts.ts ──
__mods["src/consts.ts"] = function (exports, require, module) {
"use strict";
// ── Tunable Constants ─────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.PIECE_UNICODE = exports.AI_TIME_SLICE = exports.STORAGE_KEY = exports.SOUND_DURATION = exports.SOUND_CHECK = exports.SOUND_CAPTURE = exports.SOUND_MOVE = exports.COLOR_BLACK = exports.COLOR_WHITE = exports.HIGHLIGHT_SELECT = exports.HIGHLIGHT_CHECK = exports.HIGHLIGHT_LAST = exports.HIGHLIGHT_MOVE = exports.SQUARE_DARK = exports.SQUARE_LIGHT = exports.PST = exports.PIECE_VALUES = exports.AI_DIFFICULTIES = exports.BOARD_SIZE = void 0;
exports.BOARD_SIZE = 8;
// AI depths
exports.AI_DIFFICULTIES = {
'Easy': 1,
'Medium': 2,
'Hard': 3,
};
// Eval weights for material
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, 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, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20,
],
};
// Board colors (warm wood palette)
exports.SQUARE_LIGHT = '#f0d9b5';
exports.SQUARE_DARK = '#b58863';
exports.HIGHLIGHT_MOVE = 'rgba(255,255,0,0.4)';
exports.HIGHLIGHT_LAST = 'rgba(155,199,0,0.41)';
exports.HIGHLIGHT_CHECK = 'radial-gradient(ellipse at center, rgba(255,0,0,0.8) 0%, rgba(255,0,0,0) 70%)';
exports.HIGHLIGHT_SELECT = 'rgba(20,85,30,0.5)';
// UI colors
exports.COLOR_WHITE = '#fff';
exports.COLOR_BLACK = '#000';
// Sound frequencies (Hz)
exports.SOUND_MOVE = 440;
exports.SOUND_CAPTURE = 220;
exports.SOUND_CHECK = 660;
exports.SOUND_DURATION = 0.08;
// LocalStorage key
exports.STORAGE_KEY = 'chess-game-state';
// AI time slice for chunked search (ms)
exports.AI_TIME_SLICE = 50;
// Unicode chess pieces
exports.PIECE_UNICODE = {
'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙',
'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟',
};
};
// ── module: src/board.ts ──
__mods["src/board.ts"] = function (exports, require, module) {
"use strict";
// ── Board Engine: Move Generation, Validation, Game State ─────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialBoard = initialBoard;
exports.initialCastling = initialCastling;
exports.initialGameState = initialGameState;
exports.isSquareAttacked = isSquareAttacked;
exports.isInCheck = isInCheck;
exports.legalMoves = legalMoves;
exports.makeMove = makeMove;
exports.finalizeNotation = finalizeNotation;
exports.toFEN = toFEN;
exports.evaluate = evaluate;
exports.parseMoveString = parseMoveString;
const consts_1 = require("./consts");
// ── Helpers ──────────────────────────────────────────────────────
function inBounds(r, f) {
return r >= 0 && r < consts_1.BOARD_SIZE && f >= 0 && f < consts_1.BOARD_SIZE;
}
function cloneBoard(b) {
return b.map(row => row.map(cell => cell ? { ...cell } : null));
}
function cloneCastling(c) {
return { ...c };
}
function cloneEnPassant(ep) {
return ep ? { ...ep } : null;
}
function sameSquare(a, b) {
return a.rank === b.rank && a.file === b.file;
}
function opponent(color) {
return color === 'w' ? 'b' : 'w';
}
// ── Initial Position ─────────────────────────────────────────────
function initialBoard() {
const b = Array.from({ length: 8 }, () => Array(8).fill(null));
const backRow = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
for (let f = 0; f < 8; f++) {
b[0][f] = { color: 'b', type: backRow[f] };
b[1][f] = { color: 'b', type: 'P' };
b[6][f] = { color: 'w', type: 'P' };
b[7][f] = { color: 'w', type: backRow[f] };
}
return b;
}
function initialCastling() {
return { wK: true, wQ: true, bK: true, bQ: true };
}
function initialGameState() {
return {
board: initialBoard(),
turn: 'w',
castling: initialCastling(),
enPassant: null,
halfMoveClock: 0,
fullMoveNumber: 1,
moveList: [],
capturedPieces: { w: [], b: [] },
gameOver: false,
result: null,
status: 'playing',
};
}
// ── Attack Detection ─────────────────────────────────────────────
// Is square (r, f) attacked by color `attacker` on board `b`?
function isSquareAttacked(b, r, f, attacker) {
// Pawn attacks
const pawnDir = attacker === 'w' ? -1 : 1; // White pawns move up (decreasing rank)
// Actually in our board: rank 0 is top (Black side), rank 7 is bottom (White side)
// White pawns move from rank 7 toward rank 0, so direction is -1
// Black pawns move from rank 1 toward rank 7, so direction is +1
const pr = r - pawnDir;
if (inBounds(pr, f - 1)) {
const p = b[pr][f - 1];
if (p && p.color === attacker && p.type === 'P')
return true;
}
if (inBounds(pr, f + 1)) {
const p = b[pr][f + 1];
if (p && p.color === attacker && p.type === 'P')
return true;
}
// 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 (inBounds(nr, nf)) {
const p = b[nr][nf];
if (p && p.color === attacker && p.type === 'N')
return true;
}
}
// 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 (inBounds(nr, nf)) {
const p = b[nr][nf];
if (p && p.color === attacker && p.type === 'K')
return true;
}
}
}
// Sliding pieces (rook/queen for straight, bishop/queen for diagonal)
const straightDirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
const diagDirs = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
for (const [dr, df] of straightDirs) {
let nr = r + dr, nf = f + df;
while (inBounds(nr, nf)) {
const p = b[nr][nf];
if (p) {
if (p.color === attacker && (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 (inBounds(nr, nf)) {
const p = b[nr][nf];
if (p) {
if (p.color === attacker && (p.type === 'B' || p.type === 'Q'))
return true;
break;
}
nr += dr;
nf += df;
}
}
return false;
}
// Is the king of `color` in check?
function isInCheck(b, color) {
let kr = -1, kf = -1;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = b[r][f];
if (p && p.color === color && p.type === 'K') {
kr = r;
kf = f;
break;
}
}
if (kr >= 0)
break;
}
if (kr < 0)
return false; // No king found (shouldn't happen)
return isSquareAttacked(b, kr, kf, opponent(color));
}
// ── Raw Move Generation (before legality filtering) ──────────────
function rawMoves(b, color, castling, enPassant) {
const moves = [];
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const piece = b[r][f];
if (!piece || piece.color !== color)
continue;
switch (piece.type) {
case 'P':
rawPawnMoves(b, moves, r, f, color, enPassant);
break;
case 'N':
rawKnightMoves(b, moves, r, f, color);
break;
case 'B':
rawSlidingMoves(b, moves, r, f, color, [[-1, -1], [-1, 1], [1, -1], [1, 1]]);
break;
case 'R':
rawSlidingMoves(b, moves, r, f, color, [[-1, 0], [1, 0], [0, -1], [0, 1]]);
break;
case 'Q':
rawSlidingMoves(b, moves, r, f, color, [[-1, -1], [-1, 1], [1, -1], [1, 1], [-1, 0], [1, 0], [0, -1], [0, 1]]);
break;
case 'K':
rawKingMoves(b, moves, r, f, color);
rawCastlingMoves(b, moves, r, f, color, castling);
break;
}
}
}
return moves;
}
function rawPawnMoves(b, moves, r, f, color, ep) {
const dir = color === 'w' ? -1 : 1;
const startRank = color === 'w' ? 6 : 1;
const promoRank = color === 'w' ? 0 : 7;
// Forward one
const r1 = r + dir;
if (inBounds(r1, f) && !b[r1][f]) {
if (r1 === promoRank) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: f }, piece: b[r][f], promotion: pt });
}
}
else {
moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: f }, piece: b[r][f] });
}
// Forward two from start
const r2 = r + 2 * dir;
if (r === startRank && !b[r2][f]) {
moves.push({ from: { rank: r, file: f }, to: { rank: r2, file: f }, piece: b[r][f] });
}
}
// Captures
for (const df of [-1, 1]) {
const nf = f + df;
if (!inBounds(r1, nf))
continue;
const target = b[r1][nf];
if (target && target.color !== color) {
if (r1 === promoRank) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: nf }, piece: b[r][f], captured: target, promotion: pt });
}
}
else {
moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: nf }, piece: b[r][f], captured: target });
}
}
// En passant
if (ep && r1 === ep.rank && nf === ep.file) {
moves.push({
from: { rank: r, file: f },
to: { rank: r1, file: nf },
piece: b[r][f],
captured: { color: opponent(color), type: 'P' },
isEnPassant: true,
});
}
}
}
function rawKnightMoves(b, 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 (!inBounds(nr, nf))
continue;
const target = b[nr][nf];
if (target && target.color === color)
continue;
moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f], captured: target || undefined });
}
}
function rawSlidingMoves(b, moves, r, f, color, dirs) {
for (const [dr, df] of dirs) {
let nr = r + dr, nf = f + df;
while (inBounds(nr, nf)) {
const target = b[nr][nf];
if (target) {
if (target.color !== color) {
moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f], captured: target });
}
break;
}
moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f] });
nr += dr;
nf += df;
}
}
}
function rawKingMoves(b, moves, r, f, color) {
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 (!inBounds(nr, nf))
continue;
const target = b[nr][nf];
if (target && target.color === color)
continue;
moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f], captured: target || undefined });
}
}
}
function rawCastlingMoves(b, moves, r, f, color, castling) {
const opp = opponent(color);
if (color === 'w') {
// White kingside: K from e1(7,4) to g1(7,6), R from h1(7,7) to f1(7,5)
if (castling.wK && r === 7 && f === 4) {
if (!b[7][5] && !b[7][6] && b[7][7]?.type === 'R' && b[7][7]?.color === 'w') {
if (!isSquareAttacked(b, 7, 4, opp) && !isSquareAttacked(b, 7, 5, opp) && !isSquareAttacked(b, 7, 6, opp)) {
moves.push({ from: { rank: 7, file: 4 }, to: { rank: 7, file: 6 }, piece: b[7][4], isCastling: 'K' });
}
}
}
// White queenside: K from e1(7,4) to c1(7,2), R from a1(7,0) to d1(7,3)
if (castling.wQ && r === 7 && f === 4) {
if (!b[7][3] && !b[7][2] && !b[7][1] && b[7][0]?.type === 'R' && b[7][0]?.color === 'w') {
if (!isSquareAttacked(b, 7, 4, opp) && !isSquareAttacked(b, 7, 3, opp) && !isSquareAttacked(b, 7, 2, opp)) {
moves.push({ from: { rank: 7, file: 4 }, to: { rank: 7, file: 2 }, piece: b[7][4], isCastling: 'Q' });
}
}
}
}
else {
// Black kingside: K from e8(0,4) to g8(0,6), R from h8(0,7) to f8(0,5)
if (castling.bK && r === 0 && f === 4) {
if (!b[0][5] && !b[0][6] && b[0][7]?.type === 'R' && b[0][7]?.color === 'b') {
if (!isSquareAttacked(b, 0, 4, opp) && !isSquareAttacked(b, 0, 5, opp) && !isSquareAttacked(b, 0, 6, opp)) {
moves.push({ from: { rank: 0, file: 4 }, to: { rank: 0, file: 6 }, piece: b[0][4], isCastling: 'K' });
}
}
}
// Black queenside: K from e8(0,4) to c8(0,2), R from a8(0,0) to d8(0,3)
if (castling.bQ && r === 0 && f === 4) {
if (!b[0][3] && !b[0][2] && !b[0][1] && b[0][0]?.type === 'R' && b[0][0]?.color === 'b') {
if (!isSquareAttacked(b, 0, 4, opp) && !isSquareAttacked(b, 0, 3, opp) && !isSquareAttacked(b, 0, 2, opp)) {
moves.push({ from: { rank: 0, file: 4 }, to: { rank: 0, file: 2 }, piece: b[0][4], isCastling: 'Q' });
}
}
}
}
}
// ── Legal Move Generation ────────────────────────────────────────
function legalMoves(b, color, castling, enPassant) {
const raw = rawMoves(b, color, castling, enPassant);
return raw.filter(move => {
const nb = makeMoveOnBoard(b, move);
return !isInCheck(nb, color);
});
}
// Apply a move to a board (returns new board, doesn't modify original)
function makeMoveOnBoard(b, move) {
const nb = cloneBoard(b);
const { from, to, piece } = move;
// Move piece
nb[to.rank][to.file] = move.promotion
? { color: piece.color, type: move.promotion }
: piece;
nb[from.rank][from.file] = null;
// En passant capture
if (move.isEnPassant) {
const capturedRank = from.rank; // the captured pawn is on the same rank as the moving pawn
nb[capturedRank][to.file] = null;
}
// Castling - move rook
if (move.isCastling) {
if (move.isCastling === 'K') {
// Rook from h-file to f-file
nb[to.rank][5] = nb[to.rank][7];
nb[to.rank][7] = null;
}
else {
// Rook from a-file to d-file
nb[to.rank][3] = nb[to.rank][0];
nb[to.rank][0] = null;
}
}
return nb;
}
// ── Make Move on GameState ───────────────────────────────────────
function makeMove(state, move) {
const ns = {
board: cloneBoard(state.board),
turn: opponent(state.turn),
castling: cloneCastling(state.castling),
enPassant: cloneEnPassant(state.enPassant),
halfMoveClock: state.halfMoveClock,
fullMoveNumber: state.fullMoveNumber,
moveList: [...state.moveList],
capturedPieces: {
w: [...state.capturedPieces.w],
b: [...state.capturedPieces.b],
},
gameOver: state.gameOver,
result: state.result,
status: state.status,
};
const { from, to, piece } = move;
// Move piece
ns.board[to.rank][to.file] = move.promotion
? { color: piece.color, type: move.promotion }
: piece;
ns.board[from.rank][from.file] = null;
// En passant capture
if (move.isEnPassant) {
ns.board[from.rank][to.file] = null;
ns.capturedPieces[piece.color].push({ color: opponent(piece.color), type: 'P' });
ns.halfMoveClock = 0;
}
// Regular capture
if (move.captured && !move.isEnPassant) {
ns.capturedPieces[piece.color].push(move.captured);
ns.halfMoveClock = 0;
}
// Pawn move resets half-move clock
if (piece.type === 'P' && !move.captured && !move.isEnPassant) {
ns.halfMoveClock = 0;
}
// Castling - move rook
if (move.isCastling) {
if (move.isCastling === 'K') {
ns.board[to.rank][5] = ns.board[to.rank][7];
ns.board[to.rank][7] = null;
}
else {
ns.board[to.rank][3] = ns.board[to.rank][0];
ns.board[to.rank][0] = null;
}
}
// Update castling rights
if (piece.type === 'K') {
if (piece.color === 'w') {
ns.castling.wK = false;
ns.castling.wQ = false;
}
else {
ns.castling.bK = false;
ns.castling.bQ = false;
}
}
if (piece.type === 'R') {
if (piece.color === 'w') {
if (from.rank === 7 && from.file === 0)
ns.castling.wQ = false;
if (from.rank === 7 && from.file === 7)
ns.castling.wK = false;
}
else {
if (from.rank === 0 && from.file === 0)
ns.castling.bQ = false;
if (from.rank === 0 && from.file === 7)
ns.castling.bK = false;
}
}
// If a rook is captured, remove castling rights
if (to.rank === 0 && to.file === 0)
ns.castling.bQ = false;
if (to.rank === 0 && to.file === 7)
ns.castling.bK = false;
if (to.rank === 7 && to.file === 0)
ns.castling.wQ = false;
if (to.rank === 7 && to.file === 7)
ns.castling.wK = false;
// En passant target: set if pawn moved two squares
ns.enPassant = null;
if (piece.type === 'P' && Math.abs(to.rank - from.rank) === 2) {
ns.enPassant = {
rank: (from.rank + to.rank) / 2,
file: from.file,
};
}
// Increment full move number after black's move
if (state.turn === 'b') {
ns.fullMoveNumber = state.fullMoveNumber + 1;
}
// Generate algebraic notation
move.notation = generateNotation(state, move);
// Store move in list
ns.moveList.push(move);
// Check for game end
const nextMoves = legalMoves(ns.board, ns.turn, ns.castling, ns.enPassant);
if (nextMoves.length === 0) {
ns.gameOver = true;
if (isInCheck(ns.board, ns.turn)) {
ns.status = 'checkmate';
ns.result = state.turn === 'w' ? 'White wins' : 'Black wins';
}
else {
ns.status = 'stalemate';
ns.result = 'Draw';
}
}
// Insufficient material check
if (!ns.gameOver && isInsufficientMaterial(ns.board)) {
ns.gameOver = true;
ns.status = 'insufficient';
ns.result = 'Draw';
}
// 50-move rule
if (!ns.gameOver && ns.halfMoveClock >= 100) {
ns.gameOver = true;
ns.status = 'fiftyMove';
ns.result = 'Draw';
}
return ns;
}
// ── Algebraic Notation ───────────────────────────────────────────
function fileToLetter(f) {
return String.fromCharCode(97 + f); // a-h
}
function rankToNumber(r) {
return String(8 - r); // rank 0 → "8", rank 7 → "1"
}
function generateNotation(state, move) {
const { from, to, piece } = move;
// Castling
if (move.isCastling === 'K')
return 'O-O';
if (move.isCastling === 'Q')
return 'O-O-O';
let notation = '';
// Piece letter (not for pawns)
if (piece.type !== 'P') {
notation += piece.type;
}
// Disambiguation for non-pawn pieces
if (piece.type !== 'P' && piece.type !== 'K') {
const allLegal = legalMoves(state.board, state.turn, state.castling, state.enPassant);
const ambiguous = allLegal.filter(m => m.piece.type === piece.type &&
m.to.rank === to.rank && m.to.file === to.file &&
!sameSquare(m.from, from));
if (ambiguous.length > 0) {
let disambig = '';
const sameFile = ambiguous.some(m => m.from.file === from.file);
const sameRank = ambiguous.some(m => m.from.rank === from.rank);
if (!sameFile) {
disambig = fileToLetter(from.file);
}
else if (!sameRank) {
disambig = rankToNumber(from.rank);
}
else {
disambig = fileToLetter(from.file) + rankToNumber(from.rank);
}
notation += disambig;
}
}
// Pawn capture file
if (piece.type === 'P' && move.captured) {
notation += fileToLetter(from.file);
}
// Capture symbol
if (move.captured) {
notation += 'x';
}
// Destination square
notation += fileToLetter(to.file) + rankToNumber(to.rank);
// En passant
if (move.isEnPassant) {
notation += ' e.p.';
}
// Promotion
if (move.promotion) {
notation += '=' + move.promotion;
}
// Check / checkmate (append after the move is made)
// We'll add this in a post-process step
return notation;
}
// Add check/mate symbols to the notation of the last move
function finalizeNotation(state) {
if (state.moveList.length === 0)
return;
const lastMove = state.moveList[state.moveList.length - 1];
if (!lastMove.notation)
return;
const inCheck = isInCheck(state.board, state.turn);
if (state.status === 'checkmate') {
lastMove.notation = lastMove.notation.replace(/[+#]$/, '') + '#';
}
else if (inCheck) {
lastMove.notation = lastMove.notation.replace(/[+#]$/, '') + '+';
}
}
// ── Insufficient Material ────────────────────────────────────────
function isInsufficientMaterial(b) {
const pieces = [[], []];
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = b[r][f];
if (p)
pieces[p.color === 'w' ? 0 : 1].push(p);
}
}
// K vs K
if (pieces[0].length === 1 && pieces[1].length === 1)
return true;
// K+B vs K or K+N vs K
if (pieces[0].length === 1 && pieces[1].length === 2) {
const minor = pieces[1].find(p => p.type === 'B' || p.type === 'N');
if (minor)
return true;
}
if (pieces[1].length === 1 && pieces[0].length === 2) {
const minor = pieces[0].find(p => p.type === 'B' || p.type === 'N');
if (minor)
return true;
}
return false;
}
// ── FEN ──────────────────────────────────────────────────────────
function toFEN(state) {
let fen = '';
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) {
if (empty > 0) {
fen += empty;
empty = 0;
}
fen += (p.color === 'w' ? p.type : p.type.toLowerCase());
}
else {
empty++;
}
}
if (empty > 0)
fen += empty;
if (r < 7)
fen += '/';
}
fen += ' ' + state.turn;
// Castling
let castling = '';
if (state.castling.wK)
castling += 'K';
if (state.castling.wQ)
castling += 'Q';
if (state.castling.bK)
castling += 'k';
if (state.castling.bQ)
castling += 'q';
fen += ' ' + (castling || '-');
// En passant
if (state.enPassant) {
fen += ' ' + fileToLetter(state.enPassant.file) + rankToNumber(state.enPassant.rank);
}
else {
fen += ' -';
}
fen += ' ' + state.halfMoveClock;
fen += ' ' + state.fullMoveNumber;
return fen;
}
// ── Evaluation (for AI) ──────────────────────────────────────────
function evaluate(b, color) {
let score = 0;
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const p = b[r][f];
if (!p)
continue;
const isWhite = p.color === 'w';
const material = consts_1.PIECE_VALUES[p.type];
// PST index: for White, rank 7 = index 0; for Black, mirror
let pstIndex;
if (isWhite) {
pstIndex = r * 8 + f;
}
else {
pstIndex = (7 - r) * 8 + f;
}
const pstValue = consts_1.PST[p.type] ? consts_1.PST[p.type][pstIndex] : 0;
if (isWhite) {
score += material + pstValue;
}
else {
score -= material + pstValue;
}
}
}
// Return from the perspective of `color`
return color === 'w' ? score : -score;
}
// ── Debug helpers ────────────────────────────────────────────────
// Parse a move string like "e2e4" or "e2e4q" and return a Move (or null)
function parseMoveString(state, str) {
if (str.length < 4)
return null;
const fromFile = str.charCodeAt(0) - 97;
const fromRank = 8 - parseInt(str[1]);
const toFile = str.charCodeAt(2) - 97;
const toRank = 8 - parseInt(str[3]);
const promoChar = str[4];
const piece = state.board[fromRank]?.[fromFile];
if (!piece)
return null;
const legal = legalMoves(state.board, state.turn, state.castling, state.enPassant);
return legal.find(m => m.from.rank === fromRank && m.from.file === fromFile &&
m.to.rank === toRank && m.to.file === toFile &&
(!promoChar || m.promotion === promoChar.toUpperCase())) || null;
}
};
// ── 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;
exports.findBestMoveChunked = findBestMoveChunked;
const board_1 = require("./board");
const INF = 1e9;
// Move ordering: captures first (by MVV-LVA), then checks
function orderMoves(moves, board) {
const score = (m) => {
let s = 0;
// Captures: MVV-LVA (most valuable victim, least valuable attacker)
if (m.captured) {
const victimValues = { P: 1, N: 3, B: 3, R: 5, Q: 9, K: 0 };
const attackerValues = { P: 0, N: 1, B: 2, R: 3, Q: 4, K: 5 };
s = 10 * (victimValues[m.captured.type] || 0) - (attackerValues[m.piece.type] || 0);
}
// Promotion
if (m.promotion) {
s += 80; // High priority for promotions
}
// Checks
if (m.isCastling)
s += 5;
return s;
};
moves.sort((a, b) => score(b) - score(a));
}
// Quiescence search to avoid horizon effect
function quiescence(board, color, castling, enPassant, alpha, beta) {
const standPat = (0, board_1.evaluate)(board, color);
if (standPat >= beta)
return beta;
if (alpha < standPat)
alpha = standPat;
const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
orderMoves(moves, board);
// Only consider captures and promotions for quiescence
const tacticalMoves = moves.filter(m => m.captured || m.promotion || m.isCastling);
for (const move of tacticalMoves) {
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const score = -quiescence(ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha);
if (score >= beta)
return beta;
if (score > alpha)
alpha = score;
}
return alpha;
}
function minimax(depth, board, color, castling, enPassant, alpha, beta, maximizing) {
if (depth === 0) {
return quiescence(board, color, castling, enPassant, alpha, beta);
}
const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
if (moves.length === 0) {
if ((0, board_1.isInCheck)(board, color)) {
return maximizing ? -INF + (100 - depth) : INF - (100 - depth);
}
return 0; // stalemate
}
orderMoves(moves, board);
if (maximizing) {
let maxEval = -INF;
for (const move of moves) {
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const eval_ = -minimax(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, false);
if (eval_ > maxEval)
maxEval = eval_;
if (eval_ > alpha)
alpha = eval_;
if (alpha >= beta)
break;
}
return maxEval;
}
else {
let minEval = INF;
for (const move of moves) {
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const eval_ = -minimax(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, true);
if (eval_ < minEval)
minEval = eval_;
if (eval_ < beta)
beta = eval_;
if (alpha >= beta)
break;
}
return minEval;
}
}
// Find the best move for the given color
function findBestMove(board, color, castling, enPassant, depth) {
const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
if (moves.length === 0)
return null;
orderMoves(moves, board);
let bestMove = moves[0];
let bestScore = -INF;
for (const move of moves) {
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const score = -minimax(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -INF, -bestScore, false);
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return bestMove;
}
// Chunked version: yields control every few nodes to avoid freezing UI
let _nodeCount = 0;
const NODES_PER_SLICE = 2000;
function minimaxChunked(depth, board, color, castling, enPassant, alpha, beta, maximizing, onProgress) {
_nodeCount++;
if (_nodeCount % NODES_PER_SLICE === 0 && onProgress()) {
return maximizing ? -INF : INF; // abort signal
}
if (depth === 0) {
return quiescence(board, color, castling, enPassant, alpha, beta);
}
const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
if (moves.length === 0) {
if ((0, board_1.isInCheck)(board, color)) {
return maximizing ? -INF + (100 - depth) : INF - (100 - depth);
}
return 0;
}
orderMoves(moves, board);
if (maximizing) {
let maxEval = -INF;
for (const move of moves) {
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const eval_ = -minimaxChunked(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, false, onProgress);
if (eval_ > maxEval)
maxEval = eval_;
if (eval_ > alpha)
alpha = eval_;
if (alpha >= beta)
break;
}
return maxEval;
}
else {
let minEval = INF;
for (const move of moves) {
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const eval_ = -minimaxChunked(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, true, onProgress);
if (eval_ < minEval)
minEval = eval_;
if (eval_ < beta)
beta = eval_;
if (alpha >= beta)
break;
}
return minEval;
}
}
function findBestMoveChunked(board, color, castling, enPassant, depth) {
return new Promise((resolve) => {
_nodeCount = 0;
const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
if (moves.length === 0) {
resolve(null);
return;
}
orderMoves(moves, board);
let bestMove = moves[0];
let bestScore = -INF;
let moveIndex = 0;
function processNext() {
if (moveIndex >= moves.length) {
resolve(bestMove);
return;
}
const move = moves[moveIndex];
moveIndex++;
const ns = (0, board_1.makeMove)({
board, turn: color, castling, enPassant,
halfMoveClock: 0, fullMoveNumber: 1,
moveList: [], capturedPieces: { w: [], b: [] },
gameOver: false, result: null, status: 'playing',
}, move);
const score = -minimaxChunked(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -INF, -bestScore, false, () => {
// Yield to the event loop
return false; // never abort
});
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
// Schedule next move evaluation
setTimeout(processNext, 0);
}
processNext();
});
}
};
// ── 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.setMuted = setMuted;
exports.isMuted = isMuted;
exports.playMove = playMove;
exports.playCapture = playCapture;
exports.playCheck = playCheck;
exports.playGameOver = playGameOver;
const consts_1 = require("./consts");
let audioCtx = null;
let muted = false;
function getAudioCtx() {
if (!audioCtx) {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
return audioCtx;
}
function setMuted(m) {
muted = m;
}
function isMuted() {
return muted;
}
function playTone(freq, duration, type = 'sine') {
if (muted)
return;
try {
const ctx = getAudioCtx();
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, ctx.currentTime);
gain.gain.setValueAtTime(0.15, ctx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime);
osc.stop(ctx.currentTime + duration);
}
catch {
// Audio not available
}
}
function playMove() {
playTone(consts_1.SOUND_MOVE, consts_1.SOUND_DURATION);
}
function playCapture() {
playTone(consts_1.SOUND_CAPTURE, consts_1.SOUND_DURATION * 1.5, 'triangle');
}
function playCheck() {
playTone(consts_1.SOUND_CHECK, consts_1.SOUND_DURATION * 2, 'square');
}
function playGameOver() {
if (muted)
return;
try {
const ctx = getAudioCtx();
const notes = [523, 659, 784, 1047]; // C E G C
notes.forEach((freq, i) => {
const osc = ctx.createOscillator();
const gain = ctx.createGain();
osc.type = 'sine';
osc.frequency.setValueAtTime(freq, ctx.currentTime + i * 0.15);
gain.gain.setValueAtTime(0.12, ctx.currentTime + i * 0.15);
gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + i * 0.15 + 0.3);
osc.connect(gain);
gain.connect(ctx.destination);
osc.start(ctx.currentTime + i * 0.15);
osc.stop(ctx.currentTime + i * 0.15 + 0.3);
});
}
catch {
// Audio not available
}
}
};
// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
// ── Persistence: localStorage Save/Restore ───────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveState = saveState;
exports.loadState = loadState;
exports.clearState = clearState;
const consts_1 = require("./consts");
function serializePiece(p) {
if (!p)
return null;
return p.color + p.type;
}
function deserializePiece(s) {
if (!s)
return null;
return { color: s[0], type: s[1] };
}
function saveState(state, playerColor, difficulty, flipBoard) {
try {
const serialized = {
board: state.board.map(row => row.map(cell => serializePiece(cell))),
turn: state.turn,
castling: state.castling,
enPassant: state.enPassant,
halfMoveClock: state.halfMoveClock,
fullMoveNumber: state.fullMoveNumber,
moveList: state.moveList.map(m => ({
from: m.from,
to: m.to,
piece: m.piece,
captured: m.captured,
isEnPassant: m.isEnPassant,
isCastling: m.isCastling,
promotion: m.promotion,
notation: m.notation,
})),
capturedPieces: {
w: state.capturedPieces.w,
b: state.capturedPieces.b,
},
gameOver: state.gameOver,
result: state.result,
status: state.status,
playerColor,
difficulty,
flipBoard,
};
localStorage.setItem(consts_1.STORAGE_KEY, JSON.stringify(serialized));
}
catch {
// Storage not available
}
}
function loadState() {
try {
const raw = localStorage.getItem(consts_1.STORAGE_KEY);
if (!raw)
return null;
const data = JSON.parse(raw);
const state = {
board: data.board.map(row => row.map(cell => deserializePiece(cell))),
turn: data.turn,
castling: data.castling,
enPassant: data.enPassant,
halfMoveClock: data.halfMoveClock,
fullMoveNumber: data.fullMoveNumber,
moveList: data.moveList,
capturedPieces: {
w: data.capturedPieces.w.map(p => p),
b: data.capturedPieces.b.map(p => p),
},
gameOver: data.gameOver,
result: data.result,
status: data.status,
};
return {
state,
playerColor: data.playerColor,
difficulty: data.difficulty,
flipBoard: data.flipBoard,
};
}
catch {
return null;
}
}
function clearState() {
try {
localStorage.removeItem(consts_1.STORAGE_KEY);
}
catch {
// ignore
}
}
};
// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ── Rendering: Board, Pieces, Highlights ─────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRender = initRender;
exports.setFlip = setFlip;
exports.renderBoard = renderBoard;
exports.getSquareFromEvent = getSquareFromEvent;
const consts_1 = require("./consts");
let boardEl;
let flipBoard = false;
function initRender() {
boardEl = document.getElementById('board');
}
function visRank(r) {
return flipBoard ? 7 - r : r;
}
function visFile(f) {
return flipBoard ? 7 - f : f;
}
function setFlip(f) {
flipBoard = f;
}
// Render the full board
function renderBoard(board, selectedSquare, legalDestinations, lastMove, inCheck, playerColor) {
boardEl.innerHTML = '';
for (let vr = 0; vr < 8; vr++) {
for (let vf = 0; vf < 8; vf++) {
const r = flipBoard ? 7 - vr : vr;
const f = flipBoard ? 7 - vf : vf;
const isLight = (r + f) % 2 === 0;
const sq = document.createElement('div');
sq.className = 'square ' + (isLight ? 'light' : 'dark');
sq.dataset.rank = String(r);
sq.dataset.file = String(f);
// Coordinate labels
if (vf === 0) {
const label = document.createElement('span');
label.className = 'coord-label coord-rank';
label.textContent = String(8 - r);
sq.appendChild(label);
}
if (vr === 7) {
const label = document.createElement('span');
label.className = 'coord-label coord-file';
label.textContent = String.fromCharCode(97 + f);
sq.appendChild(label);
}
// Last move highlight
if (lastMove) {
if ((lastMove.from.rank === r && lastMove.from.file === f) ||
(lastMove.to.rank === r && lastMove.to.file === f)) {
sq.classList.add('last-move');
}
}
// Selected square highlight
if (selectedSquare && selectedSquare.rank === r && selectedSquare.file === f) {
sq.classList.add('selected');
}
// Check highlight on king
if (inCheck) {
const piece = board[r][f];
if (piece && piece.type === 'K' && piece.color === playerColor) {
sq.classList.add('in-check');
}
}
// Legal move indicators
const isLegalDest = legalDestinations.some(d => d.rank === r && d.file === f);
if (isLegalDest) {
const indicator = document.createElement('div');
const targetPiece = board[r][f];
if (targetPiece) {
indicator.className = 'legal-ring';
}
else {
indicator.className = 'legal-dot';
}
sq.appendChild(indicator);
}
// Piece
const piece = board[r][f];
if (piece) {
const pieceEl = document.createElement('span');
pieceEl.className = 'piece';
pieceEl.textContent = consts_1.PIECE_UNICODE[piece.color + piece.type];
// Color the pieces
if (piece.color === 'w') {
pieceEl.style.color = '#fff';
pieceEl.style.textShadow = '0 0 2px #000, 0 1px 3px rgba(0,0,0,0.4)';
}
else {
pieceEl.style.color = '#1a1a1a';
pieceEl.style.textShadow = '0 0 2px rgba(255,255,255,0.3), 0 1px 3px rgba(0,0,0,0.3)';
}
sq.appendChild(pieceEl);
}
boardEl.appendChild(sq);
}
}
}
// Get the board square under a DOM element
function getSquareFromEvent(e) {
const target = e.target;
const sq = target.closest('.square');
if (!sq)
return null;
const rank = parseInt(sq.dataset.rank || '');
const file = parseInt(sq.dataset.file || '');
if (isNaN(rank) || isNaN(file))
return null;
return { rank, file };
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
// ── Game Controller: Input, State Management, AI Integration ─────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initGame = initGame;
const render_1 = require("./render");
const persistence_1 = require("./persistence");
// ── Game State ────────────────────────────────────────────────────
let gameState;
let playerColor = 'w';
let difficulty = 2;
let analysisMode = false;
let selectedSquare = null;
let legalDestinations = [];
let aiThinking = false;
let pendingPromotion = null;
let dragSource = null;
let dragPieceEl = null;
// UI elements
let moveListEl;
let turnIndicatorEl;
let gameStatusEl;
let capturedWhiteEl;
let capturedBlackEl;
let promoPickerEl;
let promoPiecesEl;
function initGame() {
// Try to restore from localStorage
const saved = (0, persistence_1.loadState)();
if (saved) {
gameState = saved.state;
playerColor = saved.playerColor;
difficulty = saved.difficulty;
(0, render_1.setFlip)(saved.flipBoard);
analysisMode = false;
// Update side selector
const sideSelect = document.getElementById('side-select');
sideSelect.value = playerColor;
const diffSelect = document.getElementById('difficulty-select');
diffSelect.value = String(difficulty);
}
else {
gameState = createInitialState();
}
(0, render_1.initRender)();
moveListEl = document.getElementById('move-list');
turnIndicatorEl = document.getElementById('turn-indicator');
gameStatusEl = document.getElementById('game-status');
capturedWhiteEl = document.getElementById('captured-white');
capturedBlackEl = document.getElementById('captured-black');
promoPickerEl = document.getElementById('promotion-picker');
promoPiecesEl = document.getElementById('promo-pieces');
// Wire up controls
document.getElementById('btn-new-game').addEventListener('click', handleNewGame);
document.getElementById('btn-undo').addEventListener('click', handleUndo);
document.getElementById('btn-flip').addEventListener('click', handleFlip);
document.getElementById('btn-mute').addEventListener('click', handleMute);
document.getElementById('difficulty-select').addEventListener('change', handleDifficultyChange);
document.getElementById('side-select').addEventListener('change', handleSideChange);
// Board click handler
const boardEl = document.getElementById('board');
boardEl.addEventListener('click', handleBoardClick);
// Drag and drop
boardEl.addEventListener('mousedown', handleMouseDown);
boardEl.addEventListener('mousemove', handleMouseMove);
boardEl.addEventListener('mouseup', handleMouseUp);
boardEl.addEventListener('mouseleave', handleMouseUp);
boardEl.addEventListener('touchstart', handleTouchStart, { passive: false });
boardEl.addEventListener('touchmove', handleTouchMove, { passive: false });
boardEl.addEventListener('touchend', handleTouchEnd);
// Initial render
updateAll();
saveGameState();
}
function createInitialState() {
const { initialGameState } = requireBoard();
return initialGameState();
}
function requireBoard() {
// Dynamic import to avoid circular deps
const { initialGameState } = window;
return { initialGameState };
}
// Actually, let me import directly
// The above won't work. Let me restructure.
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>