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

Reviews & generation details
Reviews
No human review yet. Unreviewed does not mean unsuccessful.
Generation context
Imported historical artifact. Its exact original instructions and lineage are not known.
Generation prompt · exact text unavailable
See the current prompt for reference. It may differ from the instructions used to generate this artifact.
Inspect original source 79,807 bytes · SHA-256 7207922518a2
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess — AI Opponent</title>
<style>
/* ── css: layout ── */
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: 'Segoe UI', 'Helvetica Neue', Arial, sans-serif;
background: #2d2416;
color: #333;
display: flex;
justify-content: center;
align-items: flex-start;
min-height: 100vh;
padding: 20px;
}
#game-container {
display: flex;
gap: 24px;
align-items: flex-start;
}
#board-area {
flex-shrink: 0;
}
#board-canvas {
display: block;
cursor: pointer;
border-radius: 4px;
width: 560px;
height: 560px;
}
#side-panel {
width: 260px;
display: flex;
flex-direction: column;
gap: 12px;
}
#turn-indicator {
background: #f5eedd;
padding: 10px 16px;
border-radius: 6px;
font-size: 18px;
font-weight: 600;
text-align: center;
border: 2px solid #b58863;
}
#status-bar {
background: #1a1a2e;
color: #ffd700;
padding: 10px 16px;
border-radius: 6px;
font-size: 15px;
font-weight: 600;
text-align: center;
min-height: 40px;
display: flex;
align-items: center;
justify-content: center;
}
#captured-area {
display: flex;
flex-direction: column;
gap: 4px;
}
.captured-tray {
background: #f5eedd;
padding: 6px 10px;
border-radius: 4px;
min-height: 30px;
font-size: 20px;
letter-spacing: 2px;
line-height: 1.2;
word-break: break-all;
}
#move-list-container {
background: #f5eedd;
border-radius: 6px;
overflow: hidden;
max-height: 280px;
display: flex;
flex-direction: column;
}
#move-list-header {
padding: 8px 12px;
font-weight: 700;
font-size: 13px;
color: #6b4423;
border-bottom: 2px solid #b58863;
flex-shrink: 0;
}
#move-list {
padding: 8px 12px;
font-size: 13px;
font-family: 'Courier New', monospace;
overflow-y: auto;
flex: 1;
line-height: 1.6;
}
.move-number {
color: #888;
margin-right: 4px;
}
#controls {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
#controls button {
flex: 1;
padding: 8px 10px;
border: 2px solid #b58863;
background: #f5eedd;
border-radius: 6px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: #4a3520;
min-width: 60px;
transition: background 0.15s;
}
#controls button:hover {
background: #e8dcc8;
}
#difficulty-area {
display: flex;
align-items: center;
gap: 8px;
background: #f5eedd;
padding: 8px 12px;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
}
#ai-depth {
flex: 1;
padding: 4px;
border: 1px solid #b58863;
border-radius: 4px;
font-size: 13px;
background: #fff;
}
#thinking-indicator {
background: #f5eedd;
padding: 8px;
border-radius: 6px;
text-align: center;
font-size: 14px;
color: #6b4423;
animation: pulse 1s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.5; }
}
#new-game-dialog {
background: #f5eedd;
border: 2px solid #b58863;
border-radius: 8px;
padding: 16px;
display: flex;
flex-direction: column;
gap: 10px;
}
#new-game-dialog.hidden { display: none; }
.dialog-title {
font-size: 16px;
font-weight: 700;
color: #6b4423;
}
.dialog-body {
font-size: 14px;
display: flex;
flex-direction: column;
gap: 6px;
}
.dialog-body label {
display: flex;
align-items: center;
gap: 6px;
}
.dialog-actions {
display: flex;
gap: 8px;
}
.dialog-actions button {
flex: 1;
padding: 8px;
border: 2px solid #b58863;
background: #fff;
border-radius: 6px;
cursor: pointer;
font-weight: 600;
color: #4a3520;
}
.dialog-actions button:first-child {
background: #6b4423;
color: #fff;
border-color: #5a3a1d;
}
.hidden { display: none !important; }
</style>
</head>
<body>
<div id="game-container">
<div id="board-area">
<canvas id="board-canvas" width="560" height="560"></canvas>
</div>
<div id="side-panel">
<div id="turn-indicator">White to move</div>
<div id="status-bar"></div>
<div id="captured-area">
<div id="captured-white" class="captured-tray"></div>
<div id="captured-black" class="captured-tray"></div>
</div>
<div id="move-list-container">
<div id="move-list-header">Moves</div>
<div id="move-list"></div>
</div>
<div id="controls">
<button id="btn-undo" title="Undo">↩ Undo</button>
<button id="btn-flip" title="Flip Board">🔄 Flip</button>
<button id="btn-new" title="New Game">New</button>
<button id="btn-sound" title="Toggle Sound">🔊</button>
</div>
<div id="difficulty-area">
<label>AI: </label>
<select id="ai-depth">
<option value="1">Easy</option>
<option value="2">Medium</option>
<option value="3" selected="">Hard</option>
</select>
</div>
<div id="new-game-dialog" class="hidden">
<div class="dialog-title">New Game</div>
<div class="dialog-body">
<label><input type="radio" name="side" value="white" checked=""> Play as White</label><br>
<label><input type="radio" name="side" value="black"> Play as Black</label><br>
<label><input type="checkbox" id="analysis-cb"> Analysis Mode (no AI)</label>
</div>
<div class="dialog-actions">
<button id="btn-start">Start</button>
<button id="btn-cancel-new">Cancel</button>
</div>
</div>
<div id="thinking-indicator" class="hidden">🤔 AI thinking...</div>
</div>
</div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/main.ts":{"./game":"src/game.ts","./renderer":"src/renderer.ts","./constants":"src/constants.ts","./types":"src/types.ts"},"src/engine.ts":{"./types":"src/types.ts"},"src/ai.ts":{"./types":"src/types.ts","./constants":"src/constants.ts","./engine":"src/engine.ts"},"src/renderer.ts":{"./constants":"src/constants.ts","./types":"src/types.ts","./engine":"src/engine.ts"},"src/sound.ts":{"./constants":"src/constants.ts"},"src/game.ts":{"./types":"src/types.ts","./engine":"src/engine.ts","./ai":"src/ai.ts","./sound":"src/sound.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const game_1 = require("./game");
const renderer_1 = require("./renderer");
const constants_1 = require("./constants");
// ── Main Entry ──────────────────────────────────────────────────────
const canvas = document.getElementById('board-canvas');
const turnIndicator = document.getElementById('turn-indicator');
const statusBar = document.getElementById('status-bar');
const capturedWhiteEl = document.getElementById('captured-white');
const capturedBlackEl = document.getElementById('captured-black');
const moveListEl = document.getElementById('move-list');
const btnUndo = document.getElementById('btn-undo');
const btnFlip = document.getElementById('btn-flip');
const btnNew = document.getElementById('btn-new');
const btnSound = document.getElementById('btn-sound');
const aiDepthSelect = document.getElementById('ai-depth');
const newGameDialog = document.getElementById('new-game-dialog');
const btnStart = document.getElementById('btn-start');
const btnCancelNew = document.getElementById('btn-cancel-new');
const thinkingEl = document.getElementById('thinking-indicator');
// Init game
const game = new game_1.GameController();
const renderer = new renderer_1.Renderer(canvas, {
onSquareClick: handleSquareClick,
onSquareDragStart: () => { },
onSquareDragEnd: handleDragEnd,
});
// Set AI depth from select
game.setAiDepth(parseInt(aiDepthSelect.value));
// Wire renderer click for promotion
canvas.addEventListener('click', (e) => {
const promoPiece = renderer.onPromoClick(e.clientX, e.clientY);
if (promoPiece) {
game.confirmPromotion(promoPiece);
}
});
// Track selected square
let selectedSq = null;
function handleSquareClick(sq) {
const state = game.getState();
// If we have a pending promo, ignore board clicks (handled separately)
if (game.hasPendingPromo())
return;
const piece = state.board[sq];
if (selectedSq === null) {
// Select a piece
if (piece && piece.color === game.getPlayerColor()) {
selectedSq = sq;
const moves = game.legalMoves().filter(m => m.from === sq);
renderer.setSelected(sq);
renderer.setLegalMoves(moves);
}
}
else {
// Try to move
const piece = state.board[sq];
if (sq === selectedSq) {
// Deselect
selectedSq = null;
renderer.setSelected(null);
renderer.setLegalMoves([]);
return;
}
if (piece && piece.color === game.getPlayerColor()) {
// Select different piece
selectedSq = sq;
const moves = game.legalMoves().filter(m => m.from === sq);
renderer.setSelected(sq);
renderer.setLegalMoves(moves);
return;
}
// Try move
const state2 = game.getState();
if (state2.turn === game.getPlayerColor()) {
const legal = game.legalMoves();
const matches = legal.filter(m => m.from === selectedSq && m.to === sq);
if (matches.length > 0) {
const promoMoves = matches.filter(m => m.promotion);
if (promoMoves.length > 0) {
// Show promotion picker
renderer.setPendingPromo({ from: selectedSq, to: sq });
selectedSq = null;
renderer.setSelected(null);
renderer.setLegalMoves([]);
game.setPendingPromo(selectedSq, sq);
return;
}
}
game.tryMove(selectedSq, sq);
}
selectedSq = null;
renderer.setSelected(null);
renderer.setLegalMoves([]);
}
}
function handleDragEnd(from, to) {
if (to === null)
return;
if (game.getState().turn !== game.getPlayerColor())
return;
const legal = game.legalMoves();
const matches = legal.filter(m => m.from === from && m.to === to);
if (matches.length > 0) {
const promoMoves = matches.filter(m => m.promotion);
if (promoMoves.length > 0) {
renderer.setPendingPromo({ from, to });
game.setPendingPromo(from, to);
return;
}
game.tryMove(from, to);
}
}
function gameHasPendingPromo() {
return game.pendingPromo !== null;
}
// Refresh display
function render() {
const state = game.getState();
const lastMove = game.getLastMove();
renderer.setLastMove(lastMove);
// Re-apply selection if active
if (selectedSq !== null) {
renderer.setSelected(selectedSq);
const moves = game.legalMoves().filter(m => m.from === selectedSq);
renderer.setLegalMoves(moves);
}
renderer.render(state);
// Turn indicator
const turnText = state.turn === 'w' ? 'White' : 'Black';
const isPlayerTurn = state.turn === game.getPlayerColor();
turnIndicator.textContent = isPlayerTurn ? `${turnText} to move (You)` : `${turnText} to move (AI)`;
// Captured pieces
const cw = state.capturedWhite.map(t => constants_1.PIECE_UNICODE['w' + t]).join('');
const cb = state.capturedBlack.map(t => constants_1.PIECE_UNICODE['b' + t]).join('');
capturedWhiteEl.textContent = cw;
capturedBlackEl.textContent = cb;
// Move list
updateMoveList(state.moveList);
// AI thinking
thinkingEl.className = game.isAiThinking() ? '' : 'hidden';
}
function updateMoveList(moves) {
let html = '';
for (let i = 0; i < moves.length; i += 2) {
const num = Math.floor(i / 2) + 1;
html += `<span class="move-number">${num}.</span> ${moves[i]}`;
if (i + 1 < moves.length) {
html += ` ${moves[i + 1]}`;
}
html += '<br>';
}
moveListEl.innerHTML = html;
moveListEl.scrollTop = moveListEl.scrollHeight;
}
// Game over handler
game.onGameOver((status) => {
switch (status) {
case 'checkmate': {
const winner = game.getState().turn === 'w' ? 'Black' : 'White';
statusBar.textContent = `Checkmate! ${winner} wins!`;
break;
}
case 'stalemate':
statusBar.textContent = 'Stalemate — Draw!';
break;
case 'insufficient':
statusBar.textContent = 'Insufficient material — Draw!';
break;
case 'fiftyMove':
statusBar.textContent = '50-move rule — Draw!';
break;
}
selectedSq = null;
renderer.setSelected(null);
renderer.setLegalMoves([]);
render();
});
game.onAiThinking((thinking) => {
if (!thinking) {
render();
}
});
// Initial render
render();
// ── Button handlers ─────────────────────────────────────────────────
btnUndo.addEventListener('click', () => {
game.undo();
selectedSq = null;
renderer.setSelected(null);
renderer.setLegalMoves([]);
statusBar.textContent = '';
render();
});
let flipped = false;
btnFlip.addEventListener('click', () => {
flipped = !flipped;
renderer.setFlipped(flipped);
render();
});
btnNew.addEventListener('click', () => {
newGameDialog.className = '';
});
btnStart.addEventListener('click', () => {
const white = document.querySelector('input[name="side"]:checked');
const analysis = document.getElementById('analysis-cb');
const color = white.value === 'white' ? 'w' : 'b';
game.startNewGame(color, parseInt(aiDepthSelect.value), analysis.checked);
selectedSq = null;
renderer.setSelected(null);
renderer.setLegalMoves([]);
statusBar.textContent = '';
newGameDialog.className = 'hidden';
render();
});
btnCancelNew.addEventListener('click', () => {
newGameDialog.className = 'hidden';
});
btnSound.addEventListener('click', () => {
const muted = game.getSound().toggle();
btnSound.textContent = muted ? '🔇' : '🔊';
});
aiDepthSelect.addEventListener('change', () => {
game.setAiDepth(parseInt(aiDepthSelect.value));
});
// ── Expose debug interface ──────────────────────────────────────────
window.game = {
fen: () => game.fen(),
legalMoves: () => game.legalMoves(),
move: (notation) => {
// Parse "e2e4" format
if (notation.length >= 4) {
const from = parseSq(notation[0] + notation[1]);
const to = parseSq(notation[2] + notation[3]);
if (from !== null && to !== null) {
return game.tryMove(from, to);
}
}
return false;
},
state: () => game.getState(),
};
function parseSq(name) {
const f = name.charCodeAt(0) - 97;
const r = parseInt(name[1]) - 1;
if (f < 0 || f > 7 || r < 0 || r > 7)
return null;
return r * 8 + f;
}
};
// ── module: src/constants.ts ──
__mods["src/constants.ts"] = function (exports, require, module) {
"use strict";
// ── Tunable Constants ──────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.SOUND_FREQ = exports.PIECE_NAMES = exports.PIECE_UNICODE = exports.PIECE_VALUES = exports.AI_LEVELS = exports.COLORS = void 0;
exports.COLORS = {
light: '#f0d9b5',
dark: '#b58863',
highlight: 'rgba(255, 255, 0, 0.45)',
lastMove: 'rgba(155, 199, 0, 0.41)',
checkGlow: '#ff0000',
legalDot: 'rgba(0, 0, 0, 0.18)',
captureRing: 'rgba(0, 0, 0, 0.18)',
selected: 'rgba(100, 180, 255, 0.55)',
text: '#333',
panel: '#e8e0d4',
boardBorder: '#6b4423',
gameOver: '#1a1a2e',
};
exports.AI_LEVELS = [
{ label: 'Easy', depth: 1 },
{ label: 'Medium', depth: 2 },
{ label: 'Hard', depth: 3 },
];
exports.PIECE_VALUES = {
P: 100, N: 320, B: 330, R: 500, Q: 900, K: 20000,
};
exports.PIECE_UNICODE = {
wK: '♔', wQ: '♕', wR: '♖', wB: '♗', wN: '♘', wP: '♙',
bK: '♚', bQ: '♛', bR: '♜', bB: '♝', bN: '♞', bP: '♟',
};
exports.PIECE_NAMES = {
K: 'K', Q: 'Q', R: 'R', B: 'B', N: 'N', P: '',
};
exports.SOUND_FREQ = {
move: { freq: 600, dur: 0.08 },
capture: { freq: 300, dur: 0.12 },
check: { freq: 800, dur: 0.2 },
mate: { freq: 200, dur: 0.4 },
};
};
// ── module: src/types.ts ──
__mods["src/types.ts"] = function (exports, require, module) {
"use strict";
// ── Core Types ─────────────────────────────────────────────────────
Object.defineProperty(exports, "__esModule", { value: true });
exports.sq = sq;
exports.fileOf = fileOf;
exports.rankOf = rankOf;
exports.sqName = sqName;
exports.parseSq = parseSq;
// ── Square helpers ─────────────────────────────────────────────────
function sq(file, rank) {
return rank * 8 + file;
}
function fileOf(s) { return s % 8; }
function rankOf(s) { return Math.floor(s / 8); }
function sqName(s) {
return String.fromCharCode(97 + fileOf(s)) + (rankOf(s) + 1);
}
function parseSq(name) {
if (name.length < 2)
return null;
const f = name.charCodeAt(0) - 97;
const r = parseInt(name[1]) - 1;
if (f < 0 || f > 7 || r < 0 || r > 7)
return null;
return sq(f, r);
}
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.initialBoard = initialBoard;
exports.initialCastling = initialCastling;
exports.initialGameState = initialGameState;
exports.isSquareAttacked = isSquareAttacked;
exports.generateLegalMoves = generateLegalMoves;
exports.makeMove = makeMove;
exports.checkGameStatus = checkGameStatus;
exports.isInsufficientMaterial = isInsufficientMaterial;
exports.isInCheck = isInCheck;
const types_1 = require("./types");
// ── Initial Position ───────────────────────────────────────────────
function initialBoard() {
const board = new Array(64).fill(null);
const backRank = ['R', 'N', 'B', 'Q', 'K', 'B', 'N', 'R'];
for (let f = 0; f < 8; f++) {
board[(0, types_1.sq)(f, 0)] = { type: backRank[f], color: 'b' };
board[(0, types_1.sq)(f, 1)] = { type: 'P', color: 'b' };
board[(0, types_1.sq)(f, 6)] = { type: 'P', color: 'w' };
board[(0, types_1.sq)(f, 7)] = { type: backRank[f], color: 'w' };
}
return board;
}
function initialCastling() {
return { wK: true, wQ: true, bK: true, bQ: true };
}
function initialGameState() {
return {
board: initialBoard(),
turn: 'w',
castling: initialCastling(),
enPassant: null,
halfMove: 0,
fullMove: 1,
moveList: [],
capturedWhite: [],
capturedBlack: [],
};
}
// ── Attack Detection (for isSquareAttacked) ────────────────────────
function pawnAttacks(color, board, s) {
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
const dir = color === 'w' ? -1 : 1;
const atk = [];
if (r + dir >= 0 && r + dir <= 7) {
if (f - 1 >= 0)
atk.push((0, types_1.sq)(f - 1, r + dir));
if (f + 1 <= 7)
atk.push((0, types_1.sq)(f + 1, r + dir));
}
return atk;
}
function knightAttacks(s) {
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
const offsets = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]];
const out = [];
for (const [df, dr] of offsets) {
const nf = f + df, nr = r + dr;
if (nf >= 0 && nf <= 7 && nr >= 0 && nr <= 7)
out.push((0, types_1.sq)(nf, nr));
}
return out;
}
function slidingAttacks(s, board) {
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
const out = [];
const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1], [-1, -1], [-1, 1], [1, -1], [1, 1]];
for (let i = 0; i < 8; i++) {
const [df, dr] = dirs[i];
for (let step = 1; step < 8; step++) {
const nf = f + df * step, nr = r + dr * step;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
break;
const ns = (0, types_1.sq)(nf, nr);
out.push(ns);
if (board[ns] !== null)
break;
}
}
return out;
}
function bishopAttacks(s, board) {
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
const out = [];
const dirs = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
for (const [df, dr] of dirs) {
for (let step = 1; step < 8; step++) {
const nf = f + df * step, nr = r + dr * step;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
break;
const ns = (0, types_1.sq)(nf, nr);
out.push(ns);
if (board[ns] !== null)
break;
}
}
return out;
}
function rookAttacks(s, board) {
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
const out = [];
const dirs = [[-1, 0], [1, 0], [0, -1], [0, 1]];
for (const [df, dr] of dirs) {
for (let step = 1; step < 8; step++) {
const nf = f + df * step, nr = r + dr * step;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
break;
const ns = (0, types_1.sq)(nf, nr);
out.push(ns);
if (board[ns] !== null)
break;
}
}
return out;
}
function isSquareAttacked(board, s, byColor) {
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
// Check pawn attacks
const pawnDir = byColor === 'w' ? -1 : 1; // pawns attack from their perspective
const pawnFromRank = r - pawnDir;
if (pawnFromRank >= 0 && pawnFromRank <= 7) {
if (f - 1 >= 0) {
const p = board[(0, types_1.sq)(f - 1, pawnFromRank)];
if (p && p.color === byColor && p.type === 'P')
return true;
}
if (f + 1 <= 7) {
const p = board[(0, types_1.sq)(f + 1, pawnFromRank)];
if (p && p.color === byColor && p.type === 'P')
return true;
}
}
// Knight
for (const ns of knightAttacks(s)) {
const p = board[ns];
if (p && p.color === byColor && p.type === 'N')
return true;
}
// King
const kr = (0, types_1.rankOf)(s), kf = (0, types_1.fileOf)(s);
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (df === 0 && dr === 0)
continue;
const nf = kf + df, nr = kr + dr;
if (nf >= 0 && nf <= 7 && nr >= 0 && nr <= 7) {
const p = board[(0, types_1.sq)(nf, nr)];
if (p && p.color === byColor && p.type === 'K')
return true;
}
}
}
// Sliding (bishop/queen diagonals)
for (const ns of bishopAttacks(s, board)) {
const p = board[ns];
if (p && p.color === byColor && (p.type === 'B' || p.type === 'Q'))
return true;
}
// Sliding (rook/queen straights)
for (const ns of rookAttacks(s, board)) {
const p = board[ns];
if (p && p.color === byColor && (p.type === 'R' || p.type === 'Q'))
return true;
}
return false;
}
// ── Pseudo-Legal Move Generation ───────────────────────────────────
function addPseudoMoves(board, turn, castling, enPassant, moves) {
for (let from = 0; from < 64; from++) {
const piece = board[from];
if (!piece || piece.color !== turn)
continue;
const fr = (0, types_1.rankOf)(from), ff = (0, types_1.fileOf)(from);
switch (piece.type) {
case 'P': {
const dir = turn === 'w' ? -1 : 1;
const startRank = turn === 'w' ? 6 : 1;
const promoRank = turn === 'w' ? 0 : 7;
const singleTo = fr + dir;
// Single push
if (singleTo >= 0 && singleTo <= 7 && !board[(0, types_1.sq)(ff, singleTo)]) {
if (singleTo === promoRank) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ from, to: (0, types_1.sq)(ff, singleTo), piece: 'P', color: turn, promotion: pt });
}
}
else {
moves.push({ from, to: (0, types_1.sq)(ff, singleTo), piece: 'P', color: turn });
}
// Double push
if (fr === startRank) {
const doubleTo = fr + dir * 2;
if (!board[(0, types_1.sq)(ff, doubleTo)]) {
moves.push({ from, to: (0, types_1.sq)(ff, doubleTo), piece: 'P', color: turn });
}
}
}
// Captures
for (const df of [-1, 1]) {
const nf = ff + df;
if (nf < 0 || nf > 7 || singleTo < 0 || singleTo > 7)
continue;
const to = (0, types_1.sq)(nf, singleTo);
const target = board[to];
if (target && target.color !== turn) {
if (singleTo === promoRank) {
for (const pt of ['Q', 'R', 'B', 'N']) {
moves.push({ from, to, piece: 'P', color: turn, captured: target.type, promotion: pt });
}
}
else {
moves.push({ from, to, piece: 'P', color: turn, captured: target.type });
}
}
// En passant
if (enPassant !== null && to === enPassant) {
moves.push({ from, to, piece: 'P', color: turn, captured: 'P', enPassant: true });
}
}
break;
}
case 'N': {
for (const [df, dr] of [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]]) {
const nf = ff + df, nr = fr + dr;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
continue;
const to = (0, types_1.sq)(nf, nr);
const target = board[to];
if (!target || target.color !== turn) {
const cap = target ? target.type : undefined;
moves.push({ from, to, piece: 'N', color: turn, captured: cap });
}
}
break;
}
case 'B': {
for (const [df, dr] of [[-1, -1], [-1, 1], [1, -1], [1, 1]]) {
for (let step = 1; step < 8; step++) {
const nf = ff + df * step, nr = fr + dr * step;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
break;
const to = (0, types_1.sq)(nf, nr);
const target = board[to];
if (!target) {
moves.push({ from, to, piece: 'B', color: turn });
}
else {
if (target.color !== turn)
moves.push({ from, to, piece: 'B', color: turn, captured: target.type });
break;
}
}
}
break;
}
case 'R': {
for (const [df, dr] of [[-1, 0], [1, 0], [0, -1], [0, 1]]) {
for (let step = 1; step < 8; step++) {
const nf = ff + df * step, nr = fr + dr * step;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
break;
const to = (0, types_1.sq)(nf, nr);
const target = board[to];
if (!target) {
moves.push({ from, to, piece: 'R', color: turn });
}
else {
if (target.color !== turn)
moves.push({ from, to, piece: 'R', color: turn, captured: target.type });
break;
}
}
}
break;
}
case 'Q': {
for (const [df, dr] of [[-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1]]) {
for (let step = 1; step < 8; step++) {
const nf = ff + df * step, nr = fr + dr * step;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
break;
const to = (0, types_1.sq)(nf, nr);
const target = board[to];
if (!target) {
moves.push({ from, to, piece: 'Q', color: turn });
}
else {
if (target.color !== turn)
moves.push({ from, to, piece: 'Q', color: turn, captured: target.type });
break;
}
}
}
break;
}
case 'K': {
for (let df = -1; df <= 1; df++) {
for (let dr = -1; dr <= 1; dr++) {
if (df === 0 && dr === 0)
continue;
const nf = ff + df, nr = fr + dr;
if (nf < 0 || nf > 7 || nr < 0 || nr > 7)
continue;
const to = (0, types_1.sq)(nf, nr);
const target = board[to];
if (!target || target.color !== turn) {
const cap = target ? target.type : undefined;
moves.push({ from, to, piece: 'K', color: turn, captured: cap });
}
}
}
// Castling
const kRight = turn === 'w' ? 'wK' : 'bK';
const qRight = turn === 'w' ? 'wQ' : 'bQ';
const baseRank = turn === 'w' ? 7 : 0;
const hr = (0, types_1.sq)(7, baseRank);
const ar = (0, types_1.sq)(0, baseRank);
if (castling[kRight]) {
const krPiece = board[hr];
const ks = (0, types_1.sq)(6, baseRank);
if (krPiece && krPiece.type === 'R' && krPiece.color === turn && !board[ks]) {
if (!isSquareAttacked(board, (0, types_1.sq)(4, baseRank), opposite(turn)) &&
!isSquareAttacked(board, (0, types_1.sq)(5, baseRank), opposite(turn)) &&
!isSquareAttacked(board, (0, types_1.sq)(6, baseRank), opposite(turn))) {
moves.push({ from: (0, types_1.sq)(4, baseRank), to: (0, types_1.sq)(6, baseRank), piece: 'K', color: turn, castling: 'K' });
}
}
}
if (castling[qRight]) {
const qrPiece = board[ar];
const qs = (0, types_1.sq)(2, baseRank);
const bs = (0, types_1.sq)(3, baseRank);
if (qrPiece && qrPiece.type === 'R' && qrPiece.color === turn && !board[qs] && !board[bs]) {
if (!isSquareAttacked(board, (0, types_1.sq)(4, baseRank), opposite(turn)) &&
!isSquareAttacked(board, (0, types_1.sq)(3, baseRank), opposite(turn)) &&
!isSquareAttacked(board, (0, types_1.sq)(2, baseRank), opposite(turn))) {
moves.push({ from: (0, types_1.sq)(4, baseRank), to: (0, types_1.sq)(2, baseRank), piece: 'K', color: turn, castling: 'Q' });
}
}
}
break;
}
}
}
}
function opposite(c) { return c === 'w' ? 'b' : 'w'; }
// ── Apply Move ──────────────────────────────────────────────────────
function applyMove(board, move) {
const nb = [...board];
const fromPiece = nb[move.from];
nb[move.to] = move.promotion
? { type: move.promotion, color: move.color }
: { ...fromPiece };
nb[move.from] = null;
// En passant capture
if (move.enPassant) {
const capturedRank = (0, types_1.rankOf)(move.from);
nb[(0, types_1.sq)((0, types_1.fileOf)(move.to), capturedRank)] = null;
}
// Castling rook move
if (move.castling) {
const rank = move.color === 'w' ? 7 : 0;
if (move.castling === 'K') {
nb[(0, types_1.sq)(5, rank)] = nb[(0, types_1.sq)(7, rank)];
nb[(0, types_1.sq)(7, rank)] = null;
}
else {
nb[(0, types_1.sq)(3, rank)] = nb[(0, types_1.sq)(0, rank)];
nb[(0, types_1.sq)(0, rank)] = null;
}
}
return nb;
}
function findKing(board, color) {
for (let i = 0; i < 64; i++) {
if (board[i]?.type === 'K' && board[i]?.color === color)
return i;
}
return -1;
}
// ── Legal Move Filtering ───────────────────────────────────────────
function generateLegalMoves(state) {
const { board, turn, castling, enPassant } = state;
const pseudo = [];
addPseudoMoves(board, turn, castling, enPassant, pseudo);
return pseudo.filter(move => {
const nb = applyMove(board, move);
const kingSq = findKing(nb, turn);
if (kingSq < 0)
return false; // shouldn't happen
return !isSquareAttacked(nb, kingSq, opposite(turn));
});
}
// ── Make Move (returns new state) ──────────────────────────────────
function makeMove(state, move) {
const nb = applyMove(state.board, move);
const newColor = opposite(state.turn);
// Update castling rights
const nc = { ...state.castling };
// King moved
if (move.piece === 'K') {
if (move.color === 'w') {
nc.wK = false;
nc.wQ = false;
}
else {
nc.bK = false;
nc.bQ = false;
}
}
// Rook moved or captured
if (move.piece === 'R') {
if (move.from === (0, types_1.sq)(7, 7))
nc.wK = false;
if (move.from === (0, types_1.sq)(0, 7))
nc.wQ = false;
if (move.from === (0, types_1.sq)(7, 0))
nc.bK = false;
if (move.from === (0, types_1.sq)(0, 0))
nc.bQ = false;
}
if (move.captured === 'R') {
if (move.to === (0, types_1.sq)(7, 7))
nc.wK = false;
if (move.to === (0, types_1.sq)(0, 7))
nc.wQ = false;
if (move.to === (0, types_1.sq)(7, 0))
nc.bK = false;
if (move.to === (0, types_1.sq)(0, 0))
nc.bQ = false;
}
// En passant square
let newEnPassant = null;
if (move.piece === 'P' && Math.abs((0, types_1.rankOf)(move.to) - (0, types_1.rankOf)(move.from)) === 2) {
newEnPassant = (0, types_1.sq)((0, types_1.fileOf)(move.from), ((0, types_1.rankOf)(move.from) + (0, types_1.rankOf)(move.to)) / 2);
}
// Half-move clock
let halfMove = state.halfMove + 1;
if (move.piece === 'P' || move.captured)
halfMove = 0;
// Full move number
const fullMove = newColor === 'w' ? state.fullMove + 1 : state.fullMove;
// Captured pieces
const capturedWhite = [...state.capturedWhite];
const capturedBlack = [...state.capturedBlack];
if (move.captured) {
if (move.captured === 'P' || move.captured === 'N' || move.captured === 'B' ||
move.captured === 'R' || move.captured === 'Q') {
// The captured piece belongs to the opponent
if (state.turn === 'w')
capturedBlack.push(move.captured);
else
capturedWhite.push(move.captured);
}
}
// Algebraic notation
const notation = toAlgebraic(state, move, nb, newColor, nc, newEnPassant);
return {
board: nb,
turn: newColor,
castling: nc,
enPassant: newEnPassant,
halfMove,
fullMove,
moveList: [...state.moveList, notation],
capturedWhite,
capturedBlack,
};
}
// ── Algebraic Notation ─────────────────────────────────────────────
function toAlgebraic(state, move, newBoard, nextColor, newCastling, newEnPassant) {
if (move.castling === 'K')
return 'O-O';
if (move.castling === 'Q')
return 'O-O-O';
const { board } = state;
const piece = move.piece;
let n = '';
if (piece !== 'P') {
n = piece;
// Disambiguation
const legal = generateLegalMoves(state);
const ambiguous = legal.filter(m => m.piece === piece && m.to === move.to && m.from !== move.from);
if (ambiguous.length > 0) {
const sameFile = ambiguous.some(m => (0, types_1.fileOf)(m.from) === (0, types_1.fileOf)(move.from));
const sameRank = ambiguous.some(m => (0, types_1.rankOf)(m.from) === (0, types_1.rankOf)(move.from));
if (!sameFile) {
n += String.fromCharCode(97 + (0, types_1.fileOf)(move.from));
}
else if (!sameRank) {
n += ((0, types_1.rankOf)(move.from) + 1);
}
else {
n += String.fromCharCode(97 + (0, types_1.fileOf)(move.from)) + ((0, types_1.rankOf)(move.from) + 1);
}
}
}
const isCapture = !!move.captured;
if (isCapture) {
if (piece === 'P')
n += String.fromCharCode(97 + (0, types_1.fileOf)(move.from));
n += 'x';
}
n += (0, types_1.sqName)(move.to);
if (move.enPassant)
n += ' e.p.';
if (move.promotion)
n += '=' + move.promotion;
// Check / Checkmate
const nextState = {
board: newBoard,
turn: nextColor,
castling: newCastling,
enPassant: newEnPassant,
halfMove: 0,
fullMove: 0,
moveList: [],
capturedWhite: [],
capturedBlack: [],
};
const nextMoves = generateLegalMoves(nextState);
const kingSq = findKing(newBoard, nextColor);
const inCheck = kingSq >= 0 && isSquareAttacked(newBoard, kingSq, state.turn);
if (inCheck) {
n += nextMoves.length === 0 ? '#' : '+';
}
return n;
}
// ── Game Status ────────────────────────────────────────────────────
function checkGameStatus(state) {
const legal = generateLegalMoves(state);
if (legal.length === 0) {
const kingSq = findKing(state.board, state.turn);
if (kingSq >= 0 && isSquareAttacked(state.board, kingSq, opposite(state.turn))) {
return { status: 'checkmate', winner: opposite(state.turn) };
}
return { status: 'stalemate' };
}
if (state.halfMove >= 100) {
return { status: 'fiftyMove' };
}
if (isInsufficientMaterial(state.board)) {
return { status: 'insufficient' };
}
return { status: 'playing' };
}
function isInsufficientMaterial(board) {
const white = [];
const black = [];
for (const p of board) {
if (!p)
continue;
if (p.color === 'w')
white.push(p);
else
black.push(p);
}
// K vs K
if (white.length === 1 && black.length === 1)
return true;
// K+B vs K or K+N vs K
if (white.length === 1 && black.length === 2) {
const minor = black.find(p => p.type === 'B' || p.type === 'N');
if (minor && black.every(p => p.type === 'K' || p.type === 'B' || p.type === 'N'))
return true;
}
if (black.length === 1 && white.length === 2) {
const minor = white.find(p => p.type === 'B' || p.type === 'N');
if (minor && white.every(p => p.type === 'K' || p.type === 'B' || p.type === 'N'))
return true;
}
return false;
}
function isInCheck(state, color) {
const kingSq = findKing(state.board, color);
if (kingSq < 0)
return false;
return isSquareAttacked(state.board, kingSq, opposite(color));
}
};
// ── module: src/ai.ts ──
__mods["src/ai.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.evaluate = evaluate;
exports.findBestMove = findBestMove;
exports.cancelSearch = cancelSearch;
const constants_1 = require("./constants");
const engine_1 = require("./engine");
const types_1 = require("./types");
// ── Piece-Square Tables (from White's perspective, flip for Black) ──
const PST_PAWN = [
0, 0, 0, 0, 0, 0, 0, 0,
50, 50, 50, 50, 50, 50, 50, 50,
10, 10, 20, 30, 30, 20, 10, 10,
5, 5, 10, 25, 25, 10, 5, 5,
0, 0, 0, 20, 20, 0, 0, 0,
5, -5, -10, 0, 0, -10, -5, 5,
5, 10, 10, -20, -20, 10, 10, 5,
0, 0, 0, 0, 0, 0, 0, 0,
];
const PST_KNIGHT = [
-50, -40, -30, -30, -30, -30, -40, -50,
-40, -20, 0, 0, 0, 0, -20, -40,
-30, 0, 10, 15, 15, 10, 0, -30,
-30, 5, 15, 20, 20, 15, 5, -30,
-30, 0, 15, 20, 20, 15, 0, -30,
-30, 5, 10, 15, 15, 10, 5, -30,
-40, -20, 0, 5, 5, 0, -20, -40,
-50, -40, -30, -30, -30, -30, -40, -50,
];
const PST_BISHOP = [
-20, -10, -10, -10, -10, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 5, 5, 10, 10, 5, 5, -10,
-10, 0, 10, 10, 10, 10, 0, -10,
-10, 10, 10, 10, 10, 10, 10, -10,
-10, 5, 0, 0, 0, 0, 5, -10,
-20, -10, -10, -10, -10, -10, -10, -20,
];
const PST_ROOK = [
0, 0, 0, 0, 0, 0, 0, 0,
5, 10, 10, 10, 10, 10, 10, 5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
-5, 0, 0, 0, 0, 0, 0, -5,
0, 0, 0, 5, 5, 0, 0, 0,
];
const PST_QUEEN = [
-20, -10, -10, -5, -5, -10, -10, -20,
-10, 0, 0, 0, 0, 0, 0, -10,
-10, 0, 5, 5, 5, 5, 0, -10,
-5, 0, 5, 5, 5, 5, 0, -5,
0, 0, 5, 5, 5, 5, 0, -5,
-10, 5, 5, 5, 5, 5, 0, -10,
-10, 0, 5, 0, 0, 0, 0, -10,
-20, -10, -10, -5, -5, -10, -10, -20,
];
const PST_KING_MID = [
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-30, -40, -40, -50, -50, -40, -40, -30,
-20, -30, -30, -40, -40, -30, -30, -20,
-10, -20, -20, -20, -20, -20, -20, -10,
20, 20, 0, 0, 0, 0, 20, 20,
20, 30, 10, 0, 0, 10, 30, 20,
];
const PST = {
P: PST_PAWN, N: PST_KNIGHT, B: PST_BISHOP,
R: PST_ROOK, Q: PST_QUEEN, K: PST_KING_MID,
};
function pstScore(piece, s) {
const t = PST[piece.type];
if (!t)
return 0;
if (piece.color === 'w')
return t[s];
// Flip for black: mirror rank
const r = (0, types_1.rankOf)(s), f = (0, types_1.fileOf)(s);
return t[(0, types_1.sq)(f, 7 - r)];
}
// ── Evaluation ─────────────────────────────────────────────────────
function evaluate(state) {
let score = 0;
for (let i = 0; i < 64; i++) {
const p = state.board[i];
if (!p)
continue;
const val = constants_1.PIECE_VALUES[p.type] + pstScore(p, i);
if (p.color === 'w')
score += val;
else
score -= val;
}
// Mobility bonus (simplified)
return score;
}
// ── Move Ordering ──────────────────────────────────────────────────
function orderMoves(moves) {
return [...moves].sort((a, b) => {
// Captures first (MVV-LVA style)
const aCap = a.captured ? constants_1.PIECE_VALUES[a.captured] : 0;
const bCap = b.captured ? constants_1.PIECE_VALUES[b.captured] : 0;
if (aCap !== bCap)
return bCap - aCap;
// Promotions next
if (a.promotion && !b.promotion)
return -1;
if (!a.promotion && b.promotion)
return 1;
return 0;
});
}
// ── Quiescence Search ──────────────────────────────────────────────
function quiesce(state, alpha, beta, isMax) {
const standPat = evaluate(state);
if (isMax) {
if (standPat >= beta)
return beta;
if (alpha < standPat)
alpha = standPat;
}
else {
if (standPat <= alpha)
return alpha;
if (beta > standPat)
beta = standPat;
}
const moves = (0, engine_1.generateLegalMoves)(state);
const ordered = orderMoves(moves).filter(m => m.captured || m.promotion);
for (const move of ordered) {
const ns = (0, engine_1.makeMove)(state, move);
const score = quiesce(ns, alpha, beta, !isMax);
if (isMax) {
if (score > alpha)
alpha = score;
if (alpha >= beta)
return beta;
}
else {
if (score < beta)
beta = score;
if (alpha >= beta)
return alpha;
}
}
return isMax ? alpha : beta;
}
// ── Minimax with Alpha-Beta ────────────────────────────────────────
let nodesSearched = 0;
let searchActive = false;
function minimax(state, depth, alpha, beta, isMax) {
if (!searchActive)
return evaluate(state);
nodesSearched++;
if (depth === 0) {
return quiesce(state, alpha, beta, isMax);
}
const moves = (0, engine_1.generateLegalMoves)(state);
if (moves.length === 0) {
// Checkmate or stalemate
const { board, turn } = state;
let kingSq = -1;
for (let i = 0; i < 64; i++) {
if (board[i]?.type === 'K' && board[i]?.color === turn) {
kingSq = i;
break;
}
}
const opp = turn === 'w' ? 'b' : 'w';
if (kingSq >= 0 && (0, engine_1.isSquareAttacked)(board, kingSq, opp)) {
return isMax ? -99999 + (3 - depth) : 99999 - (3 - depth);
}
return 0; // stalemate
}
const ordered = orderMoves(moves);
if (isMax) {
let best = -Infinity;
for (const move of ordered) {
const ns = (0, engine_1.makeMove)(state, move);
const score = minimax(ns, depth - 1, alpha, beta, false);
if (score > best)
best = score;
if (best > alpha)
alpha = best;
if (alpha >= beta)
break;
}
return best;
}
else {
let best = Infinity;
for (const move of ordered) {
const ns = (0, engine_1.makeMove)(state, move);
const score = minimax(ns, depth - 1, alpha, beta, true);
if (score < best)
best = score;
if (best < beta)
beta = best;
if (alpha >= beta)
break;
}
return best;
}
}
// ── AI Move Selection (async, chunked) ─────────────────────────────
function findBestMove(state, aiColor, depth, onProgress) {
return new Promise((resolve) => {
searchActive = true;
nodesSearched = 0;
const moves = (0, engine_1.generateLegalMoves)(state);
if (moves.length === 0) {
searchActive = false;
resolve(null);
return;
}
const ordered = orderMoves(moves);
const isMax = aiColor === 'w';
// Process moves in chunks to avoid blocking the UI
let idx = 0;
let bestMove = null;
let bestScore = isMax ? -Infinity : Infinity;
function processChunk() {
if (!searchActive) {
resolve(bestMove);
return;
}
const chunkSize = Math.max(1, Math.ceil(ordered.length / 4));
const end = Math.min(idx + chunkSize, ordered.length);
for (; idx < end; idx++) {
const move = ordered[idx];
const ns = (0, engine_1.makeMove)(state, move);
const score = minimax(ns, depth - 1, -Infinity, Infinity, !isMax);
if (onProgress)
onProgress(nodesSearched);
if (isMax) {
if (score > bestScore) {
bestScore = score;
bestMove = move;
}
}
else {
if (score < bestScore) {
bestScore = score;
bestMove = move;
}
}
}
if (idx < ordered.length) {
setTimeout(processChunk, 0);
}
else {
searchActive = false;
resolve(bestMove);
}
}
setTimeout(processChunk, 10);
});
}
function cancelSearch() {
searchActive = false;
}
};
// ── module: src/renderer.ts ──
__mods["src/renderer.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Renderer = void 0;
const constants_1 = require("./constants");
const constants_2 = require("./constants");
const types_1 = require("./types");
const engine_1 = require("./engine");
class Renderer {
constructor(canvas, callbacks) {
this.size = 560;
this.sqSize = 70;
this.flipped = false;
this.selected = null;
this.legalMoves = [];
this.lastMove = null;
this.pendingPromo = null;
this.dragSquare = null;
this.dragX = 0;
this.dragY = 0;
this.dragging = false;
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.canvas.width = this.size;
this.canvas.height = this.size;
this.callbacks = callbacks;
this.wireEvents();
}
wireEvents() {
this.canvas.addEventListener('mousedown', (e) => this.onMouseDown(e));
this.canvas.addEventListener('mousemove', (e) => this.onMouseMove(e));
this.canvas.addEventListener('mouseup', (e) => this.onMouseUp(e));
this.canvas.addEventListener('mouseleave', (e) => this.onMouseUp(e));
this.canvas.addEventListener('touchstart', (e) => this.onTouchStart(e), { passive: false });
this.canvas.addEventListener('touchmove', (e) => this.onTouchMove(e), { passive: false });
this.canvas.addEventListener('touchend', (e) => this.onTouchEnd(e), { passive: false });
}
getSquareFromCoords(clientX, clientY) {
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
const file = this.flipped ? 7 - Math.floor(x / this.sqSize) : Math.floor(x / this.sqSize);
const rank = this.flipped ? 7 - Math.floor(y / this.sqSize) : Math.floor(y / this.sqSize);
if (file < 0 || file > 7 || rank < 0 || rank > 7)
return null;
return { sq: (0, types_1.sq)(file, rank), x, y };
}
onMouseDown(e) {
const hit = this.getSquareFromCoords(e.clientX, e.clientY);
if (!hit)
return;
if (this.pendingPromo)
return;
this.dragSquare = hit.sq;
this.dragX = hit.x;
this.dragY = hit.y;
this.dragging = false;
}
onMouseMove(e) {
if (this.dragSquare === null || this.pendingPromo)
return;
const hit = this.getSquareFromCoords(e.clientX, e.clientY);
if (hit) {
const dx = hit.x - this.dragX;
const dy = hit.y - this.dragY;
if (!this.dragging && (Math.abs(dx) > 8 || Math.abs(dy) > 8)) {
this.dragging = true;
}
this.dragX = hit.x;
this.dragY = hit.y;
}
}
onMouseUp(e) {
if (this.dragSquare === null || this.pendingPromo)
return;
const hit = this.getSquareFromCoords(e.clientX, e.clientY);
const to = hit ? hit.sq : null;
if (this.dragging && to !== null && to !== this.dragSquare) {
// Drag-and-drop move
this.callbacks.onSquareDragStart(this.dragSquare);
this.callbacks.onSquareDragEnd(this.dragSquare, to);
}
else if (!this.dragging) {
// Click
this.callbacks.onSquareClick(this.dragSquare);
}
this.dragSquare = null;
this.dragging = false;
}
onTouchStart(e) {
e.preventDefault();
if (e.touches.length !== 1)
return;
const t = e.touches[0];
const hit = this.getSquareFromCoords(t.clientX, t.clientY);
if (!hit)
return;
if (this.pendingPromo)
return;
this.dragSquare = hit.sq;
this.dragX = hit.x;
this.dragY = hit.y;
this.dragging = false;
}
onTouchMove(e) {
e.preventDefault();
if (this.dragSquare === null || this.pendingPromo)
return;
if (e.touches.length !== 1)
return;
const t = e.touches[0];
const hit = this.getSquareFromCoords(t.clientX, t.clientY);
if (hit) {
const dx = hit.x - this.dragX;
const dy = hit.y - this.dragY;
if (!this.dragging && (Math.abs(dx) > 12 || Math.abs(dy) > 12)) {
this.dragging = true;
}
this.dragX = hit.x;
this.dragY = hit.y;
}
}
onTouchEnd(e) {
e.preventDefault();
if (this.dragSquare === null || this.pendingPromo)
return;
const to = this.dragging && this.changedDragSquare() ? this.dragSquare : null;
if (this.dragging && to !== null && to !== this.dragSquare) {
this.callbacks.onSquareDragStart(this.dragSquare);
this.callbacks.onSquareDragEnd(this.dragSquare, to);
}
else if (!this.dragging) {
this.callbacks.onSquareClick(this.dragSquare);
}
this.dragSquare = null;
this.dragging = false;
}
changedDragSquare() {
// The dragSquare is the start; on touch end we check if moved
// For simplicity, touch end just uses the original click
return false;
}
setFlipped(flipped) {
this.flipped = flipped;
}
setSelected(s) {
this.selected = s;
}
setLegalMoves(moves) {
this.legalMoves = moves;
}
setLastMove(m) {
this.lastMove = m;
}
setPendingPromo(promo) {
this.pendingPromo = promo;
}
render(state) {
const ctx = this.ctx;
const sz = this.sqSize;
// Draw board
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
const x = drawFile * sz, y = drawRank * sz;
const isLight = (r + f) % 2 === 0;
ctx.fillStyle = isLight ? constants_1.COLORS.light : constants_1.COLORS.dark;
ctx.fillRect(x, y, sz, sz);
}
}
// Last move highlight
if (this.lastMove) {
for (const s of [this.lastMove.from, this.lastMove.to]) {
const f = (0, types_1.fileOf)(s), r = (0, types_1.rankOf)(s);
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
ctx.fillStyle = constants_1.COLORS.lastMove;
ctx.fillRect(drawFile * sz, drawRank * sz, sz, sz);
}
}
// Selected highlight
if (this.selected !== null) {
const f = (0, types_1.fileOf)(this.selected), r = (0, types_1.rankOf)(this.selected);
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
ctx.fillStyle = constants_1.COLORS.selected;
ctx.fillRect(drawFile * sz, drawRank * sz, sz, sz);
}
// Legal move dots / capture rings
for (const move of this.legalMoves) {
const f = (0, types_1.fileOf)(move.to), r = (0, types_1.rankOf)(move.to);
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
const cx = drawFile * sz + sz / 2;
const cy = drawRank * sz + sz / 2;
if (state.board[move.to]) {
// Capture ring
ctx.strokeStyle = constants_1.COLORS.captureRing;
ctx.lineWidth = 4;
ctx.beginPath();
ctx.arc(cx, cy, sz / 2 - 4, 0, Math.PI * 2);
ctx.stroke();
}
else {
// Dot
ctx.fillStyle = constants_1.COLORS.legalDot;
ctx.beginPath();
ctx.arc(cx, cy, sz / 6, 0, Math.PI * 2);
ctx.fill();
}
}
// Check glow on king
const inCheckColor = state.turn;
const kingSq = this.findKing(state.board, inCheckColor);
if (kingSq >= 0 && (0, engine_1.isInCheck)(state, inCheckColor)) {
const f = (0, types_1.fileOf)(kingSq), r = (0, types_1.rankOf)(kingSq);
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
// Red glow
const grad = ctx.createRadialGradient(drawFile * sz + sz / 2, drawRank * sz + sz / 2, 0, drawFile * sz + sz / 2, drawRank * sz + sz / 2, sz / 2);
grad.addColorStop(0, 'rgba(255, 50, 50, 0.6)');
grad.addColorStop(1, 'rgba(255, 0, 0, 0.0)');
ctx.fillStyle = grad;
ctx.fillRect(drawFile * sz, drawRank * sz, sz, sz);
}
// Draw pieces
ctx.font = `${sz * 0.75}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let r = 0; r < 8; r++) {
for (let f = 0; f < 8; f++) {
const s = (0, types_1.sq)(f, r);
const piece = state.board[s];
if (!piece)
continue;
// Skip dragging piece (drawn separately)
if (this.dragging && this.dragSquare === s)
continue;
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
const cx = drawFile * sz + sz / 2;
const cy = drawRank * sz + sz / 2;
const key = piece.color + piece.type;
const glyph = constants_2.PIECE_UNICODE[key];
if (!glyph)
continue;
// Shadow for depth
ctx.fillStyle = 'rgba(0,0,0,0.25)';
ctx.fillText(glyph, cx + 2, cy + 2);
// Piece
ctx.fillStyle = piece.color === 'w' ? '#fff' : '#1a1a1a';
ctx.strokeStyle = piece.color === 'w' ? '#1a1a1a' : '#fff';
ctx.lineWidth = 0.5;
ctx.fillText(glyph, cx, cy);
ctx.strokeText(glyph, cx, cy);
}
}
// Draw dragging piece
if (this.dragging && this.dragSquare !== null) {
const piece = state.board[this.dragSquare];
if (piece) {
const key = piece.color + piece.type;
const glyph = constants_2.PIECE_UNICODE[key];
if (glyph) {
ctx.font = `${sz * 0.85}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillStyle = piece.color === 'w' ? '#fff' : '#1a1a1a';
ctx.strokeStyle = piece.color === 'w' ? '#1a1a1a' : '#fff';
ctx.lineWidth = 1;
ctx.fillText(glyph, this.dragX, this.dragY);
ctx.strokeText(glyph, this.dragX, this.dragY);
}
}
}
// Board coordinates
ctx.font = '11px sans-serif';
ctx.fillStyle = 'rgba(0,0,0,0.45)';
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
for (let f = 0; f < 8; f++) {
const drawFile = this.flipped ? 7 - f : f;
ctx.fillText(String.fromCharCode(97 + f), drawFile * sz + sz / 2, this.size - 3);
}
ctx.textAlign = 'right';
ctx.textBaseline = 'middle';
for (let r = 0; r < 8; r++) {
const drawRank = this.flipped ? 7 - r : r;
ctx.fillText((8 - r).toString(), 10, drawRank * sz + sz / 2);
}
// Promotion overlay
if (this.pendingPromo) {
this.drawPromoOverlay();
}
// Border
ctx.strokeStyle = constants_1.COLORS.boardBorder;
ctx.lineWidth = 4;
ctx.strokeRect(2, 2, this.size - 4, this.size - 4);
}
findKing(board, color) {
for (let i = 0; i < 64; i++) {
if (board[i]?.type === 'K' && board[i]?.color === color)
return i;
}
return -1;
}
drawPromoOverlay() {
if (!this.pendingPromo)
return;
const ctx = this.ctx;
const sz = this.sqSize;
const f = (0, types_1.fileOf)(this.pendingPromo.to);
const r = (0, types_1.rankOf)(this.pendingPromo.to);
const drawFile = this.flipped ? 7 - f : f;
const drawRank = this.flipped ? 7 - r : r;
// Dim background
ctx.fillStyle = 'rgba(0,0,0,0.4)';
ctx.fillRect(0, 0, this.size, this.size);
// Promotion panel
const panelW = sz * 4;
const panelH = sz;
const px = (this.size - panelW) / 2;
const py = (this.size - panelH) / 2;
ctx.fillStyle = '#f5eedd';
ctx.strokeStyle = '#6b4423';
ctx.lineWidth = 3;
ctx.fillRect(px, py, panelW, panelH);
ctx.strokeRect(px, py, panelW, panelH);
const pieces = ['Q', 'R', 'B', 'N'];
ctx.font = `${sz * 0.7}px serif`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
for (let i = 0; i < 4; i++) {
const cx = px + (i + 0.5) * sz;
const cy = py + panelH / 2;
const key = 'w' + pieces[i]; // always white for display
const glyph = constants_2.PIECE_UNICODE[key];
if (glyph) {
ctx.fillStyle = '#1a1a1a';
ctx.fillText(glyph, cx, cy);
}
}
}
// Returns the promotion piece type chosen, or null if not on a promo piece
onPromoClick(clientX, clientY) {
if (!this.pendingPromo)
return null;
const sz = this.sqSize;
const panelW = sz * 4;
const panelH = sz;
const px = (this.size - panelW) / 2;
const py = (this.size - panelH) / 2;
const rect = this.canvas.getBoundingClientRect();
const scaleX = this.canvas.width / rect.width;
const scaleY = this.canvas.height / rect.height;
const x = (clientX - rect.left) * scaleX;
const y = (clientY - rect.top) * scaleY;
if (x >= px && x <= px + panelW && y >= py && y <= py + panelH) {
const idx = Math.floor((x - px) / sz);
if (idx >= 0 && idx < 4) {
const pieces = ['Q', 'R', 'B', 'N'];
return pieces[idx];
}
}
return null;
}
}
exports.Renderer = Renderer;
};
// ── module: src/sound.ts ──
__mods["src/sound.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.SoundEngine = void 0;
const constants_1 = require("./constants");
// ── Sound Engine (Web Audio API) ────────────────────────────────────
class SoundEngine {
constructor() {
this.ctx = null;
this.muted = false;
}
getAudioContext() {
if (!this.ctx) {
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
}
if (this.ctx.state === 'suspended') {
this.ctx.resume();
}
return this.ctx;
}
play(type) {
if (this.muted)
return;
const audio = this.getAudioContext();
const config = constants_1.SOUND_FREQ[type];
const osc = audio.createOscillator();
const gain = audio.createGain();
osc.type = type === 'capture' ? 'square' : 'sine';
osc.frequency.value = config.freq;
gain.gain.setValueAtTime(0.3, audio.currentTime);
gain.gain.exponentialRampToValueAtTime(0.001, audio.currentTime + config.dur);
osc.connect(gain);
gain.connect(audio.destination);
osc.start();
osc.stop(audio.currentTime + config.dur + 0.05);
}
mute() { this.muted = true; }
unmute() { this.muted = false; }
toggle() { this.muted = !this.muted; return this.muted; }
isMuted() { return this.muted; }
}
exports.SoundEngine = SoundEngine;
};
// ── module: src/game.ts ──
__mods["src/game.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.GameController = void 0;
const engine_1 = require("./engine");
const ai_1 = require("./ai");
const sound_1 = require("./sound");
const types_1 = require("./types");
// ── Persistence ─────────────────────────────────────────────────────
const STORAGE_KEY = 'chess-game-save';
function boardToSave(board) {
return board.map(p => p ? `${p.color}${p.type}` : '');
}
function boardFromSave(arr) {
return arr.map(s => {
if (!s)
return null;
return { color: s[0], type: s.slice(1) };
});
}
class GameController {
constructor() {
// Callbacks
this.onMoveCompleteCb = null;
this.onGameOverCb = null;
this.onAiThinkingCb = null;
// ── Player Input ──────────────────────────────────────────────────
this.pendingPromo = null;
this.sound = new sound_1.SoundEngine();
this.playerColor = 'w';
this.aiDepth = 3;
this.analysisMode = false;
this.aiThinking = false;
this.lastMove = null;
this.history = [];
const saved = this.tryLoad();
if (saved) {
this.restore(saved);
}
else {
this.state = (0, engine_1.initialGameState)();
this.aiColor = this.analysisMode ? null : (this.playerColor === 'w' ? 'b' : 'w');
this.history = [this.state];
}
}
// ── Public getters ────────────────────────────────────────────────
getState() { return this.state; }
getLastMove() { return this.lastMove; }
isAiThinking() { return this.aiThinking; }
getAiColor() { return this.aiColor; }
getPlayerColor() { return this.playerColor; }
isAnalysisMode() { return this.analysisMode; }
getSound() { return this.sound; }
getAiDepth() { return this.aiDepth; }
// ── Config ────────────────────────────────────────────────────────
setAiDepth(depth) { this.aiDepth = depth; }
startNewGame(color, depth, analysis) {
(0, ai_1.cancelSearch)();
this.aiThinking = false;
this.pendingPromo = null;
this.state = (0, engine_1.initialGameState)();
this.lastMove = null;
this.playerColor = color;
this.aiDepth = depth;
this.analysisMode = analysis;
this.aiColor = analysis ? null : (color === 'w' ? 'b' : 'w');
this.history = [this.state];
this.save();
this.notifyMove({});
}
setAnalysisMode(mode) {
this.analysisMode = mode;
this.aiColor = mode ? null : (this.playerColor === 'w' ? 'b' : 'w');
}
// ── Callbacks ─────────────────────────────────────────────────────
onMoveComplete(cb) { this.onMoveCompleteCb = cb; }
onGameOver(cb) { this.onGameOverCb = cb; }
onAiThinking(cb) { this.onAiThinkingCb = cb; }
// ── Debug ─────────────────────────────────────────────────────────
legalMoves() { return (0, engine_1.generateLegalMoves)(this.state); }
fen() {
let fen = '';
let empty = 0;
for (let r = 7; r >= 0; r--) {
for (let f = 0; f < 8; f++) {
const p = this.state.board[r * 8 + f];
if (!p) {
empty++;
}
else {
if (empty > 0) {
fen += empty.toString();
empty = 0;
}
fen += p.color === 'w' ? p.type : p.type.toLowerCase();
}
}
if (empty > 0) {
fen += empty.toString();
empty = 0;
}
if (r > 0)
fen += '/';
}
fen += ` ${this.state.turn}`;
let castle = '';
if (this.state.castling.wK)
castle += 'K';
if (this.state.castling.wQ)
castle += 'Q';
if (this.state.castling.bK)
castle += 'k';
if (this.state.castling.bQ)
castle += 'q';
fen += ` ${castle || '-'}`;
fen += ` ${this.state.enPassant !== null ? (0, types_1.sqName)(this.state.enPassant) : '-'}`;
fen += ` ${this.state.halfMove} ${this.state.fullMove}`;
return fen;
}
hasPendingPromo() { return this.pendingPromo !== null; }
setPendingPromo(from, to) {
this.pendingPromo = { from, to };
}
selectSquare(from) {
if (this.aiThinking)
return;
if (this.state.turn !== this.playerColor)
return;
if (this.pendingPromo)
return;
// Store selected square, legal moves will be computed by renderer
}
tryMove(from, to) {
if (this.aiThinking)
return false;
if (this.state.turn !== this.playerColor)
return false;
if (this.pendingPromo)
return false;
const legal = (0, engine_1.generateLegalMoves)(this.state);
const matches = legal.filter(m => m.from === from && m.to === to);
if (matches.length === 0)
return false;
// Check for promotion
const promoMoves = matches.filter(m => m.promotion);
if (promoMoves.length > 0) {
this.pendingPromo = { from, to };
return false; // Tell UI to show picker
}
this.executeMove(matches[0]);
return true;
}
confirmPromotion(piece) {
if (!this.pendingPromo)
return false;
const { from, to } = this.pendingPromo;
const legal = (0, engine_1.generateLegalMoves)(this.state);
const move = legal.find(m => m.from === from && m.to === to && m.promotion === piece);
if (move) {
this.pendingPromo = null;
this.executeMove(move);
return true;
}
return false;
}
cancelPromo() {
this.pendingPromo = null;
}
// ── Execute Move ──────────────────────────────────────────────────
executeMove(move) {
this.state = (0, engine_1.makeMove)(this.state, move);
this.lastMove = move;
this.history.push(this.state);
// Sound
if (move.captured)
this.sound.play('capture');
else
this.sound.play('move');
const status = (0, engine_1.checkGameStatus)(this.state);
if (status.status !== 'playing') {
this.sound.play(status.status === 'checkmate' ? 'mate' : 'check');
if (this.onGameOverCb)
this.onGameOverCb(status.status);
}
else {
// Check sound
if ((0, engine_1.isInCheck)(this.state, this.state.turn)) {
this.sound.play('check');
}
}
this.notifyMove(move);
this.save();
// AI turn
if (this.aiColor && this.state.turn === this.aiColor && status.status === 'playing') {
this.startAiTurn();
}
}
notifyMove(move) {
if (this.onMoveCompleteCb)
this.onMoveCompleteCb(move, this.state);
}
// ── AI ────────────────────────────────────────────────────────────
startAiTurn() {
if (!this.aiColor)
return;
this.aiThinking = true;
if (this.onAiThinkingCb)
this.onAiThinkingCb(true);
(0, ai_1.findBestMove)(this.state, this.aiColor, this.aiDepth).then(move => {
this.aiThinking = false;
if (this.onAiThinkingCb)
this.onAiThinkingCb(false);
if (move) {
this.executeMove(move);
}
});
}
// ── Undo ──────────────────────────────────────────────────────────
undo() {
if (this.aiThinking)
return false;
if (this.pendingPromo)
return false;
if (this.history.length <= 1)
return false;
// Undo 2 moves (player + AI), unless analysis mode (1 move)
const undoCount = this.analysisMode ? 1 : 2;
const targetLen = this.history.length - undoCount;
if (targetLen < 1) {
// Can't undo that many, just undo what we can
this.state = this.history[0];
this.history = [this.state];
this.lastMove = null;
}
else {
this.state = this.history[targetLen - 1];
this.history = this.history.slice(0, targetLen);
this.lastMove = this.history.length > 1 ? null : null;
// Restore lastMove from previous state
}
this.save();
this.notifyMove({});
return true;
}
// ── Persistence ───────────────────────────────────────────────────
save() {
try {
const data = {
board: boardToSave(this.state.board),
turn: this.state.turn,
castling: this.state.castling,
enPassant: this.state.enPassant,
halfMove: this.state.halfMove,
fullMove: this.state.fullMove,
moveList: this.state.moveList,
capturedWhite: this.state.capturedWhite,
capturedBlack: this.state.capturedBlack,
playerColor: this.playerColor,
aiDepth: this.aiDepth,
analysisMode: this.analysisMode,
lastMoveFrom: this.lastMove?.from ?? null,
lastMoveTo: this.lastMove?.to ?? null,
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
}
catch (_e) { /* ignore */ }
}
tryLoad() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return null;
return JSON.parse(raw);
}
catch (_e) {
return null;
}
}
restore(data) {
this.playerColor = data.playerColor;
this.aiDepth = data.aiDepth;
this.analysisMode = data.analysisMode;
this.aiColor = this.analysisMode ? null : (this.playerColor === 'w' ? 'b' : 'w');
this.state = {
board: boardFromSave(data.board),
turn: data.turn,
castling: data.castling,
enPassant: data.enPassant,
halfMove: data.halfMove,
fullMove: data.fullMove,
moveList: data.moveList,
capturedWhite: data.capturedWhite,
capturedBlack: data.capturedBlack,
};
this.lastMove = null;
if (data.lastMoveFrom !== null && data.lastMoveTo !== null) {
this.lastMove = {
from: data.lastMoveFrom,
to: data.lastMoveTo,
piece: this.state.board[data.lastMoveTo]?.type || 'P',
color: this.state.turn === 'w' ? 'b' : 'w',
};
}
this.history = [this.state];
}
clearSave() {
try {
localStorage.removeItem(STORAGE_KEY);
}
catch (_e) { }
}
}
exports.GameController = GameController;
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>