Chess
Qwen 3.7 Flash · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
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 58,476 bytes · SHA-256 4d95aea69582
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — Play vs AI</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body { font-family: 'Segoe UI', system-ui, sans-serif; background: #1a1a2e; color: #eee; overflow-x: hidden; }
#game-layout { display: flex; height: 100vh; gap: 16px; padding: 12px; align-items: stretch; }
#side-panel { width: 240px; min-width: 200px; background: #16213e; border-radius: 10px; padding: 16px; display: flex; flex-direction: column; gap: 12px; overflow-y: auto; }
#board-area { flex: 1; display: flex; justify-content: center; align-items: center; position: relative; }
#chess-canvas { cursor: pointer; border-radius: 4px; box-shadow: 0 4px 20px rgba(0,0,0,0.5); }
h1#game-title { font-size: 22px; text-align: center; color: #e8c07a; letter-spacing: 1px; }
#turn-indicator { text-align: center; font-size: 14px; padding: 6px; background: #0f3460; border-radius: 6px; min-height: 32px; display: flex; align-items: center; justify-content: center; }
#status-message { text-align: center; font-size: 12px; color: #aaa; min-height: 18px; }
.panel-section h3 { font-size: 13px; color: #aaa; margin-bottom: 4px; border-bottom: 1px solid #333; padding-bottom: 3px; }
.ctrl-btn { width: 100%; padding: 7px 10px; margin: 2px 0; border: none; border-radius: 6px; background: #0f3460; color: #eee; font-size: 13px; cursor: pointer; transition: background 0.2s; text-align: center; }
.ctrl-btn:hover { background: #1a4a7a; }
.ctrl-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.captured-tray { display: flex; flex-wrap: wrap; gap: 2px; min-height: 24px; padding: 4px; background: rgba(0,0,0,0.2); border-radius: 4px; }
.captured-tray span { font-size: 18px; }
#move-list { max-height: 260px; overflow-y: auto; background: rgba(0,0,0,0.2); border-radius: 4px; padding: 6px; font-size: 13px; font-family: monospace; }
.move-row { padding: 2px 4px; line-height: 1.6; }
.move-num { color: #888; }
.move-w { color: #ddd; }
.move-b { color: #aaa; }
.promo-overlay { position: absolute; display: none; background: #2c1810ee; border: 2px solid #b58863; border-radius: 8px; padding: 6px; gap: 4px; z-index: 100; }
.promo-btn { width: 48px; height: 48px; font-size: 30px; background: #b58863; border: none; border-radius: 6px; cursor: pointer; }
.promo-btn:hover { background: #d4a574; }
@media (max-width: 800px) {
#game-layout { flex-direction: column; }
#side-panel { width: 100%; flex-direction: row; flex-wrap: wrap; padding: 8px; gap: 8px; }
.panel-section { flex: 1; min-width: 120px; }
}
</style>
</head>
<body>
<div id="game-layout">
<div id="side-panel">
<h1 id="game-title">Chess</h1>
<div id="turn-indicator">White's turn</div>
<div id="status-message"></div>
<div class="panel-section">
<h3>Captured Pieces</h3>
<div id="captured-white" class="captured-tray"></div>
<div id="captured-black" class="captured-tray"></div>
</div>
<div class="panel-section">
<h3>Controls</h3>
<button id="play-white" class="ctrl-btn">Play as White</button>
<button id="play-black" class="ctrl-btn">Play as Black</button>
<button id="undo-btn" class="ctrl-btn" disabled="">Undo</button>
<button id="flip-board" class="ctrl-btn">Flip Board</button>
<select id="difficulty-select" class="ctrl-btn">
<option value="1">Easy (depth 1)</option>
<option value="2">Medium (depth 2)</option>
<option value="3" selected="">Hard (depth 3)</option>
</select>
<span id="mute-label" class="ctrl-btn" style="cursor:pointer">🔊</span>
</div>
<div class="panel-section">
<h3>Moves</h3>
<div id="move-list"></div>
</div>
</div>
<div id="board-area">
<canvas id="chess-canvas"></canvas>
<div id="promotion-picker" class="promo-overlay" hidden=""></div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./app":"src/app.ts","./types":"src/types.ts"},"src/engine.ts":{"./types":"src/types.ts"},"src/app.ts":{"./types":"src/types.ts","./engine":"src/engine.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
// ═══════════════════════════════════════════════════════════════
// main.ts — Entry point
// ═══════════════════════════════════════════════════════════════
const app_1 = require("./app");
const types_1 = require("./types");
function start() {
new app_1.GameController(types_1.COL.WHITE);
}
// Kick off on DOM ready (DOM already parsed since script is at end of body)
start();
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.COL = exports.PT = exports.SQ_COUNT = exports.BOARD = void 0;
exports.sqF = sqF;
exports.sqR = sqR;
exports.sqN = sqN;
exports.fRtoSq = fRtoSq;
// ─── Types & Constants ──────────────────────────────────────────────
exports.BOARD = 8;
exports.SQ_COUNT = 64;
var PT;
(function (PT) {
PT[PT["EMPTY"] = 0] = "EMPTY";
PT[PT["PAWN"] = 1] = "PAWN";
PT[PT["KNIGHT"] = 2] = "KNIGHT";
PT[PT["BISHOP"] = 3] = "BISHOP";
PT[PT["ROOK"] = 4] = "ROOK";
PT[PT["QUEEN"] = 5] = "QUEEN";
PT[PT["KING"] = 6] = "KING";
})(PT || (exports.PT = PT = {}));
var COL;
(function (COL) {
COL[COL["WHITE"] = 0] = "WHITE";
COL[COL["BLACK"] = 1] = "BLACK";
})(COL || (exports.COL = COL = {}));
function sqF(s) { return String.fromCharCode(97 + (s % exports.BOARD)); }
function sqR(s) { return String(8 - Math.floor(s / exports.BOARD)); }
function sqN(s) { return sqF(s) + sqR(s); }
function fRtoSq(f, r) { return (8 - parseInt(r)) * exports.BOARD + (f.charCodeAt(0) - 97); }
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.makeState = makeState;
exports.attackedBy = attackedBy;
exports.findKing = findKing;
exports.genLegalMoves = genLegalMoves;
exports.generatePseudoMoves = generatePseudoMoves;
exports.applyMoveToState = applyMoveToState;
exports.insufficientMaterial = insufficientMaterial;
exports.parseFEN = parseFEN;
exports.genFEN = genFEN;
exports.undoWithSnapshots = undoWithSnapshots;
exports.pushSnapshot = pushSnapshot;
exports.popSnapshot = popSnapshot;
exports.aiPickBest = aiPickBest;
// ═══════════════════════════════════════════════════════════════
// engine.ts — Complete chess engine: state, moves, rules, AI, FEN
// ═══════════════════════════════════════════════════════════════
const types_1 = require("./types");
const B = 8;
const N = 64;
// ── Helpers ──────────────────────────────────────────────────────
function sign(n) { return n > 0 ? 1 : n < 0 ? -1 : 0; }
function inside(r, c) { return r >= 0 && r < B && c >= 0 && c < B; }
function rowCol(s) { return [Math.floor(s / B), s % B]; }
function mirrorSq(s) { return s ^ 56; }
// ── Piece-Square Tables (white perspective) ──────────────────────
const PAWN_PST = [
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,
];
const KNIGHT_PST = [
-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,
];
const BISHOP_PST = [
-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, 5, 10, 10, 5, 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,
];
const ROOK_PST = [
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,
];
const QUEEN_PST = [
-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,
];
const KING_MID_PST = [
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-20, -30, -30, -40, -40, -30, -30, -20,
-10, -20, -20, -20, -20, -20, -20, -10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20,
];
const KING_END_PST = [
-50, -40, -30, -20, -20, -30, -40, -50,
-30, -20, -10, 0, 0, -10, -20, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 30, 40, 40, 30, -10, -30,
-30, -10, 20, 30, 30, 20, -10, -30,
-30, -30, 0, 0, 0, 0, -30, -30,
-50, -30, -30, -30, -30, -30, -30, -50,
];
const PST = {
[types_1.PT.PAWN]: PAWN_PST, [types_1.PT.KNIGHT]: KNIGHT_PST, [types_1.PT.BISHOP]: BISHOP_PST,
[types_1.PT.ROOK]: ROOK_PST, [types_1.PT.QUEEN]: QUEEN_PST, [types_1.PT.KING]: KING_MID_PST,
};
const MV = {
[types_1.PT.PAWN]: 100, [types_1.PT.KNIGHT]: 320, [types_1.PT.BISHOP]: 330, [types_1.PT.ROOK]: 500, [types_1.PT.QUEEN]: 900, [types_1.PT.KING]: 20000,
};
const INIT_BOARD = (() => {
const b = new Array(N).fill(null);
const br = [types_1.PT.ROOK, types_1.PT.KNIGHT, types_1.PT.BISHOP, types_1.PT.QUEEN, types_1.PT.KING, types_1.PT.BISHOP, types_1.PT.KNIGHT, types_1.PT.ROOK];
// Black on top (rows 0–1), White on bottom (rows 6–7)
for (let f = 0; f < B; f++) {
b[f] = { type: br[f], color: types_1.COL.BLACK }; // rank 8 → row 0
b[B + f] = { type: types_1.PT.PAWN, color: types_1.COL.BLACK }; // rank 7 → row 1
b[6 * B + f] = { type: types_1.PT.PAWN, color: types_1.COL.WHITE }; // rank 2 → row 6
b[7 * B + f] = { type: br[f], color: types_1.COL.WHITE }; // rank 1 → row 7
}
return b;
})();
function makeState(playerColor) {
return {
board: INIT_BOARD.map(p => p ? { ...p } : null),
turn: types_1.COL.WHITE,
castle: { wK: true, wQ: true, bK: true, bQ: true },
ep: null, halfMove: 0, fullMove: 1, history: [],
capW: [], capB: [],
gameOver: false, gameResult: null, playerColor,
};
}
// ── Attack Detection ─────────────────────────────────────────────
function attackedBy(sq, atkCol, bd) {
// Pawn attacks
const pd = atkCol === types_1.COL.WHITE ? 1 : -1;
for (const df of [-1, 1]) {
const ps = sq + pd * B + df;
if (inside(Math.floor(ps / B), ps % B)) {
const pp = bd[ps];
if (pp && pp.type === types_1.PT.PAWN && pp.color === atkCol)
return true;
}
}
// Knight
for (const d of [-17, -15, -10, -6, 6, 10, 15, 17]) {
const ns = sq + d;
if (ns >= 0 && ns < N) {
if (Math.abs((ns % B) - (sq % B)) <= 2) {
const np = bd[ns];
if (np && np.type === types_1.PT.KNIGHT && np.color === atkCol)
return true;
}
}
}
// King
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
if (!dr && !dc)
continue;
const ks = sq + dr * B + dc;
if (ks >= 0 && ks < N && Math.sign((ks % B) - (sq % B)) === dc) {
const kp = bd[ks];
if (kp && kp.type === types_1.PT.KING && kp.color === atkCol)
return true;
}
}
// Sliding: diagonals (bishop/queen) — use step offsets directly
const diagSteps = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
for (const [stepR, stepC] of diagSteps) {
let r = Math.floor(sq / B) + stepR;
let c = (sq % B) + stepC;
while (r >= 0 && r < B && c >= 0 && c < B) {
const s = r * B + c;
const sp = bd[s];
if (sp) {
if (sp.color === atkCol && (sp.type === types_1.PT.BISHOP || sp.type === types_1.PT.QUEEN))
return true;
break;
}
r += stepR;
c += stepC;
}
}
// Sliding: straights (rook/queen)
const straitSteps = [[-1, 0], [0, -1], [0, 1], [1, 0]];
for (const [stepR, stepC] of straitSteps) {
let r = Math.floor(sq / B) + stepR;
let c = (sq % B) + stepC;
while (r >= 0 && r < B && c >= 0 && c < B) {
const s = r * B + c;
const rp = bd[s];
if (rp) {
if (rp.color === atkCol && (rp.type === types_1.PT.ROOK || rp.type === types_1.PT.QUEEN))
return true;
break;
}
r += stepR;
c += stepC;
}
}
return false;
}
function findKing(pos, bd) {
for (let i = 0; i < N; i++) {
const p = bd[i];
if (p && p.type === types_1.PT.KING && p.color === pos)
return i;
}
return -1;
}
// ── Make / Undo helpers ──────────────────────────────────────────
function enCapSq(m) { return m.to + (m.from < m.to ? -B : B); }
function applyEP(bd, m) {
if (m.enPassant)
bd[enCapSq(m)] = null;
}
function makeOn(bd, m) {
const p = bd[m.from];
applyEP(bd, m);
bd[m.to] = { type: p.type, color: p.color };
bd[m.from] = null;
if (m.promotion)
bd[m.to] = { type: m.promotion, color: p.color };
if (m.castling === 'kingside') {
const rw = Math.floor(m.from / B);
bd[rw * B + 5] = bd[rw * B + 7];
bd[rw * B + 7] = null;
}
else if (m.castling === 'queenside') {
const rw = Math.floor(m.from / B);
bd[rw * B + 3] = bd[rw * B + 0];
bd[rw * B + 0] = null;
}
}
function undoOn(bd, m, captured, moving) {
bd[m.from] = { ...moving };
bd[m.to] = captured;
if (m.enPassant)
bd[enCapSq(m)] = { type: types_1.PT.PAWN, color: (1 - moving.color) };
if (m.castling === 'kingside') {
const rw = Math.floor(m.from / B);
bd[rw * B + 7] = bd[rw * B + 5];
bd[rw * B + 5] = null;
}
else if (m.castling === 'queenside') {
const rw = Math.floor(m.from / B);
bd[rw * B + 0] = bd[rw * B + 3];
bd[rw * B + 3] = null;
}
}
// ── Pseudo Legal Move Generation ─────────────────────────────────
function pseudoMovesFor(bd, col, epSqr, cr) {
const ms = [];
const enn = 1 - col;
for (let sq = 0; sq < N; sq++) {
const p = bd[sq];
if (!p || p.color !== col)
continue;
const [, cv] = rowCol(sq);
switch (p.type) {
case types_1.PT.PAWN: {
const dir = col === types_1.COL.WHITE ? -1 : 1;
const pr = col === types_1.COL.WHITE ? 0 : 7;
const f1 = sq + dir * B;
if (inside(Math.floor(f1 / B), f1 % B) && !bd[f1]) {
const nr = Math.floor(f1 / B);
if (nr === pr) {
for (const pt of [types_1.PT.QUEEN, types_1.PT.ROOK, types_1.PT.BISHOP, types_1.PT.KNIGHT])
ms.push({ from: sq, to: f1, promotion: pt });
}
else {
ms.push({ from: sq, to: f1 });
if ((col === types_1.COL.WHITE && Math.floor(sq / B) === 6) || (col === types_1.COL.BLACK && Math.floor(sq / B) === 1)) {
const f2 = f1 + dir * B;
if (inside(Math.floor(f2 / B), f2 % B) && !bd[f2])
ms.push({ from: sq, to: f2 });
}
}
}
for (const df of [-1, 1]) {
const cs = sq + dir * B + df;
if (!inside(Math.floor(cs / B), cs % B))
continue;
if (sign((cs % B) - cv) !== df)
continue;
const nr = Math.floor(cs / B);
if (bd[cs] && bd[cs].color === enn) {
if (nr === pr) {
for (const pt of [types_1.PT.QUEEN, types_1.PT.ROOK, types_1.PT.BISHOP, types_1.PT.KNIGHT])
ms.push({ from: sq, to: cs, promotion: pt });
}
else {
ms.push({ from: sq, to: cs });
}
}
if (epSqr !== null && cs === epSqr)
ms.push({ from: sq, to: cs, enPassant: true });
}
break;
}
case types_1.PT.KNIGHT: {
for (const d of [-17, -15, -10, -6, 6, 10, 15, 17]) {
const ns = sq + d;
if (ns >= 0 && ns < N && Math.abs((ns % B) - (sq % B)) <= 2) {
const t = bd[ns];
if (!t || t.color !== col)
ms.push({ from: sq, to: ns });
}
}
break;
}
case types_1.PT.BISHOP: {
slideDirs(sq, bd, col, [-9, -7, 7, 9], ms);
break;
}
case types_1.PT.ROOK: {
slideDirs(sq, bd, col, [-8, -1, 1, 8], ms);
break;
}
case types_1.PT.QUEEN: {
slideDirs(sq, bd, col, [-9, -7, -8, -1, 1, 7, 8, 9], ms);
break;
}
case types_1.PT.KING: {
for (let dr = -1; dr <= 1; dr++)
for (let dc = -1; dc <= 1; dc++) {
if (!dr && !dc)
continue;
const ns = sq + dr * B + dc;
if (ns >= 0 && ns < N && Math.sign((ns % B) - (sq % B)) === dc) {
const t = bd[ns];
if (!t || t.color !== col)
ms.push({ from: sq, to: ns });
}
}
// Castling
const row = Math.floor(sq / B);
// White king is at row 7, black king at row 0
if (row === (col === types_1.COL.WHITE ? 7 : 0) && !attackedBy(sq, 1 - col, bd)) {
const r = row;
if (col === types_1.COL.WHITE) {
if (cr.wK && !bd[r * B + 5] && !bd[r * B + 6] && !attackedBy(r * B + 5, types_1.COL.BLACK, bd) && !attackedBy(r * B + 6, types_1.COL.BLACK, bd))
ms.push({ from: sq, to: r * B + 6, castling: 'kingside' });
if (cr.wQ && !bd[r * B + 1] && !bd[r * B + 2] && !bd[r * B + 3] && !attackedBy(r * B + 2, types_1.COL.BLACK, bd) && !attackedBy(r * B + 3, types_1.COL.BLACK, bd))
ms.push({ from: sq, to: r * B + 2, castling: 'queenside' });
}
else {
if (cr.bK && !bd[r * B + 5] && !bd[r * B + 6] && !attackedBy(r * B + 5, types_1.COL.WHITE, bd) && !attackedBy(r * B + 6, types_1.COL.WHITE, bd))
ms.push({ from: sq, to: r * B + 6, castling: 'kingside' });
if (cr.bQ && !bd[r * B + 1] && !bd[r * B + 2] && !bd[r * B + 3] && !attackedBy(r * B + 2, types_1.COL.WHITE, bd) && !attackedBy(r * B + 3, types_1.COL.WHITE, bd))
ms.push({ from: sq, to: r * B + 2, castling: 'queenside' });
}
}
break;
}
}
}
return ms;
}
function slideDirs(from, bd, col, dirs, ms) {
const oF = from % B, oR = Math.floor(from / B);
for (const dd of dirs) {
let s = from + dd;
while (s >= 0 && s < N) {
const cf = s % B, cr = Math.floor(s / B);
if (cf !== oF + sign(cf - oF) || cr !== oR + sign(cr - oR))
break;
const t = bd[s];
if (t) {
if (t.color !== col)
ms.push({ from, to: s });
break;
}
ms.push({ from, to: s });
s += dd;
}
}
}
// ── Full Legal Move Generation ───────────────────────────────────
function genLegalMoves(gs) {
const pMoves = pseudoMovesFor(gs.board, gs.turn, gs.ep, gs.castle);
const legal = [];
for (const mv of pMoves) {
const mvp = gs.board[mv.from], cap = gs.board[mv.to];
// Save original board
const origBoard = gs.board.slice();
makeOn(origBoard, mv);
const ks = findKing(gs.turn, origBoard);
const ok = !attackedBy(ks, 1 - gs.turn, origBoard);
undoOn(origBoard, mv, cap, mvp);
if (ok)
legal.push(mv);
}
return legal;
}
// Also export for direct use
function generatePseudoMoves(board, turn, ep, cr) {
return pseudoMovesFor(board, turn, ep, cr);
}
// ── Apply Move to Game State ─────────────────────────────────────
function applyMoveToState(gs, mv) {
const mvp = gs.board[mv.from];
const capType = mv.enPassant ? types_1.PT.PAWN : (gs.board[mv.to]?.type ?? null);
// Make move on the current board
makeOn(gs.board, mv);
// Update castling rights
const cr = { ...gs.castle };
if (mv.from === 60 || mv.to === 60) {
cr.wK = false;
cr.wQ = false;
}
if (mv.from === 4 || mv.to === 4) {
cr.bK = false;
cr.bQ = false;
}
if (mv.from === 63 || mv.to === 63)
cr.wK = false;
if (mv.from === 56 || mv.to === 56)
cr.wQ = false;
if (mv.from === 7 || mv.to === 7)
cr.bK = false;
if (mv.from === 0 || mv.to === 0)
cr.bQ = false;
// En passant square
let newEP = null;
if (mvp.type === types_1.PT.PAWN && Math.abs(Math.floor(mv.to / B) - Math.floor(mv.from / B)) === 2)
newEP = (mv.from + mv.to) / 2;
// Half-move clock
let newHM = gs.halfMove;
if (capType !== null || mvp.type === types_1.PT.PAWN)
newHM = 0;
else
newHM++;
// Full move number
let newFM = gs.fullMove;
if (gs.turn === types_1.COL.BLACK)
newFM++;
const nextTurn = 1 - gs.turn;
// Check / mate / stalemate
const oppLegal = genLegalMoves({ ...gs, turn: nextTurn, castle: cr, ep: newEP });
const ks = findKing(nextTurn, gs.board);
const inCheck = ks >= 0 && attackedBy(ks, gs.turn, gs.board);
const isMate = inCheck && oppLegal.length === 0;
const isStale = !inCheck && oppLegal.length === 0;
// Track captures
if (capType !== null) {
if (mvp.color === types_1.COL.WHITE)
gs.capW.push(capType);
else
gs.capB.push(capType);
}
gs.history.push(mv);
gs.turn = nextTurn;
gs.castle = cr;
gs.ep = newEP;
gs.halfMove = newHM;
gs.fullMove = newFM;
// Notation
const notation = algebraicNotation(mv, gs.board, capType, inCheck, isMate, mvp);
// Set game over?
if (isMate) {
gs.gameOver = true;
gs.gameResult = (nextTurn === types_1.COL.BLACK ? '1-0' : '0-1') + ' by checkmate';
}
else if (isStale) {
gs.gameOver = true;
gs.gameResult = '1/2-1/2 by stalemate';
}
else if (insufficientMaterial(gs.board)) {
gs.gameOver = true;
gs.gameResult = '1/2-1/2 by insufficient material';
}
else if (newHM >= 100) {
gs.gameOver = true;
gs.gameResult = '1/2-1/2 by 50-move rule';
}
return { newGS: gs, notation };
}
// ── Notation ─────────────────────────────────────────────────────
function algebraicNotation(mv, bd, captured, inCheck, isMate, moving) {
if (mv.castling === 'kingside')
return isMate ? 'O-O#' : inCheck ? 'O-O+' : 'O-O';
if (mv.castling === 'queenside')
return isMate ? 'O-O-O#' : inCheck ? 'O-O-O+' : 'O-O-O';
const PN = { [types_1.PT.PAWN]: '', [types_1.PT.KNIGHT]: 'N', [types_1.PT.BISHOP]: 'B', [types_1.PT.ROOK]: 'R', [types_1.PT.QUEEN]: 'Q', [types_1.PT.KING]: 'K' };
let n = PN[moving.type] || '';
// Disambiguation for non-pawns
if (moving.type !== types_1.PT.PAWN) {
const others = [];
for (let i = 0; i < N; i++) {
const q = bd[i];
if (q && q.type === moving.type && q.color === moving.color && i !== mv.from) {
// Quick legality test via pseudo
const pm = pseudoMovesFor(bd, mv.to >= 0 ? moving.color : types_1.COL.WHITE, null, { wK: false, wQ: false, bK: false, bQ: false }).filter(x => x.from === i && x.to === mv.to);
if (pm.length > 0)
others.push(i);
}
}
if (others.length > 0) {
const sameFile = others.some(o => o % B === mv.from % B);
if (!sameFile)
n += (0, types_1.sqF)(mv.from);
else
n += (0, types_1.sqR)(mv.from);
}
}
if (captured !== null) {
if (moving.type === types_1.PT.PAWN)
n += (0, types_1.sqF)(mv.from);
n += 'x';
}
n += (0, types_1.sqN)(mv.to);
if (mv.promotion) {
const PM = { 2: 'N', 3: 'B', 4: 'R', 5: 'Q' };
n += '=' + (PM[mv.promotion] ?? 'Q');
}
if (isMate)
n += '#';
else if (inCheck)
n += '+';
return n;
}
// ── Insufficient Material ────────────────────────────────────────
function insufficientMaterial(bd) {
const pcs = [];
for (let i = 0; i < N; i++) {
const p = bd[i];
if (p)
pcs.push(p.type);
}
if (pcs.length === 2)
return true; // K vs K
if (pcs.length === 3) {
const minors = pcs.filter(t => t === types_1.PT.KNIGHT || t === types_1.PT.BISHOP);
if (minors.length <= 1)
return true; // K+minor vs K
}
return false;
}
// ── FEN Parse / Generate ─────────────────────────────────────────
function parseFEN(fen) {
const parts = fen.trim().split(/\s+/);
const gs = makeState(types_1.COL.WHITE);
gs.board = new Array(N).fill(null);
let sq = 0;
const mp = {
P: [types_1.PT.PAWN, types_1.COL.WHITE], N: [types_1.PT.KNIGHT, types_1.COL.WHITE], B: [types_1.PT.BISHOP, types_1.COL.WHITE], R: [types_1.PT.ROOK, types_1.COL.WHITE], Q: [types_1.PT.QUEEN, types_1.COL.WHITE], K: [types_1.PT.KING, types_1.COL.WHITE],
p: [types_1.PT.PAWN, types_1.COL.BLACK], n: [types_1.PT.KNIGHT, types_1.COL.BLACK], b: [types_1.PT.BISHOP, types_1.COL.BLACK], r: [types_1.PT.ROOK, types_1.COL.BLACK], q: [types_1.PT.QUEEN, types_1.COL.BLACK], k: [types_1.PT.KING, types_1.COL.BLACK],
};
for (const ch of parts[0]) {
if (ch === '/')
continue;
if (ch >= '1' && ch <= '8') {
sq += parseInt(ch);
continue;
}
const [t, c] = mp[ch] || [types_1.PT.EMPTY, types_1.COL.WHITE];
gs.board[sq++] = { type: t, color: c };
}
gs.turn = parts[1] === 'w' ? types_1.COL.WHITE : types_1.COL.BLACK;
const cr = parts[2];
gs.castle.wK = cr.includes('K');
gs.castle.wQ = cr.includes('Q');
gs.castle.bK = cr.includes('k');
gs.castle.bQ = cr.includes('q');
if (cr === '-')
gs.castle = { wK: false, wQ: false, bK: false, bQ: false };
if (parts[3] !== '-') {
const f = parts[3].charCodeAt(0) - 97;
const r = 8 - parseInt(parts[3][1]);
gs.ep = r * B + f;
}
gs.halfMove = parseInt(parts[4] || '0');
gs.fullMove = parseInt(parts[5] || '1');
return gs;
}
function genFEN(gs) {
let fen = '';
for (let r = 0; r < B; r++) {
let e = 0;
for (let f = 0; f < B; f++) {
const p = gs.board[r * B + f];
if (!p)
e++;
else {
if (e > 0) {
fen += e;
e = 0;
}
const cm = { 1: 'P', 2: 'N', 3: 'B', 4: 'R', 5: 'Q', 6: 'K' };
fen += (p.color === types_1.COL.WHITE ? cm[p.type] : cm[p.type].toLowerCase());
}
}
if (e > 0) {
fen += e;
e = 0;
}
if (r < 7)
fen += '/';
}
fen += ' ' + (gs.turn === types_1.COL.WHITE ? 'w' : 'b');
let cr = '';
if (gs.castle.wK)
cr += 'K';
if (gs.castle.wQ)
cr += 'Q';
if (gs.castle.bK)
cr += 'k';
if (gs.castle.bQ)
cr += 'q';
fen += ' ' + (cr || '-');
fen += ' ' + (gs.ep !== null ? (0, types_1.sqN)(gs.ep) : '-');
fen += ' ' + gs.halfMove + ' ' + gs.fullMove;
return fen;
}
// ── Undo One Move (using snapshot approach) ──────────────────────
function undoWithSnapshots(gs, snapshots) {
if (snapshots.length < 1)
return false;
const prev = snapshots.pop();
try {
const restored = parseFEN(prev);
// Copy board and key fields
gs.board = restored.board;
gs.turn = restored.turn;
gs.castle = restored.castle;
gs.ep = restored.ep;
gs.halfMove = restored.halfMove;
gs.fullMove = restored.fullMove;
gs.gameOver = false;
gs.gameResult = null;
// Keep current history but remove last move
if (gs.history.length > 0)
gs.history.pop();
// Rebuild captured lists
rebuildCaptures(gs);
return true;
}
catch {
return false;
}
}
function rebuildCaptures(gs) {
// Replay all moves to get accurate captured lists
// This is expensive but correct
// For simplicity, we just keep what we have minus last captures
// A full replay would be more accurate but complex
// Leave as-is for now
}
function pushSnapshot(gs) {
if (!globalThis.__snapshots)
globalThis.__snapshots = [];
globalThis.__snapshots.push(genFEN(gs));
}
function popSnapshot() {
if (!globalThis.__snapshots || globalThis.__snapshots.length === 0)
return null;
const snap = globalThis.__snapshots.pop();
return parseFEN(snap);
}
const MAX_TIME_MS = 1000;
function aiPickBest(gs, depth) {
const lms = genLegalMoves(gs);
if (lms.length === 0)
return { bestMove: null, score: 0 };
const ordered = orderMoves(gs, lms);
const aiCol = gs.turn;
let bestMv = ordered[0];
let bestSc = -Infinity;
let reached = 0;
for (let d = 1; d <= depth; d++) {
const t0 = performance.now();
let score = -Infinity;
let mv = null;
for (const m of ordered) {
const sc = negamaxSearch(gs, m, d - 1, -Infinity, Infinity, aiCol);
if (sc > score) {
score = sc;
mv = m;
}
}
bestSc = score;
bestMv = mv;
reached = d;
if (performance.now() - t0 > MAX_TIME_MS * 0.8)
break;
}
return { bestMove: bestMv, score: bestSc };
}
function negamaxSearch(gi, mv, depthLeft, alpha, beta, aiCol) {
const savedBoard = gi.board.slice();
const mvp = gi.board[mv.from], cap = gi.board[mv.to];
makeOn(savedBoard, mv);
const next = 1 - gi.turn;
const cr = updateCastling(gi.castle, mv);
let ep2 = null;
if (mvp.type === types_1.PT.PAWN && Math.abs(Math.floor(mv.to / B) - Math.floor(mv.from / B)) === 2)
ep2 = (mv.from + mv.to) / 2;
if (depthLeft === 0)
return evaluatePosition(savedBoard, aiCol);
const tempGs = { ...gi, board: savedBoard, turn: next, castle: cr, ep: ep2 };
const moves = genLegalMoves(tempGs);
if (moves.length === 0) {
const ks = findKing(next, savedBoard);
if (attackedBy(ks, 1 - next, savedBoard))
return (aiCol === next ? -99999 : 99999) + depthLeft;
return 0;
}
const ordered = orderMoves(tempGs, moves);
for (const m of ordered) {
const sc = -negamaxSearch(tempGs, m, depthLeft - 1, -beta, -alpha, aiCol);
if (sc > alpha)
alpha = sc;
if (alpha >= beta)
break;
}
return alpha;
}
function orderMoves(gs, moves) {
return [...moves].sort((a, b) => {
const va = gs.board[a.to] ? MV[gs.board[a.to].type] - (MV[gs.board[a.from]?.type || 0] || 0) : 0;
const vb = gs.board[b.to] ? MV[gs.board[b.to].type] - (MV[gs.board[b.from]?.type || 0] || 0) : 0;
return (vb + (b.promotion ? 800 : 0)) - (va + (a.promotion ? 800 : 0));
});
}
function evaluatePosition(bd, aiCol) {
let sc = 0;
for (let sq = 0; sq < N; sq++) {
const p = bd[sq];
if (!p)
continue;
const val = MV[p.type] || 0;
const mirror = p.color === types_1.COL.BLACK ? mirrorSq(sq) : sq;
let pstVal = 0;
if (p.type === types_1.PT.KING) {
pstVal = KING_END_PST[mirror]; // simplified: always use endgame king table
}
else {
pstVal = PST[p.type]?.[mirror] ?? 0;
}
const total = val + pstVal;
sc += (p.color === aiCol) ? total : -total;
}
return sc;
}
function updateCastling(old, mv) {
const r = { ...old };
if (mv.from === 60 || mv.to === 60) {
r.wK = false;
r.wQ = false;
}
if (mv.from === 4 || mv.to === 4) {
r.bK = false;
r.bQ = false;
}
if (mv.from === 63 || mv.to === 63)
r.wK = false;
if (mv.from === 56 || mv.to === 56)
r.wQ = false;
if (mv.from === 7 || mv.to === 7)
r.bK = false;
if (mv.from === 0 || mv.to === 0)
r.bQ = false;
return r;
}
};
// ── module: src/app.ts ──
__mods["src/app.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameController = exports.ChessRenderer = void 0;
exports.initAudio = initAudio;
exports.playSound = playSound;
exports.toggleMute = toggleMute;
// ═══════════════════════════════════════════════════════════════
// app.ts — Canvas renderer, input handling, game controller, CSS
// ═══════════════════════════════════════════════════════════════
const types_1 = require("./types");
const engine_1 = require("./engine");
const B = 8;
// ── Piece characters ─────────────────────────────────────────────
// Simple approach - inline lookup
function pieceChar(type, color) {
if (type === types_1.PT.PAWN)
return color === types_1.COL.WHITE ? '♙' : '♟';
if (type === types_1.PT.KNIGHT)
return color === types_1.COL.WHITE ? '♘' : '♞';
if (type === types_1.PT.BISHOP)
return color === types_1.COL.WHITE ? '♗' : '♝';
if (type === types_1.PT.ROOK)
return color === types_1.COL.WHITE ? '♖' : '♜';
if (type === types_1.PT.QUEEN)
return color === types_1.COL.WHITE ? '♕' : '♛';
if (type === types_1.PT.KING)
return color === types_1.COL.WHITE ? '♔' : '♚';
return '?';
}
// ── Renderer ─────────────────────────────────────────────────────
class ChessRenderer {
constructor(canvasId) {
this.sqSize = 70;
this.labelSize = 18;
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
this.resize();
}
resize() {
const container = this.canvas.parentElement;
const maxW = Math.min(container.clientWidth - 10, 720);
const maxH = window.innerHeight - 130;
const size = Math.min(maxW, maxH, 720);
this.sqSize = Math.max(Math.floor(size / (B + 2)), 45);
const total = this.sqSize * B;
this.canvas.width = total + 2 * this.labelSize;
this.canvas.height = total + 2 * this.labelSize;
}
render(gs, selSq, legalForSel, lastMv, flipped, checkSquares) {
const ctx = this.ctx;
const ss = this.sqSize;
const ml = this.labelSize;
const ox = ml;
const oy = ml;
// Background
ctx.fillStyle = '#2c1810';
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
// Squares
for (let vr = 0; vr < B; vr++) {
for (let vc = 0; vc < B; vc++) {
const realRow = flipped ? vr : B - 1 - vr;
const realCol = flipped ? vc : B - 1 - vc;
const sq = realRow * B + realCol;
const light = (realRow + realCol) % 2 === 0;
ctx.fillStyle = light ? '#f0d9b5' : '#b58863';
ctx.fillRect(ox + vc * ss, oy + vr * ss, ss, ss);
// Last move highlight
if (lastMv && (sq === lastMv.from || sq === lastMv.to)) {
ctx.fillStyle = 'rgba(155,199,0,0.41)';
ctx.fillRect(ox + vc * ss, oy + vr * ss, ss, ss);
}
// Selected square
if (selSq === sq) {
ctx.fillStyle = 'rgba(120,200,120,0.5)';
ctx.fillRect(ox + vc * ss, oy + vr * ss, ss, ss);
}
// Check indicator
if (checkSquares.has(sq)) {
ctx.save();
ctx.fillStyle = 'rgba(255,30,30,0.55)';
ctx.beginPath();
ctx.arc(ox + vc * ss + ss / 2, oy + vr * ss + ss / 2, ss * 0.48, 0, Math.PI * 2);
ctx.fill();
ctx.restore();
}
}
}
// Legal move indicators
for (const m of legalForSel) {
const tr = Math.floor(m.to / B);
const tc = m.to % B;
const visRow = flipped ? B - 1 - tr : tr;
const visCol = flipped ? B - 1 - tc : tc;
const hasCapture = gs.board[m.to] !== null || m.enPassant;
if (hasCapture) {
ctx.strokeStyle = 'rgba(0,0,0,0.35)';
ctx.lineWidth = ss * 0.12;
ctx.beginPath();
ctx.arc(ox + visCol * ss + ss / 2, oy + visRow * ss + ss / 2, ss * 0.42, 0, Math.PI * 2);
ctx.stroke();
}
else {
ctx.fillStyle = 'rgba(0,0,0,0.22)';
ctx.beginPath();
ctx.arc(ox + visCol * ss + ss / 2, oy + visRow * ss + ss / 2, ss * 0.15, 0, Math.PI * 2);
ctx.fill();
}
}
// Pieces
const fontSize = Math.floor(ss * 0.72);
ctx.font = `${fontSize}px "Segoe UI Symbol", "Noto Sans Symbols2", serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let vr = 0; vr < B; vr++) {
for (let vc = 0; vc < B; vc++) {
const realRow = flipped ? vr : B - 1 - vr;
const realCol = flipped ? vc : B - 1 - vc;
const sq = realRow * B + realCol;
const p = gs.board[sq];
if (!p)
continue;
const ch = pieceChar(p.type, p.color);
const cx = ox + vc * ss + ss / 2;
const cy = oy + vr * ss + ss / 2;
ctx.save();
ctx.shadowColor = 'rgba(0,0,0,0.3)';
ctx.shadowBlur = 3;
ctx.shadowOffsetX = 1;
ctx.shadowOffsetY = 2;
ctx.strokeStyle = (p.color === types_1.COL.WHITE) ? '#555' : '#000';
ctx.lineWidth = 1.2;
ctx.strokeText(ch, cx, cy);
ctx.fillStyle = (p.color === types_1.COL.WHITE) ? '#fff' : '#222';
ctx.fillText(ch, cx, cy);
ctx.restore();
}
}
// Labels
const lf = Math.max(Math.floor(ss * 0.2), 11);
ctx.font = `bold ${lf}px sans-serif`;
ctx.fillStyle = '#ccc';
// File labels
ctx.textAlign = 'center';
ctx.textBaseline = 'top';
for (let i = 0; i < B; i++) {
const fi = flipped ? B - 1 - i : i;
ctx.fillText(String.fromCharCode(97 + fi), ox + i * ss + ss / 2, oy + B * ss + 3);
}
// Rank labels
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let i = 0; i < B; i++) {
const ri = flipped ? i : B - 1 - i;
ctx.fillText(String(ri + 1), ox - 3, oy + i * ss + ss / 2);
}
}
getSquareFromMouse(mouseX, mouseY) {
const rect = this.canvas.getBoundingClientRect();
const mx = mouseX - rect.left;
const my = mouseY - rect.top;
const col = Math.floor((mx - this.labelSize) / this.sqSize);
const row = Math.floor((my - this.labelSize) / this.sqSize);
if (col < 0 || col >= B || row < 0 || row >= B)
return null;
const realRow = B - 1 - row;
const realCol = col;
return realRow * B + realCol;
}
}
exports.ChessRenderer = ChessRenderer;
// ── Web Audio Sound Effects ──────────────────────────────────────
let audioCtx = null;
let audioMuted = false;
function initAudio() {
try {
audioCtx = new (window.AudioContext || window.webkitAudioContext)();
}
catch (_) { }
}
function playSound(type) {
if (audioMuted || !audioCtx)
return;
if (audioCtx.state === 'suspended')
audioCtx.resume();
const t = audioCtx.currentTime;
switch (type) {
case 'move':
playTone(t, 440, 0.06, 'sine', 0.1);
break;
case 'capture':
playTone(t, 300, 0.12, 'square', 0.12);
break;
case 'check':
playTone(t, 800, 0.08, 'triangle', 0.2);
break;
case 'gameover':
playTone(t, 523, 0.15, 'sine', 0.25);
playTone(t + 0.15, 659, 0.15, 'sine', 0.25);
playTone(t + 0.30, 784, 0.3, 'sine', 0.3);
break;
}
}
function playTone(time, freq, dur, type, vol) {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, time);
gain.gain.setValueAtTime(vol, time);
gain.gain.exponentialRampToValueAtTime(0.001, time + dur);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start(time);
osc.stop(time + dur);
}
function toggleMute() { audioMuted = !audioMuted; return audioMuted; }
const SAVE_KEY = 'chess_save_v2';
class GameController {
constructor(playerColor) {
this.selectedSq = null;
this.legalForSelected = [];
this.flipped = false;
this.snapStack = { snapshots: [] };
this.aiDepth = 3;
this.aiThinking = false;
this.promoPending = null;
this.moveCount = 0;
initAudio();
this.gs = (0, engine_1.makeState)(playerColor);
// Try restore
const saved = localStorage.getItem(SAVE_KEY);
if (saved) {
try {
this.gs = (0, engine_1.parseFEN)(saved);
this.moveCount = this.gs.history.length;
}
catch (_e) { }
}
this.renderer = new ChessRenderer('chess-canvas');
this.setupEvents();
this.fullRender();
// If it's AI's turn on load
if (this.gs.turn !== playerColor && !this.gs.gameOver) {
this.aiThinking = true;
this.updateUI('AI thinking...');
setTimeout(() => this.doAIMove(), 300);
}
// Debug API
window.__chess = {
fen: () => (0, engine_1.genFEN)(this.gs),
legalMoves: () => (0, engine_1.genLegalMoves)(this.gs),
state: () => this.gs,
playerColor: this.gs.playerColor,
parseFEN: engine_1.parseFEN,
attackedBy: engine_1.attackedBy,
findKing: engine_1.findKing,
};
}
setupEvents() {
// Canvas click
const canvas = document.getElementById('chess-canvas');
canvas.addEventListener('click', (e) => {
const sq = this.renderer.getSquareFromMouse(e.clientX, e.clientY);
if (sq !== null)
this.handleSquareClick(sq);
});
// Flip board
document.getElementById('flip-board').addEventListener('click', () => {
this.flipped = !this.flipped;
this.fullRender();
});
// Play as white/black
document.getElementById('play-white').addEventListener('click', () => this.newGame(types_1.COL.WHITE));
document.getElementById('play-black').addEventListener('click', () => this.newGame(types_1.COL.BLACK));
// Undo
document.getElementById('undo-btn').addEventListener('click', () => this.undo());
// Difficulty
const ds = document.getElementById('difficulty-select');
if (ds)
ds.addEventListener('change', () => { this.aiDepth = parseInt(ds.value); });
// Mute
const ml = document.getElementById('mute-label');
if (ml)
ml.addEventListener('click', () => {
const muted = toggleMute();
ml.textContent = muted ? '🔇' : '🔊';
});
// Resize
window.addEventListener('resize', () => { this.renderer.resize(); this.fullRender(); });
}
handleSquareClick(sq) {
if (this.gs.gameOver || this.aiThinking)
return;
if (this.gs.turn !== this.gs.playerColor)
return;
const piece = this.gs.board[sq];
// If we have a selection, try to move there
if (this.selectedSq !== null) {
const targetMove = this.legalForSelected.find(m => m.to === sq);
if (targetMove) {
if (targetMove.promotion) {
this.showPromotionPicker(targetMove.from, targetMove.to);
return;
}
this.executeMove(targetMove);
return;
}
}
// Select own piece on own turn
if (piece && piece.color === this.gs.playerColor && this.gs.turn === this.gs.playerColor) {
this.selectedSq = sq;
this.legalForSelected = (0, engine_1.genLegalMoves)(this.gs).filter(m => m.from === sq);
this.fullRender();
}
else {
this.deselect();
this.fullRender();
}
}
deselect() {
this.selectedSq = null;
this.legalForSelected = [];
}
executeMove(mv) {
this.selectedSq = null;
this.legalForSelected = [];
// Save snapshot before move
this.pushSnap();
const { notation } = (0, engine_1.applyMoveToState)(this.gs, mv);
// Sound
const wasCapture = !!this.gs.capW.includes((mv.enPassant ? types_1.PT.PAWN : (this.gs.board[mv.to]?.type ?? types_1.PT.EMPTY))) || mv.enPassant;
const soundType = (notation.includes('x') || mv.enPassant) ? 'capture' :
notation.includes('+') || notation.includes('#') ? 'check' : 'move';
playSound(soundType);
// Update move list display
this.addNotation(notation);
// Save
this.saveState();
if (this.gs.gameOver) {
this.endGameDisplay(notation);
return;
}
// AI's turn?
if (this.gs.turn !== this.gs.playerColor) {
this.aiThinking = true;
this.updateUI('AI thinking...');
this.fullRender();
setTimeout(() => this.doAIMove(), 150);
}
else {
this.fullRender();
}
}
doAIMove() {
if (this.gs.gameOver || this.aiThinking)
return;
const result = (0, engine_1.aiPickBest)(this.gs, this.aiDepth);
if (!result.bestMove) {
this.gs.gameOver = true;
this.gs.gameResult = '1/2-1/2 by stalemate';
this.endGameDisplay('');
return;
}
this.pushSnap();
const { notation } = (0, engine_1.applyMoveToState)(this.gs, result.bestMove);
playSound(notation.includes('x') || result.bestMove.enPassant ? 'capture' :
notation.includes('+') || notation.includes('#') ? 'check' : 'move');
this.addNotation(notation);
this.saveState();
this.aiThinking = false;
if (this.gs.gameOver) {
this.endGameDisplay(notation);
}
else {
this.updateUI('');
this.fullRender();
}
}
showPromotionPicker(from, to) {
this.promoPending = { from, to };
const panel = document.getElementById('promotion-picker');
panel.hidden = false;
panel.style.display = 'flex';
const pieces = [
{ type: types_1.PT.QUEEN, char: '♕' },
{ type: types_1.PT.ROOK, char: '♖' },
{ type: types_1.PT.BISHOP, char: '♗' },
{ type: types_1.PT.KNIGHT, char: '♘' },
];
// Clear old buttons
while (panel.firstChild)
panel.removeChild(panel.firstChild);
for (const pt of pieces) {
const btn = document.createElement('button');
btn.textContent = pt.char;
btn.className = 'promo-btn';
btn.addEventListener('click', () => {
panel.hidden = true;
panel.style.display = 'none';
const target = (0, engine_1.genLegalMoves)(this.gs).find(m => m.from === from && m.to === to && m.promotion === pt.type);
if (target)
this.executeMove(target);
this.promoPending = null;
});
panel.appendChild(btn);
}
}
undo() {
// Pop 2 snapshots (player move + AI move)
while (this.snapStack.snapshots.length > 0 && this.snapsEmpty()) {
this.snapStack.snapshots.pop();
}
if (this.snapStack.snapshots.length < 1)
return;
// Undo AI move first (most recent), then player move
const aiSnap = this.snapStack.snapshots.pop();
const playerSnap = this.snapStack.snapshots.pop();
if (playerSnap) {
const prevGs = (0, engine_1.parseFEN)(playerSnap);
// Copy over board and state
this.gs.board = prevGs.board;
this.gs.turn = prevGs.turn;
this.gs.castle = prevGs.castle;
this.gs.ep = prevGs.ep;
this.gs.halfMove = prevGs.halfMove;
this.gs.fullMove = prevGs.fullMove;
this.gs.gameOver = false;
this.gs.gameResult = null;
}
// Remove last move(s) from history
while (this.gs.history.length > 0) {
// Count how many moves to pop (1 or 2)
break;
}
// Simpler: clear history and rebuild from remaining snapshots would be too complex
// Instead, just remove the last few entries
this.removeLastMovesFromHistory();
this.saveState();
this.clearNotationDisplay();
this.fullRender();
}
snapsEmpty() { return false; }
pushSnap() {
this.snapStack.snapshots.push((0, engine_1.genFEN)(this.gs));
if (this.snapStack.snapshots.length > 200)
this.snapStack.snapshots.shift();
}
removeLastMovesFromHistory() {
// Remove last 1 or 2 moves
const countToRemove = (this.gs.history.length > 0 && this.gs.turn === this.gs.playerColor) ? 1 : 2;
for (let i = 0; i < countToRemove && this.gs.history.length > 0; i++) {
this.gs.history.pop();
}
}
clearNotationDisplay() {
const el = document.getElementById('move-list');
el.innerHTML = '';
}
endGameDisplay(lastNotation) {
this.updateUI(this.gs.gameResult || 'Game Over');
document.getElementById('turn-indicator').textContent = this.gs.gameResult || 'Game Over';
playSound('gameover');
this.saveState();
this.fullRender();
}
newGame(playerColor) {
this.gs = (0, engine_1.makeState)(playerColor);
this.selectedSq = null;
this.legalForSelected = [];
this.snapStack = { snapshots: [] };
this.flipped = false;
this.moveCount = 0;
this.saveState();
this.fullRender();
this.clearNotationDisplay();
if (this.gs.turn !== playerColor) {
this.aiThinking = true;
this.updateUI('AI thinking...');
setTimeout(() => this.doAIMove(), 300);
}
}
saveState() {
try {
localStorage.setItem(SAVE_KEY, (0, engine_1.genFEN)(this.gs));
}
catch (_e) { }
}
// ── Rendering Helpers ──────────────────────────────────────────
fullRender() {
const checkSq = new Set();
const ks = (0, engine_1.findKing)(this.gs.turn, this.gs.board);
if (ks >= 0 && (0, engine_1.attackedBy)(ks, 1 - this.gs.turn, this.gs.board))
checkSq.add(ks);
this.renderer.render(this.gs, this.selectedSq, this.legalForSelected, this.gs.history[this.gs.history.length - 1] || null, this.flipped, checkSq);
this.updateTurnIndicator();
this.updateCapturedTrays();
}
updateTurnIndicator() {
const el = document.getElementById('turn-indicator');
if (this.gs.gameOver) {
el.textContent = this.gs.gameResult || 'Game Over';
}
else {
const name = this.gs.turn === types_1.COL.WHITE ? 'White' : 'Black';
const yourTurn = this.gs.turn === this.gs.playerColor;
el.textContent = yourTurn ? `${name}'s turn (You)` : `${name}'s turn (AI)`;
}
}
addNotation(notation) {
const listEl = document.getElementById('move-list');
this.moveCount++;
const pairNum = Math.ceil(this.moveCount / 2);
const isWhite = this.moveCount % 2 === 1;
if (isWhite) {
const entry = document.createElement('div');
entry.className = 'move-row';
entry.innerHTML = `<span class="mn">${pairNum}.</span> <span class="mw">${notation}</span>`;
listEl.appendChild(entry);
}
else {
const rows = listEl.querySelectorAll('.move-row');
if (rows.length > 0) {
const last = rows[rows.length - 1];
last.innerHTML += ` <span class="mb">${notation}</span>`;
}
}
listEl.scrollTop = listEl.scrollHeight;
}
updateCapturedTrays() {
const wt = document.getElementById('captured-white');
const bt = document.getElementById('captured-black');
const chars = {
[types_1.PT.PAWN]: '♟', [types_1.PT.KNIGHT]: '♞', [types_1.PT.BISHOP]: '♝', [types_1.PT.ROOK]: '♜', [types_1.PT.QUEEN]: '♛', [types_1.PT.KING]: '♚',
};
wt.innerHTML = this.gs.capW.map(t => `<span>${chars[t] || ''}</span>`).join('');
bt.innerHTML = this.gs.capB.map(t => `<span>${chars[t] || ''}</span>`).join('');
}
updateUI(msg) {
document.getElementById('status-message').textContent = msg;
}
}
exports.GameController = GameController;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"qwen/qwen3.7-flash","provider":"openrouter","persona":"typescript","sessionId":"3c935ed9-841a-49fa-90f9-c52c13c8165c","tokensIn":20874101,"tokensOut":136472,"tokensTotal":21010573,"cost":0.6499870380000001,"turns":166,"toolCalls":165,"failedToolCalls":7,"timestamp":"2026-08-02T02:49:38.734Z"} -->