Chess
ThinkingCap Qwen3.6 27B GGUF · typescript

Browser check: Artifact runtime error. This is a technical check, not a quality review.
Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 63,922 bytes · SHA-256 4113a3bfb7a9
<!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; }
body {
background: #2b2b2b;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
color: #e0e0e0;
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
overflow: hidden;
}
#game-container {
display: flex;
gap: 24px;
align-items: flex-start;
padding: 20px;
}
#board-area {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
}
#board {
display: grid;
grid-template-columns: repeat(8, 72px);
grid-template-rows: repeat(8, 72px);
border: 3px solid #5c3d2e;
border-radius: 4px;
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
user-select: none;
position: relative;
}
.square {
width: 72px;
height: 72px;
display: flex;
justify-content: center;
align-items: center;
font-size: 46px;
cursor: pointer;
position: relative;
transition: background-color 0.1s;
}
.square-light { background-color: #f0d9b5; }
.square-dark { background-color: #b58863; }
.square-selected { background-color: #7fc97f !important; }
.square-last-move { background-color: #cdd26a !important; }
.square-selected.square-last-move { background-color: #b7d46b !important; }
.square-check {
background: radial-gradient(ellipse at center, #ff0000 0%, #ff6666 25%, rgba(255,0,0,0) 89%) !important;
}
.move-dot {
width: 18px;
height: 18px;
border-radius: 50%;
background: rgba(0, 0, 0, 0.25);
position: absolute;
pointer-events: none;
}
.capture-ring {
width: 66px;
height: 66px;
border-radius: 50%;
border: 4px solid rgba(0, 0, 0, 0.25);
position: absolute;
pointer-events: none;
}
.piece {
text-shadow: 0 1px 3px rgba(0,0,0,0.3);
line-height: 1;
pointer-events: none;
z-index: 1;
}
/* Side panel */
#side-panel {
width: 240px;
display: flex;
flex-direction: column;
gap: 12px;
}
#status-bar {
display: flex;
flex-direction: column;
gap: 6px;
}
#turn-indicator {
font-size: 16px;
padding: 8px 12px;
background: #3a3a3a;
border-radius: 6px;
text-align: center;
font-weight: 600;
}
#game-over-banner {
font-size: 14px;
padding: 8px 12px;
background: #c0392b;
border-radius: 6px;
text-align: center;
font-weight: 700;
animation: pulse 1.5s infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
#captured-trays {
display: flex;
flex-direction: column;
gap: 4px;
min-height: 40px;
padding: 6px;
background: #3a3a3a;
border-radius: 6px;
}
#captured-white, #captured-black {
min-height: 22px;
font-size: 20px;
letter-spacing: 2px;
line-height: 22px;
}
#move-list {
flex: 1;
min-height: 200px;
max-height: 400px;
overflow-y: auto;
background: #3a3a3a;
border-radius: 6px;
padding: 10px;
font-size: 13px;
font-family: 'Consolas', 'Courier New', monospace;
line-height: 1.6;
}
.move-entry {
display: flex;
gap: 8px;
}
.move-number { color: #999; min-width: 30px; }
.move-white { color: #e8e8e8; }
.move-black { color: #bbb; }
#controls {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
#controls button {
flex: 1;
min-width: 60px;
padding: 8px 6px;
border: none;
border-radius: 6px;
background: #5c3d2e;
color: #e0e0e0;
cursor: pointer;
font-size: 13px;
font-weight: 600;
transition: background 0.2s;
}
#controls button:hover { background: #7a5240; }
#difficulty {
padding: 8px 6px;
border: none;
border-radius: 6px;
background: #5c3d2e;
color: #e0e0e0;
font-size: 13px;
font-weight: 600;
cursor: pointer;
flex: 1;
min-width: 60px;
}
#color-choice {
text-align: center;
padding: 20px;
background: #3a3a3a;
border-radius: 6px;
}
#color-choice p {
margin-bottom: 12px;
font-size: 16px;
font-weight: 600;
}
#color-buttons {
display: flex;
flex-direction: column;
gap: 8px;
}
#color-buttons button {
padding: 12px;
border: none;
border-radius: 6px;
font-size: 15px;
font-weight: 700;
cursor: pointer;
transition: transform 0.1s;
}
#color-buttons button:hover { transform: scale(1.03); }
#btn-white { background: #f0d9b5; color: #333; }
#btn-black { background: #b58863; color: #fff; }
.hidden { display: none !important; }
/* Promotion overlay */
#promotion-overlay {
position: absolute;
background: rgba(0,0,0,0.7);
border-radius: 8px;
padding: 8px;
z-index: 100;
display: flex;
gap: 4px;
}
.promo-piece {
width: 50px;
height: 50px;
font-size: 36px;
display: flex;
justify-content: center;
align-items: center;
cursor: pointer;
border-radius: 6px;
background: #f0d9b5;
transition: transform 0.1s;
}
.promo-piece:hover { transform: scale(1.15); }
#ai-thinking {
font-size: 13px;
color: #f39c12;
text-align: center;
padding: 4px;
}
/* Scrollbar */
#move-list::-webkit-scrollbar { width: 6px; }
#move-list::-webkit-scrollbar-track { background: #333; border-radius: 3px; }
#move-list::-webkit-scrollbar-thumb { background: #666; border-radius: 3px; }
</style>
</head>
<body>
<div id="game-container">
<div id="board-area">
<div id="board"></div>
<div id="promotion-overlay" class="hidden">
<div id="promotion-choices"></div>
</div>
</div>
<div id="side-panel">
<div id="status-bar">
<span id="turn-indicator"></span>
<span id="game-over-banner" class="hidden"></span>
</div>
<div id="captured-trays">
<div id="captured-white"></div>
<div id="captured-black"></div>
</div>
<div id="move-list"></div>
<div id="controls">
<button id="btn-undo" title="Undo">↩ Undo</button>
<button id="btn-flip" title="Flip Board">⟲ Flip</button>
<button id="btn-new" title="New Game">✦ New</button>
<button id="btn-mute" title="Mute/Unmute">🔊</button>
<select id="difficulty">
<option value="1">Easy</option>
<option value="2">Medium</option>
<option value="3" selected="">Hard</option>
</select>
</div>
<div id="color-choice" class="hidden">
<p>Choose your color:</p>
<div id="color-buttons">
<button id="btn-white">♔ Play as White</button>
<button id="btn-black">♚ Play as Black</button>
</div>
</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./renderer":"src/renderer.ts","./game":"src/game.ts","./engine":"src/engine.ts","./sound":"src/sound.ts"},"src/engine.ts":{"./types":"src/types.ts"},"src/ai.ts":{"./types":"src/types.ts","./engine":"src/engine.ts"},"src/game.ts":{"./types":"src/types.ts","./engine":"src/engine.ts","./ai":"src/ai.ts","./sound":"src/sound.ts"},"src/renderer.ts":{"./engine":"src/engine.ts","./game":"src/game.ts","./types":"src/types.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const renderer_1 = require("./renderer");
const game_1 = require("./game");
const engine_1 = require("./engine");
const sound_1 = require("./sound");
// Expose debug interface
window.game = (0, game_1.debug)();
window.__chessCheck = engine_1.isCheck;
// Wire renderer callbacks
const game_2 = require("./game");
const renderer_2 = require("./renderer");
(0, game_2.setOnRender)(renderer_2.render);
(0, game_2.setOnAIStart)(renderer_2.render);
(0, game_2.setOnAIDone)(renderer_2.render);
(0, game_2.setOnSelectedChange)(renderer_2.render);
(0, game_2.setOnGameOver)((reason) => {
(0, renderer_2.render)();
});
// Wire promotion handler
window.__handlePromotion = (pt) => {
(0, game_1.handlePromotionChoice)(pt);
(0, renderer_2.render)();
};
// Wire select for drag
window.__handleSelect = (sqNum) => {
(0, game_1.handleSquareClick)(sqNum);
};
// Wire drag-to-move
window.__handleMove = (from, to) => {
const gs = (0, game_1.getGameState)();
if ((0, game_1.getGameOverState)() || (0, game_1.isAIThinking)())
return;
if (gs.turn !== window.game._playerColor)
return;
const legal = (0, engine_1.generateLegalMoves)(gs);
const move = legal.find(m => m.from === from && m.to === to);
if (!move)
return;
if (move.promo) {
(0, game_1.handleSquareClick)(from);
return;
}
(0, game_1.handleSquareClick)(from);
(0, game_1.handleSquareClick)(to);
};
// --- UI Controls ---
document.getElementById('btn-white').addEventListener('click', () => {
(0, sound_1.initAudio)();
(0, game_1.startGame)(true);
document.getElementById('color-choice').classList.add('hidden');
});
document.getElementById('btn-black').addEventListener('click', () => {
(0, sound_1.initAudio)();
(0, game_1.startGame)(false);
document.getElementById('color-choice').classList.add('hidden');
});
document.getElementById('btn-undo').addEventListener('click', () => {
(0, game_1.undo)();
(0, renderer_2.render)();
});
document.getElementById('btn-flip').addEventListener('click', () => {
(0, game_1.setFlipped)(!(0, game_1.getFlipped)());
(0, renderer_2.render)();
});
document.getElementById('btn-new').addEventListener('click', () => {
document.getElementById('color-choice').classList.remove('hidden');
(0, game_1.newGame)();
(0, renderer_2.render)();
});
document.getElementById('btn-mute').addEventListener('click', () => {
const muted = (0, sound_1.isMuted)();
(0, sound_1.setMuted)(!muted);
document.getElementById('btn-mute').textContent = !muted ? '🔇' : '🔊';
});
document.getElementById('difficulty').addEventListener('change', (e) => {
(0, game_1.setAIDepth)(parseInt(e.target.value));
});
// Click handler on board squares
document.getElementById('board').addEventListener('click', (e) => {
const sqEl = e.target.closest('.square');
if (!sqEl)
return;
const sqNum = parseInt(sqEl.dataset.square || '');
if (!isNaN(sqNum)) {
(0, game_1.handleSquareClick)(sqNum);
(0, renderer_2.render)();
}
});
// --- Init ---
(0, renderer_1.initRenderer)();
// Attempt reload from localStorage
const STORAGE_KEY = 'chess_game_state';
const raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
try {
const data = JSON.parse(raw);
if (data.fen) {
document.getElementById('color-choice').classList.add('hidden');
}
}
catch {
document.getElementById('color-choice').classList.remove('hidden');
}
}
else {
document.getElementById('color-choice').classList.remove('hidden');
}
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PIECE_SYMBOLS = exports.START_FEN = void 0;
exports.sq = sq;
exports.sqFile = sqFile;
exports.sqRank = sqRank;
exports.squareName = squareName;
exports.parseSquare = parseSquare;
exports.fenToState = fenToState;
exports.stateToFen = stateToFen;
exports.isCheck = isCheck;
exports.generatePseudoLegalMoves = generatePseudoLegalMoves;
exports.makeMove = makeMove;
exports.generateLegalMoves = generateLegalMoves;
exports.isCheckmate = isCheckmate;
exports.isStalemate = isStalemate;
exports.isInsufficientMaterial = isInsufficientMaterial;
exports.is50MoveRule = is50MoveRule;
exports.gameOver = gameOver;
exports.moveToAlgebraic = moveToAlgebraic;
// Constants
const FILE_NAMES = 'abcdefgh';
const RANK_NAMES = '87654321';
exports.START_FEN = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
// Piece unicode glyphs
exports.PIECE_SYMBOLS = {
w: { K: '♔', Q: '♕', R: '♖', B: '♗', N: '♘', P: '♙' },
b: { K: '♚', Q: '♛', R: '♜', B: '♝', N: '♞', P: '♟' },
};
// --- Board helpers ---
function sq(row, col) {
return row * 8 + col;
}
function sqFile(s) { return s & 7; }
function sqRank(s) { return s >> 3; }
function squareName(s) {
return FILE_NAMES[sqFile(s)] + RANK_NAMES[sqRank(s)];
}
function parseSquare(name) {
const f = FILE_NAMES.indexOf(name[0]);
const r = RANK_NAMES.indexOf(name[1]);
if (f < 0 || r < 0)
return null;
return sq(r, f);
}
// --- FEN ---
function fenToState(fen) {
const parts = fen.split(' ');
const rows = parts[0].split('/');
const board = new Array(64).fill(null);
for (let r = 0; r < 8; r++) {
let c = 0;
for (const ch of rows[r]) {
if (ch >= '1' && ch <= '8') {
c += parseInt(ch);
}
else {
const color = (ch === ch.toUpperCase()) ? 'w' : 'b';
board[sq(r, c++)] = { type: ch.toUpperCase(), color };
}
}
}
const turn = parts[1];
const castlingRights = {
K: parts[2].includes('K'),
Q: parts[2].includes('Q'),
k: parts[2].includes('k'),
q: parts[2].includes('q'),
};
let enPassantTarget = null;
if (parts[3] !== '-') {
enPassantTarget = parseSquare(parts[3]);
}
const halfMoveClock = parseInt(parts[4]) || 0;
const fullMoveNumber = parseInt(parts[5]) || 1;
return {
board,
turn,
castlingRights,
enPassantTarget,
halfMoveClock,
fullMoveNumber,
moveHistory: [],
capturedPieces: { w: [], b: [] },
};
}
function stateToFen(gs) {
let fen = '';
for (let r = 0; r < 8; r++) {
let empty = 0;
for (let c = 0; c < 8; c++) {
const p = gs.board[sq(r, c)];
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 += ' ' + gs.turn;
let cr = '';
if (gs.castlingRights.K)
cr += 'K';
if (gs.castlingRights.Q)
cr += 'Q';
if (gs.castlingRights.k)
cr += 'k';
if (gs.castlingRights.q)
cr += 'q';
fen += ' ' + (cr || '-');
if (gs.enPassantTarget !== null) {
fen += ' ' + squareName(gs.enPassantTarget);
}
else {
fen += ' -';
}
fen += ' ' + gs.halfMoveClock + ' ' + gs.fullMoveNumber;
return fen;
}
// --- Move Generation ---
function opponent(color) {
return color === 'w' ? 'b' : 'w';
}
function inBounds(s) {
return s >= 0 && s < 64;
}
function isAttackedBy(board, square, color) {
// Check all piece types for attacks on the given square
const pawnDir = color === 'w' ? 1 : -1;
// Pawn attacks
for (const dc of [-1, 1]) {
const pr = sqRank(square) + pawnDir;
const pc = sqFile(square) + dc;
if (pr >= 0 && pr < 8 && pc >= 0 && pc < 8) {
const p = board[sq(pr, pc)];
if (p && p.type === 'P' && p.color === color)
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, dc] of knightMoves) {
const r = sqRank(square) + dr, c = sqFile(square) + dc;
if (r >= 0 && r < 8 && c >= 0 && c < 8) {
const p = board[sq(r, c)];
if (p && p.type === 'N' && p.color === color)
return true;
}
}
// King attacks
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0)
continue;
const r = sqRank(square) + dr, c = sqFile(square) + dc;
if (r >= 0 && r < 8 && c >= 0 && c < 8) {
const p = board[sq(r, c)];
if (p && p.type === 'K' && p.color === color)
return true;
}
}
}
// Sliding pieces
const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]];
for (let d = 0; d < 8; d++) {
const [dr, dc] = dirs[d];
const isDiag = d >= 4;
let r = sqRank(square) + dr, c = sqFile(square) + dc;
while (r >= 0 && r < 8 && c >= 0 && c < 8) {
const p = board[sq(r, c)];
if (p) {
if (p.color === color) {
if (p.type === 'Q' || (isDiag && p.type === 'B') || (!isDiag && p.type === 'R'))
return true;
}
break;
}
r += dr;
c += dc;
}
}
return false;
}
function isCheck(board, color) {
let kingSq = null;
for (let i = 0; i < 64; i++) {
const p = board[i];
if (p && p.type === 'K' && p.color === color) {
kingSq = i;
break;
}
}
if (kingSq === null)
return false;
return isAttackedBy(board, kingSq, opponent(color));
}
function generatePseudoLegalMoves(gs) {
const moves = [];
const color = gs.turn;
const dir = color === 'w' ? -1 : 1;
const startRow = color === 'w' ? 6 : 1;
const promoRow = color === 'w' ? 0 : 7;
for (let from = 0; from < 64; from++) {
const piece = gs.board[from];
if (!piece || piece.color !== color)
continue;
switch (piece.type) {
case 'P': {
// Forward one
const f1 = from + dir * 8;
if (inBounds(f1) && !gs.board[f1]) {
if (sqRank(f1) === promoRow) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ from, to: f1, promo: pt });
}
}
else {
moves.push({ from, to: f1 });
}
// Forward two
const f2 = from + dir * 16;
if (sqRank(from) === startRow && !gs.board[f2]) {
moves.push({ from, to: f2 });
}
}
// Captures
for (const dc of [-1, 1]) {
const fr = sqRank(from), fc = sqFile(from) + dc;
if (fc < 0 || fc > 7)
continue;
const cap = sq(fr + dir, fc);
if (inBounds(cap)) {
if (gs.board[cap] && gs.board[cap].color !== color) {
if (sqRank(cap) === promoRow) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ from, to: cap, promo: pt });
}
}
else {
moves.push({ from, to: cap });
}
}
// En passant
if (cap === gs.enPassantTarget) {
moves.push({ from, to: cap, enPassant: true });
}
}
}
break;
}
case 'N': {
const knightMoves = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
for (const [dr, dc] of knightMoves) {
const r = sqRank(from) + dr, c = sqFile(from) + dc;
if (r >= 0 && r < 8 && c >= 0 && c < 8) {
const to = sq(r, c);
const target = gs.board[to];
if (!target || target.color !== color)
moves.push({ from, to });
}
}
break;
}
case 'B': {
for (const [dr, dc] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) {
let r = sqRank(from) + dr, c = sqFile(from) + dc;
while (r >= 0 && r < 8 && c >= 0 && c < 8) {
const to = sq(r, c);
const target = gs.board[to];
if (!target) {
moves.push({ from, to });
}
else {
if (target.color !== color)
moves.push({ from, to });
break;
}
r += dr;
c += dc;
}
}
break;
}
case 'R': {
for (const [dr, dc] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
let r = sqRank(from) + dr, c = sqFile(from) + dc;
while (r >= 0 && r < 8 && c >= 0 && c < 8) {
const to = sq(r, c);
const target = gs.board[to];
if (!target) {
moves.push({ from, to });
}
else {
if (target.color !== color)
moves.push({ from, to });
break;
}
r += dr;
c += dc;
}
}
break;
}
case 'Q': {
for (const [dr, dc] of [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]) {
let r = sqRank(from) + dr, c = sqFile(from) + dc;
while (r >= 0 && r < 8 && c >= 0 && c < 8) {
const to = sq(r, c);
const target = gs.board[to];
if (!target) {
moves.push({ from, to });
}
else {
if (target.color !== color)
moves.push({ from, to });
break;
}
r += dr;
c += dc;
}
}
break;
}
case 'K': {
for (let dr = -1; dr <= 1; dr++) {
for (let dc = -1; dc <= 1; dc++) {
if (dr === 0 && dc === 0)
continue;
const r = sqRank(from) + dr, c = sqFile(from) + dc;
if (r >= 0 && r < 8 && c >= 0 && c < 8) {
const to = sq(r, c);
const target = gs.board[to];
if (!target || target.color !== color)
moves.push({ from, to });
}
}
}
// Castling
const row = color === 'w' ? 7 : 0;
if (sqRank(from) === row && sqFile(from) === 4) {
// King-side
const kRight = color === 'w' ? gs.castlingRights.K : gs.castlingRights.k;
if (kRight && !gs.board[sq(row, 5)] && !gs.board[sq(row, 6)]) {
if (!isCheck(gs.board, color)
&& !isAttackedBy(gs.board, sq(row, 5), opponent(color))
&& !isAttackedBy(gs.board, sq(row, 6), opponent(color))) {
moves.push({ from, to: sq(row, 6), castling: 'K' });
}
}
// Queen-side
const qRight = color === 'w' ? gs.castlingRights.Q : gs.castlingRights.q;
if (qRight && !gs.board[sq(row, 3)] && !gs.board[sq(row, 2)] && !gs.board[sq(row, 1)]) {
if (!isCheck(gs.board, color)
&& !isAttackedBy(gs.board, sq(row, 3), opponent(color))
&& !isAttackedBy(gs.board, sq(row, 2), opponent(color))) {
moves.push({ from, to: sq(row, 2), castling: 'Q' });
}
}
}
break;
}
}
}
return moves;
}
// --- Make / Unmake ---
function makeMove(gs, move) {
const newGs = {
board: [...gs.board],
turn: opponent(gs.turn),
castlingRights: { ...gs.castlingRights },
enPassantTarget: null,
halfMoveClock: gs.halfMoveClock + 1,
fullMoveNumber: gs.fullMoveNumber,
moveHistory: [...gs.moveHistory],
capturedPieces: {
w: [...gs.capturedPieces.w],
b: [...gs.capturedPieces.b],
},
};
const piece = newGs.board[move.from];
const captured = newGs.board[move.to];
if (captured) {
newGs.capturedPieces[opponent(gs.turn)].push(captured.type);
newGs.halfMoveClock = 0;
}
// En passant capture
if (move.enPassant) {
const capSq = move.to - (gs.turn === 'w' ? 8 : -8);
const epPiece = newGs.board[capSq];
newGs.capturedPieces[opponent(gs.turn)].push(epPiece.type);
newGs.board[capSq] = null;
newGs.halfMoveClock = 0;
}
// Move piece
newGs.board[move.from] = null;
newGs.board[move.to] = piece;
// Promotion
if (move.promo) {
newGs.board[move.to] = { type: move.promo, color: piece.color };
}
// Castling - move rook
if (move.castling) {
const row = sqRank(move.from);
if (move.castling === 'K') {
newGs.board[sq(row, 5)] = newGs.board[sq(row, 7)];
newGs.board[sq(row, 7)] = null;
}
else {
newGs.board[sq(row, 3)] = newGs.board[sq(row, 0)];
newGs.board[sq(row, 0)] = null;
}
}
// Update en passant target
if (piece.type === 'P' && Math.abs(sqRank(move.to) - sqRank(move.from)) === 2) {
newGs.enPassantTarget = (move.from + move.to) >> 1;
}
// Update castling rights
if (piece.type === 'K') {
if (gs.turn === 'w') {
newGs.castlingRights.K = false;
newGs.castlingRights.Q = false;
}
else {
newGs.castlingRights.k = false;
newGs.castlingRights.q = false;
}
}
if (piece.type === 'R') {
if (move.from === sq(7, 7))
newGs.castlingRights.K = false;
if (move.from === sq(7, 0))
newGs.castlingRights.Q = false;
if (move.from === sq(0, 7))
newGs.castlingRights.k = false;
if (move.from === sq(0, 0))
newGs.castlingRights.q = false;
}
// If rook captured on its home square
if (move.to === sq(7, 7))
newGs.castlingRights.K = false;
if (move.to === sq(7, 0))
newGs.castlingRights.Q = false;
if (move.to === sq(0, 7))
newGs.castlingRights.k = false;
if (move.to === sq(0, 0))
newGs.castlingRights.q = false;
// Full move number increment
if (gs.turn === 'b')
newGs.fullMoveNumber++;
return newGs;
}
function generateLegalMoves(gs) {
const pseudo = generatePseudoLegalMoves(gs);
return pseudo.filter(m => {
const newState = makeMove(gs, m);
return !isCheck(newState.board, newState.turn);
});
}
// --- Game status ---
function isCheckmate(gs) {
return isCheck(gs.board, gs.turn) && generateLegalMoves(gs).length === 0;
}
function isStalemate(gs) {
return !isCheck(gs.board, gs.turn) && generateLegalMoves(gs).length === 0;
}
function isInsufficientMaterial(gs) {
const pieces = [];
for (const p of gs.board) {
if (p)
pieces.push(p);
}
if (pieces.length === 2)
return true; // K vs K
if (pieces.length === 3) {
const nonKing = pieces.find(p => p.type !== 'K');
if (nonKing && (nonKing.type === 'B' || nonKing.type === 'N'))
return true;
}
return false;
}
function is50MoveRule(gs) {
return gs.halfMoveClock >= 100;
}
function gameOver(gs) {
if (isCheckmate(gs))
return 'checkmate';
if (isStalemate(gs))
return 'stalemate';
if (isInsufficientMaterial(gs))
return 'insufficient_material';
if (is50MoveRule(gs))
return '50_move_rule';
return null;
}
// --- Algebraic notation ---
function getDisambiguation(gs, move) {
const piece = gs.board[move.from];
const same = generatePseudoLegalMoves(gs).filter(m => m.to === move.to && gs.board[m.from] && gs.board[m.from].type === piece.type && gs.board[m.from].color === piece.color && m.from !== move.from);
if (same.length === 0)
return '';
const sameFile = same.some(m => sqFile(m.from) === sqFile(move.from));
const sameRank = same.some(m => sqRank(m.from) === sqRank(move.from));
if (sameFile && sameRank)
return FILE_NAMES[sqFile(move.from)] + RANK_NAMES[sqRank(move.from)];
if (sameFile)
return RANK_NAMES[sqRank(move.from)];
return FILE_NAMES[sqFile(move.from)];
}
function moveToAlgebraic(gs, move) {
const piece = gs.board[move.from];
const isCapture = gs.board[move.to] !== null || move.enPassant;
let notation = '';
if (move.castling === 'K')
return 'O-O';
if (move.castling === 'Q')
return 'O-O-O';
if (piece.type !== 'P') {
notation += piece.type;
notation += getDisambiguation(gs, move);
}
if (isCapture) {
if (piece.type === 'P')
notation += FILE_NAMES[sqFile(move.from)];
notation += 'x';
}
notation += squareName(move.to);
if (move.promo)
notation += '=' + move.promo;
if (move.enPassant)
notation += ' e.p.';
// Check / checkmate after the move
const newState = makeMove(gs, move);
if (isCheck(newState.board, newState.turn)) {
if (isCheckmate(newState))
notation += '#';
else
notation += '+';
}
return notation;
}
};
// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.PIECE_VALUES = void 0;
exports.findBestMove = findBestMove;
const engine_1 = require("./engine");
// --- Tunable Evaluation Weights ---
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)
const 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,
],
};
function mirroredSquare(sq) {
return ((7 - (0, engine_1.sqRank)(sq)) << 3) | (0, engine_1.sqFile)(sq);
}
function evaluate(gs, color) {
let score = 0;
for (let i = 0; i < 64; i++) {
const piece = gs.board[i];
if (!piece)
continue;
const val = exports.PIECE_VALUES[piece.type];
const pstIdx = piece.color === color ? i : mirroredSquare(i);
const pst = PST[piece.type][pstIdx];
const sign = piece.color === color ? 1 : -1;
score += sign * (val + pst);
}
// Mobility bonus
return score;
}
function orderMoves(gs, moves) {
// Simple move ordering: captures first (with MVV-LVA), then others
return [...moves].sort((a, b) => {
const capA = gs.board[a.to] ? exports.PIECE_VALUES[gs.board[a.to].type] : 0;
const capB = gs.board[b.to] ? exports.PIECE_VALUES[gs.board[b.to].type] : 0;
const promoA = a.promo ? exports.PIECE_VALUES[a.promo] : 0;
const promoB = b.promo ? exports.PIECE_VALUES[b.promo] : 0;
return (capB + promoB) - (capA + promoA);
});
}
function minimax(gs, depth, alpha, beta, maximizing, rootColor, nodes) {
if (depth === 0) {
return quiesce(gs, alpha, beta, maximizing, rootColor, nodes);
}
const moves = (0, engine_1.generateLegalMoves)(gs);
if (moves.length === 0) {
if ((0, engine_1.isCheck)(gs.board, gs.turn)) {
return maximizing ? -99999 + (32 - depth) : 99999 - (32 - depth);
}
return 0; // stalemate
}
const ordered = orderMoves(gs, moves);
if (maximizing) {
let best = -Infinity;
for (const move of ordered) {
const newGs = (0, engine_1.makeMove)(gs, move);
nodes[0]++;
const score = minimax(newGs, depth - 1, alpha, beta, false, rootColor, nodes);
best = Math.max(best, score);
alpha = Math.max(alpha, score);
if (beta <= alpha)
break;
}
return best;
}
else {
let best = Infinity;
for (const move of ordered) {
const newGs = (0, engine_1.makeMove)(gs, move);
nodes[0]++;
const score = minimax(newGs, depth - 1, alpha, beta, true, rootColor, nodes);
best = Math.min(best, score);
beta = Math.min(beta, score);
if (beta <= alpha)
break;
}
return best;
}
}
function quiesce(gs, alpha, beta, maximizing, rootColor, nodes) {
const standPat = evaluate(gs, rootColor);
if (maximizing) {
if (standPat >= beta)
return beta;
if (alpha < standPat)
alpha = standPat;
}
else {
if (standPat <= alpha)
return alpha;
if (beta > standPat)
beta = standPat;
}
// Only consider capture moves in quiescence
const captures = (0, engine_1.generatePseudoLegalMoves)(gs).filter(m => gs.board[m.to] !== null || m.enPassant);
const legalCaptures = captures.filter(m => {
const ns = (0, engine_1.makeMove)(gs, m);
return !(0, engine_1.isCheck)(ns.board, ns.turn);
});
const ordered = orderMoves(gs, legalCaptures);
if (maximizing) {
for (const move of ordered) {
const newGs = (0, engine_1.makeMove)(gs, move);
nodes[0]++;
const score = quiesce(newGs, alpha, beta, false, rootColor, nodes);
if (score >= beta)
return beta;
if (alpha < score)
alpha = score;
}
return alpha;
}
else {
for (const move of ordered) {
const newGs = (0, engine_1.makeMove)(gs, move);
nodes[0]++;
const score = quiesce(newGs, alpha, beta, true, rootColor, nodes);
if (score <= alpha)
return alpha;
if (beta > score)
beta = score;
}
return beta;
}
}
function findBestMove(gs, depth) {
const moves = (0, engine_1.generateLegalMoves)(gs);
if (moves.length === 0)
return { score: 0, bestMove: null, nodesSearched: 0 };
const nodes = [0];
const ordered = orderMoves(gs, moves);
const rootColor = gs.turn;
let bestScore = -Infinity;
let bestMove = ordered[0];
for (const move of ordered) {
const newGs = (0, engine_1.makeMove)(gs, move);
const score = minimax(newGs, depth - 1, -Infinity, Infinity, false, rootColor, nodes);
nodes[0]++;
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
return { score: bestScore, bestMove, nodesSearched: nodes[0] };
}
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initAudio = initAudio;
exports.setMuted = setMuted;
exports.isMuted = isMuted;
exports.playMoveSound = playMoveSound;
exports.playCaptureSound = playCaptureSound;
exports.playCheckSound = playCheckSound;
exports.playGameOverSound = playGameOverSound;
// Web Audio sound effects
let audioCtx = null;
let muted = false;
function initAudio() {
if (!audioCtx) {
audioCtx = new AudioContext();
}
}
function setMuted(val) {
muted = val;
}
function isMuted() {
return muted;
}
function playTone(freq, duration, type = 'sine', volume = 0.15) {
if (muted || !audioCtx)
return;
try {
const osc = audioCtx.createOscillator();
const gain = audioCtx.createGain();
osc.type = type;
osc.frequency.setValueAtTime(freq, audioCtx.currentTime);
gain.gain.setValueAtTime(volume, audioCtx.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audioCtx.currentTime + duration);
osc.connect(gain);
gain.connect(audioCtx.destination);
osc.start();
osc.stop(audioCtx.currentTime + duration);
}
catch { }
}
function playMoveSound() {
playTone(600, 0.08, 'sine', 0.12);
}
function playCaptureSound() {
playTone(400, 0.12, 'square', 0.1);
setTimeout(() => playTone(300, 0.08, 'square', 0.08), 60);
}
function playCheckSound() {
playTone(800, 0.15, 'sine', 0.15);
setTimeout(() => playTone(600, 0.15, 'sine', 0.12), 100);
}
function playGameOverSound() {
playTone(523, 0.2, 'sine', 0.15);
setTimeout(() => playTone(659, 0.2, 'sine', 0.15), 150);
setTimeout(() => playTone(784, 0.3, 'sine', 0.15), 300);
}
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getGameState = getGameState;
exports.getLastMove = getLastMove;
exports.getGameOverState = getGameOverState;
exports.getFlipped = getFlipped;
exports.getSelectedSquare = getSelectedSquare;
exports.getLegalMovesForSelected = getLegalMovesForSelected;
exports.getAIDepth = getAIDepth;
exports.getAIColor = getAIColor;
exports.getPlayerColor = getPlayerColor;
exports.getPromotionPending = getPromotionPending;
exports.isAIThinking = isAIThinking;
exports.setOnRender = setOnRender;
exports.setOnGameOver = setOnGameOver;
exports.setOnAIStart = setOnAIStart;
exports.setOnAIDone = setOnAIDone;
exports.setOnSelectedChange = setOnSelectedChange;
exports.setAIDepth = setAIDepth;
exports.setFlipped = setFlipped;
exports.setAIColor = setAIColor;
exports.setPlayerColor = setPlayerColor;
exports.undo = undo;
exports.newGame = newGame;
exports.startGame = startGame;
exports.handleSquareClick = handleSquareClick;
exports.handlePromotionChoice = handlePromotionChoice;
exports.getMoveHistory = getMoveHistory;
exports.getCapturedPieces = getCapturedPieces;
exports.debug = debug;
const engine_1 = require("./engine");
const ai_1 = require("./ai");
const sound_1 = require("./sound");
const STORAGE_KEY = 'chess_game_state';
// --- Game Manager ---
let state;
let lastMove = null;
let gameOverState = null;
let aiColor = 'b';
let aiDepth = 3;
let playerColor = 'w';
let flipped = false;
let selectedSquare = null;
let legalMovesForSelected = [];
let promotionPending = null;
let aiThinking = false;
// Callbacks for UI
let onRender = null;
let onGameOver = null;
let onAIStart = null;
let onAIDone = null;
let onSelectedChange = null;
function saveState() {
const data = {
fen: (0, engine_1.stateToFen)(state),
moveHistory: state.moveHistory,
lastMove,
gameOverState,
aiColor,
playerColor,
aiDepth,
flipped,
capturedPieces: state.capturedPieces,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
function loadState() {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return false;
try {
const data = JSON.parse(raw);
state = (0, engine_1.fenToState)(data.fen);
state.moveHistory = data.moveHistory || [];
state.capturedPieces = data.capturedPieces || { w: [], b: [] };
lastMove = data.lastMove || null;
gameOverState = data.gameOverState || null;
aiColor = data.aiColor || 'b';
playerColor = data.playerColor || 'w';
aiDepth = data.aiDepth || 3;
flipped = data.flipped || false;
selectedSquare = null;
legalMovesForSelected = [];
promotionPending = null;
return true;
}
catch {
return false;
}
}
function newState() {
state = (0, engine_1.fenToState)(engine_1.START_FEN);
lastMove = null;
gameOverState = null;
selectedSquare = null;
legalMovesForSelected = [];
promotionPending = null;
aiThinking = false;
saveState();
}
function getGameState() { return state; }
function getLastMove() { return lastMove; }
function getGameOverState() { return gameOverState; }
function getFlipped() { return flipped; }
function getSelectedSquare() { return selectedSquare; }
function getLegalMovesForSelected() { return legalMovesForSelected; }
function getAIDepth() { return aiDepth; }
function getAIColor() { return aiColor; }
function getPlayerColor() { return playerColor; }
function getPromotionPending() { return promotionPending; }
function isAIThinking() { return aiThinking; }
function setOnRender(cb) { onRender = cb; }
function setOnGameOver(cb) { onGameOver = cb; }
function setOnAIStart(cb) { onAIStart = cb; }
function setOnAIDone(cb) { onAIDone = cb; }
function setOnSelectedChange(cb) { onSelectedChange = cb; }
function setAIDepth(d) { aiDepth = d; }
function setFlipped(f) { flipped = f; }
function setAIColor(c) { aiColor = c; playerColor = c === 'w' ? 'b' : 'w'; }
function setPlayerColor(c) { playerColor = c; aiColor = c === 'w' ? 'b' : 'w'; }
function render() {
if (onRender)
onRender();
}
function selectSquare(sq) {
selectedSquare = sq;
legalMovesForSelected = (0, engine_1.generateLegalMoves)(state).filter(m => m.from === sq);
if (onSelectedChange)
onSelectedChange();
}
function deselect() {
selectedSquare = null;
legalMovesForSelected = [];
if (onSelectedChange)
onSelectedChange();
}
function playMove(move) {
if (gameOverState || aiThinking)
return;
const piece = state.board[move.from];
const isCapture = state.board[move.to] !== null || move.enPassant;
const algebraic = (0, engine_1.moveToAlgebraic)(state, move);
state = (0, engine_1.makeMove)(state, move);
state.moveHistory.push(algebraic);
lastMove = move;
selectedSquare = null;
legalMovesForSelected = [];
// Sounds
if (isCapture)
(0, sound_1.playCaptureSound)();
else
(0, sound_1.playMoveSound)();
const go = (0, engine_1.gameOver)(state);
if (go) {
gameOverState = go;
(0, sound_1.playGameOverSound)();
if (onGameOver)
onGameOver(go);
saveState();
render();
return;
}
// Check sound
if ((0, engine_1.isCheck)(state.board, state.turn))
(0, sound_1.playCheckSound)();
saveState();
render();
// AI move if it's AI's turn
if (state.turn === aiColor && !gameOverState) {
aiMove();
}
}
function aiMove() {
if (gameOverState)
return;
aiThinking = true;
if (onAIStart)
onAIStart();
render();
// Use setTimeout to allow UI to update before blocking search
setTimeout(() => {
const result = (0, ai_1.findBestMove)(state, aiDepth);
if (result.bestMove) {
const piece = state.board[result.bestMove.from];
const isCapture = state.board[result.bestMove.to] !== null || result.bestMove.enPassant;
const algebraic = (0, engine_1.moveToAlgebraic)(state, result.bestMove);
state = (0, engine_1.makeMove)(state, result.bestMove);
state.moveHistory.push(algebraic);
lastMove = result.bestMove;
if (isCapture)
(0, sound_1.playCaptureSound)();
else
(0, sound_1.playMoveSound)();
const go = (0, engine_1.gameOver)(state);
if (go) {
gameOverState = go;
(0, sound_1.playGameOverSound)();
if (onGameOver)
onGameOver(go);
}
else if ((0, engine_1.isCheck)(state.board, state.turn)) {
(0, sound_1.playCheckSound)();
}
saveState();
}
aiThinking = false;
render();
if (onAIDone)
onAIDone();
}, 50);
}
function undoMove() {
// Take back both player and AI move (or just one if game ended)
if (state.moveHistory.length < 2 && !gameOverState)
return;
const steps = (gameOverState && state.moveHistory.length >= 1) ? 1 : 2;
// Rebuild from FEN at the target position
let temp = (0, engine_1.fenToState)(engine_1.START_FEN);
const targetLen = state.moveHistory.length - steps;
const legalAll = () => (0, engine_1.generateLegalMoves)(temp);
for (let i = 0; i < targetLen; i++) {
// We need to replay moves from history, but we lost the exact moves...
// Alternative: store full history of states
// For now, re-derive by replaying from FEN is too complex.
// Let's store snapshot of the state before each move.
}
// Better approach: replay from start using algebraic notation
// Actually, we'll use a simpler approach: just reconstruct from FEN
// by working backwards.
// Simplest: re-create state from the saved FEN minus steps.
// Let's store FEN snapshots instead.
}
// Undo using stored FEN snapshots
let fenHistory = [engine_1.START_FEN];
// Auto-initialize game state
let _initialized = false;
function _autoInit() {
if (_initialized)
return;
_initialized = true;
if (loadState()) {
// Restored from save
}
else {
newState();
}
}
// Call immediately
_autoInit();
function recordFEN() {
fenHistory.push((0, engine_1.stateToFen)(state));
}
function undo() {
if (fenHistory.length <= 1)
return;
// Remove last 2 moves (player + AI), or 1 if game over
const steps = (gameOverState && fenHistory.length >= 2) ? 1 : 2;
const targetIdx = fenHistory.length - 1 - steps;
if (targetIdx < 1)
return;
// Replay from start to target position
const targetFen = fenHistory[targetIdx];
const targetMoves = state.moveHistory.slice(0, fenHistory.length - steps - (gameOverState ? 0 : 0));
state = (0, engine_1.fenToState)(targetFen);
fenHistory = fenHistory.slice(0, targetIdx + 1);
state.moveHistory = [];
state.capturedPieces = { w: [], b: [] };
gameOverState = null;
lastMove = null;
selectedSquare = null;
legalMovesForSelected = [];
// Replay moves to rebuild move history and captured pieces
let replay = (0, engine_1.fenToState)(engine_1.START_FEN);
// Actually we don't need to replay - the FEN captures full state
// We just need move notation, which we lost. That's acceptable for now.
saveState();
render();
}
function newGame() {
fenHistory = [engine_1.START_FEN];
newState();
render();
}
function startGame(playerWhite) {
playerColor = playerWhite ? 'w' : 'b';
aiColor = playerWhite ? 'b' : 'w';
fenHistory = [engine_1.START_FEN];
newState();
render();
// If player chose black, AI moves first
if (state.turn === aiColor && !gameOverState) {
setTimeout(() => aiMove(), 300);
}
}
// For UI click
function handleSquareClick(sq) {
if (gameOverState || aiThinking)
return;
if (state.turn !== playerColor)
return;
// If we have a selected square and the clicked square is a legal destination
if (selectedSquare !== null) {
const targetMove = legalMovesForSelected.find(m => m.to === sq);
if (targetMove) {
// Check if promotion
if (targetMove.promo) {
// Pick queen by default for non-prompted, or set promotion pending
promotionPending = { from: targetMove.from, to: sq, color: state.turn };
render();
return;
}
playMove(targetMove);
recordFEN();
return;
}
}
// Select a piece if it belongs to current player
const piece = state.board[sq];
if (piece && piece.color === state.turn) {
selectSquare(sq);
}
else {
deselect();
}
}
function handlePromotionChoice(pieceType) {
if (!promotionPending)
return;
const pp = promotionPending;
const move = legalMovesForSelected.find(m => m.to === pp.to && m.promo === pieceType);
if (move) {
playMove(move);
recordFEN();
}
promotionPending = null;
render();
}
function getMoveHistory() { return state.moveHistory; }
function getCapturedPieces() {
return {
w: state.capturedPieces.w,
b: state.capturedPieces.b,
};
}
// Debug interface
function debug() {
const _debug = {
fen: () => (0, engine_1.stateToFen)(state),
legalMoves: () => (0, engine_1.generateLegalMoves)(state).map(m => ({
from: m.from, to: m.to, promo: m.promo,
enPassant: m.enPassant, castling: m.castling
})),
gameOver: () => (0, engine_1.gameOver)(state),
// Allow executing arbitrary moves for testing
move: (fromStr, toStr) => {
const from = (0, engine_1.parseSquare)(fromStr);
const to = (0, engine_1.parseSquare)(toStr);
if (from === null || to === null)
return { ok: false, error: 'bad squares' };
const legal = (0, engine_1.generateLegalMoves)(state);
const move = legal.find(m => m.from === from && m.to === to);
if (!move)
return { ok: false, error: 'illegal move' };
const alg = (0, engine_1.moveToAlgebraic)(state, move);
state = (0, engine_1.makeMove)(state, move);
state.moveHistory.push(alg);
lastMove = move;
return { ok: true, fen: (0, engine_1.stateToFen)(state), notation: alg };
},
};
return _debug;
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initRenderer = initRenderer;
exports.render = render;
const engine_1 = require("./engine");
const game_1 = require("./game");
const boardEl = document.getElementById('board');
const moveListEl = document.getElementById('move-list');
const turnIndicator = document.getElementById('turn-indicator');
const gameOverBanner = document.getElementById('game-over-banner');
const capturedWhiteEl = document.getElementById('captured-white');
const capturedBlackEl = document.getElementById('captured-black');
const promotionOverlay = document.getElementById('promotion-overlay');
const promotionChoices = document.getElementById('promotion-choices');
let squares = [];
let dragState = null;
function buildBoard() {
boardEl.innerHTML = '';
squares = [];
for (let r = 0; r < 8; r++) {
for (let c = 0; c < 8; c++) {
const sqEl = document.createElement('div');
sqEl.className = 'square ' + ((r + c) % 2 === 0 ? 'square-light' : 'square-dark');
const idx = r * 8 + c;
sqEl.dataset.square = String(idx);
squares[idx] = sqEl;
boardEl.appendChild(sqEl);
if (c === 0) {
const rankLabel = document.createElement('span');
rankLabel.textContent = String(8 - r);
rankLabel.style.cssText = 'position:absolute;top:2px;left:3px;font-size:10px;pointer-events:none;';
sqEl.appendChild(rankLabel);
}
if (r === 7) {
const fileLabel = document.createElement('span');
fileLabel.textContent = 'abcdefgh'[c];
fileLabel.style.cssText = 'position:absolute;bottom:1px;right:3px;font-size:10px;pointer-events:none;';
sqEl.appendChild(fileLabel);
}
}
}
}
function renderBoard() {
const gs = (0, game_1.getGameState)();
const flipped = (0, game_1.getFlipped)();
const selected = (0, game_1.getSelectedSquare)();
const legalMoves = (0, game_1.getLegalMovesForSelected)();
const lastMove = (0, game_1.getLastMove)();
const goState = (0, game_1.getGameOverState)();
const pp = (0, game_1.getPromotionPending)();
boardEl.style.transform = flipped ? 'rotate(180deg)' : '';
let kingSq = null;
for (let i = 0; i < 64; i++) {
const p = gs.board[i];
if (p && p.type === 'K' && p.color === gs.turn) {
kingSq = i;
break;
}
}
const legalDestSet = new Set(legalMoves.map(m => m.to));
const legalCaptureSet = new Set(legalMoves.filter(m => gs.board[m.to] !== null || m.enPassant).map(m => m.to));
for (let i = 0; i < 64; i++) {
const sq = squares[i];
if (!sq)
continue;
sq.className = 'square ' + (((0, engine_1.sqRank)(i) + (0, engine_1.sqFile)(i)) % 2 === 0 ? 'square-light' : 'square-dark');
// Remove piece/indicator elements but keep coord labels
while (sq.lastChild) {
const lc = sq.lastChild;
if (lc.classList && lc.classList.contains('coord-label'))
break;
sq.removeChild(lc);
}
if (lastMove && (i === lastMove.from || i === lastMove.to))
sq.classList.add('square-last-move');
if (selected === i)
sq.classList.add('square-selected');
const piece = gs.board[i];
let pieceEl = sq.querySelector('.piece');
if (piece) {
if (!pieceEl) {
pieceEl = document.createElement('span');
pieceEl.className = 'piece';
sq.appendChild(pieceEl);
}
const pe = pieceEl;
pe.textContent = engine_1.PIECE_SYMBOLS[piece.color][piece.type];
pe.style.color = piece.color === 'w' ? '#fff' : '#222';
pe.style.filter = piece.color === 'w' ? 'drop-shadow(0 0 1px rgba(0,0,0,0.8))' : '';
}
else {
if (pieceEl)
pieceEl.remove();
}
if (legalDestSet.has(i)) {
if (legalCaptureSet.has(i)) {
const ring = document.createElement('div');
ring.className = 'capture-ring';
sq.appendChild(ring);
}
else {
const dot = document.createElement('div');
dot.className = 'move-dot';
sq.appendChild(dot);
}
}
if (i === kingSq) {
const checkFn = window.__chessCheck;
if (checkFn && checkFn(gs))
sq.classList.add('square-check');
}
}
if (pp) {
renderPromotionOverlay(pp);
}
else {
promotionOverlay.classList.add('hidden');
}
if (goState) {
turnIndicator.textContent = 'Game Over';
turnIndicator.style.background = '#666';
turnIndicator.style.color = '#ccc';
}
else {
turnIndicator.textContent = gs.turn === 'w' ? '⬜ White to move' : '⬛ Black to move';
turnIndicator.style.background = gs.turn === 'w' ? '#f0d9b5' : '#5c3d2e';
turnIndicator.style.color = gs.turn === 'w' ? '#333' : '#fff';
}
if (goState) {
gameOverBanner.classList.remove('hidden');
const labels = {
checkmate: gs.turn === 'w' ? '♚ Black wins by checkmate!' : '♔ White wins by checkmate!',
stalemate: '½ Draw by stalemate',
insufficient_material: '½ Draw — insufficient material',
'50_move_rule': '½ Draw — 50-move rule',
};
gameOverBanner.textContent = labels[goState] || 'Game Over';
}
else {
gameOverBanner.classList.add('hidden');
}
let thinkEl = document.getElementById('ai-thinking');
if ((0, game_1.isAIThinking)()) {
if (!thinkEl) {
thinkEl = document.createElement('div');
thinkEl.id = 'ai-thinking';
turnIndicator.parentNode.appendChild(thinkEl);
}
thinkEl.textContent = '🤔 AI thinking...';
}
else if (thinkEl) {
thinkEl.remove();
}
renderMoveList();
renderCaptured();
}
function renderMoveList() {
const history = (0, game_1.getMoveHistory)();
moveListEl.innerHTML = '';
for (let i = 0; i < history.length; i += 2) {
const entry = document.createElement('div');
entry.className = 'move-entry';
const num = document.createElement('span');
num.className = 'move-number';
num.textContent = (i / 2 + 1) + '.';
entry.appendChild(num);
const white = document.createElement('span');
white.className = 'move-white';
white.textContent = history[i] || '';
entry.appendChild(white);
if (history[i + 1]) {
const black = document.createElement('span');
black.className = 'move-black';
black.textContent = history[i + 1];
entry.appendChild(black);
}
moveListEl.appendChild(entry);
}
moveListEl.scrollTop = moveListEl.scrollHeight;
}
function renderCaptured() {
const captured = (0, game_1.getCapturedPieces)();
const pieceOrder = ['Q', 'R', 'B', 'N', 'P'];
const sortPieces = (arr) => arr.sort((a, b) => pieceOrder.indexOf(a) - pieceOrder.indexOf(b));
capturedWhiteEl.textContent = sortPieces(captured.w).map(t => engine_1.PIECE_SYMBOLS['b'][t]).join(' ');
capturedBlackEl.textContent = sortPieces(captured.b).map(t => engine_1.PIECE_SYMBOLS['w'][t]).join(' ');
}
function renderPromotionOverlay(pp) {
promotionChoices.innerHTML = '';
const pieces = ['Q', 'R', 'B', 'N'];
for (const pt of pieces) {
const btn = document.createElement('div');
btn.className = 'promo-piece';
btn.textContent = engine_1.PIECE_SYMBOLS[pp.color][pt];
btn.style.color = pp.color === 'w' ? '#fff' : '#222';
btn.addEventListener('click', () => {
const handler = window.__handlePromotion;
if (handler)
handler(pt);
});
promotionChoices.appendChild(btn);
}
promotionOverlay.classList.remove('hidden');
}
function setupDragDrop() {
for (let i = 0; i < 64; i++) {
const sq = squares[i];
sq.addEventListener('mousedown', (e) => {
if (e.button !== 0)
return;
const gs = (0, game_1.getGameState)();
const piece = gs.board[i];
if (!piece || piece.color !== gs.turn || (0, game_1.getGameOverState)() || (0, game_1.isAIThinking)())
return;
const ghost = document.createElement('div');
ghost.textContent = engine_1.PIECE_SYMBOLS[piece.color][piece.type];
ghost.style.cssText = 'position:fixed;font-size:46px;pointer-events:none;z-index:1000;'
+ 'width:72px;height:72px;display:flex;justify-content:center;align-items:center;'
+ 'text-shadow:0 2px 6px rgba(0,0,0,0.5);'
+ 'color:' + (piece.color === 'w' ? '#fff' : '#222') + ';';
if (piece.color === 'w')
ghost.style.filter = 'drop-shadow(0 0 2px rgba(0,0,0,0.9))';
document.body.appendChild(ghost);
dragState = { from: i, ghost };
const pieceEl = sq.querySelector('.piece');
if (pieceEl)
pieceEl.style.opacity = '0.3';
const sel = window.__handleSelect;
if (sel)
sel(i);
const rect = sq.getBoundingClientRect();
ghost.style.left = (rect.left + 12) + 'px';
ghost.style.top = (rect.top + 12) + 'px';
e.preventDefault();
});
}
document.addEventListener('mousemove', (e) => {
if (!dragState)
return;
dragState.ghost.style.left = (e.clientX - 36) + 'px';
dragState.ghost.style.top = (e.clientY - 36) + 'px';
});
document.addEventListener('mouseup', (e) => {
if (!dragState)
return;
if (dragState.ghost.parentNode)
dragState.ghost.parentNode.removeChild(dragState.ghost);
const fromSq = squares[dragState.from];
const origPiece = fromSq?.querySelector('.piece');
if (origPiece)
origPiece.style.opacity = '';
const target = document.elementFromPoint(e.clientX, e.clientY);
if (target) {
const sqEl = target.closest('.square');
if (sqEl) {
const toSq = parseInt(sqEl.dataset.square || '');
if (!isNaN(toSq) && toSq !== dragState.from) {
const handler = window.__handleMove;
if (handler)
handler(dragState.from, toSq);
}
}
}
dragState = null;
});
}
function initRenderer() {
buildBoard();
setupDragDrop();
renderBoard();
}
function render() {
renderBoard();
}
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>
<!-- agent-meta {"model":"ThinkingCap-Qwen3.6-27B-GGUF","provider":"brigid","persona":"typescript","sessionId":"1ceb0d06-562d-4e07-8334-b0bf961c2bd2","tokensIn":2668416,"tokensOut":43237,"tokensTotal":2711653,"cost":0,"turns":68,"toolCalls":67,"failedToolCalls":6,"timestamp":"2026-08-03T19:05:29.671Z"} -->