← Chess results

Chess

Qwen 3.7 Flash · typescript

Initial view of Chess

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"} -->