← Chess results

Chess

Qwen 3.6 27b · 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 61,560 bytes · SHA-256 8cc8c276fe10
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — Play Against AI</title>
<style>
/* ── css: layout ── */
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

html, body {
  width: 100%;
  height: 100%;
  overflow: hidden;
  background: #312e2b;
  font-family: 'Segoe UI', system-ui, -apple-system, sans-serif;
  color: #e0d6c8;
}

#game-container {
  display: flex;
  justify-content: center;
  align-items: flex-start;
  gap: 24px;
  padding: 20px;
  height: 100%;
  overflow-y: auto;
}

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

#board {
  display: grid;
  grid-template-columns: repeat(8, 1fr);
  grid-template-rows: repeat(8, 1fr);
  width: min(560px, calc(100vw - 300px));
  height: min(560px, calc(100vw - 300px));
  min-width: 280px;
  min-height: 280px;
  border: 3px solid #5c5652;
  border-radius: 4px;
  box-shadow: 0 8px 32px rgba(0,0,0,0.5);
  user-select: none;
  -webkit-user-select: none;
}

.square {
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: min(56px, calc((100vw - 300px) / 14));
  cursor: pointer;
  position: relative;
  transition: background-color 0.1s;
}

.square.light {
  background-color: #f0d9b5;
}

.square.dark {
  background-color: #b58863;
}

.square.selected {
  background-color: rgba(20, 85, 30, 0.5) !important;
}

.square.last-move {
  background-color: rgba(155, 199, 0, 0.41) !important;
}

.square.in-check {
  background: radial-gradient(ellipse at center, rgba(255,0,0,0.8) 0%, rgba(255,0,0,0) 70%);
}

.square .legal-dot {
  position: absolute;
  width: 28%;
  height: 28%;
  background: rgba(0, 0, 0, 0.15);
  border-radius: 50%;
  pointer-events: none;
}

.square .legal-ring {
  position: absolute;
  width: 90%;
  height: 90%;
  border: 4px solid rgba(0, 0, 0, 0.15);
  border-radius: 50%;
  pointer-events: none;
}

.piece {
  z-index: 1;
  line-height: 1;
  text-shadow: 0 1px 3px rgba(0,0,0,0.3);
  transition: transform 0.05s;
}

.piece:hover {
  transform: scale(1.08);
}

/* Coordinate labels */
.coord-label {
  position: absolute;
  font-size: 10px;
  font-weight: bold;
  pointer-events: none;
  opacity: 0.7;
}
.coord-file {
  bottom: 2px;
  right: 4px;
}
.coord-rank {
  top: 2px;
  left: 4px;
}
.square.light .coord-label { color: #b58863; }
.square.dark .coord-label { color: #f0d9b5; }
/* ── css: side-panel ── */
#side-panel {
  width: 220px;
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex-shrink: 0;
  max-height: min(560px, calc(100vw - 300px));
}

#game-info {
  background: #44403c;
  border-radius: 8px;
  padding: 12px;
  text-align: center;
}

#turn-indicator {
  font-size: 16px;
  font-weight: 600;
  padding: 6px 0;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
}

#turn-indicator .turn-dot {
  width: 14px;
  height: 14px;
  border-radius: 50%;
  border: 2px solid #888;
}

#turn-indicator .turn-dot.white {
  background: #fff;
}

#turn-indicator .turn-dot.black {
  background: #333;
}

#game-status {
  font-size: 13px;
  color: #e0a45d;
  min-height: 20px;
  font-weight: 500;
}

#captured-trays {
  background: #44403c;
  border-radius: 8px;
  padding: 8px;
  display: flex;
  flex-direction: column;
  gap: 4px;
}

.captured-tray {
  min-height: 28px;
  font-size: 20px;
  line-height: 1.4;
  display: flex;
  flex-wrap: wrap;
  gap: 1px;
  padding: 2px;
}

#move-list-container {
  flex: 1;
  background: #44403c;
  border-radius: 8px;
  display: flex;
  flex-direction: column;
  overflow: hidden;
  min-height: 120px;
}

#move-list-header {
  font-size: 13px;
  font-weight: 600;
  padding: 8px 12px;
  border-bottom: 1px solid #555;
  flex-shrink: 0;
}

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

.move-entry {
  display: flex;
  gap: 8px;
}

.move-number {
  color: #888;
  min-width: 28px;
  text-align: right;
}

.move-white, .move-black {
  min-width: 55px;
  cursor: default;
}

.move-white:hover, .move-black:hover {
  text-decoration: underline;
}

#controls {
  background: #44403c;
  border-radius: 8px;
  padding: 12px;
  display: flex;
  flex-direction: column;
  gap: 8px;
}

#difficulty-selector, #side-selector {
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 13px;
}

#difficulty-selector select, #side-selector select {
  background: #333;
  color: #e0d6c8;
  border: 1px solid #666;
  border-radius: 4px;
  padding: 3px 6px;
  font-size: 12px;
  cursor: pointer;
}

#controls button {
  background: #5c5652;
  color: #e0d6c8;
  border: 1px solid #777;
  border-radius: 6px;
  padding: 8px 12px;
  font-size: 13px;
  cursor: pointer;
  transition: background 0.15s;
  font-weight: 500;
}

#controls button:hover {
  background: #7a726e;
}

#controls button:active {
  background: #4a4540;
}

/* Promotion picker overlay */
#promotion-picker {
  position: absolute;
  background: #333;
  border: 2px solid #888;
  border-radius: 8px;
  padding: 8px;
  z-index: 100;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  box-shadow: 0 8px 24px rgba(0,0,0,0.6);
}

#promotion-picker.hidden {
  display: none;
}

.promo-title {
  font-size: 12px;
  color: #ccc;
  margin-bottom: 2px;
}

.promo-pieces {
  display: flex;
  gap: 4px;
}

.promo-piece {
  font-size: 36px;
  width: 48px;
  height: 48px;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  border-radius: 6px;
  background: #555;
  transition: background 0.1s;
}

.promo-piece:hover {
  background: #777;
}

/* Scrollbar styling */
#move-list::-webkit-scrollbar {
  width: 6px;
}
#move-list::-webkit-scrollbar-track {
  background: #333;
}
#move-list::-webkit-scrollbar-thumb {
  background: #666;
  border-radius: 3px;
}

/* Game over banner */
.game-over-banner {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  background: rgba(0,0,0,0.85);
  color: #f0d9b5;
  padding: 20px 40px;
  border-radius: 12px;
  font-size: 22px;
  font-weight: 700;
  text-align: center;
  z-index: 50;
  pointer-events: none;
  box-shadow: 0 4px 20px rgba(0,0,0,0.5);
  border: 2px solid #e0a45d;
}

/* AI thinking indicator */
.ai-thinking {
  font-size: 12px;
  color: #e0a45d;
  font-style: italic;
}
</style>
</head>
<body>
<div id="game-container">
  <div id="board-area">
    <div id="board"></div>
    <div id="promotion-picker" class="hidden">
      <div class="promo-title">Promote to:</div>
      <div class="promo-pieces" id="promo-pieces"></div>
    </div>
  </div>
  <div id="side-panel">
    <div id="game-info">
      <div id="turn-indicator">White to move</div>
      <div id="game-status"></div>
    </div>
    <div id="captured-trays">
      <div id="captured-white" class="captured-tray"></div>
      <div id="captured-black" class="captured-tray"></div>
    </div>
    <div id="move-list-container">
      <div id="move-list-header">Moves</div>
      <div id="move-list"></div>
    </div>
    <div id="controls">
      <div id="difficulty-selector">
        <label>Difficulty:</label>
        <select id="difficulty-select">
          <option value="1">Easy</option>
          <option value="2" selected="">Medium</option>
          <option value="3">Hard</option>
        </select>
      </div>
      <div id="side-selector">
        <label>Play as:</label>
        <select id="side-select">
          <option value="w" selected="">White</option>
          <option value="b">Black</option>
          <option value="both">Both (Analysis)</option>
        </select>
      </div>
      <button id="btn-new-game">New Game</button>
      <button id="btn-undo">Undo</button>
      <button id="btn-flip">Flip Board</button>
      <button id="btn-mute">🔊 Sound</button>
    </div>
  </div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/consts.ts":{"./types":"src/types.ts"},"src/board.ts":{"./types":"src/types.ts","./consts":"src/consts.ts"},"src/ai.ts":{"./types":"src/types.ts","./board":"src/board.ts"},"src/sound.ts":{"./consts":"src/consts.ts"},"src/persistence.ts":{"./types":"src/types.ts","./consts":"src/consts.ts"},"src/render.ts":{"./types":"src/types.ts","./consts":"src/consts.ts"},"src/game.ts":{"./types":"src/types.ts","./board":"src/board.ts","./ai":"src/ai.ts","./render":"src/render.ts","./sound":"src/sound.ts","./persistence":"src/persistence.ts"}};
function __require(id) {
  if (__cache[id]) return __cache[id].exports;
  var module = __cache[id] = { exports: {} };
  var factory = __mods[id];
  if (!factory) throw new Error("Module not found: " + id);
  factory(module.exports, function (spec) {
    var target = (__map[id] && __map[id][spec]) || spec;
    return __require(target);
  }, module);
  return module.exports;
}

// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// Entry point. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
function start() {
    // Replace this with the real startup sequence.
}
start();
};

// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Core Chess Types ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
};

// ── module: src/consts.ts ──
__mods["src/consts.ts"] = function (exports, require, module) {
"use strict";
// ── Tunable Constants ─────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.PIECE_UNICODE = exports.AI_TIME_SLICE = exports.STORAGE_KEY = exports.SOUND_DURATION = exports.SOUND_CHECK = exports.SOUND_CAPTURE = exports.SOUND_MOVE = exports.COLOR_BLACK = exports.COLOR_WHITE = exports.HIGHLIGHT_SELECT = exports.HIGHLIGHT_CHECK = exports.HIGHLIGHT_LAST = exports.HIGHLIGHT_MOVE = exports.SQUARE_DARK = exports.SQUARE_LIGHT = exports.PST = exports.PIECE_VALUES = exports.AI_DIFFICULTIES = exports.BOARD_SIZE = void 0;
exports.BOARD_SIZE = 8;
// AI depths
exports.AI_DIFFICULTIES = {
    'Easy': 1,
    'Medium': 2,
    'Hard': 3,
};
// Eval weights for material
exports.PIECE_VALUES = {
    P: 100,
    N: 320,
    B: 330,
    R: 500,
    Q: 900,
    K: 20000,
};
// Piece-square tables (from White's perspective; mirrored for Black)
// Index: rank*8+file, rank 0 = White's back rank
exports.PST = {
    P: [
        0, 0, 0, 0, 0, 0, 0, 0,
        50, 50, 50, 50, 50, 50, 50, 50,
        10, 10, 20, 30, 30, 20, 10, 10,
        5, 5, 10, 25, 25, 10, 5, 5,
        0, 0, 0, 20, 20, 0, 0, 0,
        5, -5, -10, 0, 0, -10, -5, 5,
        5, 10, 10, -20, -20, 10, 10, 5,
        0, 0, 0, 0, 0, 0, 0, 0,
    ],
    N: [
        -50, -40, -30, -30, -30, -30, -40, -50,
        -40, -20, 0, 0, 0, 0, -20, -40,
        -30, 0, 10, 15, 15, 10, 0, -30,
        -30, 5, 15, 20, 20, 15, 5, -30,
        -30, 0, 15, 20, 20, 15, 0, -30,
        -30, 5, 10, 15, 15, 10, 5, -30,
        -40, -20, 0, 5, 5, 0, -20, -40,
        -50, -40, -30, -30, -30, -30, -40, -50,
    ],
    B: [
        -20, -10, -10, -10, -10, -10, -10, -20,
        -10, 0, 0, 0, 0, 0, 0, -10,
        -10, 0, 10, 10, 10, 10, 0, -10,
        -10, 5, 5, 10, 10, 5, 5, -10,
        -10, 0, 10, 10, 10, 10, 0, -10,
        -10, 10, 10, 10, 10, 10, 10, -10,
        -10, 5, 0, 0, 0, 0, 5, -10,
        -20, -10, -10, -10, -10, -10, -10, -20,
    ],
    R: [
        0, 0, 0, 0, 0, 0, 0, 0,
        5, 10, 10, 10, 10, 10, 10, 5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        -5, 0, 0, 0, 0, 0, 0, -5,
        0, 0, 0, 5, 5, 0, 0, 0,
    ],
    Q: [
        -20, -10, -10, -5, -5, -10, -10, -20,
        -10, 0, 0, 0, 0, 0, 0, -10,
        -10, 0, 5, 5, 5, 5, 0, -10,
        -5, 0, 5, 5, 5, 5, 0, -5,
        0, 0, 5, 5, 5, 5, 0, -5,
        -10, 5, 5, 5, 5, 5, 0, -10,
        -10, 0, 5, 0, 0, 0, 0, -10,
        -20, -10, -10, -5, -5, -10, -10, -20,
    ],
    K: [
        -30, -40, -40, -50, -50, -40, -40, -30,
        -30, -40, -40, -50, -50, -40, -40, -30,
        -30, -40, -40, -50, -50, -40, -40, -30,
        -30, -40, -40, -50, -50, -40, -40, -30,
        -20, -30, -30, -40, -40, -30, -30, -20,
        -10, -20, -20, -20, -20, -20, -20, -10,
        20, 20, 0, 0, 0, 0, 20, 20,
        20, 30, 10, 0, 0, 10, 30, 20,
    ],
};
// Board colors (warm wood palette)
exports.SQUARE_LIGHT = '#f0d9b5';
exports.SQUARE_DARK = '#b58863';
exports.HIGHLIGHT_MOVE = 'rgba(255,255,0,0.4)';
exports.HIGHLIGHT_LAST = 'rgba(155,199,0,0.41)';
exports.HIGHLIGHT_CHECK = 'radial-gradient(ellipse at center, rgba(255,0,0,0.8) 0%, rgba(255,0,0,0) 70%)';
exports.HIGHLIGHT_SELECT = 'rgba(20,85,30,0.5)';
// UI colors
exports.COLOR_WHITE = '#fff';
exports.COLOR_BLACK = '#000';
// Sound frequencies (Hz)
exports.SOUND_MOVE = 440;
exports.SOUND_CAPTURE = 220;
exports.SOUND_CHECK = 660;
exports.SOUND_DURATION = 0.08;
// LocalStorage key
exports.STORAGE_KEY = 'chess-game-state';
// AI time slice for chunked search (ms)
exports.AI_TIME_SLICE = 50;
// Unicode chess pieces
exports.PIECE_UNICODE = {
    'wK': '♔', 'wQ': '♕', 'wR': '♖', 'wB': '♗', 'wN': '♘', 'wP': '♙',
    'bK': '♚', 'bQ': '♛', 'bR': '♜', 'bB': '♝', 'bN': '♞', 'bP': '♟',
};
};

// ── module: src/board.ts ──
__mods["src/board.ts"] = function (exports, require, module) {
"use strict";
// ── Board Engine: Move Generation, Validation, Game State ─────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialBoard = initialBoard;
exports.initialCastling = initialCastling;
exports.initialGameState = initialGameState;
exports.isSquareAttacked = isSquareAttacked;
exports.isInCheck = isInCheck;
exports.legalMoves = legalMoves;
exports.makeMove = makeMove;
exports.finalizeNotation = finalizeNotation;
exports.toFEN = toFEN;
exports.evaluate = evaluate;
exports.parseMoveString = parseMoveString;
const consts_1 = require("./consts");
// ── Helpers ──────────────────────────────────────────────────────
function inBounds(r, f) {
    return r >= 0 && r < consts_1.BOARD_SIZE && f >= 0 && f < consts_1.BOARD_SIZE;
}
function cloneBoard(b) {
    return b.map(row => row.map(cell => cell ? { ...cell } : null));
}
function cloneCastling(c) {
    return { ...c };
}
function cloneEnPassant(ep) {
    return ep ? { ...ep } : null;
}
function sameSquare(a, b) {
    return a.rank === b.rank && a.file === b.file;
}
function opponent(color) {
    return color === 'w' ? 'b' : 'w';
}
// ── Initial Position ─────────────────────────────────────────────
function initialBoard() {
    const b = Array.from({ length: 8 }, () => Array(8).fill(null));
    const backRow = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
    for (let f = 0; f < 8; f++) {
        b[0][f] = { color: 'b', type: backRow[f] };
        b[1][f] = { color: 'b', type: 'P' };
        b[6][f] = { color: 'w', type: 'P' };
        b[7][f] = { color: 'w', type: backRow[f] };
    }
    return b;
}
function initialCastling() {
    return { wK: true, wQ: true, bK: true, bQ: true };
}
function initialGameState() {
    return {
        board: initialBoard(),
        turn: 'w',
        castling: initialCastling(),
        enPassant: null,
        halfMoveClock: 0,
        fullMoveNumber: 1,
        moveList: [],
        capturedPieces: { w: [], b: [] },
        gameOver: false,
        result: null,
        status: 'playing',
    };
}
// ── Attack Detection ─────────────────────────────────────────────
// Is square (r, f) attacked by color `attacker` on board `b`?
function isSquareAttacked(b, r, f, attacker) {
    // Pawn attacks
    const pawnDir = attacker === 'w' ? -1 : 1; // White pawns move up (decreasing rank)
    // Actually in our board: rank 0 is top (Black side), rank 7 is bottom (White side)
    // White pawns move from rank 7 toward rank 0, so direction is -1
    // Black pawns move from rank 1 toward rank 7, so direction is +1
    const pr = r - pawnDir;
    if (inBounds(pr, f - 1)) {
        const p = b[pr][f - 1];
        if (p && p.color === attacker && p.type === 'P')
            return true;
    }
    if (inBounds(pr, f + 1)) {
        const p = b[pr][f + 1];
        if (p && p.color === attacker && p.type === 'P')
            return true;
    }
    // Knight attacks
    const knightMoves = [
        [-2, -1], [-2, 1], [-1, -2], [-1, 2],
        [1, -2], [1, 2], [2, -1], [2, 1],
    ];
    for (const [dr, df] of knightMoves) {
        const nr = r + dr, nf = f + df;
        if (inBounds(nr, nf)) {
            const p = b[nr][nf];
            if (p && p.color === attacker && p.type === 'N')
                return true;
        }
    }
    // King attacks
    for (let dr = -1; dr <= 1; dr++) {
        for (let df = -1; df <= 1; df++) {
            if (dr === 0 && df === 0)
                continue;
            const nr = r + dr, nf = f + df;
            if (inBounds(nr, nf)) {
                const p = b[nr][nf];
                if (p && p.color === attacker && p.type === 'K')
                    return true;
            }
        }
    }
    // Sliding pieces (rook/queen for straight, bishop/queen for diagonal)
    const straightDirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
    const diagDirs = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
    for (const [dr, df] of straightDirs) {
        let nr = r + dr, nf = f + df;
        while (inBounds(nr, nf)) {
            const p = b[nr][nf];
            if (p) {
                if (p.color === attacker && (p.type === 'R' || p.type === 'Q'))
                    return true;
                break;
            }
            nr += dr;
            nf += df;
        }
    }
    for (const [dr, df] of diagDirs) {
        let nr = r + dr, nf = f + df;
        while (inBounds(nr, nf)) {
            const p = b[nr][nf];
            if (p) {
                if (p.color === attacker && (p.type === 'B' || p.type === 'Q'))
                    return true;
                break;
            }
            nr += dr;
            nf += df;
        }
    }
    return false;
}
// Is the king of `color` in check?
function isInCheck(b, color) {
    let kr = -1, kf = -1;
    for (let r = 0; r < 8; r++) {
        for (let f = 0; f < 8; f++) {
            const p = b[r][f];
            if (p && p.color === color && p.type === 'K') {
                kr = r;
                kf = f;
                break;
            }
        }
        if (kr >= 0)
            break;
    }
    if (kr < 0)
        return false; // No king found (shouldn't happen)
    return isSquareAttacked(b, kr, kf, opponent(color));
}
// ── Raw Move Generation (before legality filtering) ──────────────
function rawMoves(b, color, castling, enPassant) {
    const moves = [];
    for (let r = 0; r < 8; r++) {
        for (let f = 0; f < 8; f++) {
            const piece = b[r][f];
            if (!piece || piece.color !== color)
                continue;
            switch (piece.type) {
                case 'P':
                    rawPawnMoves(b, moves, r, f, color, enPassant);
                    break;
                case 'N':
                    rawKnightMoves(b, moves, r, f, color);
                    break;
                case 'B':
                    rawSlidingMoves(b, moves, r, f, color, [[-1, -1], [-1, 1], [1, -1], [1, 1]]);
                    break;
                case 'R':
                    rawSlidingMoves(b, moves, r, f, color, [[-1, 0], [1, 0], [0, -1], [0, 1]]);
                    break;
                case 'Q':
                    rawSlidingMoves(b, moves, r, f, color, [[-1, -1], [-1, 1], [1, -1], [1, 1], [-1, 0], [1, 0], [0, -1], [0, 1]]);
                    break;
                case 'K':
                    rawKingMoves(b, moves, r, f, color);
                    rawCastlingMoves(b, moves, r, f, color, castling);
                    break;
            }
        }
    }
    return moves;
}
function rawPawnMoves(b, moves, r, f, color, ep) {
    const dir = color === 'w' ? -1 : 1;
    const startRank = color === 'w' ? 6 : 1;
    const promoRank = color === 'w' ? 0 : 7;
    // Forward one
    const r1 = r + dir;
    if (inBounds(r1, f) && !b[r1][f]) {
        if (r1 === promoRank) {
            for (const pt of ['Q', 'R', 'B', 'N']) {
                moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: f }, piece: b[r][f], promotion: pt });
            }
        }
        else {
            moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: f }, piece: b[r][f] });
        }
        // Forward two from start
        const r2 = r + 2 * dir;
        if (r === startRank && !b[r2][f]) {
            moves.push({ from: { rank: r, file: f }, to: { rank: r2, file: f }, piece: b[r][f] });
        }
    }
    // Captures
    for (const df of [-1, 1]) {
        const nf = f + df;
        if (!inBounds(r1, nf))
            continue;
        const target = b[r1][nf];
        if (target && target.color !== color) {
            if (r1 === promoRank) {
                for (const pt of ['Q', 'R', 'B', 'N']) {
                    moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: nf }, piece: b[r][f], captured: target, promotion: pt });
                }
            }
            else {
                moves.push({ from: { rank: r, file: f }, to: { rank: r1, file: nf }, piece: b[r][f], captured: target });
            }
        }
        // En passant
        if (ep && r1 === ep.rank && nf === ep.file) {
            moves.push({
                from: { rank: r, file: f },
                to: { rank: r1, file: nf },
                piece: b[r][f],
                captured: { color: opponent(color), type: 'P' },
                isEnPassant: true,
            });
        }
    }
}
function rawKnightMoves(b, moves, r, f, color) {
    const offsets = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
    for (const [dr, df] of offsets) {
        const nr = r + dr, nf = f + df;
        if (!inBounds(nr, nf))
            continue;
        const target = b[nr][nf];
        if (target && target.color === color)
            continue;
        moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f], captured: target || undefined });
    }
}
function rawSlidingMoves(b, moves, r, f, color, dirs) {
    for (const [dr, df] of dirs) {
        let nr = r + dr, nf = f + df;
        while (inBounds(nr, nf)) {
            const target = b[nr][nf];
            if (target) {
                if (target.color !== color) {
                    moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f], captured: target });
                }
                break;
            }
            moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f] });
            nr += dr;
            nf += df;
        }
    }
}
function rawKingMoves(b, moves, r, f, color) {
    for (let dr = -1; dr <= 1; dr++) {
        for (let df = -1; df <= 1; df++) {
            if (dr === 0 && df === 0)
                continue;
            const nr = r + dr, nf = f + df;
            if (!inBounds(nr, nf))
                continue;
            const target = b[nr][nf];
            if (target && target.color === color)
                continue;
            moves.push({ from: { rank: r, file: f }, to: { rank: nr, file: nf }, piece: b[r][f], captured: target || undefined });
        }
    }
}
function rawCastlingMoves(b, moves, r, f, color, castling) {
    const opp = opponent(color);
    if (color === 'w') {
        // White kingside: K from e1(7,4) to g1(7,6), R from h1(7,7) to f1(7,5)
        if (castling.wK && r === 7 && f === 4) {
            if (!b[7][5] && !b[7][6] && b[7][7]?.type === 'R' && b[7][7]?.color === 'w') {
                if (!isSquareAttacked(b, 7, 4, opp) && !isSquareAttacked(b, 7, 5, opp) && !isSquareAttacked(b, 7, 6, opp)) {
                    moves.push({ from: { rank: 7, file: 4 }, to: { rank: 7, file: 6 }, piece: b[7][4], isCastling: 'K' });
                }
            }
        }
        // White queenside: K from e1(7,4) to c1(7,2), R from a1(7,0) to d1(7,3)
        if (castling.wQ && r === 7 && f === 4) {
            if (!b[7][3] && !b[7][2] && !b[7][1] && b[7][0]?.type === 'R' && b[7][0]?.color === 'w') {
                if (!isSquareAttacked(b, 7, 4, opp) && !isSquareAttacked(b, 7, 3, opp) && !isSquareAttacked(b, 7, 2, opp)) {
                    moves.push({ from: { rank: 7, file: 4 }, to: { rank: 7, file: 2 }, piece: b[7][4], isCastling: 'Q' });
                }
            }
        }
    }
    else {
        // Black kingside: K from e8(0,4) to g8(0,6), R from h8(0,7) to f8(0,5)
        if (castling.bK && r === 0 && f === 4) {
            if (!b[0][5] && !b[0][6] && b[0][7]?.type === 'R' && b[0][7]?.color === 'b') {
                if (!isSquareAttacked(b, 0, 4, opp) && !isSquareAttacked(b, 0, 5, opp) && !isSquareAttacked(b, 0, 6, opp)) {
                    moves.push({ from: { rank: 0, file: 4 }, to: { rank: 0, file: 6 }, piece: b[0][4], isCastling: 'K' });
                }
            }
        }
        // Black queenside: K from e8(0,4) to c8(0,2), R from a8(0,0) to d8(0,3)
        if (castling.bQ && r === 0 && f === 4) {
            if (!b[0][3] && !b[0][2] && !b[0][1] && b[0][0]?.type === 'R' && b[0][0]?.color === 'b') {
                if (!isSquareAttacked(b, 0, 4, opp) && !isSquareAttacked(b, 0, 3, opp) && !isSquareAttacked(b, 0, 2, opp)) {
                    moves.push({ from: { rank: 0, file: 4 }, to: { rank: 0, file: 2 }, piece: b[0][4], isCastling: 'Q' });
                }
            }
        }
    }
}
// ── Legal Move Generation ────────────────────────────────────────
function legalMoves(b, color, castling, enPassant) {
    const raw = rawMoves(b, color, castling, enPassant);
    return raw.filter(move => {
        const nb = makeMoveOnBoard(b, move);
        return !isInCheck(nb, color);
    });
}
// Apply a move to a board (returns new board, doesn't modify original)
function makeMoveOnBoard(b, move) {
    const nb = cloneBoard(b);
    const { from, to, piece } = move;
    // Move piece
    nb[to.rank][to.file] = move.promotion
        ? { color: piece.color, type: move.promotion }
        : piece;
    nb[from.rank][from.file] = null;
    // En passant capture
    if (move.isEnPassant) {
        const capturedRank = from.rank; // the captured pawn is on the same rank as the moving pawn
        nb[capturedRank][to.file] = null;
    }
    // Castling - move rook
    if (move.isCastling) {
        if (move.isCastling === 'K') {
            // Rook from h-file to f-file
            nb[to.rank][5] = nb[to.rank][7];
            nb[to.rank][7] = null;
        }
        else {
            // Rook from a-file to d-file
            nb[to.rank][3] = nb[to.rank][0];
            nb[to.rank][0] = null;
        }
    }
    return nb;
}
// ── Make Move on GameState ───────────────────────────────────────
function makeMove(state, move) {
    const ns = {
        board: cloneBoard(state.board),
        turn: opponent(state.turn),
        castling: cloneCastling(state.castling),
        enPassant: cloneEnPassant(state.enPassant),
        halfMoveClock: state.halfMoveClock,
        fullMoveNumber: state.fullMoveNumber,
        moveList: [...state.moveList],
        capturedPieces: {
            w: [...state.capturedPieces.w],
            b: [...state.capturedPieces.b],
        },
        gameOver: state.gameOver,
        result: state.result,
        status: state.status,
    };
    const { from, to, piece } = move;
    // Move piece
    ns.board[to.rank][to.file] = move.promotion
        ? { color: piece.color, type: move.promotion }
        : piece;
    ns.board[from.rank][from.file] = null;
    // En passant capture
    if (move.isEnPassant) {
        ns.board[from.rank][to.file] = null;
        ns.capturedPieces[piece.color].push({ color: opponent(piece.color), type: 'P' });
        ns.halfMoveClock = 0;
    }
    // Regular capture
    if (move.captured && !move.isEnPassant) {
        ns.capturedPieces[piece.color].push(move.captured);
        ns.halfMoveClock = 0;
    }
    // Pawn move resets half-move clock
    if (piece.type === 'P' && !move.captured && !move.isEnPassant) {
        ns.halfMoveClock = 0;
    }
    // Castling - move rook
    if (move.isCastling) {
        if (move.isCastling === 'K') {
            ns.board[to.rank][5] = ns.board[to.rank][7];
            ns.board[to.rank][7] = null;
        }
        else {
            ns.board[to.rank][3] = ns.board[to.rank][0];
            ns.board[to.rank][0] = null;
        }
    }
    // Update castling rights
    if (piece.type === 'K') {
        if (piece.color === 'w') {
            ns.castling.wK = false;
            ns.castling.wQ = false;
        }
        else {
            ns.castling.bK = false;
            ns.castling.bQ = false;
        }
    }
    if (piece.type === 'R') {
        if (piece.color === 'w') {
            if (from.rank === 7 && from.file === 0)
                ns.castling.wQ = false;
            if (from.rank === 7 && from.file === 7)
                ns.castling.wK = false;
        }
        else {
            if (from.rank === 0 && from.file === 0)
                ns.castling.bQ = false;
            if (from.rank === 0 && from.file === 7)
                ns.castling.bK = false;
        }
    }
    // If a rook is captured, remove castling rights
    if (to.rank === 0 && to.file === 0)
        ns.castling.bQ = false;
    if (to.rank === 0 && to.file === 7)
        ns.castling.bK = false;
    if (to.rank === 7 && to.file === 0)
        ns.castling.wQ = false;
    if (to.rank === 7 && to.file === 7)
        ns.castling.wK = false;
    // En passant target: set if pawn moved two squares
    ns.enPassant = null;
    if (piece.type === 'P' && Math.abs(to.rank - from.rank) === 2) {
        ns.enPassant = {
            rank: (from.rank + to.rank) / 2,
            file: from.file,
        };
    }
    // Increment full move number after black's move
    if (state.turn === 'b') {
        ns.fullMoveNumber = state.fullMoveNumber + 1;
    }
    // Generate algebraic notation
    move.notation = generateNotation(state, move);
    // Store move in list
    ns.moveList.push(move);
    // Check for game end
    const nextMoves = legalMoves(ns.board, ns.turn, ns.castling, ns.enPassant);
    if (nextMoves.length === 0) {
        ns.gameOver = true;
        if (isInCheck(ns.board, ns.turn)) {
            ns.status = 'checkmate';
            ns.result = state.turn === 'w' ? 'White wins' : 'Black wins';
        }
        else {
            ns.status = 'stalemate';
            ns.result = 'Draw';
        }
    }
    // Insufficient material check
    if (!ns.gameOver && isInsufficientMaterial(ns.board)) {
        ns.gameOver = true;
        ns.status = 'insufficient';
        ns.result = 'Draw';
    }
    // 50-move rule
    if (!ns.gameOver && ns.halfMoveClock >= 100) {
        ns.gameOver = true;
        ns.status = 'fiftyMove';
        ns.result = 'Draw';
    }
    return ns;
}
// ── Algebraic Notation ───────────────────────────────────────────
function fileToLetter(f) {
    return String.fromCharCode(97 + f); // a-h
}
function rankToNumber(r) {
    return String(8 - r); // rank 0 → "8", rank 7 → "1"
}
function generateNotation(state, move) {
    const { from, to, piece } = move;
    // Castling
    if (move.isCastling === 'K')
        return 'O-O';
    if (move.isCastling === 'Q')
        return 'O-O-O';
    let notation = '';
    // Piece letter (not for pawns)
    if (piece.type !== 'P') {
        notation += piece.type;
    }
    // Disambiguation for non-pawn pieces
    if (piece.type !== 'P' && piece.type !== 'K') {
        const allLegal = legalMoves(state.board, state.turn, state.castling, state.enPassant);
        const ambiguous = allLegal.filter(m => m.piece.type === piece.type &&
            m.to.rank === to.rank && m.to.file === to.file &&
            !sameSquare(m.from, from));
        if (ambiguous.length > 0) {
            let disambig = '';
            const sameFile = ambiguous.some(m => m.from.file === from.file);
            const sameRank = ambiguous.some(m => m.from.rank === from.rank);
            if (!sameFile) {
                disambig = fileToLetter(from.file);
            }
            else if (!sameRank) {
                disambig = rankToNumber(from.rank);
            }
            else {
                disambig = fileToLetter(from.file) + rankToNumber(from.rank);
            }
            notation += disambig;
        }
    }
    // Pawn capture file
    if (piece.type === 'P' && move.captured) {
        notation += fileToLetter(from.file);
    }
    // Capture symbol
    if (move.captured) {
        notation += 'x';
    }
    // Destination square
    notation += fileToLetter(to.file) + rankToNumber(to.rank);
    // En passant
    if (move.isEnPassant) {
        notation += ' e.p.';
    }
    // Promotion
    if (move.promotion) {
        notation += '=' + move.promotion;
    }
    // Check / checkmate (append after the move is made)
    // We'll add this in a post-process step
    return notation;
}
// Add check/mate symbols to the notation of the last move
function finalizeNotation(state) {
    if (state.moveList.length === 0)
        return;
    const lastMove = state.moveList[state.moveList.length - 1];
    if (!lastMove.notation)
        return;
    const inCheck = isInCheck(state.board, state.turn);
    if (state.status === 'checkmate') {
        lastMove.notation = lastMove.notation.replace(/[+#]$/, '') + '#';
    }
    else if (inCheck) {
        lastMove.notation = lastMove.notation.replace(/[+#]$/, '') + '+';
    }
}
// ── Insufficient Material ────────────────────────────────────────
function isInsufficientMaterial(b) {
    const pieces = [[], []];
    for (let r = 0; r < 8; r++) {
        for (let f = 0; f < 8; f++) {
            const p = b[r][f];
            if (p)
                pieces[p.color === 'w' ? 0 : 1].push(p);
        }
    }
    // K vs K
    if (pieces[0].length === 1 && pieces[1].length === 1)
        return true;
    // K+B vs K or K+N vs K
    if (pieces[0].length === 1 && pieces[1].length === 2) {
        const minor = pieces[1].find(p => p.type === 'B' || p.type === 'N');
        if (minor)
            return true;
    }
    if (pieces[1].length === 1 && pieces[0].length === 2) {
        const minor = pieces[0].find(p => p.type === 'B' || p.type === 'N');
        if (minor)
            return true;
    }
    return false;
}
// ── FEN ──────────────────────────────────────────────────────────
function toFEN(state) {
    let fen = '';
    for (let r = 0; r < 8; r++) {
        let empty = 0;
        for (let f = 0; f < 8; f++) {
            const p = state.board[r][f];
            if (p) {
                if (empty > 0) {
                    fen += empty;
                    empty = 0;
                }
                fen += (p.color === 'w' ? p.type : p.type.toLowerCase());
            }
            else {
                empty++;
            }
        }
        if (empty > 0)
            fen += empty;
        if (r < 7)
            fen += '/';
    }
    fen += ' ' + state.turn;
    // Castling
    let castling = '';
    if (state.castling.wK)
        castling += 'K';
    if (state.castling.wQ)
        castling += 'Q';
    if (state.castling.bK)
        castling += 'k';
    if (state.castling.bQ)
        castling += 'q';
    fen += ' ' + (castling || '-');
    // En passant
    if (state.enPassant) {
        fen += ' ' + fileToLetter(state.enPassant.file) + rankToNumber(state.enPassant.rank);
    }
    else {
        fen += ' -';
    }
    fen += ' ' + state.halfMoveClock;
    fen += ' ' + state.fullMoveNumber;
    return fen;
}
// ── Evaluation (for AI) ──────────────────────────────────────────
function evaluate(b, color) {
    let score = 0;
    for (let r = 0; r < 8; r++) {
        for (let f = 0; f < 8; f++) {
            const p = b[r][f];
            if (!p)
                continue;
            const isWhite = p.color === 'w';
            const material = consts_1.PIECE_VALUES[p.type];
            // PST index: for White, rank 7 = index 0; for Black, mirror
            let pstIndex;
            if (isWhite) {
                pstIndex = r * 8 + f;
            }
            else {
                pstIndex = (7 - r) * 8 + f;
            }
            const pstValue = consts_1.PST[p.type] ? consts_1.PST[p.type][pstIndex] : 0;
            if (isWhite) {
                score += material + pstValue;
            }
            else {
                score -= material + pstValue;
            }
        }
    }
    // Return from the perspective of `color`
    return color === 'w' ? score : -score;
}
// ── Debug helpers ────────────────────────────────────────────────
// Parse a move string like "e2e4" or "e2e4q" and return a Move (or null)
function parseMoveString(state, str) {
    if (str.length < 4)
        return null;
    const fromFile = str.charCodeAt(0) - 97;
    const fromRank = 8 - parseInt(str[1]);
    const toFile = str.charCodeAt(2) - 97;
    const toRank = 8 - parseInt(str[3]);
    const promoChar = str[4];
    const piece = state.board[fromRank]?.[fromFile];
    if (!piece)
        return null;
    const legal = legalMoves(state.board, state.turn, state.castling, state.enPassant);
    return legal.find(m => m.from.rank === fromRank && m.from.file === fromFile &&
        m.to.rank === toRank && m.to.file === toFile &&
        (!promoChar || m.promotion === promoChar.toUpperCase())) || null;
}
};

// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
// ── AI: Minimax with Alpha-Beta Pruning ──────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.findBestMove = findBestMove;
exports.findBestMoveChunked = findBestMoveChunked;
const board_1 = require("./board");
const INF = 1e9;
// Move ordering: captures first (by MVV-LVA), then checks
function orderMoves(moves, board) {
    const score = (m) => {
        let s = 0;
        // Captures: MVV-LVA (most valuable victim, least valuable attacker)
        if (m.captured) {
            const victimValues = { P: 1, N: 3, B: 3, R: 5, Q: 9, K: 0 };
            const attackerValues = { P: 0, N: 1, B: 2, R: 3, Q: 4, K: 5 };
            s = 10 * (victimValues[m.captured.type] || 0) - (attackerValues[m.piece.type] || 0);
        }
        // Promotion
        if (m.promotion) {
            s += 80; // High priority for promotions
        }
        // Checks
        if (m.isCastling)
            s += 5;
        return s;
    };
    moves.sort((a, b) => score(b) - score(a));
}
// Quiescence search to avoid horizon effect
function quiescence(board, color, castling, enPassant, alpha, beta) {
    const standPat = (0, board_1.evaluate)(board, color);
    if (standPat >= beta)
        return beta;
    if (alpha < standPat)
        alpha = standPat;
    const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
    orderMoves(moves, board);
    // Only consider captures and promotions for quiescence
    const tacticalMoves = moves.filter(m => m.captured || m.promotion || m.isCastling);
    for (const move of tacticalMoves) {
        const ns = (0, board_1.makeMove)({
            board, turn: color, castling, enPassant,
            halfMoveClock: 0, fullMoveNumber: 1,
            moveList: [], capturedPieces: { w: [], b: [] },
            gameOver: false, result: null, status: 'playing',
        }, move);
        const score = -quiescence(ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha);
        if (score >= beta)
            return beta;
        if (score > alpha)
            alpha = score;
    }
    return alpha;
}
function minimax(depth, board, color, castling, enPassant, alpha, beta, maximizing) {
    if (depth === 0) {
        return quiescence(board, color, castling, enPassant, alpha, beta);
    }
    const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
    if (moves.length === 0) {
        if ((0, board_1.isInCheck)(board, color)) {
            return maximizing ? -INF + (100 - depth) : INF - (100 - depth);
        }
        return 0; // stalemate
    }
    orderMoves(moves, board);
    if (maximizing) {
        let maxEval = -INF;
        for (const move of moves) {
            const ns = (0, board_1.makeMove)({
                board, turn: color, castling, enPassant,
                halfMoveClock: 0, fullMoveNumber: 1,
                moveList: [], capturedPieces: { w: [], b: [] },
                gameOver: false, result: null, status: 'playing',
            }, move);
            const eval_ = -minimax(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, false);
            if (eval_ > maxEval)
                maxEval = eval_;
            if (eval_ > alpha)
                alpha = eval_;
            if (alpha >= beta)
                break;
        }
        return maxEval;
    }
    else {
        let minEval = INF;
        for (const move of moves) {
            const ns = (0, board_1.makeMove)({
                board, turn: color, castling, enPassant,
                halfMoveClock: 0, fullMoveNumber: 1,
                moveList: [], capturedPieces: { w: [], b: [] },
                gameOver: false, result: null, status: 'playing',
            }, move);
            const eval_ = -minimax(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, true);
            if (eval_ < minEval)
                minEval = eval_;
            if (eval_ < beta)
                beta = eval_;
            if (alpha >= beta)
                break;
        }
        return minEval;
    }
}
// Find the best move for the given color
function findBestMove(board, color, castling, enPassant, depth) {
    const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
    if (moves.length === 0)
        return null;
    orderMoves(moves, board);
    let bestMove = moves[0];
    let bestScore = -INF;
    for (const move of moves) {
        const ns = (0, board_1.makeMove)({
            board, turn: color, castling, enPassant,
            halfMoveClock: 0, fullMoveNumber: 1,
            moveList: [], capturedPieces: { w: [], b: [] },
            gameOver: false, result: null, status: 'playing',
        }, move);
        const score = -minimax(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -INF, -bestScore, false);
        if (score > bestScore) {
            bestScore = score;
            bestMove = move;
        }
    }
    return bestMove;
}
// Chunked version: yields control every few nodes to avoid freezing UI
let _nodeCount = 0;
const NODES_PER_SLICE = 2000;
function minimaxChunked(depth, board, color, castling, enPassant, alpha, beta, maximizing, onProgress) {
    _nodeCount++;
    if (_nodeCount % NODES_PER_SLICE === 0 && onProgress()) {
        return maximizing ? -INF : INF; // abort signal
    }
    if (depth === 0) {
        return quiescence(board, color, castling, enPassant, alpha, beta);
    }
    const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
    if (moves.length === 0) {
        if ((0, board_1.isInCheck)(board, color)) {
            return maximizing ? -INF + (100 - depth) : INF - (100 - depth);
        }
        return 0;
    }
    orderMoves(moves, board);
    if (maximizing) {
        let maxEval = -INF;
        for (const move of moves) {
            const ns = (0, board_1.makeMove)({
                board, turn: color, castling, enPassant,
                halfMoveClock: 0, fullMoveNumber: 1,
                moveList: [], capturedPieces: { w: [], b: [] },
                gameOver: false, result: null, status: 'playing',
            }, move);
            const eval_ = -minimaxChunked(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, false, onProgress);
            if (eval_ > maxEval)
                maxEval = eval_;
            if (eval_ > alpha)
                alpha = eval_;
            if (alpha >= beta)
                break;
        }
        return maxEval;
    }
    else {
        let minEval = INF;
        for (const move of moves) {
            const ns = (0, board_1.makeMove)({
                board, turn: color, castling, enPassant,
                halfMoveClock: 0, fullMoveNumber: 1,
                moveList: [], capturedPieces: { w: [], b: [] },
                gameOver: false, result: null, status: 'playing',
            }, move);
            const eval_ = -minimaxChunked(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -beta, -alpha, true, onProgress);
            if (eval_ < minEval)
                minEval = eval_;
            if (eval_ < beta)
                beta = eval_;
            if (alpha >= beta)
                break;
        }
        return minEval;
    }
}
function findBestMoveChunked(board, color, castling, enPassant, depth) {
    return new Promise((resolve) => {
        _nodeCount = 0;
        const moves = (0, board_1.legalMoves)(board, color, castling, enPassant);
        if (moves.length === 0) {
            resolve(null);
            return;
        }
        orderMoves(moves, board);
        let bestMove = moves[0];
        let bestScore = -INF;
        let moveIndex = 0;
        function processNext() {
            if (moveIndex >= moves.length) {
                resolve(bestMove);
                return;
            }
            const move = moves[moveIndex];
            moveIndex++;
            const ns = (0, board_1.makeMove)({
                board, turn: color, castling, enPassant,
                halfMoveClock: 0, fullMoveNumber: 1,
                moveList: [], capturedPieces: { w: [], b: [] },
                gameOver: false, result: null, status: 'playing',
            }, move);
            const score = -minimaxChunked(depth - 1, ns.board, ns.turn, ns.castling, ns.enPassant, -INF, -bestScore, false, () => {
                // Yield to the event loop
                return false; // never abort
            });
            if (score > bestScore) {
                bestScore = score;
                bestMove = move;
            }
            // Schedule next move evaluation
            setTimeout(processNext, 0);
        }
        processNext();
    });
}
};

// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
// ── Sound Effects via Web Audio API ──────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.setMuted = setMuted;
exports.isMuted = isMuted;
exports.playMove = playMove;
exports.playCapture = playCapture;
exports.playCheck = playCheck;
exports.playGameOver = playGameOver;
const consts_1 = require("./consts");
let audioCtx = null;
let muted = false;
function getAudioCtx() {
    if (!audioCtx) {
        audioCtx = new (window.AudioContext || window.webkitAudioContext)();
    }
    return audioCtx;
}
function setMuted(m) {
    muted = m;
}
function isMuted() {
    return muted;
}
function playTone(freq, duration, type = 'sine') {
    if (muted)
        return;
    try {
        const ctx = getAudioCtx();
        const osc = ctx.createOscillator();
        const gain = ctx.createGain();
        osc.type = type;
        osc.frequency.setValueAtTime(freq, ctx.currentTime);
        gain.gain.setValueAtTime(0.15, ctx.currentTime);
        gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + duration);
        osc.connect(gain);
        gain.connect(ctx.destination);
        osc.start(ctx.currentTime);
        osc.stop(ctx.currentTime + duration);
    }
    catch {
        // Audio not available
    }
}
function playMove() {
    playTone(consts_1.SOUND_MOVE, consts_1.SOUND_DURATION);
}
function playCapture() {
    playTone(consts_1.SOUND_CAPTURE, consts_1.SOUND_DURATION * 1.5, 'triangle');
}
function playCheck() {
    playTone(consts_1.SOUND_CHECK, consts_1.SOUND_DURATION * 2, 'square');
}
function playGameOver() {
    if (muted)
        return;
    try {
        const ctx = getAudioCtx();
        const notes = [523, 659, 784, 1047]; // C E G C
        notes.forEach((freq, i) => {
            const osc = ctx.createOscillator();
            const gain = ctx.createGain();
            osc.type = 'sine';
            osc.frequency.setValueAtTime(freq, ctx.currentTime + i * 0.15);
            gain.gain.setValueAtTime(0.12, ctx.currentTime + i * 0.15);
            gain.gain.exponentialRampToValueAtTime(0.001, ctx.currentTime + i * 0.15 + 0.3);
            osc.connect(gain);
            gain.connect(ctx.destination);
            osc.start(ctx.currentTime + i * 0.15);
            osc.stop(ctx.currentTime + i * 0.15 + 0.3);
        });
    }
    catch {
        // Audio not available
    }
}
};

// ── module: src/persistence.ts ──
__mods["src/persistence.ts"] = function (exports, require, module) {
"use strict";
// ── Persistence: localStorage Save/Restore ───────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.saveState = saveState;
exports.loadState = loadState;
exports.clearState = clearState;
const consts_1 = require("./consts");
function serializePiece(p) {
    if (!p)
        return null;
    return p.color + p.type;
}
function deserializePiece(s) {
    if (!s)
        return null;
    return { color: s[0], type: s[1] };
}
function saveState(state, playerColor, difficulty, flipBoard) {
    try {
        const serialized = {
            board: state.board.map(row => row.map(cell => serializePiece(cell))),
            turn: state.turn,
            castling: state.castling,
            enPassant: state.enPassant,
            halfMoveClock: state.halfMoveClock,
            fullMoveNumber: state.fullMoveNumber,
            moveList: state.moveList.map(m => ({
                from: m.from,
                to: m.to,
                piece: m.piece,
                captured: m.captured,
                isEnPassant: m.isEnPassant,
                isCastling: m.isCastling,
                promotion: m.promotion,
                notation: m.notation,
            })),
            capturedPieces: {
                w: state.capturedPieces.w,
                b: state.capturedPieces.b,
            },
            gameOver: state.gameOver,
            result: state.result,
            status: state.status,
            playerColor,
            difficulty,
            flipBoard,
        };
        localStorage.setItem(consts_1.STORAGE_KEY, JSON.stringify(serialized));
    }
    catch {
        // Storage not available
    }
}
function loadState() {
    try {
        const raw = localStorage.getItem(consts_1.STORAGE_KEY);
        if (!raw)
            return null;
        const data = JSON.parse(raw);
        const state = {
            board: data.board.map(row => row.map(cell => deserializePiece(cell))),
            turn: data.turn,
            castling: data.castling,
            enPassant: data.enPassant,
            halfMoveClock: data.halfMoveClock,
            fullMoveNumber: data.fullMoveNumber,
            moveList: data.moveList,
            capturedPieces: {
                w: data.capturedPieces.w.map(p => p),
                b: data.capturedPieces.b.map(p => p),
            },
            gameOver: data.gameOver,
            result: data.result,
            status: data.status,
        };
        return {
            state,
            playerColor: data.playerColor,
            difficulty: data.difficulty,
            flipBoard: data.flipBoard,
        };
    }
    catch {
        return null;
    }
}
function clearState() {
    try {
        localStorage.removeItem(consts_1.STORAGE_KEY);
    }
    catch {
        // ignore
    }
}
};

// ── module: src/render.ts ──
__mods["src/render.ts"] = function (exports, require, module) {
"use strict";
// ── Rendering: Board, Pieces, Highlights ─────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRender = initRender;
exports.setFlip = setFlip;
exports.renderBoard = renderBoard;
exports.getSquareFromEvent = getSquareFromEvent;
const consts_1 = require("./consts");
let boardEl;
let flipBoard = false;
function initRender() {
    boardEl = document.getElementById('board');
}
function visRank(r) {
    return flipBoard ? 7 - r : r;
}
function visFile(f) {
    return flipBoard ? 7 - f : f;
}
function setFlip(f) {
    flipBoard = f;
}
// Render the full board
function renderBoard(board, selectedSquare, legalDestinations, lastMove, inCheck, playerColor) {
    boardEl.innerHTML = '';
    for (let vr = 0; vr < 8; vr++) {
        for (let vf = 0; vf < 8; vf++) {
            const r = flipBoard ? 7 - vr : vr;
            const f = flipBoard ? 7 - vf : vf;
            const isLight = (r + f) % 2 === 0;
            const sq = document.createElement('div');
            sq.className = 'square ' + (isLight ? 'light' : 'dark');
            sq.dataset.rank = String(r);
            sq.dataset.file = String(f);
            // Coordinate labels
            if (vf === 0) {
                const label = document.createElement('span');
                label.className = 'coord-label coord-rank';
                label.textContent = String(8 - r);
                sq.appendChild(label);
            }
            if (vr === 7) {
                const label = document.createElement('span');
                label.className = 'coord-label coord-file';
                label.textContent = String.fromCharCode(97 + f);
                sq.appendChild(label);
            }
            // Last move highlight
            if (lastMove) {
                if ((lastMove.from.rank === r && lastMove.from.file === f) ||
                    (lastMove.to.rank === r && lastMove.to.file === f)) {
                    sq.classList.add('last-move');
                }
            }
            // Selected square highlight
            if (selectedSquare && selectedSquare.rank === r && selectedSquare.file === f) {
                sq.classList.add('selected');
            }
            // Check highlight on king
            if (inCheck) {
                const piece = board[r][f];
                if (piece && piece.type === 'K' && piece.color === playerColor) {
                    sq.classList.add('in-check');
                }
            }
            // Legal move indicators
            const isLegalDest = legalDestinations.some(d => d.rank === r && d.file === f);
            if (isLegalDest) {
                const indicator = document.createElement('div');
                const targetPiece = board[r][f];
                if (targetPiece) {
                    indicator.className = 'legal-ring';
                }
                else {
                    indicator.className = 'legal-dot';
                }
                sq.appendChild(indicator);
            }
            // Piece
            const piece = board[r][f];
            if (piece) {
                const pieceEl = document.createElement('span');
                pieceEl.className = 'piece';
                pieceEl.textContent = consts_1.PIECE_UNICODE[piece.color + piece.type];
                // Color the pieces
                if (piece.color === 'w') {
                    pieceEl.style.color = '#fff';
                    pieceEl.style.textShadow = '0 0 2px #000, 0 1px 3px rgba(0,0,0,0.4)';
                }
                else {
                    pieceEl.style.color = '#1a1a1a';
                    pieceEl.style.textShadow = '0 0 2px rgba(255,255,255,0.3), 0 1px 3px rgba(0,0,0,0.3)';
                }
                sq.appendChild(pieceEl);
            }
            boardEl.appendChild(sq);
        }
    }
}
// Get the board square under a DOM element
function getSquareFromEvent(e) {
    const target = e.target;
    const sq = target.closest('.square');
    if (!sq)
        return null;
    const rank = parseInt(sq.dataset.rank || '');
    const file = parseInt(sq.dataset.file || '');
    if (isNaN(rank) || isNaN(file))
        return null;
    return { rank, file };
}
};

// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
// ── Game Controller: Input, State Management, AI Integration ─────
Object.defineProperty(exports, "__esModule", { value: true });
exports.initGame = initGame;
const render_1 = require("./render");
const persistence_1 = require("./persistence");
// ── Game State ────────────────────────────────────────────────────
let gameState;
let playerColor = 'w';
let difficulty = 2;
let analysisMode = false;
let selectedSquare = null;
let legalDestinations = [];
let aiThinking = false;
let pendingPromotion = null;
let dragSource = null;
let dragPieceEl = null;
// UI elements
let moveListEl;
let turnIndicatorEl;
let gameStatusEl;
let capturedWhiteEl;
let capturedBlackEl;
let promoPickerEl;
let promoPiecesEl;
function initGame() {
    // Try to restore from localStorage
    const saved = (0, persistence_1.loadState)();
    if (saved) {
        gameState = saved.state;
        playerColor = saved.playerColor;
        difficulty = saved.difficulty;
        (0, render_1.setFlip)(saved.flipBoard);
        analysisMode = false;
        // Update side selector
        const sideSelect = document.getElementById('side-select');
        sideSelect.value = playerColor;
        const diffSelect = document.getElementById('difficulty-select');
        diffSelect.value = String(difficulty);
    }
    else {
        gameState = createInitialState();
    }
    (0, render_1.initRender)();
    moveListEl = document.getElementById('move-list');
    turnIndicatorEl = document.getElementById('turn-indicator');
    gameStatusEl = document.getElementById('game-status');
    capturedWhiteEl = document.getElementById('captured-white');
    capturedBlackEl = document.getElementById('captured-black');
    promoPickerEl = document.getElementById('promotion-picker');
    promoPiecesEl = document.getElementById('promo-pieces');
    // Wire up controls
    document.getElementById('btn-new-game').addEventListener('click', handleNewGame);
    document.getElementById('btn-undo').addEventListener('click', handleUndo);
    document.getElementById('btn-flip').addEventListener('click', handleFlip);
    document.getElementById('btn-mute').addEventListener('click', handleMute);
    document.getElementById('difficulty-select').addEventListener('change', handleDifficultyChange);
    document.getElementById('side-select').addEventListener('change', handleSideChange);
    // Board click handler
    const boardEl = document.getElementById('board');
    boardEl.addEventListener('click', handleBoardClick);
    // Drag and drop
    boardEl.addEventListener('mousedown', handleMouseDown);
    boardEl.addEventListener('mousemove', handleMouseMove);
    boardEl.addEventListener('mouseup', handleMouseUp);
    boardEl.addEventListener('mouseleave', handleMouseUp);
    boardEl.addEventListener('touchstart', handleTouchStart, { passive: false });
    boardEl.addEventListener('touchmove', handleTouchMove, { passive: false });
    boardEl.addEventListener('touchend', handleTouchEnd);
    // Initial render
    updateAll();
    saveGameState();
}
function createInitialState() {
    const { initialGameState } = requireBoard();
    return initialGameState();
}
function requireBoard() {
    // Dynamic import to avoid circular deps
    const { initialGameState } = window;
    return { initialGameState };
}
// Actually, let me import directly
// The above won't work. Let me restructure.
};

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