Chess
nex agi Nex N2.5 mini GGUF · 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 44,939 bytes · SHA-256 91fada2c90e8
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Chess</title>
<style>
</style>
</head>
<body>
<div id="app"></div>
<script>
(function () {
'use strict';
var __mods = {};
var __cache = {};
var __map = {"src/app.ts":{"./engine":"src/engine.ts"}};
function __require(id) {
if (__cache[id]) return __cache[id].exports;
var module = __cache[id] = { exports: {} };
var factory = __mods[id];
if (!factory) throw new Error("Module not found: " + id);
factory(module.exports, function (spec) {
var target = (__map[id] && __map[id][spec]) || spec;
return __require(target);
}, module);
return module.exports;
}
// ── module: src/main.ts ──
__mods["src/main.ts"] = function (exports, require, module) {
"use strict";
// Entry point. Runs after the DOM is parsed and after any classic
// <script> imports, so document queries and CDN globals are available.
// Import your other modules from here.
Object.defineProperty(exports, "__esModule", { value: true });
exports.start = start;
function start() {
// Replace this with the real startup sequence.
}
start();
};
// ── module: src/engine.ts ──
__mods["src/engine.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Engine = exports.PieceType = exports.Color = void 0;
var Color;
(function (Color) {
Color[Color["White"] = 0] = "White";
Color[Color["Black"] = 1] = "Black";
})(Color || (exports.Color = Color = {}));
var PieceType;
(function (PieceType) {
PieceType[PieceType["None"] = 0] = "None";
PieceType[PieceType["Pawn"] = 1] = "Pawn";
PieceType[PieceType["Knight"] = 2] = "Knight";
PieceType[PieceType["Bishop"] = 3] = "Bishop";
PieceType[PieceType["Rook"] = 4] = "Rook";
PieceType[PieceType["Queen"] = 5] = "Queen";
PieceType[PieceType["King"] = 6] = "King";
})(PieceType || (exports.PieceType = PieceType = {}));
const ROW = (a) => a >>> 4;
const COL = (a) => a & 15;
const SQ = (r, c) => (r << 4) | c;
const inside = (sq) => {
const r = ROW(sq), c = COL(sq);
return r >= 0 && r < 8 && c >= 0 && c < 8;
};
const other = (color) => color ^ 1;
const pawnStep = (color) => (color === Color.White ? -1 : 1);
const homeRank = (color) => (color === Color.White ? 7 : 0);
const startRank = (color) => (color === Color.White ? 6 : 1);
const PIECE_CHARS = ['', 'p', 'n', 'b', 'r', 'q', 'k'];
const PIECE_TYPES = { p: 1, n: 2, b: 3, r: 4, q: 5, k: 6 };
const WHITE_KING_CASTLE = 1;
const WHITE_QUEEN_CASTLE = 2;
const BLACK_KING_CASTLE = 4;
const BLACK_QUEEN_CASTLE = 8;
const KING_SIDE = 16;
const QUEEN_SIDE = 32;
const PROMOTION = 64;
const MATERIAL = {
[PieceType.Pawn]: 100, [PieceType.Knight]: 320, [PieceType.Bishop]: 330,
[PieceType.Rook]: 500, [PieceType.Queen]: 900, [PieceType.King]: 20000
};
const WHITE_PST = [
[0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 2, 3, 3, 2, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0]
];
const BLACK_PST = WHITE_PST.slice().reverse();
const ALL_DELTAS = [[1, 0], [-1, 0], [0, 1], [0, -1], [1, 1], [1, -1], [-1, 1], [-1, -1]];
const KNIGHT_DELTAS = [[2, 1], [2, -1], [-2, 1], [-2, -1], [1, 2], [1, -2], [-1, 2], [-1, -2]];
const BISHOP_DELTAS = [[1, 1], [1, -1], [-1, 1], [-1, -1]];
const ROOK_DELTAS = [[1, 0], [-1, 0], [0, 1], [0, -1]];
class Engine {
constructor() {
this.board = new Int8Array(64).fill(0);
this.side = Color.White;
this.castling = WHITE_KING_CASTLE | WHITE_QUEEN_CASTLE | BLACK_KING_CASTLE | BLACK_QUEEN_CASTLE;
this.epSquare = -1;
this.halfmove = 0;
this.moveNumber = 1;
this.moveList = [];
this.lastMove = null;
this.selected = -1;
this.promoSquare = -1;
this.gameOver = false;
this.winner = null;
this.drawReason = '';
this.positionHashes = [];
}
fen() {
let s = '';
for (let r = 0; r < 8; r++) {
let empties = 0;
for (let c = 0; c < 8; c++) {
const piece = this.board[SQ(r, c)];
if (piece === 0)
empties++;
else {
if (empties)
s += empties;
empties = 0;
s += PIECE_CHARS[Math.abs(piece)];
}
}
if (empties)
s += empties;
s += '/';
}
s += this.side === Color.White ? 'w' : 'b';
let rights = '';
if (this.castling & WHITE_KING_CASTLE)
rights += 'K';
if (this.castling & WHITE_QUEEN_CASTLE)
rights += 'Q';
if (this.castling & BLACK_KING_CASTLE)
rights += 'a';
if (this.castling & BLACK_QUEEN_CASTLE)
rights += 'q';
s += rights || '-';
s += ' ' + (this.epSquare >= 0 ? 'abcdefgh'[COL(this.epSquare)] + (8 - ROW(this.epSquare)) : '-') + ' ' + this.halfmove + ' ' + this.moveNumber;
return s;
}
kingSquare(color) {
const expected = color === Color.White ? 6 : -6;
for (let i = 0; i < 64; i++)
if (this.board[i] === expected)
return i;
return -1;
}
pawnAttacks(sq, by) {
const r = ROW(sq), c = COL(sq), result = [];
for (const dc of [-1, 1]) {
const nr = r + pawnStep(by), nc = c + dc;
if (inside(SQ(nr, nc)))
result.push(SQ(nr, nc));
}
return result;
}
squareAttackedBy(sq, by) {
const r = ROW(sq), c = COL(sq);
for (const p of this.pawnAttacks(sq, by))
if (this.board[p] === (by === Color.White ? 1 : -1))
return true;
for (const [dr, dc] of KNIGHT_DELTAS) {
const nr = r + dr, nc = c + dc;
if (inside(SQ(nr, nc)) && this.board[SQ(nr, nc)] === (by === Color.White ? 2 : -2))
return true;
}
for (const [dr, dc] of ALL_DELTAS) {
const nr = r + dr, nc = c + dc;
if (inside(SQ(nr, nc)) && this.board[SQ(nr, nc)] === (by === Color.White ? 6 : -6))
return true;
}
for (const [dr, dc] of BISHOP_DELTAS) {
let nr = r + dr, nc = c + dc;
while (inside(SQ(nr, nc))) {
const piece = this.board[SQ(nr, nc)];
if (piece !== 0) {
const wanted = by === Color.White ? 3 : -3;
if (piece === wanted || piece === (by === Color.White ? 5 : -5))
return true;
break;
}
nr += dr;
nc += dc;
}
}
for (const [dr, dc] of ROOK_DELTAS) {
let nr = r + dr, nc = c + dc;
while (inside(SQ(nr, nc))) {
const piece = this.board[SQ(nr, nc)];
if (piece !== 0) {
const wanted = by === Color.White ? 4 : -4;
if (piece === wanted || piece === (by === Color.White ? 5 : -5))
return true;
break;
}
nr += dr;
nc += dc;
}
}
return false;
}
inCheck(side = this.side) {
return this.squareAttackedBy(this.kingSquare(side), other(side));
}
makeMove(from, to, promotion, doublePush = false, epCapture = -1, ep = false, castle = false, rookFrom = -1, rookTo = -1) {
return {
from, to, promotion,
flags: (doublePush ? 2 : 0) | (ep ? 4 : 0) | (castle && this.board[from] > 0 ? KING_SIDE : castle && this.board[from] < 0 ? QUEEN_SIDE : 0) | (promotion ? PROMOTION : 0),
captured: ep ? this.board[from + pawnStep(this.side)] : this.board[to],
doublePush, epCapture, rookFrom: rookFrom >= 0 ? rookFrom : from, rookTo: rookTo >= 0 ? rookTo : to,
prevHalfmove: this.halfmove, prevCastling: this.castling
};
}
pseudoMovesFor(sq, color) {
const result = [];
const piece = this.board[sq];
if (piece === 0 || (piece > 0) !== (color === Color.White))
return result;
const r = ROW(sq), c = COL(sq), step = pawnStep(color), home = homeRank(color), goal = startRank(color);
if (Math.abs(piece) === PieceType.Pawn) {
const one = sq + step * 8;
if (inside(one) && this.board[one] === 0) {
if (ROW(one) === goal)
for (const promo of ['Q', 'R', 'B', 'N'])
result.push(this.makeMove(sq, one, promo));
else {
result.push(this.makeMove(sq, one));
if (r === home)
result.push(this.makeMove(sq, one + step * 8, undefined, true));
}
}
for (const dc of [-1, 1]) {
const nr = r + step, nc = c + dc;
if (!inside(SQ(nr, nc)))
continue;
const target = this.board[SQ(nr, nc)];
if (target !== 0 && (target > 0) === (color === Color.White)) {
if (ROW(nr) === goal)
for (const promo of ['Q', 'R', 'B', 'N'])
result.push(this.makeMove(sq, SQ(nr, nc), promo));
else
result.push(this.makeMove(sq, SQ(nr, nc)));
}
else if (ROW(nr) === 5 && sq === this.epSquare)
result.push(this.makeMove(sq, SQ(nr, nc), undefined, false, SQ(nr, nc), true));
}
}
else if (Math.abs(piece) === PieceType.Knight) {
for (const [dr, dc] of KNIGHT_DELTAS) {
const nr = r + dr, nc = c + dc;
if (!inside(SQ(nr, nc)))
continue;
const target = this.board[SQ(nr, nc)];
if (target === 0 || (target > 0) === (color === Color.White))
result.push(this.makeMove(sq, SQ(nr, nc)));
}
}
else if (Math.abs(piece) === PieceType.King) {
for (const [dr, dc] of ALL_DELTAS) {
const nr = r + dr, nc = c + dc;
if (!inside(SQ(nr, nc)))
continue;
const target = this.board[SQ(nr, nc)];
if (target === 0 || (target > 0) === (color === Color.White))
result.push(this.makeMove(sq, SQ(nr, nc)));
}
const rook = color === Color.White ? 4 : -4, king = color === Color.White ? 6 : -6;
if (this.board[sq] === king && this.board[SQ(r, c + 1)] === rook && this.board[SQ(r, c + 2)] === rook && this.board[SQ(r, c + 3)] === rook && this.board[SQ(r, c + 1)] === 0 && this.board[SQ(r, c + 2)] === 0 && this.board[SQ(r, c + 3)] === 0 && this.castling & (color === Color.White ? WHITE_KING_CASTLE : BLACK_KING_CASTLE) && !this.squareAttackedBy(sq, other(color)) && !this.squareAttackedBy(SQ(r, c + 1), other(color)) && !this.squareAttackedBy(SQ(r, c + 2), other(color)))
result.push(this.makeMove(sq, SQ(r, c + 2), undefined, false, -1, false, true, SQ(r, 7), SQ(r, 5)));
if (this.board[sq] === king && this.board[SQ(r, c - 1)] === rook && this.board[SQ(r, c - 2)] === rook && this.board[SQ(r, c - 3)] === rook && this.board[SQ(r, c - 1)] === 0 && this.board[SQ(r, c - 2)] === 0 && this.board[SQ(r, c - 3)] === 0 && this.castling & (color === Color.White ? WHITE_QUEEN_CASTLE : BLACK_QUEEN_CASTLE) && !this.squareAttackedBy(sq, other(color)) && !this.squareAttackedBy(SQ(r, c - 1), other(color)) && !this.squareAttackedBy(SQ(r, c - 2), other(color)))
result.push(this.makeMove(sq, SQ(r, c - 2), undefined, false, -1, false, true, SQ(r, 0), SQ(r, 3)));
}
else {
const dirs = Math.abs(piece) === PieceType.Bishop ? BISHOP_DELTAS : Math.abs(piece) === PieceType.Rook ? ROOK_DELTAS : ALL_DELTAS;
for (const [dr, dc] of dirs) {
let nr = r + dr, nc = c + dc;
while (inside(SQ(nr, nc))) {
const target = this.board[SQ(nr, nc)];
if (target === 0)
result.push(this.makeMove(sq, SQ(nr, nc)));
else {
if ((target > 0) === (color === Color.White))
result.push(this.makeMove(sq, SQ(nr, nc)));
break;
}
nr += dr;
nc += dc;
}
}
}
return result;
}
legalMoves() {
const side = this.side, result = [];
for (let sq = 0; sq < 64; sq++) {
const piece = this.board[sq];
if (piece === 0 || (piece > 0) !== (side === Color.White))
continue;
for (const move of this.pseudoMovesFor(sq, side)) {
const clone = this.clone();
clone.applyMove(move, false);
if (!clone.inCheck(side))
result.push(move);
}
}
return result;
}
legalMoveCount() { return this.legalMoves().length; }
clone() {
const copy = new Engine();
copy.board = this.board.slice();
copy.side = this.side;
copy.castling = this.castling;
copy.epSquare = this.epSquare;
copy.halfmove = this.halfmove;
copy.moveNumber = this.moveNumber;
copy.moveList = this.moveList.slice();
copy.lastMove = this.lastMove;
copy.selected = this.selected;
copy.promoSquare = this.promoSquare;
copy.gameOver = this.gameOver;
copy.winner = this.winner;
copy.drawReason = this.drawReason;
copy.positionHashes = this.positionHashes.slice();
return copy;
}
applyMoveToEngine(move) { this.applyMove(move); }
applyMove(move, updateState = true) {
const piece = this.board[move.from];
if (piece === 0)
return;
const color = this.side, r = ROW(move.from), c = COL(move.from);
this.board[move.from] = 0;
this.board[move.to] = move.promotion ? (move.promotion === 'N' ? 2 : move.promotion === 'B' ? 3 : move.promotion === 'R' ? 4 : 5) : piece;
if (move.flags & 4)
this.board[move.from + pawnStep(color)] = 0;
if (move.flags & KING_SIDE) {
if (color === Color.White) {
this.board[SQ(7, 4)] = piece;
this.board[SQ(7, 7)] = 0;
this.board[SQ(7, 5)] = 4;
this.board[SQ(7, 6)] = -4;
}
else {
this.board[SQ(0, 4)] = piece;
this.board[SQ(0, 7)] = 0;
this.board[SQ(0, 5)] = -4;
this.board[SQ(0, 6)] = 4;
}
}
else if (move.flags & QUEEN_SIDE) {
if (color === Color.White) {
this.board[SQ(7, 4)] = piece;
this.board[SQ(7, 0)] = 0;
this.board[SQ(7, 3)] = 4;
this.board[SQ(7, 2)] = -4;
}
else {
this.board[SQ(0, 4)] = piece;
this.board[SQ(0, 0)] = 0;
this.board[SQ(0, 3)] = -4;
this.board[SQ(0, 2)] = 4;
}
}
if (piece === 6 || piece === -6)
this.castling = 0;
if (move.from === SQ(7, 0) || move.to === SQ(7, 0))
this.castling &= ~WHITE_KING_CASTLE;
if (move.from === SQ(7, 7) || move.to === SQ(7, 7))
this.castling &= ~WHITE_QUEEN_CASTLE;
if (move.from === SQ(0, 0) || move.to === SQ(0, 0))
this.castling &= ~BLACK_KING_CASTLE;
if (move.from === SQ(0, 7) || move.to === SQ(0, 7))
this.castling &= ~BLACK_QUEEN_CASTLE;
this.halfmove = (Math.abs(piece) === PieceType.Pawn || (move.captured && move.captured !== 0)) ? 0 : this.halfmove + 1;
this.epSquare = move.doublePush ? (move.from + move.to) >> 1 : -1;
this.side = other(color);
if (color === Color.White)
this.moveNumber += 1;
if (updateState) {
this.lastMove = move;
this.moveList.push(move);
this.selected = -1;
this.promoSquare = -1;
this.positionHashes.push(this.hash());
if (this.halfmove >= 100 || this.insufficientMaterial() || this.positionHashes.filter(h => h === this.hash()).length >= 3) {
this.gameOver = true;
this.drawReason = this.halfmove >= 100 ? 'fifty-move rule' : this.insufficientMaterial() ? 'insufficient material' : 'threefold repetition';
return;
}
const moves = this.legalMoves();
if (moves.length === 0) {
this.gameOver = true;
this.drawReason = this.inCheck() ? 'checkmate' : 'stalemate';
if (this.inCheck())
this.winner = other(this.side);
}
}
}
insufficientMaterial() {
const pieces = [];
for (let i = 0; i < 64; i++)
if (this.board[i] !== 0 && Math.abs(this.board[i]) !== PieceType.King)
pieces.push(Math.abs(this.board[i]));
if (pieces.length === 0)
return true;
if (pieces.length === 1 && (pieces[0] === PieceType.Bishop || pieces[0] === PieceType.Knight))
return true;
return false;
}
hash() {
let value = 0;
for (let i = 0; i < 64; i++)
if (this.board[i] !== 0)
value ^= (i + 1) * 131 + this.board[i] * 17;
return value;
}
static pieceValue(type) { return MATERIAL[type] ?? 0; }
static parseSquare(s) {
if (!/^[a-h][1-8]$/.test(s || ''))
return -1;
return SQ(8 - (+s[1]), 'abcdefgh'.indexOf(s[0]));
}
move(ref) {
const text = ref.trim().toLowerCase();
const from = Engine.parseSquare(text.slice(0, 2)), to = Engine.parseSquare(text.slice(2, 4));
if (from < 0 || to < 0)
return null;
const found = this.pseudoMovesFor(from, this.side).find(m => m.to === to && (!m.promotion || m.promotion.toUpperCase() === text[4]));
if (!found)
return null;
this.applyMove(found);
return found;
}
static fromFen(fen) {
const engine = new Engine();
const parts = fen.trim().split(/\s+/);
let index = 0;
for (const row of parts[0].split('/')) {
for (const ch of row) {
if (/\d/.test(ch))
index += +ch;
else {
const color = ch === ch.toUpperCase() ? Color.White : Color.Black;
const type = PIECE_TYPES[ch.toLowerCase()];
engine.board[SQ(Math.floor(index / 8), index % 8)] = type === PieceType.Pawn ? (color === Color.White ? 1 : -1) : (color === Color.White ? type : -type);
index++;
}
}
}
engine.board = Int8Array.from(engine.board);
engine.side = parts[1] === 'w' ? Color.White : Color.Black;
engine.castling = 0;
for (const ch of parts[2] || '') {
if (ch === 'K')
engine.castling |= WHITE_KING_CASTLE;
if (ch === 'Q')
engine.castling |= WHITE_QUEEN_CASTLE;
if (ch === 'a')
engine.castling |= BLACK_KING_CASTLE;
if (ch === 'q')
engine.castling |= BLACK_QUEEN_CASTLE;
}
engine.epSquare = parts[3] && parts[3] !== '-' ? SQ(8 - (+parts[3][1]), 'abcdefgh'.indexOf(parts[3][0])) : -1;
engine.halfmove = +(parts[4] || '0');
engine.moveNumber = +(parts[5] || '1');
engine.positionHashes = [engine.hash()];
return engine;
}
}
exports.Engine = Engine;
};
// ── module: src/app.ts ──
__mods["src/app.ts"] = function (exports, require, module) {
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.App = void 0;
exports.bestAiMove = bestAiMove;
exports.evaluate = evaluate;
const engine_1 = require("./engine");
const STORAGE_KEY = 'vanilla-chess-state-v1';
const PIECE_GLYPHS = { kp: '♟', kn: '♞', kb: '♝', kr: '♜', kq: '♛', kk: '♚', wp: '♙', wn: '♘', wb: '♗', wr: '♖', wq: '♕', wk: '♔' };
const squareName = (sq) => 'abcdefgh'[sq & 15] + (8 - (sq >>> 4));
const sqFromName = (name) => (/^[a-h][1-8]$/.test(name || '') ? engine_1.Engine.parseSquare(name) : null);
const pieceGlyph = (engine, sq) => { const p = engine.board[sq]; return p === 0 ? '' : PIECE_GLYPHS[(p > 0 ? 'w' : 'b') + Math.abs(p)]; };
const shortMove = (engine, move) => {
const p = Math.abs(engine.board[move.from]);
const capture = move.captured !== undefined && move.captured !== 0;
let text = p === 1 ? squareName(move.from)[0] + (capture ? 'x' : '') + squareName(move.to) : ('PNBR'[p - 2] + (capture ? 'x' : '') + squareName(move.to));
if (move.promotion)
text += '=' + move.promotion;
if (move.flags & 16)
text = 'O-O';
if (move.flags & 32)
text = 'O-O-O';
return text;
};
class App {
constructor(playerColor = engine_1.Color.White, aiColor = engine_1.Color.Black, aiDepth = 2, muted = false) {
this.aiBusy = false;
this.undoStack = [];
this.lastMove = [];
this.selected = '';
this.promoSquare = null;
this.engine = engine_1.Engine.fromFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR');
this.playerColor = playerColor;
this.aiColor = aiColor;
this.aiDepth = aiDepth;
this.flipped = false;
this.muted = muted;
this.gameOver = false;
this.winner = null;
this.drawReason = '';
this.status = { over: false, result: 'White to move' };
this.soundEnabled = !muted;
this.pieces = { board: Array.from(this.engine.board), side: this.engine.side, castling: this.engine.castling, epSquare: this.engine.epSquare, halfmove: this.engine.halfmove, moveNumber: this.engine.moveNumber, moveList: [], gameover: false, winner: null, drawReason: '', playerColor, aiColor, aiDepth, flipped: false, muted: false, lastMove: [], selected: '-1', promoSquare: null };
}
start(root) {
this.root = root;
this.pieces = this.loadPieces();
this.restore(this.pieces);
this.buildDom();
this.bindDom();
this.render();
if (this.isAiTurn())
queueMicrotask(() => this.aiMove());
}
loadPieces() { try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw)
return this.pieces;
const parsed = JSON.parse(raw);
return parsed && parsed.app ? parsed.app : this.pieces;
}
catch {
return this.pieces;
} }
save() { try {
localStorage.setItem(STORAGE_KEY, JSON.stringify({ app: this.pieces }));
}
catch { } }
restore(app) {
this.playerColor = app.playerColor;
this.aiColor = app.aiColor;
this.aiDepth = app.aiDepth;
this.flipped = app.flipped;
this.muted = app.muted;
this.gameOver = app.gameover;
this.winner = app.winner === null ? null : app.winner;
this.drawReason = app.drawReason;
this.soundEnabled = !app.muted;
this.engine = engine_1.Engine.fromFen('');
this.engine.side = app.side;
this.engine.castling = app.castling;
this.engine.epSquare = app.epSquare;
this.engine.halfmove = app.halfmove;
this.engine.moveNumber = app.moveNumber;
this.engine.positionHashes = [];
for (let i = 0; i < 64; i++)
this.engine.board[i] = app.board[i];
for (const moveRef of app.moveList)
this.engine.move(moveRef);
this.lastMove = app.lastMove.slice();
this.promoSquare = app.promoSquare || null;
this.gameOver = app.gameover;
this.winner = app.winner;
this.drawReason = app.drawReason;
}
buildDom() {
if (!this.root)
return;
this.root.innerHTML = `<div class="app-shell">
<header class="topbar"><div class="brand"><span class="brand-mark">◇</span><div><h1>Aurum Chess</h1><p>Classic rules · engine-backed play</p></div></div><div class="game-controls"><label>Play as <select id="player-color"><option value="0">White</option><option value="1">Black</option></select></label><label>AI depth <select id="ai-depth"><option value="1">Quick</option><option value="2">Balanced</option><option value="3">Strong</option></select></label><button id="new-game" class="button button-primary">New game</button><button id="undo" class="button">Undo</button><button id="flip" class="button icon-button" title="Flip board">⇅</button><button id="mute" class="button icon-button" title="Toggle sound">♪</button></div></header>
<main class="game-layout"><section class="board-column"><div class="board-frame"><div id="board" class="board" aria-label="Chess board"></div><div id="promotion" class="promotion-overlay" hidden><p>Select promotion</p><div class="promo-pieces"></div></div><div id="game-banner" class="game-banner" hidden></div></div><div class="board-caption"><span class="dot-dot"></span>Drag pieces or click a highlighted square. Captures, checks, castling, en passant, and promotion are enforced by the rule engine.</div></section><aside class="side-panel"><section class="panel-block turn-card"><div class="eyebrow">Current turn</div><div id="turn" class="turn-value">White to move</div><div id="status" class="status-line">Legal moves on board</div></section><section class="panel-block"><div class="tray-label">Captured by White</div><div id="captured-white" class="tray"></div><div class="tray-label above">Captured by Black</div><div id="captured-black" class="tray"></div></section><section class="panel-block move-block"><div class="section-heading"><span>Move list</span><span class="move-count" id="move-count">0</span></div><div id="move-list" class="move-list"><div class="empty-moves">The game is ready — White opens.</div></div></section><section class="panel-block tips"><div class="eyebrow">Table</div><div class="tips-copy">FEN is preserved locally. Use <kbd>game.fen()</kbd> and <kbd>game.move("e2e4")</kbd> in the console to inspect or drive the position.</div></section></aside></main></div>`;
this.boardEl = document.getElementById('board');
this.moveListEl = document.getElementById('move-list');
this.turnEl = document.getElementById('turn');
this.statusEl = document.getElementById('status');
this.capturedWhiteEl = document.getElementById('captured-white');
this.capturedBlackEl = document.getElementById('captured-black');
this.promotionEl = document.getElementById('promotion');
const playerSelect = document.getElementById('player-color');
if (playerSelect) {
playerSelect.value = String(this.playerColor);
playerSelect.addEventListener('change', () => { this.playerColor = +playerSelect.value; this.aiColor = 1 - this.playerColor; this.newGame(); });
}
const depthSelect = document.getElementById('ai-depth');
if (depthSelect) {
depthSelect.value = String(this.aiDepth);
depthSelect.addEventListener('change', () => { this.aiDepth = +depthSelect.value; });
}
document.getElementById('new-game')?.addEventListener('click', () => this.newGame());
document.getElementById('undo')?.addEventListener('click', () => this.undo());
document.getElementById('flip')?.addEventListener('click', () => { this.flipped = !this.flipped; this.render(); });
document.getElementById('mute')?.addEventListener('click', () => this.toggleMute());
}
bindDom() {
if (!this.boardEl)
return;
let dragSquare = null;
this.boardEl.addEventListener('pointerdown', (event) => { const square = this.squareFromEvent(event); if (square == null)
return; if (this.gameOver || this.isAiTurn())
return; dragSquare = square; });
this.boardEl.addEventListener('pointerup', (event) => { const square = this.squareFromEvent(event); if (square == null)
return; if (dragSquare == null || this.gameOver || this.isAiTurn()) {
dragSquare = null;
return;
} this.tryMove(dragSquare, square); dragSquare = null; });
this.boardEl.addEventListener('pointermove', (event) => { if (dragSquare == null)
return; const target = this.squareFromEvent(event); if (target != null && target !== dragSquare)
this.tryMove(dragSquare, target); });
}
squareFromEvent(event) { const target = event.target; return target?.dataset?.square ? sqFromName(target.dataset.square) : null; }
render() { this.renderBoard(); this.renderSidePanel(); this.renderPromotion(); this.renderBanner(); this.save(); }
renderBoard() {
if (!this.boardEl)
return;
const legal = new Set(this.engine.legalMoves().map(m => m.to));
this.boardEl.innerHTML = '';
for (let rank = 0; rank < 8; rank++)
for (let file = 0; file < 8; file++) {
const index = (this.flipped ? 7 - rank : rank) * 8 + (this.flipped ? 7 - file : file);
const square = index;
const isLight = (Math.floor(square / 8) + square % 8) % 2 === 0;
const cell = document.createElement('button');
cell.type = 'button';
cell.className = 'square' + (isLight ? ' light' : ' dark');
cell.dataset.square = squareName(square);
cell.setAttribute('aria-label', squareName(square));
if (this.lastMove.length === 2 && (this.lastMove[0] === squareName(square) || this.lastMove[1] === squareName(square)))
cell.classList.add('last-move');
const selected = this.engine.selected === square || this.selected === squareName(square);
if (selected)
cell.classList.add('selected');
if (legal.has(square))
cell.classList.add('legal');
const piece = this.engine.board[square];
if (piece !== 0) {
const pieceEl = document.createElement('span');
pieceEl.className = 'piece ' + (piece > 0 ? 'white' : 'black');
pieceEl.textContent = pieceGlyph(this.engine, square);
cell.appendChild(pieceEl);
}
const marker = document.createElement('span');
marker.className = 'square-marker';
if (legal.has(square))
marker.textContent = this.engine.board[square] !== 0 ? '' : '●';
cell.appendChild(marker);
this.boardEl.appendChild(cell);
}
if (this.engine.inCheck(this.engine.side)) {
const king = this.engine.kingSquare(this.engine.side);
if (king >= 0) {
const cell = this.boardEl.querySelector(`[data-square="${squareName(king)}"]`);
if (cell)
cell.classList.add('in-check');
}
}
}
renderSidePanel() {
if (!this.turnEl || !this.statusEl || !this.moveListEl || !this.capturedWhiteEl || !this.capturedBlackEl)
return;
const side = this.engine.side;
const sideName = side === engine_1.Color.White ? 'White' : 'Black';
this.turnEl.textContent = sideName + ' to move';
this.statusEl.textContent = this.gameOver ? (this.drawReason || 'Game over') : this.engine.inCheck(side) ? 'Check!' : 'Legal moves on board';
this.capturedWhiteEl.innerHTML = '';
this.capturedBlackEl.innerHTML = '';
const whiteCaptured = [];
const blackCaptured = [];
for (let i = 0; i < 64; i++) {
const p = this.engine.board[i];
if (p !== 0 && Math.abs(p) !== 6 && Math.abs(p) !== 1) {
const glyph = (p > 0 ? 'wp' : 'bp') + Math.abs(p);
(p > 0 ? whiteCaptured : blackCaptured).push(glyph);
}
}
for (const glyph of whiteCaptured) {
const el = document.createElement('span');
el.className = 'captured captured-black';
el.textContent = PIECE_GLYPHS[glyph];
this.capturedWhiteEl.appendChild(el);
}
for (const glyph of blackCaptured) {
const el = document.createElement('span');
el.className = 'captured captured-white';
el.textContent = PIECE_GLYPHS[glyph];
this.capturedBlackEl.appendChild(el);
}
this.moveListEl.innerHTML = '';
const moves = this.engine.moveList;
if (!moves.length) {
const empty = document.createElement('div');
empty.className = 'empty-moves';
empty.textContent = 'The game is ready — White opens.';
this.moveListEl.appendChild(empty);
}
for (let i = 0; i < moves.length; i += 2) {
const row = document.createElement('div');
row.className = 'move-row';
const num = document.createElement('span');
num.className = 'move-number';
num.textContent = (i / 2 + 1) + '.';
row.appendChild(num);
const white = document.createElement('span');
white.className = 'move-text';
white.textContent = i < moves.length ? shortMove(this.engine, moves[i]) : '';
row.appendChild(white);
const black = document.createElement('span');
black.className = 'move-text';
if (i + 1 < moves.length)
black.textContent = shortMove(this.engine, moves[i + 1]) + (this.moveResultSuffix(i + 1) || '');
row.appendChild(black);
this.moveListEl.appendChild(row);
}
this.moveListEl.scrollTop = this.moveListEl.scrollHeight;
const count = document.getElementById('move-count');
if (count)
count.textContent = String(Math.ceil(moves.length / 2));
}
moveResultSuffix(index) { const after = this.engine.clone(); after.applyMove(this.engine.moveList[index], false); return after.legalMoves().length === 0 && after.inCheck(after.side) ? '#' : after.inCheck(after.side) ? '+' : ''; }
renderPromotion() { if (!this.promotionEl)
return; const active = this.promoSquare != null; this.promotionEl.hidden = !active; if (!active || this.promoSquare == null)
return; const square = sqFromName(this.promoSquare); if (square == null)
return; const color = this.engine.board[square] > 0 ? engine_1.Color.White : engine_1.Color.Black; const picker = this.promotionEl.querySelector('.promo-pieces'); if (!picker)
return; picker.innerHTML = ''; for (const promo of ['Q', 'R', 'B', 'N']) {
const button = document.createElement('button');
button.type = 'button';
button.className = 'promo-piece';
button.textContent = ['♕', '♖', '♗', '♘'][['Q', 'R', 'B', 'N'].indexOf(promo)];
button.addEventListener('click', () => this.choosePromotion(square, promo));
picker.appendChild(button);
} }
renderBanner() { const banner = document.getElementById('game-banner'); if (!banner)
return; if (!this.gameOver) {
banner.hidden = true;
banner.textContent = '';
return;
} banner.hidden = false; banner.textContent = this.drawReason === 'checkmate' ? (this.winner === engine_1.Color.White ? 'White wins by checkmate' : 'Black wins by checkmate') : this.drawReason === 'stalemate' ? 'Draw — stalemate' : this.drawReason === 'fifty-move rule' ? 'Draw — fifty-move rule' : this.drawReason === 'insufficient material' ? 'Draw — insufficient material' : 'Draw — threefold repetition'; }
choosePromotion(square, promo) { if (this.promoSquare === null)
return; const move = this.engine.legalMoves().find(m => m.from === this.engine.selected && m.to === square && m.promotion === promo); if (!move)
return; this.engine.promoSquare = -1; this.promoSquare = null; this.applyPlayerMove(move); }
tryMove(from, to) {
if (this.engine.selected >= 0 && this.selected && from === this.engine.selected) {
this.engine.selected = -1;
this.selected = '';
this.render();
return;
}
const piece = this.engine.board[from];
if (piece !== 0 && ((piece > 0) === (this.engine.side === this.playerColor))) {
const moves = this.engine.legalMoves().filter(m => m.from === from);
if (moves.length) {
this.engine.selected = from;
this.selected = squareName(from);
if (moves.some(m => m.to === to && m.promotion)) {
this.promoSquare = squareName(to);
this.engine.promoSquare = to;
this.render();
return;
}
this.applyPlayerMove(moves.find(m => m.to === to) || moves[0]);
}
else {
this.engine.selected = from;
this.selected = squareName(from);
this.render();
}
}
else if (this.engine.selected >= 0) {
const move = this.engine.legalMoves().find(m => m.from === this.engine.selected && m.to === to);
if (move)
this.applyPlayerMove(move);
else {
this.engine.selected = this.engine.board[from] !== 0 ? from : -1;
this.selected = squareName(this.engine.selected);
this.render();
}
}
}
applyPlayerMove(move) { this.engine.applyMove(move); this.lastMove = [squareName(move.from), squareName(move.to)]; this.undoStack.push({ fen: this.engine.fen(), lastMove: this.lastMove.slice() }); this.status = { over: this.engine.gameOver, result: this.statusResult() }; if (this.statusEl)
this.statusEl.textContent = this.engine.gameOver ? (this.drawReason || 'Game over') : 'Your move'; this.render(); this.playSound(move); if (!this.engine.gameOver && this.isAiTurn())
queueMicrotask(() => this.aiMove()); }
statusResult() { return this.engine.gameOver ? (this.drawReason === 'checkmate' ? (this.winner === engine_1.Color.White ? 'White wins' : 'Black wins') : this.drawReason) : ''; }
isAiTurn() { return !this.gameOver && this.engine.side === this.aiColor && this.playerColor !== this.aiColor; }
aiMove() { if (!this.root || this.aiBusy || this.gameOver || this.engine.side !== this.aiColor)
return; this.aiBusy = true; if (this.statusEl)
this.statusEl.textContent = 'AI is thinking…'; queueMicrotask(() => { const moves = this.engine.legalMoves(); if (!moves.length) {
this.aiBusy = false;
this.render();
return;
} const chosen = bestAiMove(this.engine, this.aiDepth); if (chosen)
this.applyPlayerMove(chosen); this.aiBusy = false; this.render(); }); }
undo() { if (!this.undoStack.length || this.aiBusy)
return; const previous = this.undoStack.pop(); if (!previous)
return; this.engine = engine_1.Engine.fromFen(previous.fen); this.lastMove = previous.lastMove.slice(); this.selected = ''; this.engine.selected = -1; this.promoSquare = null; this.engine.promoSquare = -1; this.gameOver = false; this.winner = null; this.drawReason = ''; this.engine.gameOver = false; this.engine.winner = null; this.engine.drawReason = ''; this.render(); }
newGame() { const playerColor = this.playerColor; this.engine = engine_1.Engine.fromFen('rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR'); this.lastMove = []; this.selected = ''; this.engine.selected = -1; this.promoSquare = null; this.gameOver = false; this.winner = null; this.drawReason = ''; this.undoStack = []; this.aiColor = 1 - playerColor; this.pieces = { board: Array.from(this.engine.board), side: this.engine.side, castling: this.engine.castling, epSquare: this.engine.epSquare, halfmove: this.engine.halfmove, moveNumber: this.engine.moveNumber, moveList: [], gameover: false, winner: null, drawReason: '', playerColor, aiColor: this.aiColor, aiDepth: this.aiDepth, flipped: this.flipped, muted: this.muted, lastMove: [], selected: '', promoSquare: null }; this.render(); this.save(); if (this.isAiTurn())
queueMicrotask(() => this.aiMove()); }
toggleMute() { this.muted = !this.muted; this.soundEnabled = !this.muted; this.render(); const mute = document.getElementById('mute'); if (mute)
mute.textContent = this.muted ? '×' : '♪'; }
playSound(move) { if (!this.soundEnabled)
return; try {
this.audio ?? (this.audio = new (window.AudioContext || window.webkitAudioContext)());
const context = this.audio;
const now = context.currentTime;
const oscillator = context.createOscillator();
const gain = context.createGain();
oscillator.type = move.captured !== undefined && move.captured !== 0 ? 'triangle' : 'sine';
oscillator.frequency.setValueAtTime(move.captured !== undefined && move.captured !== 0 ? 185 : 330, now);
oscillator.frequency.exponentialRampToValueAtTime(move.captured !== undefined && move.captured !== 0 ? 95 : 440, now + .13);
gain.gain.setValueAtTime(.0001, now);
gain.gain.exponentialRampToValueAtTime(.16, now + .015);
gain.gain.exponentialRampToValueAtTime(.0001, now + .18);
oscillator.connect(gain);
gain.connect(context.destination);
oscillator.start(now);
oscillator.stop(now + .2);
}
catch { } }
fen() { return this.engine.fen(); }
move(ref) { if (this.gameOver || this.isAiTurn())
return false; const normalized = ref.toLowerCase().replace(/[^a-h0-9]/g, ''); const move = this.engine.legalMoves().find(m => squareName(m.from) + squareName(m.to) === normalized); if (!move)
return false; this.applyPlayerMove(move); return true; }
legalMoves() { return this.engine.legalMoves(); }
makeDebugSnapshot() { return { fen: this.fen(), moves: this.legalMoves(), status: this.status }; }
}
exports.App = App;
const PIECE_VALUE = { 1: 100, 2: 320, 3: 330, 4: 500, 5: 900, 6: 20000 };
const WHITE_PST = [
[0, 0, 0, 0, 0, 0, 0, 0, 5, 5, 5, 5, 5, 5, 5, 5, 1, 1, 2, 3, 3, 2, 1, 1, 2, 2, 3, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 5, 5, 5, 5, 5, 5, 5, 5, 0, 0, 0, 0, 0, 0, 0, 0]
];
const BLACK_PST = WHITE_PST.slice().reverse();
function evaluate(engine, side) { let total = 0; for (let sq = 0; sq < 64; sq++) {
const p = engine.board[sq];
if (!p)
continue;
const type = Math.abs(p), color = p > 0 ? engine_1.Color.White : engine_1.Color.Black;
total += color === side ? PIECE_VALUE[type] + (color === engine_1.Color.White ? WHITE_PST[Math.floor(sq / 8)][sq % 8] : BLACK_PST[Math.floor(sq / 8)][sq % 8]) : -(PIECE_VALUE[type] + (color === engine_1.Color.White ? WHITE_PST[Math.floor(sq / 8)][sq % 8] : BLACK_PST[Math.floor(sq / 8)][sq % 8]));
} return total; }
function bestAiMove(engine, depth) { const moves = engine.legalMoves().sort((a, b) => ((b.captured || 0) * 10) - ((a.captured || 0) * 10)); if (!moves.length)
return null; const start = performance.now(); let best = null; let bestScore = -Infinity; for (const move of moves) {
const clone = engine.clone();
clone.applyMove(move, false);
const score = -quiescence(clone, depth - 1, -Infinity, Infinity, performance.now() - start + 850);
if (score > bestScore) {
bestScore = score;
best = move;
}
} return performance.now() - start > 850 ? best : best; }
function quiescence(engine, depth, alpha, beta, deadline) { if (depth <= 0 || performance.now() > deadline)
return evaluate(engine, engine.side); const side = engine.side, current = evaluate(engine, side); let best = current; const moves = engine.legalMoves().sort((a, b) => ((b.captured || 0) * 10) - ((a.captured || 0) * 10)); for (const move of moves) {
const clone = engine.clone();
clone.applyMove(move, false);
const score = -quiescence(clone, depth - 1, -beta, -alpha, deadline);
best = Math.max(best, score);
if (best > alpha)
alpha = best;
if (alpha >= beta)
break;
} return best; }
};
// ── entry ──
__require("src/main.ts");
})();
</script>
</body>
</html>