import { Chess, SQUARES, type Square, type PieceSymbol } from 'chess.js'; import type { Key } from '@lichess-org/chessground/types'; import type { GameRecord, GameResult, ResultReason } from '$lib/types'; export type Dests = Map; export function toDests(chess: Chess): Dests { const dests: Dests = new Map(); for (const s of SQUARES) { const moves = chess.moves({ square: s, verbose: true }); if (moves.length) { dests.set(s as Key, moves.map((m) => m.to as Key)); } } return dests; } export function isPromotion(chess: Chess, orig: string, dest: string): boolean { const moves = chess.moves({ square: orig as Square, verbose: true }); return moves.some((m) => m.to === dest && m.promotion); } export function applyMove( chess: Chess, orig: string, dest: string, promotion?: PieceSymbol ): boolean { try { chess.move({ from: orig as Square, to: dest as Square, promotion }); return true; } catch { return false; } } export function turnColor(chess: Chess): 'white' | 'black' { return chess.turn() === 'w' ? 'white' : 'black'; } export function lastMoveSquares(chess: Chess): [Key, Key] | undefined { const history = chess.history({ verbose: true }); if (history.length === 0) return undefined; const last = history[history.length - 1]; return [last.from as Key, last.to as Key]; } export function gameResult(chess: Chess): { result: GameResult; reason: ResultReason } | null { if (!chess.isGameOver()) return null; if (chess.isCheckmate()) { return { result: chess.turn() === 'w' ? '0-1' : '1-0', reason: 'checkmate', }; } if (chess.isStalemate()) return { result: '1/2-1/2', reason: 'stalemate' }; if (chess.isInsufficientMaterial()) return { result: '1/2-1/2', reason: 'insufficient' }; if (chess.isThreefoldRepetition()) return { result: '1/2-1/2', reason: 'repetition' }; if (chess.isDraw()) return { result: '1/2-1/2', reason: 'fifty_moves' }; return null; } // Variant names other PGN readers recognize. Really Bad Chess is orthodox rules // from a shuffled position, which is what "From Position" means to them. const PGN_VARIANTS: Record = { 'really-bad-chess': 'From Position', chess960: 'Chess960', }; export type PgnVerification = | { ok: true; chess: Chess } | { ok: false; error: 'invalid' | 'mismatch' }; /** * Load `pgn` and check it continues the game we already have rather than * rewriting it. Everything an opponent publishes is untrusted. */ export function verifyPgnExtends(current: Chess, pgn: string): PgnVerification { const next = new Chess(); try { next.loadPgn(pgn); } catch { return { ok: false, error: 'invalid' }; } // A variant game that starts from a different board is a different game. if (next.getHeaders().FEN !== current.getHeaders().FEN) { return { ok: false, error: 'mismatch' }; } const ours = current.history(); const theirs = next.history(); for (let i = 0; i < Math.min(ours.length, theirs.length); i++) { if (ours[i] !== theirs[i]) return { ok: false, error: 'mismatch' }; } return { ok: true, chess: next }; } export function makeInitialPgn(fen: string, variant?: GameRecord['variant']): string { const chess = new Chess(fen); chess.setHeader('SetUp', '1'); chess.setHeader('FEN', fen); const pgnVariant = variant && PGN_VARIANTS[variant]; if (pgnVariant) chess.setHeader('Variant', pgnVariant); return chess.pgn(); } export function makePgn(chess: Chess, white?: string, black?: string): string { // Stamp the date once, on the first move. chess.js reports a "????.??.??" // placeholder until then. const existing = chess.getHeaders().Date; const date = existing && !existing.includes('?') ? existing : new Date().toISOString().slice(0, 10).replace(/-/g, '.'); const headers: [string, string][] = [ ['Event', 'checkmate.blue'], ['Site', 'https://checkmate.blue'], ['Date', date], ['Round', '-'], ]; if (white) headers.push(['White', white]); if (black) headers.push(['Black', black]); const result = gameResult(chess); headers.push(['Result', result?.result ?? '*']); for (const [key, value] of headers) { chess.setHeader(key, value); } return chess.pgn(); }