← Chess results

Chess

Qwen 3.6 27B MTP GGUF UD Q5 K XL · typescript

Initial view of Chess

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 79,807 bytes · SHA-256 7207922518a2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — AI Opponent</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }

body {
  font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
  background: #2d2416;
  color: #333;
  display: flex;
  justify-content: center;
  align-items: flex-start;
  min-height: 100vh;
  padding: 20px;
}

#game-container {
  display: flex;
  gap: 24px;
  align-items: flex-start;
}

#board-area {
  flex-shrink: 0;
}

#board-canvas {
  display: block;
  cursor: pointer;
  border-radius: 4px;
  width: 560px;
  height: 560px;
}

#side-panel {
  width: 260px;
  display: flex;
  flex-direction: column;
  gap: 12px;
}

#turn-indicator {
  background: #f5eedd;
  padding: 10px 16px;
  border-radius: 6px;
  font-size: 18px;
  font-weight: 600;
  text-align: center;
  border: 2px solid #b58863;
}

#status-bar {
  background: #1a1a2e;
  color: #ffd700;
  padding: 10px 16px;
  border-radius: 6px;
  font-size: 15px;
  font-weight: 600;
  text-align: center;
  min-height: 40px;
  display: flex;
  align-items: center;
  justify-content: center;
}

#captured-area {
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.captured-tray {
  background: #f5eedd;
  padding: 6px 10px;
  border-radius: 4px;
  min-height: 30px;
  font-size: 20px;
  letter-spacing: 2px;
  line-height: 1.2;
  word-break: break-all;
}

#move-list-container {
  background: #f5eedd;
  border-radius: 6px;
  overflow: hidden;
  max-height: 280px;
  display: flex;
  flex-direction: column;
}

#move-list-header {
  padding: 8px 12px;
  font-weight: 700;
  font-size: 13px;
  color: #6b4423;
  border-bottom: 2px solid #b58863;
  flex-shrink: 0;
}

#move-list {
  padding: 8px 12px;
  font-size: 13px;
  font-family: 'Courier New', monospace;
  overflow-y: auto;
  flex: 1;
  line-height: 1.6;
}

.move-number {
  color: #888;
  margin-right: 4px;
}

#controls {
  display: flex;
  gap: 6px;
  flex-wrap: wrap;
}

#controls button {
  flex: 1;
  padding: 8px 10px;
  border: 2px solid #b58863;
  background: #f5eedd;
  border-radius: 6px;
  cursor: pointer;
  font-size: 13px;
  font-weight: 600;
  color: #4a3520;
  min-width: 60px;
  transition: background 0.15s;
}

#controls button:hover {
  background: #e8dcc8;
}

#difficulty-area {
  display: flex;
  align-items: center;
  gap: 8px;
  background: #f5eedd;
  padding: 8px 12px;
  border-radius: 6px;
  font-size: 13px;
  font-weight: 600;
}

#ai-depth {
  flex: 1;
  padding: 4px;
  border: 1px solid #b58863;
  border-radius: 4px;
  font-size: 13px;
  background: #fff;
}

#thinking-indicator {
  background: #f5eedd;
  padding: 8px;
  border-radius: 6px;
  text-align: center;
  font-size: 14px;
  color: #6b4423;
  animation: pulse 1s ease-in-out infinite;
}

@keyframes pulse {
  0%, 100% { opacity: 1; }
  50% { opacity: 0.5; }
}

#new-game-dialog {
  background: #f5eedd;
  border: 2px solid #b58863;
  border-radius: 8px;
  padding: 16px;
  display: flex;
  flex-direction: column;
  gap: 10px;
}

#new-game-dialog.hidden { display: none; }

.dialog-title {
  font-size: 16px;
  font-weight: 700;
  color: #6b4423;
}

.dialog-body {
  font-size: 14px;
  display: flex;
  flex-direction: column;
  gap: 6px;
}

.dialog-body label {
  display: flex;
  align-items: center;
  gap: 6px;
}

.dialog-actions {
  display: flex;
  gap: 8px;
}

.dialog-actions button {
  flex: 1;
  padding: 8px;
  border: 2px solid #b58863;
  background: #fff;
  border-radius: 6px;
  cursor: pointer;
  font-weight: 600;
  color: #4a3520;
}

.dialog-actions button:first-child {
  background: #6b4423;
  color: #fff;
  border-color: #5a3a1d;
}

.hidden { display: none !important; }
</style>
</head>
<body>
<div id="game-container">
  <div id="board-area">
    <canvas id="board-canvas" width="560" height="560"></canvas>
  </div>
  <div id="side-panel">
    <div id="turn-indicator">White to move</div>
    <div id="status-bar"></div>
    <div id="captured-area">
      <div id="captured-white" class="captured-tray"></div>
      <div id="captured-black" class="captured-tray"></div>
    </div>
    <div id="move-list-container">
      <div id="move-list-header">Moves</div>
      <div id="move-list"></div>
    </div>
    <div id="controls">
      <button id="btn-undo" title="Undo">↩ Undo</button>
      <button id="btn-flip" title="Flip Board">🔄 Flip</button>
      <button id="btn-new" title="New Game">New</button>
      <button id="btn-sound" title="Toggle Sound">🔊</button>
    </div>
    <div id="difficulty-area">
      <label>AI: </label>
      <select id="ai-depth">
        <option value="1">Easy</option>
        <option value="2">Medium</option>
        <option value="3" selected="">Hard</option>
      </select>
    </div>
    <div id="new-game-dialog" class="hidden">
      <div class="dialog-title">New Game</div>
      <div class="dialog-body">
        <label><input type="radio" name="side" value="white" checked=""> Play as White</label><br>
        <label><input type="radio" name="side" value="black"> Play as Black</label><br>
        <label><input type="checkbox" id="analysis-cb"> Analysis Mode (no AI)</label>
      </div>
      <div class="dialog-actions">
        <button id="btn-start">Start</button>
        <button id="btn-cancel-new">Cancel</button>
      </div>
    </div>
    <div id="thinking-indicator" class="hidden">🤔 AI thinking...</div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./renderer":"src/renderer.ts","./constants":"src/constants.ts","./types":"src/types.ts"},"src/engine.ts":{"./types":"src/types.ts"},"src/ai.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./engine":"src/engine.ts"},"src/renderer.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./engine":"src/engine.ts"},"src/sound.ts":{"./constants":"src/constants.ts"},"src/game.ts":{"./types":"src/types.ts","./engine":"src/engine.ts","./ai":"src/ai.ts","./sound":"src/sound.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const constants_1 = require("./constants");
// ── Main Entry ──────────────────────────────────────────────────────
const canvas = document.getElementById('board-canvas');
const turnIndicator = document.getElementById('turn-indicator');
const statusBar = document.getElementById('status-bar');
const capturedWhiteEl = document.getElementById('captured-white');
const capturedBlackEl = document.getElementById('captured-black');
const moveListEl = document.getElementById('move-list');
const btnUndo = document.getElementById('btn-undo');
const btnFlip = document.getElementById('btn-flip');
const btnNew = document.getElementById('btn-new');
const btnSound = document.getElementById('btn-sound');
const aiDepthSelect = document.getElementById('ai-depth');
const newGameDialog = document.getElementById('new-game-dialog');
const btnStart = document.getElementById('btn-start');
const btnCancelNew = document.getElementById('btn-cancel-new');
const thinkingEl = document.getElementById('thinking-indicator');
// Init game
const game = new game_1.GameController();
const renderer = new renderer_1.Renderer(canvas, {
    onSquareClick: handleSquareClick,
    onSquareDragStart: () => { },
    onSquareDragEnd: handleDragEnd,
});
// Set AI depth from select
game.setAiDepth(parseInt(aiDepthSelect.value));
// Wire renderer click for promotion
canvas.addEventListener('click', (e) => {
    const promoPiece = renderer.onPromoClick(e.clientX, e.clientY);
    if (promoPiece) {
        game.confirmPromotion(promoPiece);
    }
});
// Track selected square
let selectedSq = null;
function handleSquareClick(sq) {
    const state = game.getState();
    // If we have a pending promo, ignore board clicks (handled separately)
    if (game.hasPendingPromo())
        return;
    const piece = state.board[sq];
    if (selectedSq === null) {
        // Select a piece
        if (piece && piece.color === game.getPlayerColor()) {
            selectedSq = sq;
            const moves = game.legalMoves().filter(m => m.from === sq);
            renderer.setSelected(sq);
            renderer.setLegalMoves(moves);
        }
    }
    else {
        // Try to move
        const piece = state.board[sq];
        if (sq === selectedSq) {
            // Deselect
            selectedSq = null;
            renderer.setSelected(null);
            renderer.setLegalMoves([]);
            return;
        }
        if (piece && piece.color === game.getPlayerColor()) {
            // Select different piece
            selectedSq = sq;
            const moves = game.legalMoves().filter(m => m.from === sq);
            renderer.setSelected(sq);
            renderer.setLegalMoves(moves);
            return;
        }
        // Try move
        const state2 = game.getState();
        if (state2.turn === game.getPlayerColor()) {
            const legal = game.legalMoves();
            const matches = legal.filter(m => m.from === selectedSq && m.to === sq);
            if (matches.length > 0) {
                const promoMoves = matches.filter(m => m.promotion);
                if (promoMoves.length > 0) {
                    // Show promotion picker
                    renderer.setPendingPromo({ from: selectedSq, to: sq });
                    selectedSq = null;
                    renderer.setSelected(null);
                    renderer.setLegalMoves([]);
                    game.setPendingPromo(selectedSq, sq);
                    return;
                }
            }
            game.tryMove(selectedSq, sq);
        }
        selectedSq = null;
        renderer.setSelected(null);
        renderer.setLegalMoves([]);
    }
}
function handleDragEnd(from, to) {
    if (to === null)
        return;
    if (game.getState().turn !== game.getPlayerColor())
        return;
    const legal = game.legalMoves();
    const matches = legal.filter(m => m.from === from && m.to === to);
    if (matches.length > 0) {
        const promoMoves = matches.filter(m => m.promotion);
        if (promoMoves.length > 0) {
            renderer.setPendingPromo({ from, to });
            game.setPendingPromo(from, to);
            return;
        }
        game.tryMove(from, to);
    }
}
function gameHasPendingPromo() {
    return game.pendingPromo !== null;
}
// Refresh display
function render() {
    const state = game.getState();
    const lastMove = game.getLastMove();
    renderer.setLastMove(lastMove);
    // Re-apply selection if active
    if (selectedSq !== null) {
        renderer.setSelected(selectedSq);
        const moves = game.legalMoves().filter(m => m.from === selectedSq);
        renderer.setLegalMoves(moves);
    }
    renderer.render(state);
    // Turn indicator
    const turnText = state.turn === 'w' ? 'White' : 'Black';
    const isPlayerTurn = state.turn === game.getPlayerColor();
    turnIndicator.textContent = isPlayerTurn ? `${turnText} to move (You)` : `${turnText} to move (AI)`;
    // Captured pieces
    const cw = state.capturedWhite.map(t => constants_1.PIECE_UNICODE['w' + t]).join('');
    const cb = state.capturedBlack.map(t => constants_1.PIECE_UNICODE['b' + t]).join('');
    capturedWhiteEl.textContent = cw;
    capturedBlackEl.textContent = cb;
    // Move list
    updateMoveList(state.moveList);
    // AI thinking
    thinkingEl.className = game.isAiThinking() ? '' : 'hidden';
}
function updateMoveList(moves) {
    let html = '';
    for (let i = 0; i < moves.length; i += 2) {
        const num = Math.floor(i / 2) + 1;
        html += `<span class="move-number">${num}.</span> ${moves[i]}`;
        if (i + 1 < moves.length) {
            html += ` ${moves[i + 1]}`;
        }
        html += '<br>';
    }
    moveListEl.innerHTML = html;
    moveListEl.scrollTop = moveListEl.scrollHeight;
}
// Game over handler
game.onGameOver((status) => {
    switch (status) {
        case 'checkmate': {
            const winner = game.getState().turn === 'w' ? 'Black' : 'White';
            statusBar.textContent = `Checkmate! ${winner} wins!`;
            break;
        }
        case 'stalemate':
            statusBar.textContent = 'Stalemate — Draw!';
            break;
        case 'insufficient':
            statusBar.textContent = 'Insufficient material — Draw!';
            break;
        case 'fiftyMove':
            statusBar.textContent = '50-move rule — Draw!';
            break;
    }
    selectedSq = null;
    renderer.setSelected(null);
    renderer.setLegalMoves([]);
    render();
});
game.onAiThinking((thinking) => {
    if (!thinking) {
        render();
    }
});
// Initial render
render();
// ── Button handlers ─────────────────────────────────────────────────
btnUndo.addEventListener('click', () => {
    game.undo();
    selectedSq = null;
    renderer.setSelected(null);
    renderer.setLegalMoves([]);
    statusBar.textContent = '';
    render();
});
let flipped = false;
btnFlip.addEventListener('click', () => {
    flipped = !flipped;
    renderer.setFlipped(flipped);
    render();
});
btnNew.addEventListener('click', () => {
    newGameDialog.className = '';
});
btnStart.addEventListener('click', () => {
    const white = document.querySelector('input[name="side"]:checked');
    const analysis = document.getElementById('analysis-cb');
    const color = white.value === 'white' ? 'w' : 'b';
    game.startNewGame(color, parseInt(aiDepthSelect.value), analysis.checked);
    selectedSq = null;
    renderer.setSelected(null);
    renderer.setLegalMoves([]);
    statusBar.textContent = '';
    newGameDialog.className = 'hidden';
    render();
});
btnCancelNew.addEventListener('click', () => {
    newGameDialog.className = 'hidden';
});
btnSound.addEventListener('click', () => {
    const muted = game.getSound().toggle();
    btnSound.textContent = muted ? '🔇' : '🔊';
});
aiDepthSelect.addEventListener('change', () => {
    game.setAiDepth(parseInt(aiDepthSelect.value));
});
// ── Expose debug interface ──────────────────────────────────────────
window.game = {
    fen: () => game.fen(),
    legalMoves: () => game.legalMoves(),
    move: (notation) => {
        // Parse "e2e4" format
        if (notation.length >= 4) {
            const from = parseSq(notation[0] + notation[1]);
            const to = parseSq(notation[2] + notation[3]);
            if (from !== null && to !== null) {
                return game.tryMove(from, to);
            }
        }
        return false;
    },
    state: () => game.getState(),
};
function parseSq(name) {
    const f = name.charCodeAt(0) - 97;
    const r = parseInt(name[1]) - 1;
    if (f < 0 || f > 7 || r < 0 || r > 7)
        return null;
    return r * 8 + f;
}
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ── Tunable Constants ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.SOUND_FREQ = exports.PIECE_NAMES = exports.PIECE_UNICODE = exports.PIECE_VALUES = exports.AI_LEVELS = exports.COLORS = void 0;
exports.COLORS = {
    light: '#f0d9b5',
    dark: '#b58863',
    highlight: 'rgba(255, 255, 0, 0.45)',
    lastMove: 'rgba(155, 199, 0, 0.41)',
    checkGlow: '#ff0000',
    legalDot: 'rgba(0, 0, 0, 0.18)',
    captureRing: 'rgba(0, 0, 0, 0.18)',
    selected: 'rgba(100, 180, 255, 0.55)',
    text: '#333',
    panel: '#e8e0d4',
    boardBorder: '#6b4423',
    gameOver: '#1a1a2e',
};
exports.AI_LEVELS = [
    { label: 'Easy', depth: 1 },
    { label: 'Medium', depth: 2 },
    { label: 'Hard', depth: 3 },
];
exports.PIECE_VALUES = {
    P: 100, N: 320, B: 330, R: 500, Q: 900, K: 20000,
};
exports.PIECE_UNICODE = {
    wK: '♔', wQ: '♕', wR: '♖', wB: '♗', wN: '♘', wP: '♙',
    bK: '♚', bQ: '♛', bR: '♜', bB: '♝', bN: '♞', bP: '♟',
};
exports.PIECE_NAMES = {
    K: 'K', Q: 'Q', R: 'R', B: 'B', N: 'N', P: '',
};
exports.SOUND_FREQ = {
    move: { freq: 600, dur: 0.08 },
    capture: { freq: 300, dur: 0.12 },
    check: { freq: 800, dur: 0.2 },
    mate: { freq: 200, dur: 0.4 },
};
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Core Types ─────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.sq = sq;
exports.fileOf = fileOf;
exports.rankOf = rankOf;
exports.sqName = sqName;
exports.parseSq = parseSq;
// ── Square helpers ─────────────────────────────────────────────────
function sq(file, rank) {
    return rank * 8 + file;
}
function fileOf(s) { return s % 8; }
function rankOf(s) { return Math.floor(s / 8); }
function sqName(s) {
    return String.fromCharCode(97 + fileOf(s)) + (rankOf(s) + 1);
}
function parseSq(name) {
    if (name.length < 2)
        return null;
    const f = name.charCodeAt(0) - 97;
    const r = parseInt(name[1]) - 1;
    if (f < 0 || f > 7 || r < 0 || r > 7)
        return null;
    return sq(f, r);
}
};

// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialBoard = initialBoard;
exports.initialCastling = initialCastling;
exports.initialGameState = initialGameState;
exports.isSquareAttacked = isSquareAttacked;
exports.generateLegalMoves = generateLegalMoves;
exports.makeMove = makeMove;
exports.checkGameStatus = checkGameStatus;
exports.isInsufficientMaterial = isInsufficientMaterial;
exports.isInCheck = isInCheck;
const types_1 = require("./types");
// ── Initial Position ───────────────────────────────────────────────
function initialBoard() {
    const board = new Array(64).fill(null);
    const backRank = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
    for (let f = 0; f < 8; f++) {
        board[(0, types_1.sq)(f, 0)] = { type: backRank[f], color: 'b' };
        board[(0, types_1.sq)(f, 1)] = { type: 'P', color: 'b' };
        board[(0, types_1.sq)(f, 6)] = { type: 'P', color: 'w' };
        board[(0, types_1.sq)(f, 7)] = { type: backRank[f], color: 'w' };
    }
    return board;
}
function initialCastling() {
    return { wK: true, wQ: true, bK: true, bQ: true };
}
function initialGameState() {
    return {
        board: initialBoard(),
        turn: 'w',
        castling: initialCastling(),
        enPassant: null,
        halfMove: 0,
        fullMove: 1,
        moveList: [],
        capturedWhite: [],
        capturedBlack: [],
    };
}
// ── Attack Detection (for isSquareAttacked) ────────────────────────
function pawnAttacks(color, board, s) {
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    const dir = color === 'w' ? -1 : 1;
    const atk = [];
    if (r + dir >= 0 && r + dir <= 7) {
        if (f - 1 >= 0)
            atk.push((0, types_1.sq)(f - 1, r + dir));
        if (f + 1 <= 7)
            atk.push((0, types_1.sq)(f + 1, r + dir));
    }
    return atk;
}
function knightAttacks(s) {
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    const offsets = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
    const out = [];
    for (const [df, dr] of offsets) {
        const nf = f + df, nr = r + dr;
        if (nf >= 0 && nf <= 7 && nr >= 0 && nr <= 7)
            out.push((0, types_1.sq)(nf, nr));
    }
    return out;
}
function slidingAttacks(s, board) {
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    const out = [];
    const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]];
    for (let i = 0; i < 8; i++) {
        const [df, dr] = dirs[i];
        for (let step = 1; step < 8; step++) {
            const nf = f + df * step, nr = r + dr * step;
            if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                break;
            const ns = (0, types_1.sq)(nf, nr);
            out.push(ns);
            if (board[ns] !== null)
                break;
        }
    }
    return out;
}
function bishopAttacks(s, board) {
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    const out = [];
    const dirs = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
    for (const [df, dr] of dirs) {
        for (let step = 1; step < 8; step++) {
            const nf = f + df * step, nr = r + dr * step;
            if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                break;
            const ns = (0, types_1.sq)(nf, nr);
            out.push(ns);
            if (board[ns] !== null)
                break;
        }
    }
    return out;
}
function rookAttacks(s, board) {
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    const out = [];
    const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
    for (const [df, dr] of dirs) {
        for (let step = 1; step < 8; step++) {
            const nf = f + df * step, nr = r + dr * step;
            if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                break;
            const ns = (0, types_1.sq)(nf, nr);
            out.push(ns);
            if (board[ns] !== null)
                break;
        }
    }
    return out;
}
function isSquareAttacked(board, s, byColor) {
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    // Check pawn attacks
    const pawnDir = byColor === 'w' ? -1 : 1; // pawns attack from their perspective
    const pawnFromRank = r - pawnDir;
    if (pawnFromRank >= 0 && pawnFromRank <= 7) {
        if (f - 1 >= 0) {
            const p = board[(0, types_1.sq)(f - 1, pawnFromRank)];
            if (p && p.color === byColor && p.type === 'P')
                return true;
        }
        if (f + 1 <= 7) {
            const p = board[(0, types_1.sq)(f + 1, pawnFromRank)];
            if (p && p.color === byColor && p.type === 'P')
                return true;
        }
    }
    // Knight
    for (const ns of knightAttacks(s)) {
        const p = board[ns];
        if (p && p.color === byColor && p.type === 'N')
            return true;
    }
    // King
    const kr = (0, types_1.rankOf)(s), kf = (0, types_1.fileOf)(s);
    for (let df = -1; df <= 1; df++) {
        for (let dr = -1; dr <= 1; dr++) {
            if (df === 0 && dr === 0)
                continue;
            const nf = kf + df, nr = kr + dr;
            if (nf >= 0 && nf <= 7 && nr >= 0 && nr <= 7) {
                const p = board[(0, types_1.sq)(nf, nr)];
                if (p && p.color === byColor && p.type === 'K')
                    return true;
            }
        }
    }
    // Sliding (bishop/queen diagonals)
    for (const ns of bishopAttacks(s, board)) {
        const p = board[ns];
        if (p && p.color === byColor && (p.type === 'B' || p.type === 'Q'))
            return true;
    }
    // Sliding (rook/queen straights)
    for (const ns of rookAttacks(s, board)) {
        const p = board[ns];
        if (p && p.color === byColor && (p.type === 'R' || p.type === 'Q'))
            return true;
    }
    return false;
}
// ── Pseudo-Legal Move Generation ───────────────────────────────────
function addPseudoMoves(board, turn, castling, enPassant, moves) {
    for (let from = 0; from < 64; from++) {
        const piece = board[from];
        if (!piece || piece.color !== turn)
            continue;
        const fr = (0, types_1.rankOf)(from), ff = (0, types_1.fileOf)(from);
        switch (piece.type) {
            case 'P': {
                const dir = turn === 'w' ? -1 : 1;
                const startRank = turn === 'w' ? 6 : 1;
                const promoRank = turn === 'w' ? 0 : 7;
                const singleTo = fr + dir;
                // Single push
                if (singleTo >= 0 && singleTo <= 7 && !board[(0, types_1.sq)(ff, singleTo)]) {
                    if (singleTo === promoRank) {
                        for (const pt of ['Q', 'R', 'B', 'N']) {
                            moves.push({ from, to: (0, types_1.sq)(ff, singleTo), piece: 'P', color: turn, promotion: pt });
                        }
                    }
                    else {
                        moves.push({ from, to: (0, types_1.sq)(ff, singleTo), piece: 'P', color: turn });
                    }
                    // Double push
                    if (fr === startRank) {
                        const doubleTo = fr + dir * 2;
                        if (!board[(0, types_1.sq)(ff, doubleTo)]) {
                            moves.push({ from, to: (0, types_1.sq)(ff, doubleTo), piece: 'P', color: turn });
                        }
                    }
                }
                // Captures
                for (const df of [-1, 1]) {
                    const nf = ff + df;
                    if (nf < 0 || nf > 7 || singleTo < 0 || singleTo > 7)
                        continue;
                    const to = (0, types_1.sq)(nf, singleTo);
                    const target = board[to];
                    if (target && target.color !== turn) {
                        if (singleTo === promoRank) {
                            for (const pt of ['Q', 'R', 'B', 'N']) {
                                moves.push({ from, to, piece: 'P', color: turn, captured: target.type, promotion: pt });
                            }
                        }
                        else {
                            moves.push({ from, to, piece: 'P', color: turn, captured: target.type });
                        }
                    }
                    // En passant
                    if (enPassant !== null && to === enPassant) {
                        moves.push({ from, to, piece: 'P', color: turn, captured: 'P', enPassant: true });
                    }
                }
                break;
            }
            case 'N': {
                for (const [df, dr] of [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]]) {
                    const nf = ff + df, nr = fr + dr;
                    if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                        continue;
                    const to = (0, types_1.sq)(nf, nr);
                    const target = board[to];
                    if (!target || target.color !== turn) {
                        const cap = target ? target.type : undefined;
                        moves.push({ from, to, piece: 'N', color: turn, captured: cap });
                    }
                }
                break;
            }
            case 'B': {
                for (const [df, dr] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) {
                    for (let step = 1; step < 8; step++) {
                        const nf = ff + df * step, nr = fr + dr * step;
                        if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                            break;
                        const to = (0, types_1.sq)(nf, nr);
                        const target = board[to];
                        if (!target) {
                            moves.push({ from, to, piece: 'B', color: turn });
                        }
                        else {
                            if (target.color !== turn)
                                moves.push({ from, to, piece: 'B', color: turn, captured: target.type });
                            break;
                        }
                    }
                }
                break;
            }
            case 'R': {
                for (const [df, dr] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
                    for (let step = 1; step < 8; step++) {
                        const nf = ff + df * step, nr = fr + dr * step;
                        if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                            break;
                        const to = (0, types_1.sq)(nf, nr);
                        const target = board[to];
                        if (!target) {
                            moves.push({ from, to, piece: 'R', color: turn });
                        }
                        else {
                            if (target.color !== turn)
                                moves.push({ from, to, piece: 'R', color: turn, captured: target.type });
                            break;
                        }
                    }
                }
                break;
            }
            case 'Q': {
                for (const [df, dr] of [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]) {
                    for (let step = 1; step < 8; step++) {
                        const nf = ff + df * step, nr = fr + dr * step;
                        if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                            break;
                        const to = (0, types_1.sq)(nf, nr);
                        const target = board[to];
                        if (!target) {
                            moves.push({ from, to, piece: 'Q', color: turn });
                        }
                        else {
                            if (target.color !== turn)
                                moves.push({ from, to, piece: 'Q', color: turn, captured: target.type });
                            break;
                        }
                    }
                }
                break;
            }
            case 'K': {
                for (let df = -1; df <= 1; df++) {
                    for (let dr = -1; dr <= 1; dr++) {
                        if (df === 0 && dr === 0)
                            continue;
                        const nf = ff + df, nr = fr + dr;
                        if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
                            continue;
                        const to = (0, types_1.sq)(nf, nr);
                        const target = board[to];
                        if (!target || target.color !== turn) {
                            const cap = target ? target.type : undefined;
                            moves.push({ from, to, piece: 'K', color: turn, captured: cap });
                        }
                    }
                }
                // Castling
                const kRight = turn === 'w' ? 'wK' : 'bK';
                const qRight = turn === 'w' ? 'wQ' : 'bQ';
                const baseRank = turn === 'w' ? 7 : 0;
                const hr = (0, types_1.sq)(7, baseRank);
                const ar = (0, types_1.sq)(0, baseRank);
                if (castling[kRight]) {
                    const krPiece = board[hr];
                    const ks = (0, types_1.sq)(6, baseRank);
                    if (krPiece && krPiece.type === 'R' && krPiece.color === turn && !board[ks]) {
                        if (!isSquareAttacked(board, (0, types_1.sq)(4, baseRank), opposite(turn)) &&
                            !isSquareAttacked(board, (0, types_1.sq)(5, baseRank), opposite(turn)) &&
                            !isSquareAttacked(board, (0, types_1.sq)(6, baseRank), opposite(turn))) {
                            moves.push({ from: (0, types_1.sq)(4, baseRank), to: (0, types_1.sq)(6, baseRank), piece: 'K', color: turn, castling: 'K' });
                        }
                    }
                }
                if (castling[qRight]) {
                    const qrPiece = board[ar];
                    const qs = (0, types_1.sq)(2, baseRank);
                    const bs = (0, types_1.sq)(3, baseRank);
                    if (qrPiece && qrPiece.type === 'R' && qrPiece.color === turn && !board[qs] && !board[bs]) {
                        if (!isSquareAttacked(board, (0, types_1.sq)(4, baseRank), opposite(turn)) &&
                            !isSquareAttacked(board, (0, types_1.sq)(3, baseRank), opposite(turn)) &&
                            !isSquareAttacked(board, (0, types_1.sq)(2, baseRank), opposite(turn))) {
                            moves.push({ from: (0, types_1.sq)(4, baseRank), to: (0, types_1.sq)(2, baseRank), piece: 'K', color: turn, castling: 'Q' });
                        }
                    }
                }
                break;
            }
        }
    }
}
function opposite(c) { return c === 'w' ? 'b' : 'w'; }
// ── Apply Move ──────────────────────────────────────────────────────
function applyMove(board, move) {
    const nb = [...board];
    const fromPiece = nb[move.from];
    nb[move.to] = move.promotion
        ? { type: move.promotion, color: move.color }
        : { ...fromPiece };
    nb[move.from] = null;
    // En passant capture
    if (move.enPassant) {
        const capturedRank = (0, types_1.rankOf)(move.from);
        nb[(0, types_1.sq)((0, types_1.fileOf)(move.to), capturedRank)] = null;
    }
    // Castling rook move
    if (move.castling) {
        const rank = move.color === 'w' ? 7 : 0;
        if (move.castling === 'K') {
            nb[(0, types_1.sq)(5, rank)] = nb[(0, types_1.sq)(7, rank)];
            nb[(0, types_1.sq)(7, rank)] = null;
        }
        else {
            nb[(0, types_1.sq)(3, rank)] = nb[(0, types_1.sq)(0, rank)];
            nb[(0, types_1.sq)(0, rank)] = null;
        }
    }
    return nb;
}
function findKing(board, color) {
    for (let i = 0; i < 64; i++) {
        if (board[i]?.type === 'K' && board[i]?.color === color)
            return i;
    }
    return -1;
}
// ── Legal Move Filtering ───────────────────────────────────────────
function generateLegalMoves(state) {
    const { board, turn, castling, enPassant } = state;
    const pseudo = [];
    addPseudoMoves(board, turn, castling, enPassant, pseudo);
    return pseudo.filter(move => {
        const nb = applyMove(board, move);
        const kingSq = findKing(nb, turn);
        if (kingSq < 0)
            return false; // shouldn't happen
        return !isSquareAttacked(nb, kingSq, opposite(turn));
    });
}
// ── Make Move (returns new state) ──────────────────────────────────
function makeMove(state, move) {
    const nb = applyMove(state.board, move);
    const newColor = opposite(state.turn);
    // Update castling rights
    const nc = { ...state.castling };
    // King moved
    if (move.piece === 'K') {
        if (move.color === 'w') {
            nc.wK = false;
            nc.wQ = false;
        }
        else {
            nc.bK = false;
            nc.bQ = false;
        }
    }
    // Rook moved or captured
    if (move.piece === 'R') {
        if (move.from === (0, types_1.sq)(7, 7))
            nc.wK = false;
        if (move.from === (0, types_1.sq)(0, 7))
            nc.wQ = false;
        if (move.from === (0, types_1.sq)(7, 0))
            nc.bK = false;
        if (move.from === (0, types_1.sq)(0, 0))
            nc.bQ = false;
    }
    if (move.captured === 'R') {
        if (move.to === (0, types_1.sq)(7, 7))
            nc.wK = false;
        if (move.to === (0, types_1.sq)(0, 7))
            nc.wQ = false;
        if (move.to === (0, types_1.sq)(7, 0))
            nc.bK = false;
        if (move.to === (0, types_1.sq)(0, 0))
            nc.bQ = false;
    }
    // En passant square
    let newEnPassant = null;
    if (move.piece === 'P' && Math.abs((0, types_1.rankOf)(move.to) - (0, types_1.rankOf)(move.from)) === 2) {
        newEnPassant = (0, types_1.sq)((0, types_1.fileOf)(move.from), ((0, types_1.rankOf)(move.from) + (0, types_1.rankOf)(move.to)) / 2);
    }
    // Half-move clock
    let halfMove = state.halfMove + 1;
    if (move.piece === 'P' || move.captured)
        halfMove = 0;
    // Full move number
    const fullMove = newColor === 'w' ? state.fullMove + 1 : state.fullMove;
    // Captured pieces
    const capturedWhite = [...state.capturedWhite];
    const capturedBlack = [...state.capturedBlack];
    if (move.captured) {
        if (move.captured === 'P' || move.captured === 'N' || move.captured === 'B' ||
            move.captured === 'R' || move.captured === 'Q') {
            // The captured piece belongs to the opponent
            if (state.turn === 'w')
                capturedBlack.push(move.captured);
            else
                capturedWhite.push(move.captured);
        }
    }
    // Algebraic notation
    const notation = toAlgebraic(state, move, nb, newColor, nc, newEnPassant);
    return {
        board: nb,
        turn: newColor,
        castling: nc,
        enPassant: newEnPassant,
        halfMove,
        fullMove,
        moveList: [...state.moveList, notation],
        capturedWhite,
        capturedBlack,
    };
}
// ── Algebraic Notation ─────────────────────────────────────────────
function toAlgebraic(state, move, newBoard, nextColor, newCastling, newEnPassant) {
    if (move.castling === 'K')
        return 'O-O';
    if (move.castling === 'Q')
        return 'O-O-O';
    const { board } = state;
    const piece = move.piece;
    let n = '';
    if (piece !== 'P') {
        n = piece;
        // Disambiguation
        const legal = generateLegalMoves(state);
        const ambiguous = legal.filter(m => m.piece === piece && m.to === move.to && m.from !== move.from);
        if (ambiguous.length > 0) {
            const sameFile = ambiguous.some(m => (0, types_1.fileOf)(m.from) === (0, types_1.fileOf)(move.from));
            const sameRank = ambiguous.some(m => (0, types_1.rankOf)(m.from) === (0, types_1.rankOf)(move.from));
            if (!sameFile) {
                n += String.fromCharCode(97 + (0, types_1.fileOf)(move.from));
            }
            else if (!sameRank) {
                n += ((0, types_1.rankOf)(move.from) + 1);
            }
            else {
                n += String.fromCharCode(97 + (0, types_1.fileOf)(move.from)) + ((0, types_1.rankOf)(move.from) + 1);
            }
        }
    }
    const isCapture = !!move.captured;
    if (isCapture) {
        if (piece === 'P')
            n += String.fromCharCode(97 + (0, types_1.fileOf)(move.from));
        n += 'x';
    }
    n += (0, types_1.sqName)(move.to);
    if (move.enPassant)
        n += ' e.p.';
    if (move.promotion)
        n += '=' + move.promotion;
    // Check / Checkmate
    const nextState = {
        board: newBoard,
        turn: nextColor,
        castling: newCastling,
        enPassant: newEnPassant,
        halfMove: 0,
        fullMove: 0,
        moveList: [],
        capturedWhite: [],
        capturedBlack: [],
    };
    const nextMoves = generateLegalMoves(nextState);
    const kingSq = findKing(newBoard, nextColor);
    const inCheck = kingSq >= 0 && isSquareAttacked(newBoard, kingSq, state.turn);
    if (inCheck) {
        n += nextMoves.length === 0 ? '#' : '+';
    }
    return n;
}
// ── Game Status ────────────────────────────────────────────────────
function checkGameStatus(state) {
    const legal = generateLegalMoves(state);
    if (legal.length === 0) {
        const kingSq = findKing(state.board, state.turn);
        if (kingSq >= 0 && isSquareAttacked(state.board, kingSq, opposite(state.turn))) {
            return { status: 'checkmate', winner: opposite(state.turn) };
        }
        return { status: 'stalemate' };
    }
    if (state.halfMove >= 100) {
        return { status: 'fiftyMove' };
    }
    if (isInsufficientMaterial(state.board)) {
        return { status: 'insufficient' };
    }
    return { status: 'playing' };
}
function isInsufficientMaterial(board) {
    const white = [];
    const black = [];
    for (const p of board) {
        if (!p)
            continue;
        if (p.color === 'w')
            white.push(p);
        else
            black.push(p);
    }
    // K vs K
    if (white.length === 1 && black.length === 1)
        return true;
    // K+B vs K or K+N vs K
    if (white.length === 1 && black.length === 2) {
        const minor = black.find(p => p.type === 'B' || p.type === 'N');
        if (minor && black.every(p => p.type === 'K' || p.type === 'B' || p.type === 'N'))
            return true;
    }
    if (black.length === 1 && white.length === 2) {
        const minor = white.find(p => p.type === 'B' || p.type === 'N');
        if (minor && white.every(p => p.type === 'K' || p.type === 'B' || p.type === 'N'))
            return true;
    }
    return false;
}
function isInCheck(state, color) {
    const kingSq = findKing(state.board, color);
    if (kingSq < 0)
        return false;
    return isSquareAttacked(state.board, kingSq, opposite(color));
}
};

// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = evaluate;
exports.findBestMove = findBestMove;
exports.cancelSearch = cancelSearch;
const constants_1 = require("./constants");
const engine_1 = require("./engine");
const types_1 = require("./types");
// ── Piece-Square Tables (from White's perspective, flip for Black) ──
const PST_PAWN = [
    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 PST_KNIGHT = [
    -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 PST_BISHOP = [
    -20, -10, -10, -10, -10, -10, -10, -20,
    -10, 0, 0, 0, 0, 0, 0, -10,
    -10, 0, 10, 10, 10, 10, 0, -10,
    -10, 5, 5, 10, 10, 5, 5, -10,
    -10, 0, 10, 10, 10, 10, 0, -10,
    -10, 10, 10, 10, 10, 10, 10, -10,
    -10, 5, 0, 0, 0, 0, 5, -10,
    -20, -10, -10, -10, -10, -10, -10, -20,
];
const PST_ROOK = [
    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 PST_QUEEN = [
    -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 PST_KING_MID = [
    -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 PST = {
    P: PST_PAWN, N: PST_KNIGHT, B: PST_BISHOP,
    R: PST_ROOK, Q: PST_QUEEN, K: PST_KING_MID,
};
function pstScore(piece, s) {
    const t = PST[piece.type];
    if (!t)
        return 0;
    if (piece.color === 'w')
        return t[s];
    // Flip for black: mirror rank
    const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
    return t[(0, types_1.sq)(f, 7 - r)];
}
// ── Evaluation ─────────────────────────────────────────────────────
function evaluate(state) {
    let score = 0;
    for (let i = 0; i < 64; i++) {
        const p = state.board[i];
        if (!p)
            continue;
        const val = constants_1.PIECE_VALUES[p.type] + pstScore(p, i);
        if (p.color === 'w')
            score += val;
        else
            score -= val;
    }
    // Mobility bonus (simplified)
    return score;
}
// ── Move Ordering ──────────────────────────────────────────────────
function orderMoves(moves) {
    return [...moves].sort((a, b) => {
        // Captures first (MVV-LVA style)
        const aCap = a.captured ? constants_1.PIECE_VALUES[a.captured] : 0;
        const bCap = b.captured ? constants_1.PIECE_VALUES[b.captured] : 0;
        if (aCap !== bCap)
            return bCap - aCap;
        // Promotions next
        if (a.promotion && !b.promotion)
            return -1;
        if (!a.promotion && b.promotion)
            return 1;
        return 0;
    });
}
// ── Quiescence Search ──────────────────────────────────────────────
function quiesce(state, alpha, beta, isMax) {
    const standPat = evaluate(state);
    if (isMax) {
        if (standPat >= beta)
            return beta;
        if (alpha < standPat)
            alpha = standPat;
    }
    else {
        if (standPat <= alpha)
            return alpha;
        if (beta > standPat)
            beta = standPat;
    }
    const moves = (0, engine_1.generateLegalMoves)(state);
    const ordered = orderMoves(moves).filter(m => m.captured || m.promotion);
    for (const move of ordered) {
        const ns = (0, engine_1.makeMove)(state, move);
        const score = quiesce(ns, alpha, beta, !isMax);
        if (isMax) {
            if (score > alpha)
                alpha = score;
            if (alpha >= beta)
                return beta;
        }
        else {
            if (score < beta)
                beta = score;
            if (alpha >= beta)
                return alpha;
        }
    }
    return isMax ? alpha : beta;
}
// ── Minimax with Alpha-Beta ────────────────────────────────────────
let nodesSearched = 0;
let searchActive = false;
function minimax(state, depth, alpha, beta, isMax) {
    if (!searchActive)
        return evaluate(state);
    nodesSearched++;
    if (depth === 0) {
        return quiesce(state, alpha, beta, isMax);
    }
    const moves = (0, engine_1.generateLegalMoves)(state);
    if (moves.length === 0) {
        // Checkmate or stalemate
        const { board, turn } = state;
        let kingSq = -1;
        for (let i = 0; i < 64; i++) {
            if (board[i]?.type === 'K' && board[i]?.color === turn) {
                kingSq = i;
                break;
            }
        }
        const opp = turn === 'w' ? 'b' : 'w';
        if (kingSq >= 0 && (0, engine_1.isSquareAttacked)(board, kingSq, opp)) {
            return isMax ? -99999 + (3 - depth) : 99999 - (3 - depth);
        }
        return 0; // stalemate
    }
    const ordered = orderMoves(moves);
    if (isMax) {
        let best = -Infinity;
        for (const move of ordered) {
            const ns = (0, engine_1.makeMove)(state, move);
            const score = minimax(ns, depth - 1, alpha, beta, false);
            if (score > best)
                best = score;
            if (best > alpha)
                alpha = best;
            if (alpha >= beta)
                break;
        }
        return best;
    }
    else {
        let best = Infinity;
        for (const move of ordered) {
            const ns = (0, engine_1.makeMove)(state, move);
            const score = minimax(ns, depth - 1, alpha, beta, true);
            if (score < best)
                best = score;
            if (best < beta)
                beta = best;
            if (alpha >= beta)
                break;
        }
        return best;
    }
}
// ── AI Move Selection (async, chunked) ─────────────────────────────
function findBestMove(state, aiColor, depth, onProgress) {
    return new Promise((resolve) => {
        searchActive = true;
        nodesSearched = 0;
        const moves = (0, engine_1.generateLegalMoves)(state);
        if (moves.length === 0) {
            searchActive = false;
            resolve(null);
            return;
        }
        const ordered = orderMoves(moves);
        const isMax = aiColor === 'w';
        // Process moves in chunks to avoid blocking the UI
        let idx = 0;
        let bestMove = null;
        let bestScore = isMax ? -Infinity : Infinity;
        function processChunk() {
            if (!searchActive) {
                resolve(bestMove);
                return;
            }
            const chunkSize = Math.max(1, Math.ceil(ordered.length / 4));
            const end = Math.min(idx + chunkSize, ordered.length);
            for (; idx < end; idx++) {
                const move = ordered[idx];
                const ns = (0, engine_1.makeMove)(state, move);
                const score = minimax(ns, depth - 1, -Infinity, Infinity, !isMax);
                if (onProgress)
                    onProgress(nodesSearched);
                if (isMax) {
                    if (score > bestScore) {
                        bestScore = score;
                        bestMove = move;
                    }
                }
                else {
                    if (score < bestScore) {
                        bestScore = score;
                        bestMove = move;
                    }
                }
            }
            if (idx < ordered.length) {
                setTimeout(processChunk, 0);
            }
            else {
                searchActive = false;
                resolve(bestMove);
            }
        }
        setTimeout(processChunk, 10);
    });
}
function cancelSearch() {
    searchActive = false;
}
};

// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const constants_1 = require("./constants");
const constants_2 = require("./constants");
const types_1 = require("./types");
const engine_1 = require("./engine");
class Renderer {
    constructor(canvas, callbacks) {
        this.size = 560;
        this.sqSize = 70;
        this.flipped = false;
        this.selected = null;
        this.legalMoves = [];
        this.lastMove = null;
        this.pendingPromo = null;
        this.dragSquare = null;
        this.dragX = 0;
        this.dragY = 0;
        this.dragging = false;
        this.canvas = canvas;
        this.ctx = canvas.getContext('2d');
        this.canvas.width = this.size;
        this.canvas.height = this.size;
        this.callbacks = callbacks;
        this.wireEvents();
    }
    wireEvents() {
        this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
        this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
        this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
        this.canvas.addEventListener('mouseleave', (e) => this.onMouseUp(e));
        this.canvas.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: false });
        this.canvas.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: false });
        this.canvas.addEventListener('touchend', (e) => this.onTouchEnd(e), { passive: false });
    }
    getSquareFromCoords(clientX, clientY) {
        const rect = this.canvas.getBoundingClientRect();
        const scaleX = this.canvas.width / rect.width;
        const scaleY = this.canvas.height / rect.height;
        const x = (clientX - rect.left) * scaleX;
        const y = (clientY - rect.top) * scaleY;
        const file = this.flipped ? 7 - Math.floor(x / this.sqSize) : Math.floor(x / this.sqSize);
        const rank = this.flipped ? 7 - Math.floor(y / this.sqSize) : Math.floor(y / this.sqSize);
        if (file < 0 || file > 7 || rank < 0 || rank > 7)
            return null;
        return { sq: (0, types_1.sq)(file, rank), x, y };
    }
    onMouseDown(e) {
        const hit = this.getSquareFromCoords(e.clientX, e.clientY);
        if (!hit)
            return;
        if (this.pendingPromo)
            return;
        this.dragSquare = hit.sq;
        this.dragX = hit.x;
        this.dragY = hit.y;
        this.dragging = false;
    }
    onMouseMove(e) {
        if (this.dragSquare === null || this.pendingPromo)
            return;
        const hit = this.getSquareFromCoords(e.clientX, e.clientY);
        if (hit) {
            const dx = hit.x - this.dragX;
            const dy = hit.y - this.dragY;
            if (!this.dragging && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) {
                this.dragging = true;
            }
            this.dragX = hit.x;
            this.dragY = hit.y;
        }
    }
    onMouseUp(e) {
        if (this.dragSquare === null || this.pendingPromo)
            return;
        const hit = this.getSquareFromCoords(e.clientX, e.clientY);
        const to = hit ? hit.sq : null;
        if (this.dragging && to !== null && to !== this.dragSquare) {
            // Drag-and-drop move
            this.callbacks.onSquareDragStart(this.dragSquare);
            this.callbacks.onSquareDragEnd(this.dragSquare, to);
        }
        else if (!this.dragging) {
            // Click
            this.callbacks.onSquareClick(this.dragSquare);
        }
        this.dragSquare = null;
        this.dragging = false;
    }
    onTouchStart(e) {
        e.preventDefault();
        if (e.touches.length !== 1)
            return;
        const t = e.touches[0];
        const hit = this.getSquareFromCoords(t.clientX, t.clientY);
        if (!hit)
            return;
        if (this.pendingPromo)
            return;
        this.dragSquare = hit.sq;
        this.dragX = hit.x;
        this.dragY = hit.y;
        this.dragging = false;
    }
    onTouchMove(e) {
        e.preventDefault();
        if (this.dragSquare === null || this.pendingPromo)
            return;
        if (e.touches.length !== 1)
            return;
        const t = e.touches[0];
        const hit = this.getSquareFromCoords(t.clientX, t.clientY);
        if (hit) {
            const dx = hit.x - this.dragX;
            const dy = hit.y - this.dragY;
            if (!this.dragging && (Math.abs(dx) > 12 || Math.abs(dy) > 12)) {
                this.dragging = true;
            }
            this.dragX = hit.x;
            this.dragY = hit.y;
        }
    }
    onTouchEnd(e) {
        e.preventDefault();
        if (this.dragSquare === null || this.pendingPromo)
            return;
        const to = this.dragging && this.changedDragSquare() ? this.dragSquare : null;
        if (this.dragging && to !== null && to !== this.dragSquare) {
            this.callbacks.onSquareDragStart(this.dragSquare);
            this.callbacks.onSquareDragEnd(this.dragSquare, to);
        }
        else if (!this.dragging) {
            this.callbacks.onSquareClick(this.dragSquare);
        }
        this.dragSquare = null;
        this.dragging = false;
    }
    changedDragSquare() {
        // The dragSquare is the start; on touch end we check if moved
        // For simplicity, touch end just uses the original click
        return false;
    }
    setFlipped(flipped) {
        this.flipped = flipped;
    }
    setSelected(s) {
        this.selected = s;
    }
    setLegalMoves(moves) {
        this.legalMoves = moves;
    }
    setLastMove(m) {
        this.lastMove = m;
    }
    setPendingPromo(promo) {
        this.pendingPromo = promo;
    }
    render(state) {
        const ctx = this.ctx;
        const sz = this.sqSize;
        // Draw board
        for (let r = 0; r < 8; r++) {
            for (let f = 0; f < 8; f++) {
                const drawFile = this.flipped ? 7 - f : f;
                const drawRank = this.flipped ? 7 - r : r;
                const x = drawFile * sz, y = drawRank * sz;
                const isLight = (r + f) % 2 === 0;
                ctx.fillStyle = isLight ? constants_1.COLORS.light : constants_1.COLORS.dark;
                ctx.fillRect(x, y, sz, sz);
            }
        }
        // Last move highlight
        if (this.lastMove) {
            for (const s of [this.lastMove.from, this.lastMove.to]) {
                const f = (0, types_1.fileOf)(s), r = (0, types_1.rankOf)(s);
                const drawFile = this.flipped ? 7 - f : f;
                const drawRank = this.flipped ? 7 - r : r;
                ctx.fillStyle = constants_1.COLORS.lastMove;
                ctx.fillRect(drawFile * sz, drawRank * sz, sz, sz);
            }
        }
        // Selected highlight
        if (this.selected !== null) {
            const f = (0, types_1.fileOf)(this.selected), r = (0, types_1.rankOf)(this.selected);
            const drawFile = this.flipped ? 7 - f : f;
            const drawRank = this.flipped ? 7 - r : r;
            ctx.fillStyle = constants_1.COLORS.selected;
            ctx.fillRect(drawFile * sz, drawRank * sz, sz, sz);
        }
        // Legal move dots / capture rings
        for (const move of this.legalMoves) {
            const f = (0, types_1.fileOf)(move.to), r = (0, types_1.rankOf)(move.to);
            const drawFile = this.flipped ? 7 - f : f;
            const drawRank = this.flipped ? 7 - r : r;
            const cx = drawFile * sz + sz / 2;
            const cy = drawRank * sz + sz / 2;
            if (state.board[move.to]) {
                // Capture ring
                ctx.strokeStyle = constants_1.COLORS.captureRing;
                ctx.lineWidth = 4;
                ctx.beginPath();
                ctx.arc(cx, cy, sz / 2 - 4, 0, Math.PI * 2);
                ctx.stroke();
            }
            else {
                // Dot
                ctx.fillStyle = constants_1.COLORS.legalDot;
                ctx.beginPath();
                ctx.arc(cx, cy, sz / 6, 0, Math.PI * 2);
                ctx.fill();
            }
        }
        // Check glow on king
        const inCheckColor = state.turn;
        const kingSq = this.findKing(state.board, inCheckColor);
        if (kingSq >= 0 && (0, engine_1.isInCheck)(state, inCheckColor)) {
            const f = (0, types_1.fileOf)(kingSq), r = (0, types_1.rankOf)(kingSq);
            const drawFile = this.flipped ? 7 - f : f;
            const drawRank = this.flipped ? 7 - r : r;
            // Red glow
            const grad = ctx.createRadialGradient(drawFile * sz + sz / 2, drawRank * sz + sz / 2, 0, drawFile * sz + sz / 2, drawRank * sz + sz / 2, sz / 2);
            grad.addColorStop(0, 'rgba(255, 50, 50, 0.6)');
            grad.addColorStop(1, 'rgba(255, 0, 0, 0.0)');
            ctx.fillStyle = grad;
            ctx.fillRect(drawFile * sz, drawRank * sz, sz, sz);
        }
        // Draw pieces
        ctx.font = `${sz * 0.75}px serif`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        for (let r = 0; r < 8; r++) {
            for (let f = 0; f < 8; f++) {
                const s = (0, types_1.sq)(f, r);
                const piece = state.board[s];
                if (!piece)
                    continue;
                // Skip dragging piece (drawn separately)
                if (this.dragging && this.dragSquare === s)
                    continue;
                const drawFile = this.flipped ? 7 - f : f;
                const drawRank = this.flipped ? 7 - r : r;
                const cx = drawFile * sz + sz / 2;
                const cy = drawRank * sz + sz / 2;
                const key = piece.color + piece.type;
                const glyph = constants_2.PIECE_UNICODE[key];
                if (!glyph)
                    continue;
                // Shadow for depth
                ctx.fillStyle = 'rgba(0,0,0,0.25)';
                ctx.fillText(glyph, cx + 2, cy + 2);
                // Piece
                ctx.fillStyle = piece.color === 'w' ? '#fff' : '#1a1a1a';
                ctx.strokeStyle = piece.color === 'w' ? '#1a1a1a' : '#fff';
                ctx.lineWidth = 0.5;
                ctx.fillText(glyph, cx, cy);
                ctx.strokeText(glyph, cx, cy);
            }
        }
        // Draw dragging piece
        if (this.dragging && this.dragSquare !== null) {
            const piece = state.board[this.dragSquare];
            if (piece) {
                const key = piece.color + piece.type;
                const glyph = constants_2.PIECE_UNICODE[key];
                if (glyph) {
                    ctx.font = `${sz * 0.85}px serif`;
                    ctx.textAlign = 'center';
                    ctx.textBaseline = 'middle';
                    ctx.fillStyle = piece.color === 'w' ? '#fff' : '#1a1a1a';
                    ctx.strokeStyle = piece.color === 'w' ? '#1a1a1a' : '#fff';
                    ctx.lineWidth = 1;
                    ctx.fillText(glyph, this.dragX, this.dragY);
                    ctx.strokeText(glyph, this.dragX, this.dragY);
                }
            }
        }
        // Board coordinates
        ctx.font = '11px sans-serif';
        ctx.fillStyle = 'rgba(0,0,0,0.45)';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'bottom';
        for (let f = 0; f < 8; f++) {
            const drawFile = this.flipped ? 7 - f : f;
            ctx.fillText(String.fromCharCode(97 + f), drawFile * sz + sz / 2, this.size - 3);
        }
        ctx.textAlign = 'right';
        ctx.textBaseline = 'middle';
        for (let r = 0; r < 8; r++) {
            const drawRank = this.flipped ? 7 - r : r;
            ctx.fillText((8 - r).toString(), 10, drawRank * sz + sz / 2);
        }
        // Promotion overlay
        if (this.pendingPromo) {
            this.drawPromoOverlay();
        }
        // Border
        ctx.strokeStyle = constants_1.COLORS.boardBorder;
        ctx.lineWidth = 4;
        ctx.strokeRect(2, 2, this.size - 4, this.size - 4);
    }
    findKing(board, color) {
        for (let i = 0; i < 64; i++) {
            if (board[i]?.type === 'K' && board[i]?.color === color)
                return i;
        }
        return -1;
    }
    drawPromoOverlay() {
        if (!this.pendingPromo)
            return;
        const ctx = this.ctx;
        const sz = this.sqSize;
        const f = (0, types_1.fileOf)(this.pendingPromo.to);
        const r = (0, types_1.rankOf)(this.pendingPromo.to);
        const drawFile = this.flipped ? 7 - f : f;
        const drawRank = this.flipped ? 7 - r : r;
        // Dim background
        ctx.fillStyle = 'rgba(0,0,0,0.4)';
        ctx.fillRect(0, 0, this.size, this.size);
        // Promotion panel
        const panelW = sz * 4;
        const panelH = sz;
        const px = (this.size - panelW) / 2;
        const py = (this.size - panelH) / 2;
        ctx.fillStyle = '#f5eedd';
        ctx.strokeStyle = '#6b4423';
        ctx.lineWidth = 3;
        ctx.fillRect(px, py, panelW, panelH);
        ctx.strokeRect(px, py, panelW, panelH);
        const pieces = ['Q', 'R', 'B', 'N'];
        ctx.font = `${sz * 0.7}px serif`;
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        for (let i = 0; i < 4; i++) {
            const cx = px + (i + 0.5) * sz;
            const cy = py + panelH / 2;
            const key = 'w' + pieces[i]; // always white for display
            const glyph = constants_2.PIECE_UNICODE[key];
            if (glyph) {
                ctx.fillStyle = '#1a1a1a';
                ctx.fillText(glyph, cx, cy);
            }
        }
    }
    // Returns the promotion piece type chosen, or null if not on a promo piece
    onPromoClick(clientX, clientY) {
        if (!this.pendingPromo)
            return null;
        const sz = this.sqSize;
        const panelW = sz * 4;
        const panelH = sz;
        const px = (this.size - panelW) / 2;
        const py = (this.size - panelH) / 2;
        const rect = this.canvas.getBoundingClientRect();
        const scaleX = this.canvas.width / rect.width;
        const scaleY = this.canvas.height / rect.height;
        const x = (clientX - rect.left) * scaleX;
        const y = (clientY - rect.top) * scaleY;
        if (x >= px && x <= px + panelW && y >= py && y <= py + panelH) {
            const idx = Math.floor((x - px) / sz);
            if (idx >= 0 && idx < 4) {
                const pieces = ['Q', 'R', 'B', 'N'];
                return pieces[idx];
            }
        }
        return null;
    }
}
exports.Renderer = Renderer;
};

// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SoundEngine = void 0;
const constants_1 = require("./constants");
// ── Sound Engine (Web Audio API) ────────────────────────────────────
class SoundEngine {
    constructor() {
        this.ctx = null;
        this.muted = false;
    }
    getAudioContext() {
        if (!this.ctx) {
            this.ctx = new (window.AudioContext || window.webkitAudioContext)();
        }
        if (this.ctx.state === 'suspended') {
            this.ctx.resume();
        }
        return this.ctx;
    }
    play(type) {
        if (this.muted)
            return;
        const audio = this.getAudioContext();
        const config = constants_1.SOUND_FREQ[type];
        const osc = audio.createOscillator();
        const gain = audio.createGain();
        osc.type = type === 'capture' ? 'square' : 'sine';
        osc.frequency.value = config.freq;
        gain.gain.setValueAtTime(0.3, audio.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + config.dur);
        osc.connect(gain);
        gain.connect(audio.destination);
        osc.start();
        osc.stop(audio.currentTime + config.dur + 0.05);
    }
    mute() { this.muted = true; }
    unmute() { this.muted = false; }
    toggle() { this.muted = !this.muted; return this.muted; }
    isMuted() { return this.muted; }
}
exports.SoundEngine = SoundEngine;
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameController = void 0;
const engine_1 = require("./engine");
const ai_1 = require("./ai");
const sound_1 = require("./sound");
const types_1 = require("./types");
// ── Persistence ─────────────────────────────────────────────────────
const STORAGE_KEY = 'chess-game-save';
function boardToSave(board) {
    return board.map(p => p ? `${p.color}${p.type}` : '');
}
function boardFromSave(arr) {
    return arr.map(s => {
        if (!s)
            return null;
        return { color: s[0], type: s.slice(1) };
    });
}
class GameController {
    constructor() {
        // Callbacks
        this.onMoveCompleteCb = null;
        this.onGameOverCb = null;
        this.onAiThinkingCb = null;
        // ── Player Input ──────────────────────────────────────────────────
        this.pendingPromo = null;
        this.sound = new sound_1.SoundEngine();
        this.playerColor = 'w';
        this.aiDepth = 3;
        this.analysisMode = false;
        this.aiThinking = false;
        this.lastMove = null;
        this.history = [];
        const saved = this.tryLoad();
        if (saved) {
            this.restore(saved);
        }
        else {
            this.state = (0, engine_1.initialGameState)();
            this.aiColor = this.analysisMode ? null : (this.playerColor === 'w' ? 'b' : 'w');
            this.history = [this.state];
        }
    }
    // ── Public getters ────────────────────────────────────────────────
    getState() { return this.state; }
    getLastMove() { return this.lastMove; }
    isAiThinking() { return this.aiThinking; }
    getAiColor() { return this.aiColor; }
    getPlayerColor() { return this.playerColor; }
    isAnalysisMode() { return this.analysisMode; }
    getSound() { return this.sound; }
    getAiDepth() { return this.aiDepth; }
    // ── Config ────────────────────────────────────────────────────────
    setAiDepth(depth) { this.aiDepth = depth; }
    startNewGame(color, depth, analysis) {
        (0, ai_1.cancelSearch)();
        this.aiThinking = false;
        this.pendingPromo = null;
        this.state = (0, engine_1.initialGameState)();
        this.lastMove = null;
        this.playerColor = color;
        this.aiDepth = depth;
        this.analysisMode = analysis;
        this.aiColor = analysis ? null : (color === 'w' ? 'b' : 'w');
        this.history = [this.state];
        this.save();
        this.notifyMove({});
    }
    setAnalysisMode(mode) {
        this.analysisMode = mode;
        this.aiColor = mode ? null : (this.playerColor === 'w' ? 'b' : 'w');
    }
    // ── Callbacks ─────────────────────────────────────────────────────
    onMoveComplete(cb) { this.onMoveCompleteCb = cb; }
    onGameOver(cb) { this.onGameOverCb = cb; }
    onAiThinking(cb) { this.onAiThinkingCb = cb; }
    // ── Debug ─────────────────────────────────────────────────────────
    legalMoves() { return (0, engine_1.generateLegalMoves)(this.state); }
    fen() {
        let fen = '';
        let empty = 0;
        for (let r = 7; r >= 0; r--) {
            for (let f = 0; f < 8; f++) {
                const p = this.state.board[r * 8 + f];
                if (!p) {
                    empty++;
                }
                else {
                    if (empty > 0) {
                        fen += empty.toString();
                        empty = 0;
                    }
                    fen += p.color === 'w' ? p.type : p.type.toLowerCase();
                }
            }
            if (empty > 0) {
                fen += empty.toString();
                empty = 0;
            }
            if (r > 0)
                fen += '/';
        }
        fen += ` ${this.state.turn}`;
        let castle = '';
        if (this.state.castling.wK)
            castle += 'K';
        if (this.state.castling.wQ)
            castle += 'Q';
        if (this.state.castling.bK)
            castle += 'k';
        if (this.state.castling.bQ)
            castle += 'q';
        fen += ` ${castle || '-'}`;
        fen += ` ${this.state.enPassant !== null ? (0, types_1.sqName)(this.state.enPassant) : '-'}`;
        fen += ` ${this.state.halfMove} ${this.state.fullMove}`;
        return fen;
    }
    hasPendingPromo() { return this.pendingPromo !== null; }
    setPendingPromo(from, to) {
        this.pendingPromo = { from, to };
    }
    selectSquare(from) {
        if (this.aiThinking)
            return;
        if (this.state.turn !== this.playerColor)
            return;
        if (this.pendingPromo)
            return;
        // Store selected square, legal moves will be computed by renderer
    }
    tryMove(from, to) {
        if (this.aiThinking)
            return false;
        if (this.state.turn !== this.playerColor)
            return false;
        if (this.pendingPromo)
            return false;
        const legal = (0, engine_1.generateLegalMoves)(this.state);
        const matches = legal.filter(m => m.from === from && m.to === to);
        if (matches.length === 0)
            return false;
        // Check for promotion
        const promoMoves = matches.filter(m => m.promotion);
        if (promoMoves.length > 0) {
            this.pendingPromo = { from, to };
            return false; // Tell UI to show picker
        }
        this.executeMove(matches[0]);
        return true;
    }
    confirmPromotion(piece) {
        if (!this.pendingPromo)
            return false;
        const { from, to } = this.pendingPromo;
        const legal = (0, engine_1.generateLegalMoves)(this.state);
        const move = legal.find(m => m.from === from && m.to === to && m.promotion === piece);
        if (move) {
            this.pendingPromo = null;
            this.executeMove(move);
            return true;
        }
        return false;
    }
    cancelPromo() {
        this.pendingPromo = null;
    }
    // ── Execute Move ──────────────────────────────────────────────────
    executeMove(move) {
        this.state = (0, engine_1.makeMove)(this.state, move);
        this.lastMove = move;
        this.history.push(this.state);
        // Sound
        if (move.captured)
            this.sound.play('capture');
        else
            this.sound.play('move');
        const status = (0, engine_1.checkGameStatus)(this.state);
        if (status.status !== 'playing') {
            this.sound.play(status.status === 'checkmate' ? 'mate' : 'check');
            if (this.onGameOverCb)
                this.onGameOverCb(status.status);
        }
        else {
            // Check sound
            if ((0, engine_1.isInCheck)(this.state, this.state.turn)) {
                this.sound.play('check');
            }
        }
        this.notifyMove(move);
        this.save();
        // AI turn
        if (this.aiColor && this.state.turn === this.aiColor && status.status === 'playing') {
            this.startAiTurn();
        }
    }
    notifyMove(move) {
        if (this.onMoveCompleteCb)
            this.onMoveCompleteCb(move, this.state);
    }
    // ── AI ────────────────────────────────────────────────────────────
    startAiTurn() {
        if (!this.aiColor)
            return;
        this.aiThinking = true;
        if (this.onAiThinkingCb)
            this.onAiThinkingCb(true);
        (0, ai_1.findBestMove)(this.state, this.aiColor, this.aiDepth).then(move => {
            this.aiThinking = false;
            if (this.onAiThinkingCb)
                this.onAiThinkingCb(false);
            if (move) {
                this.executeMove(move);
            }
        });
    }
    // ── Undo ──────────────────────────────────────────────────────────
    undo() {
        if (this.aiThinking)
            return false;
        if (this.pendingPromo)
            return false;
        if (this.history.length <= 1)
            return false;
        // Undo 2 moves (player + AI), unless analysis mode (1 move)
        const undoCount = this.analysisMode ? 1 : 2;
        const targetLen = this.history.length - undoCount;
        if (targetLen < 1) {
            // Can't undo that many, just undo what we can
            this.state = this.history[0];
            this.history = [this.state];
            this.lastMove = null;
        }
        else {
            this.state = this.history[targetLen - 1];
            this.history = this.history.slice(0, targetLen);
            this.lastMove = this.history.length > 1 ? null : null;
            // Restore lastMove from previous state
        }
        this.save();
        this.notifyMove({});
        return true;
    }
    // ── Persistence ───────────────────────────────────────────────────
    save() {
        try {
            const data = {
                board: boardToSave(this.state.board),
                turn: this.state.turn,
                castling: this.state.castling,
                enPassant: this.state.enPassant,
                halfMove: this.state.halfMove,
                fullMove: this.state.fullMove,
                moveList: this.state.moveList,
                capturedWhite: this.state.capturedWhite,
                capturedBlack: this.state.capturedBlack,
                playerColor: this.playerColor,
                aiDepth: this.aiDepth,
                analysisMode: this.analysisMode,
                lastMoveFrom: this.lastMove?.from ?? null,
                lastMoveTo: this.lastMove?.to ?? null,
            };
            localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
        }
        catch (_e) { /* ignore */ }
    }
    tryLoad() {
        try {
            const raw = localStorage.getItem(STORAGE_KEY);
            if (!raw)
                return null;
            return JSON.parse(raw);
        }
        catch (_e) {
            return null;
        }
    }
    restore(data) {
        this.playerColor = data.playerColor;
        this.aiDepth = data.aiDepth;
        this.analysisMode = data.analysisMode;
        this.aiColor = this.analysisMode ? null : (this.playerColor === 'w' ? 'b' : 'w');
        this.state = {
            board: boardFromSave(data.board),
            turn: data.turn,
            castling: data.castling,
            enPassant: data.enPassant,
            halfMove: data.halfMove,
            fullMove: data.fullMove,
            moveList: data.moveList,
            capturedWhite: data.capturedWhite,
            capturedBlack: data.capturedBlack,
        };
        this.lastMove = null;
        if (data.lastMoveFrom !== null && data.lastMoveTo !== null) {
            this.lastMove = {
                from: data.lastMoveFrom,
                to: data.lastMoveTo,
                piece: this.state.board[data.lastMoveTo]?.type || 'P',
                color: this.state.turn === 'w' ? 'b' : 'w',
            };
        }
        this.history = [this.state];
    }
    clearSave() {
        try {
            localStorage.removeItem(STORAGE_KEY);
        }
        catch (_e) { }
    }
}
exports.GameController = GameController;
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>