← Chess results

Chess

Qwen 3.8 · 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 95,222 bytes · SHA-256 9661a8442389
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Wood &amp; Slate Chess</title>
<style>
/* ── css: layout ── */
/* ============ base / layout ============ */
:root {
  --light: #f0d9b5;
  --dark: #b58863;
  --page: #12100e;
  --panel: #1c1815;
  --panel-2: #241f1a;
  --line: #352d26;
  --text: #e8ddcc;
  --text-dim: #a0917c;
  --accent: #d8b06a;
  --danger: #e05555;
  --board-size: min(72vh, 640px);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
  background:
    radial-gradient(1200px 700px at 50% -10%, #221c16 0%, var(--page) 60%),
    var(--page);
  color: var(--text);
  font-family: "Iowan Old Style", "Palatino Linotype", Palatino, Georgia, "Times New Roman", serif;
  min-height: 100vh;
}
#app { min-height: 100vh; display: flex; flex-direction: column; }

#topbar {
  display: flex; align-items: center; justify-content: space-between;
  padding: 10px 18px; gap: 16px; flex-wrap: wrap;
  border-bottom: 1px solid var(--line);
  background: linear-gradient(180deg, #1d1916, #171310);
  position: sticky; top: 0; z-index: 30;
}
#logo {
  font-size: 21px; letter-spacing: 0.06em; color: var(--accent);
  display: flex; align-items: center; gap: 8px; font-weight: 600;
}
#controls { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.ctl { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--text-dim); }
.ctl select, #controls button {
  font-family: inherit; font-size: 13px;
  background: var(--panel-2); color: var(--text);
  border: 1px solid var(--line); border-radius: 6px;
  padding: 5px 9px; cursor: pointer;
}
#controls button:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
#controls button:disabled { opacity: 0.4; cursor: default; }
#btn-flip { font-size: 15px; }

#layout {
  flex: 1; display: flex; align-items: flex-start; justify-content: center;
  gap: 22px; padding: 20px 16px 32px; flex-wrap: wrap;
}
#board-zone {
  position: relative;
  width: var(--board-size);
}
#board {
  width: var(--board-size);
  height: var(--board-size);
  border: 10px solid #4a3524;
  border-radius: 6px;
  box-shadow: 0 18px 50px rgba(0,0,0,0.55), 0 0 0 1px #2c2015, inset 0 0 24px rgba(0,0,0,0.35);
  background: var(--dark);
  position: relative;
  touch-action: none;
  user-select: none;
  -webkit-user-select: none;
}

#panel {
  width: 300px;
  display: flex; flex-direction: column; gap: 12px;
  background: var(--panel);
  border: 1px solid var(--line);
  border-radius: 10px;
  padding: 14px;
  box-shadow: 0 10px 30px rgba(0,0,0,0.35);
}
.panel-block { border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2); padding: 10px; }
.panel-title {
  font-size: 11px; text-transform: uppercase; letter-spacing: 0.14em;
  color: var(--text-dim); margin-bottom: 8px;
}
.turn {
  display: flex; align-items: center; gap: 9px;
  font-size: 15px; padding: 10px 12px;
  border: 1px solid var(--line); border-radius: 8px; background: var(--panel-2);
}
.turn-dot {
  width: 14px; height: 14px; border-radius: 50%; flex: none;
  box-shadow: 0 0 0 1px rgba(0,0,0,0.4) inset;
}
.turn-dot.w { background: #f5f0e6; }
.turn-dot.b { background: #262019; box-shadow: 0 0 0 1px #55483a inset; }

.tray-row { display: flex; align-items: center; gap: 8px; padding: 3px 0; }
.tray-name { font-size: 11px; color: var(--text-dim); width: 96px; flex: none; }
.tray { display: flex; flex-wrap: wrap; gap: 1px; font-size: 19px; line-height: 1.1; min-height: 22px; }
.tray .adv { color: var(--accent); font-size: 12px; margin-left: 5px; }

.move-list {
  height: 236px; overflow-y: auto; font-size: 13.5px;
  font-variant-numeric: tabular-nums;
  scrollbar-width: thin; scrollbar-color: #4a3c2e transparent;
}
.move-list::-webkit-scrollbar { width: 8px; }
.move-list::-webkit-scrollbar-thumb { background: #4a3c2e; border-radius: 4px; }
.move-row { display: flex; gap: 6px; padding: 2px 4px; border-radius: 4px; }
.move-row:nth-child(odd) { background: rgba(255,255,255,0.03); }
.move-num { color: var(--text-dim); width: 30px; flex: none; }
.move-san { width: 76px; }
.move-list .move-san:last-child { width: auto; }

.hints { font-size: 12px; color: var(--text-dim); line-height: 1.5; }
.hints p { margin: 6px 0; }
.hints code {
  font-family: ui-monospace, Menlo, Consolas, monospace; font-size: 11px;
  background: rgba(0,0,0,0.35); padding: 1px 4px; border-radius: 3px; color: var(--accent);
}

@media (max-width: 1020px) {
  #layout { justify-content: center; }
  #panel { width: var(--board-size); max-width: 640px; }
}
@media (max-width: 560px) {
  :root { --board-size: min(94vw, 520px); }
  #board { border-width: 6px; }
}
/* ── css: board ── */
/* ============ board cells, pieces, highlights ============ */
.board-wrap { position: relative; width: 100%; height: 100%; }
.board { display: grid; grid-template-columns: repeat(8, 1fr); grid-template-rows: repeat(8, 1fr); width: 100%; height: 100%; }
.sq { position: relative; overflow: hidden; }
.sq.light { background: var(--light); }
.sq.dark { background: var(--dark); }

.glyph {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  font-size: calc(var(--board-size) * 0.105);
  line-height: 1;
  pointer-events: none;
}
.glyph.wp {
  color: #f7f2e7;
  text-shadow: 0 1px 2px rgba(0,0,0,0.55), 0 0 2px rgba(0,0,0,0.6);
}
.glyph.bp {
  color: #2b241d;
  text-shadow: 0 1px 2px rgba(255,255,255,0.12);
}

/* selection / last move / check tints */
.sq.sel.light, .sq.sel.dark { box-shadow: inset 0 0 0 3px #3d9970; }
.sq.last { background-image: linear-gradient(var(--hint-last, rgba(255,213,0,0.42)), var(--hint-last, rgba(255,213,0,0.42))); }
.sq.check {
  background-image: radial-gradient(circle at 50% 45%, rgba(255,60,40,0.85) 18%, rgba(255,60,40,0.30) 42%, transparent 68%);
}
.sq.hover { box-shadow: inset 0 0 0 2px rgba(255,255,255,0.35); }

/* move hints: dots for quiet moves, rings for captures */
.hint { position: absolute; inset: 0; pointer-events: none; }
.hint.dot::after {
  content: ""; position: absolute; left: 50%; top: 50%;
  width: 30%; height: 30%;
  transform: translate(-50%, -50%);
  border-radius: 50%;
  background: rgba(25, 35, 20, 0.42);
  box-shadow: 0 0 6px rgba(0,0,0,0.3);
}
.hint.ring::after {
  content: ""; position: absolute; inset: 4%;
  border-radius: 50%;
  border: calc(var(--board-size) * 0.008) solid rgba(150, 30, 20, 0.55);
}
.sq.light .hint.dot::after { background: rgba(25, 35, 20, 0.40); }
.sq.dark .hint.dot::after { background: rgba(240, 225, 190, 0.45); }
.sq.light .hint.ring::after { border-color: rgba(150, 30, 20, 0.6); }
.sq.dark .hint.ring::after { border-color: rgba(120, 20, 10, 0.65); }

/* coordinate labels */
.coord {
  position: absolute; font-size: calc(var(--board-size) * 0.026);
  font-family: Georgia, serif; letter-spacing: 0.02em; pointer-events: none;
  color: rgba(0,0,0,0.55);
}
.sq.light .coord { color: rgba(70, 45, 20, 0.8); }
.coord.rank { left: 3%; top: 2%; }
.coord.file { right: 4%; bottom: 1.5%; }

/* drag ghost */
.drag-ghost {
  position: absolute; z-index: 20;
  width: calc(var(--board-size) * 0.125);
  height: calc(var(--board-size) * 0.125);
  display: flex; align-items: center; justify-content: center;
  font-size: calc(var(--board-size) * 0.1);
  pointer-events: none;
  opacity: 0.9;
  filter: drop-shadow(0 4px 6px rgba(0,0,0,0.5));
}

/* captured tray glyphs */
.tray .cap-glyph { display: inline-block; }
.tray .cap-glyph.wp { color: #e8e0cf; }
.tray .cap-glyph.bp { color: #6b5c49; }
/* ── css: overlays ── */
/* ============ promotion picker, banner ============ */
.promo-picker {
  display: none;
  position: absolute; z-index: 25;
  grid-auto-rows: 1fr;
  gap: 3px;
  background: #241f1a;
  border: 1px solid var(--accent);
  border-radius: 8px;
  padding: 6px;
  box-shadow: 0 12px 30px rgba(0,0,0,0.6);
}
.promo-picker.pw .promo-btn { color: #f7f2e7; text-shadow: 0 1px 2px rgba(0,0,0,0.6); }
.promo-picker.pb .promo-btn { color: #2b241d; }
.promo-btn {
  font-size: calc(var(--board-size) * 0.075);
  background: transparent; border: 1px solid var(--line);
  border-radius: 6px; cursor: pointer;
  padding: 4px 6px; line-height: 1;
  transition: background 0.1s;
}
.promo-btn:hover { background: rgba(216, 176, 106, 0.25); border-color: var(--accent); }

.banner {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%) scale(0.9);
  width: min(80%, 420px);
  padding: 22px 26px;
  text-align: center;
  font-size: 22px; letter-spacing: 0.04em;
  background: rgba(18, 14, 10, 0.94);
  border: 1px solid var(--line);
  border-radius: 10px;
  box-shadow: 0 16px 44px rgba(0,0,0,0.6);
  opacity: 0; pointer-events: none;
  transition: opacity 0.25s, transform 0.25s;
  z-index: 24;
}
.banner.show { opacity: 1; pointer-events: auto; transform: translate(-50%, -50%) scale(1); }
.banner.win { border-color: var(--accent); color: var(--accent); }
.banner.draw { border-color: #8a7a63; color: #d8cbb4; }
</style>
</head>
<body>
<div id="app"><header id="topbar">
  <div id="logo">♞ <span>Wood &amp; Slate Chess</span></div>
  <div id="controls">
    <label class="ctl"><span>Play as</span>
      <select id="sel-color">
        <option value="w">White</option>
        <option value="b">Black</option>
        <option value="none">Both (analysis)</option>
      </select>
    </label>
    <label class="ctl"><span>Depth</span>
      <select id="sel-diff">
        <option value="1">1</option>
        <option value="2">2</option>
        <option value="3">3</option>
      </select>
    </label>
    <button id="btn-flip" title="Flip board">⇅</button>
    <button id="btn-undo" title="Undo (takes back your move and the AI reply)">↶ Undo</button>
    <button id="btn-new" title="Start a new game">✦ New</button>
    <button id="btn-mute" title="Toggle sound">♫ sound</button>
  </div>
</header>
<main id="layout">
  <section id="board-zone">
    <div id="board"></div>
    <div id="banner" class="banner" aria-live="polite"></div>
  </section>
  <aside id="panel">
    <div id="turn-label" class="turn"></div>
    <div class="panel-block trays">
      <div class="tray-row"><span class="tray-name">White captured</span><div id="tray-white" class="tray"></div></div>
      <div class="tray-row"><span class="tray-name">Black captured</span><div id="tray-black" class="tray"></div></div>
    </div>
    <div class="panel-block moves-block">
      <div class="panel-title">Moves</div>
      <div id="move-list" class="move-list"></div>
    </div>
    <div class="panel-block hints">
      <div class="panel-title">How to play</div>
      <p>Click a piece, then a highlighted square — or drag it. Dots mark quiet moves; rings mark captures. The red-glowed square is the king in check.</p>
      <p>Debug: <code>game.fen()</code>, <code>game.legalMoves()</code>, <code>game.move('e2e4')</code>, <code>game.setup(fen)</code>.</p>
    </div>
  </aside>
</main></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./app":"src/app.ts"},"src/state.ts":{"./constants":"src/constants.ts"},"src/rules.ts":{"./constants":"src/constants.ts","./state":"src/state.ts"},"src/movegen.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./rules":"src/rules.ts"},"src/fen.ts":{"./constants":"src/constants.ts","./state":"src/state.ts"},"src/san.ts":{"./state":"src/state.ts","./movegen":"src/movegen.ts","./rules":"src/rules.ts"},"src/draws.ts":{"./state":"src/state.ts","./constants":"src/constants.ts","./movegen":"src/movegen.ts","./rules":"src/rules.ts"},"src/ai.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./movegen":"src/movegen.ts","./rules":"src/rules.ts"},"src/persistence.ts":{"./state":"src/state.ts","./fen":"src/fen.ts"},"src/ui.ts":{"./constants":"src/constants.ts","./state":"src/state.ts"},"src/app.ts":{"./constants":"src/constants.ts","./state":"src/state.ts","./rules":"src/rules.ts","./movegen":"src/movegen.ts","./san":"src/san.ts","./draws":"src/draws.ts","./ai":"src/ai.ts","./sound":"src/sound.ts","./persistence":"src/persistence.ts","./ui":"src/ui.ts","./fen":"src/fen.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// ENTRY — build the App from persisted state (or fresh), attach UI, expose
// the headless debug API on window.game.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
const app_1 = require("./app");
function start() {
    const app = app_1.App.restore() || new app_1.App();
    app.attach();
    app.paint();
    // Headless probe interface: game.fen(), game.move('e2e4'), game.legalMoves() ...
    window.game = app.debugApi();
    window.app = app; // full instance for deeper debugging
}
start();
};

// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// TUNABLE CONSTANTS — edit this block to re-theme or re-tune the engine
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.VAL = exports.PST = exports.FILES = exports.BLACK = exports.WHITE = exports.CONFIG = void 0;
exports.CONFIG = {
    // Board palette (light / dark squares), classic warm wood
    COLOR_LIGHT: "#f0d9b5",
    COLOR_DARK: "#b58863",
    // Highlight colors
    COLOR_LAST_MOVE: "rgba(255, 213, 0, 0.42)",
    COLOR_CHECK: "rgba(255, 46, 46, 0.75)",
    COLOR_SELECT: "rgba(30, 120, 60, 0.55)",
    COLOR_HINT_MOVE: "rgba(20, 40, 20, 0.35)", // dot for quiet moves
    COLOR_HINT_CAPTURE: "rgba(120, 20, 20, 0.45)", // ring for captures
    // Piece rendering
    GLYPH_SCALE: 0.74, // glyph size relative to square
    // AI
    AI_TIME_SLICE_MS: 12, // max ms per synchronous search chunk
    AI_BUDGET_MS: [250, 400, 700], // hard per-move time budget per difficulty (1,2,3)
    DEPTHS: [1, 2, 3], // difficulty selector
    // Evaluation
    MATE_SCORE: 100000,
    QUIESCE_LIMIT: 6, // ply of captures searched in quiescence
    PVT_WINDOW_HALF: 55, // principal variation window half-width
};
exports.WHITE = 'w';
exports.BLACK = 'b';
exports.FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h'];
// Piece square tables, indexed by square 0..63 (a1..h1 ... a8..h8),
// from the perspective of the piece's OWN color (row 0 = own back rank).
exports.PST = {
    P: [
        0, 0, 0, 0, 0, 0, 0, 0,
        50, 50, 50, 50, 50, 50, 50, 50,
        10, 10, 20, 30, 30, 20, 10, 10,
        5, 5, 10, 25, 25, 10, 5, 5,
        0, 0, 0, 20, 20, 0, 0, 0,
        5, -5, -10, 0, 0, -10, -5, 5,
        5, 10, 10, -20, -20, 10, 10, 5,
        0, 0, 0, 0, 0, 0, 0, 0,
    ],
    N: [
        -50, -40, -30, -30, -30, -30, -40, -50,
        -40, -20, 0, 0, 0, 0, -20, -40,
        -30, 0, 10, 15, 15, 10, 0, -30,
        -30, 5, 15, 20, 20, 15, 5, -30,
        -30, 0, 15, 20, 20, 15, 0, -30,
        -30, 5, 10, 15, 15, 10, 5, -30,
        -40, -20, 0, 5, 5, 0, -20, -40,
        -50, -40, -30, -30, -30, -30, -40, -50,
    ],
    B: [
        -20, -10, -10, -10, -10, -10, -10, -20,
        -10, 0, 0, 0, 0, 0, 0, -10,
        -10, 0, 5, 10, 10, 5, 0, -10,
        -10, 5, 5, 10, 10, 5, 5, -10,
        -10, 0, 10, 10, 10, 10, 0, -10,
        -10, 10, 10, 10, 10, 10, 10, -10,
        -10, 5, 0, 0, 0, 0, 5, -10,
        -20, -10, -10, -10, -10, -10, -10, -20,
    ],
    R: [
        0, 0, 0, 0, 0, 0, 0, 0,
        5, 10, 10, 10, 10, 10, 10, 5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        0, 0, 0, 5, 5, 0, 0, 0,
    ],
    Q: [
        -20, -10, -10, -5, -5, -10, -10, -20,
        -10, 0, 0, 0, 0, 0, 0, -10,
        -10, 0, 5, 5, 5, 5, 0, -10,
        -5, 0, 5, 5, 5, 5, 0, -5,
        0, 0, 5, 5, 5, 5, 0, -5,
        -10, 5, 5, 5, 5, 5, 0, -10,
        -10, 0, 5, 0, 0, 0, 0, -10,
        -20, -10, -10, -5, -5, -10, -10, -20,
    ],
    K: [
        -30, -40, -40, -50, -50, -40, -40, -30,
        -30, -40, -40, -50, -50, -40, -40, -30,
        -30, -40, -40, -50, -50, -40, -40, -30,
        -30, -40, -40, -50, -50, -40, -40, -30,
        -20, -30, -30, -40, -40, -30, -30, -20,
        -10, -20, -20, -20, -20, -20, -20, -10,
        20, 20, 0, 0, 0, 0, 20, 20,
        20, 30, 10, 0, 0, 10, 30, 20,
    ],
};
// Base material values (centipawns)
exports.VAL = { P: 100, N: 320, B: 330, R: 500, Q: 900, K: 0 };
};

// ── module: src/state.ts ──
__mods["src/state.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// BOARD REPRESENTATION
// 64-square "mailbox" style array: index 0..63, a1=0 ... h8=63.
// Board squares hold a piece string ("wP","bK") or null.
// State holds turn, castling rights, en-passant target, clocks and history.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.otherColor = exports.pieceType = exports.pieceColor = void 0;
exports.emptyBoard = emptyBoard;
exports.initialState = initialState;
exports.stateToFen = stateToFen;
exports.positionKey = positionKey;
exports.sqToAlgebraic = sqToAlgebraic;
exports.algebraicToSq = algebraicToSq;
exports.cloneState = cloneState;
exports.makeMoveOnState = makeMoveOnState;
const pieceColor = (p) => p[0];
exports.pieceColor = pieceColor;
const pieceType = (p) => p[1];
exports.pieceType = pieceType;
const otherColor = (c) => (c === 'w' ? 'b' : 'w');
exports.otherColor = otherColor;
function emptyBoard() {
    return new Array(64).fill(null);
}
// Standard initial position (FEN: rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR)
function initialState() {
    const board = emptyBoard();
    const back = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
    for (let f = 0; f < 8; f++) {
        board[f] = 'w' + back[f];
        board[8 + f] = 'wP';
        board[48 + f] = 'bP';
        board[56 + f] = 'b' + back[f];
    }
    return {
        board, turn: 'w',
        castling: { wk: true, wq: true, bk: true, bq: true },
        ep: null, halfmove: 0, fullmove: 1,
        history: [], positionCounts: new Map(),
    };
}
// --- FEN helpers ----------------------------------------------------------
const FEN_CODE = { P: 'P', N: 'N', B: 'B', R: 'R', Q: 'Q', K: 'K' };
const FEN_INV = { P: 'P', N: 'N', B: 'B', R: 'R', Q: 'Q', K: 'K' };
function stateToFen(s) {
    const rows = [];
    for (let r = 7; r >= 0; r--) {
        let row = '', empty = 0;
        for (let f = 0; f < 8; f++) {
            const p = s.board[r * 8 + f];
            if (p === null) {
                empty++;
                continue;
            }
            if (empty) {
                row += String(empty);
                empty = 0;
            }
            row += (p[0] === 'w' ? FEN_CODE[p[1]] : FEN_CODE[p[1]].toLowerCase());
        }
        if (empty)
            row += String(empty);
        rows.push(row);
    }
    let c = '';
    if (s.castling.wk)
        c += 'K';
    if (s.castling.wq)
        c += 'Q';
    if (s.castling.bk)
        c += 'k';
    if (s.castling.bq)
        c += 'q';
    if (!c)
        c = '-';
    const ep = s.ep === null ? '-' : sqToAlgebraic(s.ep);
    return `${rows.join('/')} ${s.turn} ${c} ${ep} ${s.halfmove} ${s.fullmove}`;
}
// Compact position key (no castling/clocks) — used for 3-fold repetition
function positionKey(s) {
    let key = '';
    for (let i = 0; i < 64; i++) {
        const p = s.board[i];
        key += p === null ? '.' : p;
    }
    return key + s.turn;
}
function sqToAlgebraic(sq) {
    const f = sq % 8, r = (sq >> 3) + 1;
    return 'abcdefgh'[f] + String(r);
}
function algebraicToSq(s) {
    if (s.length !== 2)
        return null;
    const f = 'abcdefgh'.indexOf(s[0]);
    const r = parseInt(s[1], 10);
    if (f < 0 || r < 1 || r > 8)
        return null;
    return ((r - 1) << 3) | f;
}
function cloneState(s) {
    return {
        board: s.board.slice(),
        turn: s.turn,
        castling: { ...s.castling },
        ep: s.ep, halfmove: s.halfmove, fullmove: s.fullmove,
    };
}
function makeMoveOnState(s, m) {
    // Returns a NEW state with the move applied (used by the engine; does not
    // touch history). Assumes m is legal for s.
    const board = s.board.slice();
    const piece = board[m.from];
    let flags = m.flags;
    board[m.from] = null;
    let nextEp = null;
    let halfmove = s.halfmove + 1;
    if (piece[1] === 'P') {
        const toFile = m.to % 8, fromFile = m.from % 8;
        if (flags === 'ep') {
            // captured pawn sits on m.to's file, m.from's rank (one rank behind m.to)
            board[m.to + (s.turn === 'w' ? -8 : 8)] = null;
        }
        else if (m.captured) {
            halfmove = 0;
        }
        if (piece[1] === 'P' && ((s.turn === 'w' && (m.to >> 3) === 7) || (s.turn === 'b' && (m.to >> 3) === 0)) && flags.startsWith('promo:')) {
            board[m.to] = s.turn + flags.slice(6);
        }
        else {
            board[m.to] = piece;
        }
        // doubled pawn -> ep target for opponent
        if (Math.abs(m.to - m.from) === 16)
            nextEp = (m.from + m.to) >> 1;
        halfmove = 0; // pawn move resets 50-move clock (covers normal and double)
    }
    else {
        if (m.captured)
            halfmove = 0;
        board[m.to] = piece;
        if (flags === 'castle') {
            // move the rook alongside
            const rank = m.from >> 3;
            const base = rank * 8;
            if ((m.to % 8) === 6) { // kingside
                board[base + 5] = board[base + 7];
                board[base + 7] = null;
            }
            else { // queenside
                board[base + 3] = board[base + 0];
                board[base + 0] = null;
            }
        }
    }
    const turn = s.turn === 'w' ? 'b' : 'w';
    const castling = { ...s.castling };
    // King move kills both rights for that side; rook move/capture kills that side
    if (piece[1] === 'K') {
        if (s.turn === 'w') {
            castling.wk = false;
            castling.wq = false;
        }
        else {
            castling.bk = false;
            castling.bq = false;
        }
    }
    const touch = (sq) => {
        if (sq === 0)
            castling.wq = false;
        else if (sq === 7)
            castling.wk = false;
        else if (sq === 56)
            castling.bq = false;
        else if (sq === 63)
            castling.bk = false;
    };
    touch(m.from);
    touch(m.to);
    // (rook captured on its corner: m.to already covers it; rook from corner: m.from covers it)
    return {
        board, turn, castling, ep: nextEp, halfmove,
        fullmove: s.fullmove + (s.turn === 'b' ? 1 : 0),
    };
}
};

// ── module: src/rules.ts ──
__mods["src/rules.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// ATTACK / CHECK RULES
// Direction deltas are (dRank, dFile) component pairs so file/rank bounds can
// be validated explicitly — no 64-index wraparound artifacts.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.ROOK_DIRS = exports.BISHOP_DIRS = exports.KING_DIRS = exports.KNIGHT_DIRS = void 0;
exports.isSquareAttacked = isSquareAttacked;
exports.findKing = findKing;
exports.inCheck = inCheck;
exports.KNIGHT_DIRS = [[1, 2], [1, -2], [2, 1], [2, -1], [-1, 2], [-1, -2], [-2, 1], [-2, -1]];
exports.KING_DIRS = [[0, 1], [0, -1], [1, 0], [1, 1], [1, -1], [-1, 0], [-1, 1], [-1, -1]];
exports.BISHOP_DIRS = [[1, 1], [1, -1], [-1, 1], [-1, -1]];
exports.ROOK_DIRS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
const inRange = (r, f) => r >= 0 && r < 8 && f >= 0 && f < 8;
// Is `sq` attacked by any piece of color `by`?
function isSquareAttacked(board, sq, by) {
    const f = sq % 8;
    const r = sq >> 3;
    // Pawns: a white pawn attacks from the rank below; black from the rank above
    const pRank = by === 'w' ? r - 1 : r + 1;
    if (pRank >= 0 && pRank < 8) {
        for (const df of [-1, 1]) {
            const cf = f + df;
            if (cf >= 0 && cf < 8 && board[pRank * 8 + cf] === by + 'P')
                return true;
        }
    }
    // Knights
    for (const [dr, df] of exports.KNIGHT_DIRS) {
        const rr = r + dr, cf = f + df;
        if (inRange(rr, cf) && board[rr * 8 + cf] === by + 'N')
            return true;
    }
    // Kings
    for (const [dr, df] of exports.KING_DIRS) {
        const rr = r + dr, cf = f + df;
        if (inRange(rr, cf) && board[rr * 8 + cf] === by + 'K')
            return true;
    }
    // Sliding: bishops/queens
    for (const [dr, df] of exports.BISHOP_DIRS) {
        let rr = r + dr, cf = f + df;
        while (inRange(rr, cf)) {
            const p = board[rr * 8 + cf];
            if (p) {
                if (p === by + 'B' || p === by + 'Q')
                    return true;
                break;
            }
            rr += dr;
            cf += df;
        }
    }
    // Sliding: rooks/queens
    for (const [dr, df] of exports.ROOK_DIRS) {
        let rr = r + dr, cf = f + df;
        while (inRange(rr, cf)) {
            const p = board[rr * 8 + cf];
            if (p) {
                if (p === by + 'R' || p === by + 'Q')
                    return true;
                break;
            }
            rr += dr;
            cf += df;
        }
    }
    return false;
}
function findKing(board, color) {
    const target = color + 'K';
    for (let i = 0; i < 64; i++)
        if (board[i] === target)
            return i;
    return -1;
}
function inCheck(board, color) {
    const k = findKing(board, color);
    return isSquareAttacked(board, k, color === 'w' ? 'b' : 'w');
}
};

// ── module: src/movegen.ts ──
__mods["src/movegen.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// LEGAL MOVE GENERATION
// Generator-based: pseudo-legal moves are produced, then every candidate is
// applied to a scratch board and the king's safety re-checked. Anything that
// leaves the mover's king in check (including castling through/onto check) is
// dropped, so the UI can only ever be offered legal moves.
// Directions are (dRank, dFile) component pairs — bounds validated explicitly.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.legalMoves = legalMoves;
exports.legalMovesFrom = legalMovesFrom;
exports.pseudoMoves = pseudoMoves;
exports.colorInCheck = colorInCheck;
const state_1 = require("./state");
const rules_1 = require("./rules");
const file = (sq) => sq % 8;
const rank = (sq) => sq >> 3;
const ok = (r, f) => r >= 0 && r < 8 && f >= 0 && f < 8;
// --- pseudo-legal generator ------------------------------------------------
function pseudoMoves(s, out) {
    const board = s.board;
    const me = s.turn;
    const opp = (0, state_1.otherColor)(me);
    for (let from = 0; from < 64; from++) {
        const piece = board[from];
        if (!piece || piece[0] !== me)
            continue;
        const t = piece[1];
        const f = file(from), r = rank(from);
        const push = (to, flags = '') => {
            out.push({ from, to, piece, captured: flags === 'ep' ? (opp + 'P') : board[to], flags });
        };
        if (t === 'P') {
            const dir = me === 'w' ? 1 : -1;
            const startRank = me === 'w' ? 1 : 6;
            const promoRank = me === 'w' ? 7 : 0;
            // one step forward
            const rr = r + dir;
            if (ok(rr, f) && board[rr * 8 + f] === null) {
                const one = rr * 8 + f;
                if (r === startRank && ok(r + dir * 2, f) && board[(r + dir * 2) * 8 + f] === null) {
                    push((r + dir * 2) * 8 + f, 'double');
                }
                if (rr === promoRank) {
                    for (const p of ['Q', 'R', 'B', 'N'])
                        push(one, 'promo:' + p);
                }
                else
                    push(one);
            }
            // diagonal captures / en passant
            for (const df of [-1, 1]) {
                const cf = f + df;
                if (!ok(rr, cf))
                    continue;
                const to = rr * 8 + cf;
                const target = board[to];
                if (target && target[0] === opp) {
                    if (rr === promoRank) {
                        for (const p of ['Q', 'R', 'B', 'N'])
                            push(to, 'promo:' + p);
                    }
                    else
                        push(to);
                }
                else if (target === null && to === s.ep) {
                    push(to, 'ep');
                }
            }
            continue;
        }
        if (t === 'N') {
            for (const [dr, df] of rules_1.KNIGHT_DIRS) {
                const rr = r + dr, cf = f + df;
                if (!ok(rr, cf))
                    continue;
                const to = rr * 8 + cf;
                const target = board[to];
                if (target === null || target[0] === opp)
                    push(to);
            }
            continue;
        }
        if (t === 'K') {
            for (const [dr, df] of rules_1.KING_DIRS) {
                const rr = r + dr, cf = f + df;
                if (!ok(rr, cf))
                    continue;
                const to = rr * 8 + cf;
                const target = board[to];
                if (target === null || target[0] === opp)
                    push(to);
            }
            // Castling: rights + empty squares only here; check-safety verified in isLegal
            const base = r * 8;
            if (f === 4) {
                if (me === 'w' && s.castling.wk && board[base + 5] === null && board[base + 6] === null
                    && board[base + 7] === 'wR' && !(0, rules_1.inCheck)(board, me))
                    push(base + 6, 'castle');
                if (me === 'b' && s.castling.bk && board[base + 5] === null && board[base + 6] === null
                    && board[base + 7] === 'bR' && !(0, rules_1.inCheck)(board, me))
                    push(base + 6, 'castle');
                if (me === 'w' && s.castling.wq && board[base + 1] === null && board[base + 2] === null && board[base + 3] === null
                    && board[base + 0] === 'wR' && !(0, rules_1.inCheck)(board, me))
                    push(base + 2, 'castle');
                if (me === 'b' && s.castling.bq && board[base + 1] === null && board[base + 2] === null && board[base + 3] === null
                    && board[base + 0] === 'bR' && !(0, rules_1.inCheck)(board, me))
                    push(base + 2, 'castle');
            }
            continue;
        }
        // Sliding pieces
        const dirs = t === 'B' ? rules_1.BISHOP_DIRS : t === 'R' ? rules_1.ROOK_DIRS : [...rules_1.BISHOP_DIRS, ...rules_1.ROOK_DIRS];
        for (const [dr, df] of dirs) {
            let rr = r + dr, cf = f + df;
            while (ok(rr, cf)) {
                const to = rr * 8 + cf;
                const target = board[to];
                if (target === null)
                    push(to);
                else {
                    if (target[0] === opp)
                        push(to);
                    break;
                }
                rr += dr;
                cf += df;
            }
        }
    }
}
// --- legality filter -------------------------------------------------------
function isLegal(s, m) {
    const board = s.board.slice();
    const piece = board[m.from];
    board[m.from] = null;
    if (m.flags === 'ep') {
        board[m.to + (s.turn === 'w' ? -8 : 8)] = null;
    }
    if (m.flags.startsWith('promo:')) {
        board[m.to] = s.turn + m.flags.slice(6);
    }
    else {
        board[m.to] = piece;
    }
    if (m.flags === 'castle') {
        const base = rank(m.from) * 8;
        if (file(m.to) === 6) {
            board[base + 5] = board[base + 7];
            board[base + 7] = null;
        }
        else {
            board[base + 3] = board[base + 0];
            board[base + 0] = null;
        }
    }
    const me = s.turn;
    const opp = (0, state_1.otherColor)(me);
    if (piece[1] === 'K') {
        // king moved: destination must be safe; castling additionally requires the
        // square the king passes over to be safe
        if ((0, rules_1.isSquareAttacked)(board, m.to, opp))
            return false;
        if (m.flags === 'castle') {
            const mid = file(m.to) === 6 ? rank(m.from) * 8 + 5 : rank(m.from) * 8 + 3;
            if ((0, rules_1.isSquareAttacked)(board, mid, opp))
                return false;
        }
        return true;
    }
    // any other piece: the king (wherever it stands) must not be left in check
    const k = (0, rules_1.findKing)(board, me);
    return !(0, rules_1.isSquareAttacked)(board, k, opp);
}
// Public API: all legal moves for the side to move.
function legalMoves(s) {
    const pseudo = [];
    pseudoMoves(s, pseudo);
    const legal = [];
    for (const m of pseudo)
        if (isLegal(s, m))
            legal.push(m);
    return legal;
}
// Legal moves for ONE piece (drives selection highlights in the UI).
function legalMovesFrom(s, from) {
    const all = legalMoves(s);
    return all.filter(m => m.from === from);
}
// Convenience: is side `color` currently in check in state s?
function colorInCheck(s, color) {
    return (0, rules_1.inCheck)(s.board, color);
}
};

// ── module: src/fen.ts ──
__mods["src/fen.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// FEN PARSING (for debug setup / position loading)
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.fenToState = fenToState;
exports.fenToFullState = fenToFullState;
const state_1 = require("./state");
const CODE_INV = {
    p: 'P', n: 'N', b: 'B', r: 'R', q: 'Q', k: 'K',
    P: 'P', N: 'N', B: 'B', R: 'R', Q: 'Q', K: 'K',
};
function fenToState(fen) {
    const parts = fen.trim().split(/\s+/);
    const board = new Array(64).fill(null);
    const rows = parts[0].split('/');
    if (rows.length !== 8)
        throw new Error('FEN: expected 8 ranks');
    for (let i = 0; i < 8; i++) {
        const row = rows[i];
        let f = 0;
        for (const ch of row) {
            if (ch >= '1' && ch <= '8') {
                f += parseInt(ch, 10);
                continue;
            }
            if (f > 7)
                throw new Error('FEN: rank overflow');
            board[(7 - i) * 8 + f] = (ch === ch.toLowerCase() ? 'b' : 'w') + CODE_INV[ch];
            f++;
        }
        if (f !== 8)
            throw new Error('FEN: rank ' + (8 - i) + ' has ' + f + ' squares');
    }
    const turn = parts[1] === 'w' ? 'w' : 'b';
    const c = parts[2] || '-';
    const castling = {
        wk: c.includes('K'), wq: c.includes('Q'),
        bk: c.includes('k'), bq: c.includes('q'),
    };
    let ep = null;
    if (parts[3] && parts[3] !== '-') {
        const f = 'abcdefgh'.indexOf(parts[3][0]);
        const r = parseInt(parts[3][1], 10);
        if (f >= 0 && r >= 1 && r <= 8)
            ep = (r - 1) * 8 + f;
    }
    return {
        board, turn, castling, ep,
        halfmove: parts[4] ? parseInt(parts[4], 10) || 0 : 0,
        fullmove: parts[5] ? parseInt(parts[5], 10) || 1 : 1,
    };
}
function fenToFullState(fen) {
    const s = fenToState(fen);
    return { ...s, history: [], positionCounts: new Map([[(0, state_1.positionKey)(s), 1]]) };
}
};

// ── module: src/san.ts ──
__mods["src/san.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// STANDARD ALGEBRAIC NOTATION
// Nf3, exd6 e.p., O-O, e8=Q+, Qxd8#, ...
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.moveToSan = moveToSan;
const state_1 = require("./state");
const movegen_1 = require("./movegen");
const rules_1 = require("./rules");
// Disambiguation: file if unique among same-piece movers to `to`,
// else rank if unique, else both.
function moveToSan(s, m) {
    const board = s.board;
    let san;
    const piece = m.piece[1];
    const fromA = (0, state_1.sqToAlgebraic)(m.from);
    const toA = (0, state_1.sqToAlgebraic)(m.to);
    if (m.flags === 'castle') {
        san = (m.to % 8) === 6 ? 'O-O' : 'O-O-O';
    }
    else if (piece === 'P') {
        const cap = (m.captured || m.flags === 'ep') ? 'abcdefgh'[m.from % 8] + 'x' : '';
        san = cap + toA;
        if (m.flags.startsWith('promo:'))
            san += '=' + m.flags.slice(6);
        if (m.flags === 'ep')
            san += ' e.p.';
    }
    else {
        let disambig = '';
        const legal = (0, movegen_1.legalMoves)(s);
        const rivals = [];
        for (let sq = 0; sq < 64; sq++) {
            if (sq === m.from || board[sq] !== m.piece)
                continue;
            const can = legal.some(l => l.from === sq && l.to === m.to);
            if (can)
                rivals.push(sq);
        }
        if (rivals.length > 0) {
            const sameFile = rivals.some(r => (r % 8) === (m.from % 8));
            const sameRank = rivals.some(r => (r >> 3) === (m.from >> 3));
            if (!sameFile)
                disambig = 'abcdefgh'[m.from % 8];
            else if (!sameRank)
                disambig = String((m.from >> 3) + 1);
            else
                disambig = fromA;
        }
        san = piece + disambig + (m.captured ? 'x' : '') + toA;
    }
    // Check / mate suffix
    const after = (0, state_1.makeMoveOnState)(s, m);
    if ((0, rules_1.inCheck)(after.board, after.turn)) {
        const replies = (0, movegen_1.legalMoves)(after);
        san += replies.length === 0 ? '#' : '+';
    }
    return san;
}
};

// ── module: src/draws.ts ──
__mods["src/draws.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// DRAW / GAME-STATUS DETECTION
// checkmate, stalemate, insufficient material (K vs K, K+B/N vs K, K+B vs K+B
// same-colored bishops), 50-move rule, threefold repetition.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.insufficientMaterial = insufficientMaterial;
exports.statusOf = statusOf;
exports.isRepetition = isRepetition;
exports.fullStatus = fullStatus;
exports.kingInCheckForBanner = kingInCheckForBanner;
const state_1 = require("./state");
const movegen_1 = require("./movegen");
const rules_1 = require("./rules");
// Insufficient material: K vs K; K+minor vs K; K+B vs K+B where both bishops
// sit on squares of the same color.
function insufficientMaterial(board) {
    const items = [];
    for (let sq = 0; sq < 64; sq++) {
        const p = board[sq];
        if (!p)
            continue;
        const t = (0, state_1.pieceType)(p);
        if (t === 'K')
            continue;
        items.push({ t, color: p[0], sq });
    }
    if (items.length === 0)
        return true; // bare kings
    if (items.length === 1) {
        const t = items[0].t;
        return t === 'B' || t === 'N';
    }
    // K+B vs K+B, same colored bishops
    if (items.length === 2 && items[0].t === 'B' && items[1].t === 'B'
        && items[0].color !== items[1].color) {
        const sqColor = (sq) => ((sq % 8) + (sq >> 3)) % 2;
        if (sqColor(items[0].sq) === sqColor(items[1].sq))
            return true;
    }
    return false;
}
function statusOf(s) {
    const legal = (0, movegen_1.legalMoves)(s);
    const checked = (0, rules_1.inCheck)(s.board, s.turn);
    if (legal.length === 0) {
        if (checked)
            return { result: 'checkmate', winner: s.turn === 'w' ? 'b' : 'w' };
        return { result: 'stalemate', winner: null };
    }
    if (insufficientMaterial(s.board))
        return { result: 'insufficient', winner: null };
    if (s.halfmove >= 100)
        return { result: 'fifty', winner: null };
    return { result: 'none', winner: null };
}
function isRepetition(s) {
    const key = (0, state_1.positionKey)(s);
    return (s.positionCounts.get(key) || 0) >= 3;
}
// Full status including threefold (requires history state)
function fullStatus(s) {
    const st = statusOf(s);
    if (st.result !== 'none')
        return st;
    if (isRepetition(s))
        return { result: 'threefold', winner: null };
    return st;
}
function kingInCheckForBanner(s) {
    if (!(0, rules_1.inCheck)(s.board, s.turn))
        return null;
    return (0, rules_1.findKing)(s.board, s.turn);
}
};

// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// AI — negamax with alpha-beta, MVV-LVA move ordering, shallow quiescence,
// principal-variation search (PVS) and O(1) make/unmake on a mutable search
// context (king positions tracked incrementally). The root loop runs in
// time slices with a hard budget so the UI never freezes and depth 3 stays
// well under a second.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.TimeSliceError = void 0;
exports.evaluate = evaluate;
exports.prepareRoot = prepareRoot;
exports.stepRoot = stepRoot;
exports.findBestMove = findBestMove;
const constants_1 = require("./constants");
const state_1 = require("./state");
const movegen_1 = require("./movegen");
const rules_1 = require("./rules");
const MATE = constants_1.CONFIG.MATE_SCORE;
class TimeSliceError extends Error {
    constructor() { super('timeslice'); this.name = 'TimeSliceError'; }
}
exports.TimeSliceError = TimeSliceError;
function initCtx(s) {
    return {
        board: s.board.slice(),
        turn: s.turn,
        castling: { ...s.castling },
        ep: s.ep,
        kingW: (0, rules_1.findKing)(s.board, 'w'),
        kingB: (0, rules_1.findKing)(s.board, 'b'),
    };
}
function makeMove(ctx, m) {
    const board = ctx.board;
    const piece = board[m.from];
    const u = {
        from: m.from, to: m.to, piece,
        captured: null, capSq: m.to, flags: m.flags,
        prevTurn: ctx.turn, prevEp: ctx.ep, prevCastling: ctx.castling,
        rookFrom: -1, rookTo: -1,
        kingW: ctx.kingW, kingB: ctx.kingB,
    };
    if (m.flags === 'ep') {
        u.capSq = m.to + (ctx.turn === 'w' ? -8 : 8);
        u.captured = board[u.capSq];
        board[u.capSq] = null;
        board[m.to] = piece;
    }
    else {
        u.captured = board[m.to];
        if (m.flags.startsWith('promo:'))
            board[m.to] = ctx.turn + m.flags.slice(6);
        else
            board[m.to] = piece;
        if (m.flags === 'castle') {
            const base = (m.from >> 3) * 8;
            if ((m.to % 8) === 6) {
                u.rookFrom = base + 7;
                u.rookTo = base + 5;
            }
            else {
                u.rookFrom = base;
                u.rookTo = base + 3;
            }
            board[u.rookTo] = board[u.rookFrom];
            board[u.rookFrom] = null;
        }
    }
    board[m.from] = null;
    if (m.flags === 'castle' || piece[1] === 'K') {
        if (piece[0] === 'w')
            ctx.kingW = m.to;
        else
            ctx.kingB = m.to;
    }
    ctx.castling = { ...u.prevCastling };
    if (piece[1] === 'K') {
        if (ctx.turn === 'w') {
            ctx.castling.wk = false;
            ctx.castling.wq = false;
        }
        else {
            ctx.castling.bk = false;
            ctx.castling.bq = false;
        }
    }
    if (m.from === 0)
        ctx.castling.wq = false;
    else if (m.from === 7)
        ctx.castling.wk = false;
    else if (m.from === 56)
        ctx.castling.bq = false;
    else if (m.from === 63)
        ctx.castling.bk = false;
    if (m.to === 0)
        ctx.castling.wq = false;
    else if (m.to === 7)
        ctx.castling.wk = false;
    else if (m.to === 56)
        ctx.castling.bq = false;
    else if (m.to === 63)
        ctx.castling.bk = false;
    ctx.ep = m.flags === 'double' ? (m.from + m.to) >> 1 : null;
    ctx.turn = (0, state_1.otherColor)(ctx.turn);
    return u;
}
function unmakeMove(ctx, u) {
    const board = ctx.board;
    board[u.from] = u.piece;
    if (u.flags === 'ep') {
        board[u.to] = null;
        board[u.capSq] = u.captured;
    }
    else {
        board[u.to] = u.captured;
    }
    if (u.flags === 'castle') {
        board[u.rookFrom] = board[u.rookTo];
        board[u.rookTo] = null;
    }
    ctx.turn = u.prevTurn;
    ctx.ep = u.prevEp;
    ctx.castling = u.prevCastling;
    ctx.kingW = u.kingW;
    ctx.kingB = u.kingB;
}
// --- evaluation (side to move's perspective) --------------------------------
function evaluate(s) {
    let score = 0;
    const board = s.board;
    for (let sq = 0; sq < 64; sq++) {
        const p = board[sq];
        if (!p)
            continue;
        const t = p[1];
        if (t === 'K')
            continue;
        const base = constants_1.VAL[t];
        const pst = constants_1.PST[t];
        if (p[0] === 'w')
            score += base + pst[sq];
        else
            score -= base + pst[63 - sq];
    }
    return s.turn === 'w' ? score : -score;
}
function evalCtx(ctx) {
    return evaluate(ctx);
}
// --- search -----------------------------------------------------------------
function search(ctx, depth, alpha, beta, ply, deadline, undos) {
    if ((Date.now() | 0) >= deadline)
        throw new TimeSliceError();
    if (depth === 0)
        return quiesce(ctx, alpha, beta, ply, deadline, undos);
    const pseudo = [];
    (0, movegen_1.pseudoMoves)(ctx, pseudo);
    orderMoves(pseudo);
    let anyLegal = false;
    let best = -Infinity;
    for (const m of pseudo) {
        const u = makeMove(ctx, m);
        undos.push(u);
        const mover = u.prevTurn;
        const kingSq = mover === 'w' ? ctx.kingW : ctx.kingB;
        const opp = (0, state_1.otherColor)(mover);
        let legal = !(0, rules_1.isSquareAttacked)(ctx.board, kingSq, opp);
        if (legal && m.flags === 'castle') {
            const mid = (m.to % 8) === 6 ? (m.from >> 3) * 8 + 5 : (m.from >> 3) * 8 + 3;
            legal = !(0, rules_1.isSquareAttacked)(ctx.board, mid, opp);
        }
        if (!legal) {
            unmakeMove(ctx, undos.pop());
            continue;
        }
        anyLegal = true;
        const score = -search(ctx, depth - 1, -beta, -alpha, ply + 1, deadline, undos);
        unmakeMove(ctx, undos.pop());
        if (score > best)
            best = score;
        if (score > alpha)
            alpha = score;
        if (alpha >= beta)
            break;
    }
    if (!anyLegal) {
        const inChk = (0, rules_1.isSquareAttacked)(ctx.board, ctx.turn === 'w' ? ctx.kingW : ctx.kingB, (0, state_1.otherColor)(ctx.turn));
        return inChk ? -(MATE - ply) : 0;
    }
    return best;
}
// --- quiescence (captures + promotions only) --------------------------------
function quiesce(ctx, alpha, beta, ply, deadline, undos) {
    if ((Date.now() | 0) >= deadline)
        throw new TimeSliceError();
    const standPat = evalCtx(ctx);
    if (standPat >= beta)
        return standPat;
    if (standPat > alpha)
        alpha = standPat;
    if (ply >= constants_1.CONFIG.QUIESCE_LIMIT)
        return alpha;
    const pseudo = [];
    (0, movegen_1.pseudoMoves)(ctx, pseudo);
    const caps = [];
    for (const m of pseudo)
        if (m.captured || m.flags.startsWith('promo:'))
            caps.push(m);
    orderMoves(caps);
    for (const m of caps) {
        const u = makeMove(ctx, m);
        undos.push(u);
        // A king move is legal iff its destination (tracked in ctx) is not attacked;
        // other pieces leave the king where it was, so the same test on the king
        // square is invalid for them — but a capture by a non-king piece can never
        // change the king's square, so checking the mover's king square (which for
        // non-king moves is unchanged) is exactly right.
        const mover = u.prevTurn;
        const kingSq = mover === 'w' ? ctx.kingW : ctx.kingB;
        const ok = mover === 'w' ? !(0, rules_1.isSquareAttacked)(ctx.board, kingSq, 'b') : !(0, rules_1.isSquareAttacked)(ctx.board, kingSq, 'w');
        if (!ok) {
            unmakeMove(ctx, undos.pop());
            continue;
        }
        const score = -quiesce(ctx, -beta, -alpha, ply + 1, deadline, undos);
        unmakeMove(ctx, undos.pop());
        if (score > alpha)
            alpha = score;
        if (alpha >= beta)
            break;
    }
    return alpha;
}
// --- move ordering: MVV-LVA + promotions first -------------------------------
const VAL_ORDER = { P: 1, N: 3, B: 3, R: 5, Q: 9, K: 0 };
function orderMoves(moves) {
    for (const m of moves) {
        let score = 0;
        if (m.captured)
            score += 10 * VAL_ORDER[m.captured[1]] - VAL_ORDER[m.piece[1]];
        if (m.flags.startsWith('promo:'))
            score += 90;
        m.order = score;
    }
    moves.sort((a, b) => (b.order || 0) - (a.order || 0));
}
// Builds the ordered list of legal root moves for the side to move.
function prepareRoot(s, depth) {
    const ctx = initCtx(s);
    const pseudo = [];
    (0, movegen_1.pseudoMoves)(ctx, pseudo);
    const legalMoves = [];
    const undos = [];
    for (const m of pseudo) {
        const u = makeMove(ctx, m);
        undos.push(u);
        const mover = u.prevTurn;
        const kingSq = mover === 'w' ? ctx.kingW : ctx.kingB;
        const opp = (0, state_1.otherColor)(mover);
        let safe = !(0, rules_1.isSquareAttacked)(ctx.board, kingSq, opp);
        if (safe && m.flags === 'castle') {
            const mid = (m.to % 8) === 6 ? (m.from >> 3) * 8 + 5 : (m.from >> 3) * 8 + 3;
            safe = !(0, rules_1.isSquareAttacked)(ctx.board, mid, opp);
        }
        unmakeMove(ctx, undos.pop());
        if (safe)
            legalMoves.push(m);
    }
    orderMoves(legalMoves);
    return { ctx, moves: legalMoves, idx: 0, alpha: -Infinity, best: null, bestScore: -Infinity, depth, undos: [], first: true };
}
// Steps the root loop until a slice deadline or budget; returns null while
// more work is pending, or the final SearchResult when done/budget-exceeded.
function stepRoot(rs, sliceDeadline, budgetDeadline) {
    try {
        while (rs.idx < rs.moves.length) {
            if ((Date.now() | 0) >= sliceDeadline)
                return null; // resume next slice
            if ((Date.now() | 0) >= budgetDeadline)
                break; // out of budget: play best so far
            const m = rs.moves[rs.idx++];
            const u = makeMove(rs.ctx, m);
            rs.undos.push(u);
            const mover = u.prevTurn;
            const kingSq = mover === 'w' ? rs.ctx.kingW : rs.ctx.kingB;
            const opp = (0, state_1.otherColor)(mover);
            let legal = !(0, rules_1.isSquareAttacked)(rs.ctx.board, kingSq, opp);
            if (legal && m.flags === 'castle') {
                const mid = (m.to % 8) === 6 ? (m.from >> 3) * 8 + 5 : (m.from >> 3) * 8 + 3;
                legal = !(0, rules_1.isSquareAttacked)(rs.ctx.board, mid, opp);
            }
            if (!legal) {
                unmakeMove(rs.ctx, rs.undos.pop());
                rs.first = false;
                continue;
            }
            let score;
            if (rs.first) {
                score = -search(rs.ctx, rs.depth - 1, -Infinity, -rs.alpha, 1, sliceDeadline, rs.undos);
                rs.first = false;
            }
            else {
                // PVS: null window first
                score = -search(rs.ctx, rs.depth - 1, -rs.alpha - 1, -rs.alpha, 1, sliceDeadline, rs.undos);
                if (score > rs.alpha && score < Infinity) {
                    score = -search(rs.ctx, rs.depth - 1, -Infinity, -rs.alpha, 1, sliceDeadline, rs.undos);
                }
            }
            unmakeMove(rs.ctx, rs.undos.pop());
            if (score > rs.bestScore) {
                rs.bestScore = score;
                rs.best = m;
            }
            if (score > rs.alpha)
                rs.alpha = score;
        }
    }
    catch (e) {
        if (e instanceof TimeSliceError) {
            // rewind any made-but-not-undone moves so ctx is consistent for next slice
            while (rs.undos.length)
                unmakeMove(rs.ctx, rs.undos.pop());
            return null;
        }
        throw e;
    }
    const move = rs.best !== null ? rs.best : rs.moves[0]; // fallback: first (best-ordered) legal move
    return { move, score: rs.bestScore, depth: rs.depth, nodes: 0 };
}
// One-shot convenience (used only for trivial cases); App uses the sliced pair.
function findBestMove(s, depth, budgetMs) {
    const rs = prepareRoot(s, depth);
    const deadline = performance.now() + budgetMs;
    while (true) {
        const res = stepRoot(rs, performance.now() + 20, deadline);
        if (res)
            return res;
    }
}
};

// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// SOUND — tiny Web Audio synths for move / capture / check / game-over.
// No assets; everything is oscillator + gain envelopes. Mute toggles.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.sound = void 0;
class Sound {
    constructor() {
        this.ctx = null;
        this.muted = false;
    }
    ensure() {
        try {
            if (!this.ctx) {
                const AC = window.AudioContext || window.webkitAudioContext;
                if (!AC)
                    return null;
                this.ctx = new AC();
            }
            if (this.ctx.state === 'suspended')
                void this.ctx.resume();
            return this.ctx;
        }
        catch {
            return null;
        }
    }
    blip(freq, dur, gainV, type, when = 0, glideTo) {
        const ctx = this.ensure();
        if (!ctx || this.muted)
            return;
        const t0 = ctx.currentTime + when;
        const osc = ctx.createOscillator();
        const g = ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, t0);
        if (glideTo)
            osc.frequency.exponentialRampToValueAtTime(Math.max(1, glideTo), t0 + dur);
        g.gain.setValueAtTime(0.0001, t0);
        g.gain.exponentialRampToValueAtTime(gainV, t0 + 0.012);
        g.gain.exponentialRampToValueAtTime(0.0001, t0 + dur);
        osc.connect(g).connect(ctx.destination);
        osc.start(t0);
        osc.stop(t0 + dur + 0.02);
    }
    play(name) {
        if (this.muted)
            return;
        switch (name) {
            case 'move':
                this.blip(210, 0.09, 0.22, 'triangle', 0, 160);
                break;
            case 'capture':
                this.blip(140, 0.12, 0.3, 'square', 0, 90);
                this.blip(90, 0.16, 0.24, 'triangle', 0.02, 60);
                break;
            case 'check':
                this.blip(660, 0.1, 0.18, 'sine');
                this.blip(880, 0.16, 0.18, 'sine', 0.09);
                break;
            case 'mate':
                this.blip(392, 0.16, 0.2, 'triangle');
                this.blip(311, 0.16, 0.2, 'triangle', 0.15);
                this.blip(233, 0.4, 0.22, 'triangle', 0.3);
                break;
            case 'start':
                this.blip(523, 0.12, 0.16, 'sine', 0, 660);
                break;
            case 'undo':
                this.blip(300, 0.1, 0.16, 'sine', 0, 220);
                break;
        }
    }
    toggle() {
        this.muted = !this.muted;
        return this.muted;
    }
}
exports.sound = new Sound();
};

// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// PERSISTENCE — serializes the full game (FEN, per-move FENs for exact undo,
// move list, position counts, settings) to localStorage; restores on reload.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.defaultSettings = defaultSettings;
exports.save = save;
exports.load = load;
exports.clear = clear;
const state_1 = require("./state");
const fen_1 = require("./fen");
const KEY = 'woodSlateChess.v1';
function defaultSettings() {
    return { aiColor: 'b', difficulty: 2, muted: false, flipped: false };
}
function save(fen, turn, history, positionCounts, settings, lastMove, gameOver) {
    const data = { fen, turn, history, positionCounts, settings, lastMove, gameOver };
    try {
        localStorage.setItem(KEY, JSON.stringify(data));
    }
    catch { /* storage blocked */ }
}
function load() {
    try {
        const raw = localStorage.getItem(KEY);
        if (!raw)
            return null;
        const data = JSON.parse(raw);
        const state = (0, fen_1.fenToFullState)(data.fen);
        const entries = (data.history || []).map(h => {
            const prev = (0, fen_1.fenToState)(h.prevFen);
            return {
                move: { from: h.from, to: h.to, piece: h.piece, captured: h.captured, flags: h.flags },
                prevBoard: prev.board,
                prevCastling: prev.castling,
                prevEp: prev.ep,
                prevHalf: prev.halfmove,
                prevFull: prev.fullmove,
                prevPositionKey: (0, state_1.positionKey)(prev),
                positionKey: h.posKey || '',
                san: h.san,
            };
        });
        state.positionCounts = new Map(data.positionCounts || []);
        const settings = { ...defaultSettings(), ...(data.settings || {}) };
        return { state, settings, lastMove: data.lastMove || null, entries };
    }
    catch {
        return null;
    }
}
function clear() {
    try {
        localStorage.removeItem(KEY);
    }
    catch { /* ignore */ }
}
};

// ── module: src/ui.ts ──
__mods["src/ui.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// UI — board rendering, click-to-move AND drag-and-drop input, promotion
// picker, highlights, move list, captured trays, controls. Pure DOM, no
// canvas; the engine never touches this file.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.HINT_SCALE = exports.GLYPHS = void 0;
exports.buildBoard = buildBoard;
exports.sqForCell = sqForCell;
exports.cellForSq = cellForSq;
exports.paintBoard = paintBoard;
exports.showPromoPicker = showPromoPicker;
exports.hidePromoPicker = hidePromoPicker;
exports.capturedTrays = capturedTrays;
exports.trayHTML = trayHTML;
exports.renderMoveList = renderMoveList;
exports.setTurnLabel = setTurnLabel;
exports.showBanner = showBanner;
exports.hideBanner = hideBanner;
const constants_1 = require("./constants");
exports.GLYPHS = {
    K: '\u265A', Q: '\u265B', R: '\u265C', B: '\u265D', N: '\u265E', P: '\u265F',
};
function buildBoard() {
    const wrapper = document.createElement('div');
    wrapper.className = 'board-wrap';
    const board = document.createElement('div');
    board.className = 'board';
    const cells = [];
    for (let i = 0; i < 64; i++) {
        const cell = document.createElement('div');
        cell.className = 'sq';
        cell.dataset.idx = String(i);
        const glyph = document.createElement('span');
        glyph.className = 'glyph';
        const hint = document.createElement('span');
        hint.className = 'hint';
        cell.appendChild(glyph);
        cell.appendChild(hint);
        board.appendChild(cell);
        cells.push(cell);
    }
    wrapper.appendChild(board);
    // promotion picker overlay (hidden by default)
    const promo = document.createElement('div');
    promo.className = 'promo-picker';
    promo.id = 'promo-picker';
    wrapper.appendChild(promo);
    return { boardEl: board, cells, wrapper };
}
// Display cell (row 0 = top) -> board square, honoring flip.
// Unflipped: top row = rank 8, left column = file a.
// Flipped:   top row = rank 1, left column = file h.
function sqForCell(dr, dc, flipped) {
    const rank = flipped ? dr : 7 - dr;
    const file = flipped ? 7 - dc : dc;
    return rank * 8 + file;
}
function cellForSq(sq, flipped) {
    const rank = sq >> 3, file = sq % 8;
    const dr = flipped ? rank : 7 - rank;
    const dc = flipped ? 7 - file : file;
    return { dr, dc };
}
// --- main board paint ------------------------------------------------------
function paintBoard(view, state, flipped, hl) {
    const { cells } = view;
    for (let dr = 0; dr < 8; dr++) {
        for (let dc = 0; dc < 8; dc++) {
            const sq = sqForCell(dr, dc, flipped);
            const cell = cells[dr * 8 + dc];
            const p = state.board[sq];
            const glyph = cell.querySelector('.glyph');
            const hint = cell.querySelector('.hint');
            // classes
            const light = ((sq >> 3) + (sq % 8)) % 2 === 1;
            cell.className = 'sq ' + (light ? 'light' : 'dark')
                + (hl.selected === sq ? ' sel' : '')
                + (hl.lastMove && (hl.lastMove.from === sq || hl.lastMove.to === sq) ? ' last' : '')
                + (hl.checkSquare === sq ? ' check' : '')
                + (hl.hoverSquare === sq ? ' hover' : '');
            // coord labels on outer edges
            cell.textContent = '';
            if (dc === 0) {
                const l = document.createElement('span');
                l.className = 'coord rank';
                l.textContent = String((sq >> 3) + 1);
                cell.appendChild(l);
            }
            if (dr === 7) {
                const l = document.createElement('span');
                l.className = 'coord file';
                l.textContent = 'abcdefgh'[sq % 8];
                cell.appendChild(l);
            }
            // piece
            if (p) {
                glyph.className = 'glyph ' + (p[0] === 'w' ? 'wp' : 'bp');
                glyph.textContent = exports.GLYPHS[p[1]];
            }
            else {
                glyph.className = 'glyph';
                glyph.textContent = '';
            }
            // hints
            const tgt = hl.targets.find(t => t.to === sq);
            if (tgt) {
                hint.className = 'hint ' + (tgt.captured || tgt.flags === 'ep' ? 'ring' : 'dot');
            }
            else {
                hint.className = 'hint';
            }
            cell.appendChild(glyph);
            cell.appendChild(hint);
        }
    }
}
// --- promotion picker ------------------------------------------------------
function showPromoPicker(view, state, toSq, flipped, onPick, onCancel) {
    const picker = view.wrapper.querySelector('.promo-picker');
    picker.innerHTML = '';
    const { dr, dc } = cellForSq(toSq, flipped);
    const cell = view.cells[dr * 8 + dc].getBoundingClientRect();
    const wrap = view.wrapper.getBoundingClientRect();
    const size = cell.width;
    const color = state.turn;
    picker.style.display = 'grid';
    picker.style.gridTemplateColumns = 'repeat(2, auto)';
    picker.style.left = (cell.left - wrap.left + size * 0.5 - size * 0.7) + 'px';
    picker.style.top = (cell.top - wrap.top + size * 0.5 - size * 0.7) + 'px';
    picker.style.width = size * 1.4 + 'px';
    picker.classList.add(color === 'w' ? 'pw' : 'pb');
    for (const pc of ['Q', 'R', 'B', 'N']) {
        const b = document.createElement('button');
        b.className = 'promo-btn';
        b.textContent = exports.GLYPHS[pc];
        b.addEventListener('pointerdown', e => { e.stopPropagation(); });
        b.addEventListener('click', e => {
            e.stopPropagation();
            picker.style.display = 'none';
            onPick(pc);
        });
        picker.appendChild(b);
    }
    picker.addEventListener('click', e => {
        if (e.target === picker) {
            picker.style.display = 'none';
            onCancel();
        }
    });
}
function hidePromoPicker(view) {
    const picker = view.wrapper.querySelector('.promo-picker');
    picker.style.display = 'none';
    picker.innerHTML = '';
}
// --- captured trays --------------------------------------------------------
const START_COUNTS = { P: 8, N: 2, B: 2, R: 2, Q: 1, K: 1 };
function capturedTrays(state) {
    const onBoard = { wP: 0, wN: 0, wB: 0, wR: 0, wQ: 0, bP: 0, bN: 0, bB: 0, bR: 0, bQ: 0 };
    for (let i = 0; i < 64; i++) {
        const p = state.board[i];
        if (p && p[1] !== 'K')
            onBoard[p] = (onBoard[p] || 0) + 1;
    }
    const byWhite = [];
    const byBlack = [];
    let whitePoints = 0, blackPoints = 0;
    const order = ['P', 'N', 'B', 'R', 'Q'];
    const PTS = { P: 1, N: 3, B: 3, R: 5, Q: 9 };
    for (const c of ['w', 'b']) {
        for (const t of order) {
            const missing = START_COUNTS[t] - (onBoard[c + t] || 0);
            for (let i = 0; i < missing; i++) {
                if (c === 'w')
                    byBlack.push({ piece: 'w' + t, type: t });
                else
                    byWhite.push({ piece: 'b' + t, type: t });
            }
        }
    }
    for (const p of byWhite)
        blackPoints += PTS[p.type]; // wait: byWhite = black pieces captured by white -> white gains
    for (const p of byBlack)
        whitePoints += PTS[p.type];
    // advantage shown to the side AHEAD
    return { byWhite, byBlack, advantage: whitePoints - blackPoints };
}
function trayHTML(list) {
    return list.map(p => '<span class="cap-glyph ' + (p.piece[0] === 'w' ? 'wp' : 'bp') + '">' + exports.GLYPHS[p.type] + '</span>').join('');
}
// --- move list -------------------------------------------------------------
function renderMoveList(el, sans) {
    el.innerHTML = '';
    for (let i = 0; i < sans.length; i += 2) {
        const row = document.createElement('div');
        row.className = 'move-row';
        const num = document.createElement('span');
        num.className = 'move-num';
        num.textContent = String(i / 2 + 1) + '.';
        const w = document.createElement('span');
        w.className = 'move-san';
        w.textContent = sans[i];
        row.appendChild(num);
        row.appendChild(w);
        if (sans[i + 1] !== undefined) {
            const b = document.createElement('span');
            b.className = 'move-san';
            b.textContent = sans[i + 1];
            row.appendChild(b);
        }
        el.appendChild(row);
    }
    el.scrollTop = el.scrollHeight;
}
// --- status / banner -------------------------------------------------------
function setTurnLabel(el, turn, checking, thinking, label) {
    el.innerHTML = '';
    const dot = document.createElement('span');
    dot.className = 'turn-dot ' + (turn === 'w' ? 'w' : 'b');
    el.appendChild(dot);
    const txt = document.createElement('span');
    txt.textContent = label + (checking ? '  — check!' : '') + (thinking ? '  · thinking…' : '');
    el.appendChild(txt);
}
function showBanner(el, text, kind) {
    el.textContent = text;
    el.className = 'banner show ' + kind;
}
function hideBanner(el) {
    el.className = 'banner';
    el.textContent = '';
}
exports.HINT_SCALE = constants_1.CONFIG.GLYPH_SCALE;
};

// ── module: src/app.ts ──
__mods["src/app.ts"] = function (exports, require, module) {
"use strict";
// ============================================================================
// APP — orchestration: input (click + drag), AI driver (time-sliced), sound,
// persistence, and the headless debug API on window.game.
// ============================================================================
Object.defineProperty(exports, "__esModule", { value: true });
exports.App = void 0;
const constants_1 = require("./constants");
const state_1 = require("./state");
const rules_1 = require("./rules");
const movegen_1 = require("./movegen");
const san_1 = require("./san");
const draws_1 = require("./draws");
const ai_1 = require("./ai");
const sound_1 = require("./sound");
const persistence_1 = require("./persistence");
const ui_1 = require("./ui");
class App {
    constructor(settings) {
        this.lastMove = null;
        this.selected = null;
        this.pendingPromo = null;
        this.thinking = false;
        this.gameOverMsg = null;
        this.aiPending = false;
        this.history = [];
        this.els = {};
        // --- AI driver (time-sliced root search with a hard budget) ---------------
        this.aiRS = null;
        this.aiBudget = 0;
        this.game = (0, state_1.initialState)();
        this.settings = settings || (0, persistence_1.defaultSettings)();
    }
    // --- lifecycle -----------------------------------------------------------
    static restore() {
        const data = (0, persistence_1.load)();
        if (!data)
            return null;
        const app = new App(data.settings);
        app.game = data.state;
        app.history = data.entries;
        app.game.history = app.history;
        app.lastMove = data.lastMove;
        app.gameOverMsg = null;
        return app;
    }
    newGame() {
        this.game = (0, state_1.initialState)();
        this.history = [];
        this.lastMove = null;
        this.selected = null;
        this.pendingPromo = null;
        this.thinking = false;
        this.aiPending = false;
        this.aiRS = null;
        this.gameOverMsg = null;
        sound_1.sound.play('start');
        this.persist();
        this.paint();
        if (this.settings.aiColor !== 'none' && this.game.turn === this.settings.aiColor) {
            this.scheduleAI();
        }
    }
    humanColor() {
        return this.settings.aiColor === 'none' ? null : (0, state_1.otherColor)(this.settings.aiColor);
    }
    isGameOver() { return this.gameOverMsg !== null; }
    // --- applying moves ------------------------------------------------------
    applyMove(m, byAI) {
        const entry = {
            move: m,
            prevBoard: this.game.board.slice(),
            prevCastling: { ...this.game.castling },
            prevEp: this.game.ep,
            prevHalf: this.game.halfmove,
            prevFull: this.game.fullmove,
            prevPositionKey: (0, state_1.positionKey)(this.game),
            positionKey: '',
            san: (0, san_1.moveToSan)(this.game, m),
        };
        const ns = (0, state_1.makeMoveOnState)(this.game, m);
        const counts = new Map(this.game.positionCounts);
        const key = (0, state_1.positionKey)(ns);
        counts.set(key, (counts.get(key) || 0) + 1);
        this.history.push(entry);
        this.game = {
            ...ns,
            history: this.history,
            positionCounts: counts,
        };
        entry.positionKey = key;
        this.lastMove = { from: m.from, to: m.to };
        this.selected = null;
        this.pendingPromo = null;
        // status first, so we know whether the game just ended
        const st = (0, draws_1.fullStatus)(this.game);
        this.recomputeStatus();
        // sound
        const opp = this.game.turn;
        const checking = !this.isGameOver() && (0, rules_1.inCheck)(this.game.board, opp);
        if (st.result === 'checkmate')
            sound_1.sound.play('mate');
        else if (st.result !== 'none')
            sound_1.sound.play('start');
        else if (checking)
            sound_1.sound.play('check');
        else if (m.captured || m.flags === 'ep')
            sound_1.sound.play('capture');
        else
            sound_1.sound.play('move');
        this.persist();
        this.paint();
        if (!this.isGameOver() && this.settings.aiColor !== 'none' && this.game.turn === this.settings.aiColor) {
            this.scheduleAI();
        }
        void byAI;
    }
    // --- human input ---------------------------------------------------------
    tryHumanMove(from, to) {
        if (this.isGameOver() || this.thinking)
            return false;
        if (this.pendingPromo)
            return false;
        const human = this.humanColor();
        if (human !== null && this.game.turn !== human)
            return false;
        const moves = (0, movegen_1.legalMovesFrom)(this.game, from);
        const candidates = moves.filter(mm => mm.to === to);
        if (candidates.length === 0)
            return false;
        const promo = candidates.find(c => c.flags.startsWith('promo:'));
        if (promo) {
            // show picker over the promotion square
            this.selected = from;
            this.pendingPromo = { from, to };
            this.paint();
            (0, ui_1.showPromoPicker)(this.view, this.game, to, this.settings.flipped, (pc) => {
                const mv = (0, movegen_1.legalMovesFrom)(this.game, from).find(x => x.to === to && x.flags === 'promo:' + pc);
                if (mv)
                    this.applyMove(mv, false);
                else {
                    this.pendingPromo = null;
                    this.paint();
                }
            }, () => { this.pendingPromo = null; this.selected = null; this.paint(); });
            return true;
        }
        this.applyMove(candidates[0], false);
        return true;
    }
    scheduleAI() {
        if (this.aiPending)
            return;
        const human = this.humanColor();
        if (human === null)
            return; // analysis mode
        if (this.game.turn !== this.settings.aiColor || this.isGameOver())
            return;
        this.aiPending = true;
        this.thinking = true;
        const depth = Math.max(1, Math.min(3, this.settings.difficulty));
        const budgetMs = constants_1.CONFIG.AI_BUDGET_MS[depth - 1] ?? constants_1.CONFIG.AI_BUDGET_MS[2];
        this.aiBudget = performance.now() + budgetMs;
        this.aiRS = (0, ai_1.prepareRoot)(this.game, depth);
        this.paint();
        setTimeout(() => this.aiStep(), 30);
    }
    aiStep() {
        if (!this.aiPending || !this.aiRS)
            return;
        const slice = performance.now() + constants_1.CONFIG.AI_TIME_SLICE_MS;
        const res = (0, ai_1.stepRoot)(this.aiRS, slice, this.aiBudget);
        if (res === null) {
            setTimeout(() => this.aiStep(), 8);
            return;
        }
        this.aiPending = false;
        this.thinking = false;
        this.aiRS = null;
        const m = res.move;
        if (m)
            this.applyMove(m, true);
        else
            this.paint();
    }
    // --- undo ----------------------------------------------------------------
    doUndo() {
        if (this.history.length === 0)
            return;
        this.aiPending = false;
        this.thinking = false;
        const human = this.humanColor();
        // Undo takes back everything down to and including the human's last move.
        // In analysis mode (human === null) it takes back a single move.
        let count = 0;
        if (human === null) {
            count = 1;
        }
        else {
            let idx = -1;
            for (let i = this.history.length - 1; i >= 0; i--) {
                if (this.history[i].move.piece[0] === human) {
                    idx = i;
                    break;
                }
            }
            count = idx === -1 ? 1 : this.history.length - idx;
        }
        count = Math.min(count, this.history.length);
        for (let i = 0; i < count; i++) {
            this.popEntry(this.history[this.history.length - 1]);
        }
        this.recomputeStatus();
        const top = this.history[this.history.length - 1];
        this.lastMove = top ? { from: top.move.from, to: top.move.to } : null;
        sound_1.sound.play('undo');
        this.persist();
        this.paint();
    }
    popEntry(e) {
        this.history.pop();
        const counts = new Map(this.game.positionCounts);
        const c = counts.get(e.positionKey) || 1;
        counts.set(e.positionKey, Math.max(0, c - 1));
        this.game = {
            board: e.prevBoard,
            turn: e.move.piece[0],
            castling: e.prevCastling,
            ep: e.prevEp,
            halfmove: e.prevHalf,
            fullmove: e.prevFull,
            history: this.history,
            positionCounts: counts,
        };
    }
    // --- settings ------------------------------------------------------------
    setAiColor(c) {
        this.settings.aiColor = c;
        this.persist();
        this.paint();
        if (!this.isGameOver() && c !== 'none' && this.game.turn === c)
            this.scheduleAI();
    }
    setDifficulty(d) {
        this.settings.difficulty = d;
        this.persist();
    }
    flip() {
        this.settings.flipped = !this.settings.flipped;
        this.persist();
        this.paint();
    }
    toggleMute() {
        const m = sound_1.sound.toggle();
        this.settings.muted = m;
        this.persist();
        return m;
    }
    // --- persistence ---------------------------------------------------------
    persist() {
        const hist = this.history.map(e => ({
            san: e.san, from: e.move.from, to: e.move.to, flags: e.move.flags,
            piece: e.move.piece, captured: e.move.captured,
            prevFen: (0, state_1.stateToFen)({
                board: e.prevBoard, turn: e.move.piece[0],
                castling: e.prevCastling, ep: e.prevEp,
                halfmove: e.prevHalf, fullmove: e.prevFull,
            }),
            posKey: e.positionKey,
        }));
        (0, persistence_1.save)((0, state_1.stateToFen)(this.game), this.game.turn, hist, Array.from(this.game.positionCounts.entries()), this.settings, this.lastMove, this.gameOverMsg);
    }
    // --- painting ------------------------------------------------------------
    paint() {
        if (!this.view)
            return;
        const hl = {
            selected: this.selected,
            targets: this.selected !== null ? (0, movegen_1.legalMovesFrom)(this.game, this.selected) : [],
            lastMove: this.lastMove,
            checkSquare: (0, rules_1.inCheck)(this.game.board, this.game.turn) ? (0, rules_1.findKing)(this.game.board, this.game.turn) : null,
            hoverSquare: null,
        };
        (0, ui_1.paintBoard)(this.view, this.game, this.settings.flipped, hl);
        if (this.pendingPromo) {
            // keep picker visible
        }
        else {
            (0, ui_1.hidePromoPicker)(this.view);
        }
        const turn = this.game.turn;
        const oppColor = (0, state_1.otherColor)(turn);
        let label = this.isGameOver()
            ? 'Game over'
            : this.thinking
                ? `Computer (${turn === 'w' ? 'White' : 'Black'}) thinking…`
                : (this.humanColor() === null ? `${turn === 'w' ? 'White' : 'Black'} to move`
                    : (turn === this.humanColor() ? `Your move (${turn === 'w' ? 'White' : 'Black'})` : `Computer's move`));
        const checking = !this.isGameOver() && (0, rules_1.inCheck)(this.game.board, turn);
        (0, ui_1.setTurnLabel)(this.els['turn-label'], turn, checking, this.thinking, label);
        // captured trays: "White captured" = black pieces white took; advantage
        // shown next to the leading side
        const trays = (0, ui_1.capturedTrays)(this.game);
        this.els['tray-white'].innerHTML =
            (0, ui_1.trayHTML)(trays.byWhite) + (trays.advantage > 0 ? `<span class="adv">+${trays.advantage}</span>` : '');
        this.els['tray-black'].innerHTML =
            (0, ui_1.trayHTML)(trays.byBlack) + (trays.advantage < 0 ? `<span class="adv">+${-trays.advantage}</span>` : '');
        (0, ui_1.renderMoveList)(this.els['move-list'], this.history.map(h => h.san));
        if (this.isGameOver())
            (0, ui_1.showBanner)(this.els['banner'], this.gameOverMsg || '', (this.gameOverMsg || '').includes('Draw') ? 'draw' : 'win');
        else
            (0, ui_1.hideBanner)(this.els['banner']);
        const undoBtn = this.els['btn-undo'];
        undoBtn.disabled = this.history.length === 0;
        const muteBtn = this.els['btn-mute'];
        muteBtn._label = this.settings.muted ? '\u2669 mute' : '\u266A sound';
        muteBtn.textContent = muteBtn._label;
    }
    // Recompute game-over state (used after loading a position directly)
    recomputeStatus() {
        const st = (0, draws_1.fullStatus)(this.game);
        if (st.result === 'none') {
            this.gameOverMsg = null;
            return;
        }
        if (st.result === 'checkmate') {
            this.gameOverMsg = `${st.winner === 'w' ? 'White' : 'Black'} wins by checkmate`;
        }
        else {
            const names = {
                stalemate: 'Draw — stalemate',
                insufficient: 'Draw — insufficient material',
                fifty: 'Draw — 50-move rule',
                threefold: 'Draw — threefold repetition',
            };
            this.gameOverMsg = names[st.result] || 'Game over';
        }
    }
    // --- input wiring --------------------------------------------------------
    attach() {
        const root = document.getElementById('app');
        this.els = {
            'board': root.querySelector('#board'),
            'turn-label': root.querySelector('#turn-label'),
            'tray-white': root.querySelector('#tray-white'),
            'tray-black': root.querySelector('#tray-black'),
            'move-list': root.querySelector('#move-list'),
            'banner': root.querySelector('#banner'),
            'btn-flip': root.querySelector('#btn-flip'),
            'btn-undo': root.querySelector('#btn-undo'),
            'btn-new': root.querySelector('#btn-new'),
            'btn-mute': root.querySelector('#btn-mute'),
            'sel-color': root.querySelector('#sel-color'),
            'sel-diff': root.querySelector('#sel-diff'),
        };
        this.view = (0, ui_1.buildBoard)();
        this.els['board'].appendChild(this.view.wrapper);
        const boardEl = this.view.boardEl;
        boardEl.addEventListener('contextmenu', e => e.preventDefault());
        // --- pointer input: click AND drag -------------------------------------
        let drag = null;
        const displayCellAt = (x, y) => {
            const el = document.elementFromPoint(x, y);
            const cell = el ? el.closest('.sq') : null;
            if (!cell || cell.dataset.idx === undefined)
                return null;
            return parseInt(cell.dataset.idx, 10);
        };
        const squareAtDisplay = (dispIdx) => {
            const dr = Math.floor(dispIdx / 8), dc = dispIdx % 8;
            return (0, ui_1.sqForCell)(dr, dc, this.settings.flipped);
        };
        boardEl.addEventListener('pointerdown', e => {
            if (e.button !== 0)
                return;
            const idx = displayCellAt(e.clientX, e.clientY);
            if (idx === null)
                return;
            const sq = squareAtDisplay(idx);
            e.preventDefault();
            if (this.isGameOver())
                return;
            // promotion picker open: board clicks cancel it
            if (this.pendingPromo) {
                (0, ui_1.hidePromoPicker)(this.view);
                this.pendingPromo = null;
                this.selected = null;
                this.paint();
                return;
            }
            if (this.thinking)
                return;
            drag = { from: sq, x: e.clientX, y: e.clientY, active: false, ghost: null };
            try {
                boardEl.setPointerCapture?.(e.pointerId);
            }
            catch { /* synthetic/inactive pointer */ }
        });
        boardEl.addEventListener('pointermove', e => {
            // hover highlight (no button pressed)
            if (!drag && (e.buttons === 0)) {
                const idx = displayCellAt(e.clientX, e.clientY);
                const sq = idx === null ? null : squareAtDisplay(idx);
                // repaint with hover
                const prevHover = this._hoverSq;
                this._hoverSq = sq;
                if (prevHover !== sq)
                    this.paintHover();
                return;
            }
            if (!drag || e.buttons === 0)
                return;
            const dx = e.clientX - drag.x, dy = e.clientY - drag.y;
            if (!drag.active && Math.hypot(dx, dy) > 8) {
                // only own pieces are draggable
                const p = this.game.board[drag.from];
                const canDrag = p && p[0] === this.game.turn && (this.humanColor() === null || this.game.turn === this.humanColor());
                if (!canDrag) {
                    drag = null;
                    return;
                }
                drag.active = true;
                const glyph = this.game.board[drag.from][1];
                const color = this.game.board[drag.from][0];
                const ghost = document.createElement('div');
                ghost.className = 'drag-ghost glyph ' + (color === 'w' ? 'wp' : 'bp');
                ghost.textContent = ui_1.GLYPHS[glyph];
                this.view.wrapper.appendChild(ghost);
                drag.ghost = ghost;
                this.selected = drag.from;
            }
            if (drag.active && drag.ghost) {
                const wrap = this.view.wrapper.getBoundingClientRect();
                drag.ghost.style.left = (e.clientX - wrap.left - 20) + 'px';
                drag.ghost.style.top = (e.clientY - wrap.top - 24) + 'px';
            }
        });
        const finishDrag = (e) => {
            if (!drag)
                return;
            const d = drag;
            drag = null;
            if (d.ghost)
                d.ghost.remove();
            const idx = displayCellAt(e.clientX, e.clientY);
            const toSq = idx === null ? null : squareAtDisplay(idx);
            if (d.active) {
                if (toSq !== null && toSq !== d.from) {
                    this.tryHumanMove(d.from, toSq);
                }
                else {
                    // dropped on itself: keep selection
                    this.selected = d.from;
                    this.paint();
                }
            }
            else {
                // simple click: select, or move if already selected
                const p = this.game.board[d.from];
                const own = p && (this.humanColor() === null ? true : p[0] === this.game.turn);
                if (this.selected !== null && this.selected !== d.from && own === false) {
                    // click on empty/opponent: attempt move from selection
                    this.tryHumanMove(this.selected, d.from);
                }
                else if (this.selected === d.from) {
                    this.selected = null;
                    this.paint();
                }
                else if (own) {
                    this.selected = d.from;
                    this.paint();
                }
                else {
                    // click elsewhere while nothing selected: try to move from selection
                    if (this.selected !== null)
                        this.tryHumanMove(this.selected, d.from);
                    this.selected = null;
                    this.paint();
                }
            }
            if (!d.active)
                this.paint();
        };
        boardEl.addEventListener('pointerup', finishDrag);
        boardEl.addEventListener('pointercancel', () => { if (drag && drag.ghost)
            drag.ghost.remove(); drag = null; this.paint(); });
        // --- buttons -----------------------------------------------------------
        this.els['btn-flip'].addEventListener('click', () => this.flip());
        this.els['btn-undo'].addEventListener('click', () => this.doUndo());
        this.els['btn-new'].addEventListener('click', () => this.newGame());
        this.els['btn-mute'].addEventListener('click', () => {
            const m = this.toggleMute();
            this.paint();
        });
        this.els['sel-color'].addEventListener('change', e => {
            const v = e.target.value;
            this.setAiColor(v);
        });
        this.els['sel-diff'].addEventListener('change', e => {
            this.setDifficulty(parseInt(e.target.value, 10));
        });
        // reflect persisted settings in controls
        const sel = this.els['sel-color'];
        sel.value = this.settings.aiColor;
        this.els['sel-diff'].value = String(this.settings.difficulty);
        // resume the AI if the restored/setup position is its turn
        if (!this.isGameOver() && this.settings.aiColor !== 'none' && this.game.turn === this.settings.aiColor) {
            this.scheduleAI();
        }
    }
    paintHover() {
        if (!this.view)
            return;
        const hl = {
            selected: this.selected,
            targets: this.selected !== null ? (0, movegen_1.legalMovesFrom)(this.game, this.selected) : [],
            lastMove: this.lastMove,
            checkSquare: (0, rules_1.inCheck)(this.game.board, this.game.turn) ? (0, rules_1.findKing)(this.game.board, this.game.turn) : null,
            hoverSquare: this._hoverSq,
        };
        (0, ui_1.paintBoard)(this.view, this.game, this.settings.flipped, hl);
    }
    // --- headless debug API --------------------------------------------------
    debugApi() {
        const app = this;
        const toCode = (m) => {
            let code = (0, state_1.sqToAlgebraic)(m.from) + (0, state_1.sqToAlgebraic)(m.to);
            if (m.flags.startsWith('promo:'))
                code += m.flags.slice(6).toLowerCase();
            if (m.flags === 'ep')
                code += 'e';
            return code;
        };
        return {
            // current state
            state: () => app.game,
            fen: () => (0, state_1.stateToFen)(app.game),
            turn: () => app.game.turn,
            // all legal moves as codes (e2e4, e7e8q, d5e6e)
            legalMoves: () => (0, movegen_1.legalMoves)(app.game).map(toCode),
            // pseudo-legal moves (pre-legality-filter) for debugging
            pseudoMoves: () => {
                const p = [];
                (0, movegen_1.pseudoMoves)(app.game, p);
                return p.map(toCode);
            },
            legalFrom: (sqCode) => {
                const sq = (0, state_1.algebraicToSq)(String(sqCode));
                if (sq === null)
                    return [];
                return (0, movegen_1.legalMovesFrom)(app.game, sq).map(toCode);
            },
            attacked: (sqCode, color) => {
                const sq = (0, state_1.algebraicToSq)(String(sqCode));
                if (sq === null)
                    return false;
                return (0, rules_1.isSquareAttacked)(app.game.board, sq, color === 'w' ? 'w' : 'b');
            },
            // play a move by code; returns SAN or null if illegal/unavailable
            move: (code) => {
                if (app.thinking || app.isGameOver())
                    return null;
                const c = String(code).toLowerCase().replace('e.p.', '');
                if (c.length < 4)
                    return null;
                const from = (0, state_1.algebraicToSq)(c.slice(0, 2));
                const to = (0, state_1.algebraicToSq)(c.slice(2, 4));
                if (from === null || to === null)
                    return null;
                const fifth = c.length >= 5 ? c.slice(4, 5) : null;
                const promo = fifth === 'q' || fifth === 'r' || fifth === 'b' || fifth === 'n' ? fifth : null;
                const legal = (0, movegen_1.legalMoves)(app.game);
                const mv = legal.find(m => m.from === from && m.to === to &&
                    (promo ? m.flags === 'promo:' + promo.toUpperCase() : !m.flags.startsWith('promo:')));
                if (!mv)
                    return null;
                const san = (0, san_1.moveToSan)(app.game, mv);
                app.applyMove(mv, false);
                return san;
            },
            // helpers
            newGame: () => app.newGame(),
            undo: () => app.doUndo(),
            setup: (fen) => {
                const { fenToFullState } = requireFen();
                app.game = fenToFullState(fen);
                app.history = [];
                app.lastMove = null;
                app.selected = null;
                app.pendingPromo = null;
                app.thinking = false;
                app.aiPending = false;
                app.recomputeStatus();
                app.persist();
                app.paint();
                if (!app.isGameOver() && app.settings.aiColor !== 'none' && app.game.turn === app.settings.aiColor) {
                    app.scheduleAI();
                }
            },
            recompute: () => { app.recomputeStatus(); app.paint(); return (0, draws_1.fullStatus)(app.game).result; },
            status: () => (0, draws_1.fullStatus)(app.game).result,
            // static evaluation from the mover's perspective (centipawns)
            evaluate: () => (0, ai_1.evaluate)(app.game),
        };
    }
}
exports.App = App;
function requireFen() {
    return { fenToFullState: fen_1.fenToFullState };
}
const fen_1 = require("./fen");
};

// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"Qwen3.8","provider":"brigid","persona":"typescript","sessionId":"3edca28c-6426-499b-be70-ff24c20d9909","tokensIn":15031570,"tokensOut":348504,"tokensTotal":15380074,"cost":0,"turns":162,"toolCalls":243,"failedToolCalls":25,"timestamp":"2026-09-09T01:26:17.709Z"} -->