From 2b6ba895c5ad54d6bf87a3aa97a64037d6b808c1 Mon Sep 17 00:00:00 2001 From: Chander Ramesh Date: Sat, 13 Dec 2025 20:34:35 -0800 Subject: [PATCH] Add notation support for checkmate, timeout, and draw --- src/test/utils.test.ts | 38 +++++++++++++++++++++++++++++++++- src/utils.ts | 46 +++++++++++++++++++++++++++++++++++++----- 2 files changed, 78 insertions(+), 6 deletions(-) diff --git a/src/test/utils.test.ts b/src/test/utils.test.ts index da19f35..4b641df 100644 --- a/src/test/utils.test.ts +++ b/src/test/utils.test.ts @@ -37,6 +37,17 @@ describe('Utils', () => { expect(movesToString(moves, true)).toBe(`${encoded},R`); }); + test('Move strings support all termination markers', () => { + const moves: Move[] = [ + { from: 'A1', to: 'A2', enPassant: false, promotion: null }, + ]; + const base = movesToString(moves); + expect(movesToString(moves, 'resignation')).toBe(`${base},R`); + expect(movesToString(moves, 'checkmate')).toBe(`${base},#`); + expect(movesToString(moves, 'draw')).toBe(`${base},D`); + expect(movesToString(moves, 'timeout')).toBe(`${base},T`); + }); + test('Move strings round-trip without losing information', () => { const moves: Move[] = [ { from: 'B1', to: 'B3', enPassant: false, promotion: null }, @@ -187,7 +198,32 @@ describe('Utils', () => { expect(stringToMoves(`${base},R`)).toEqual(expected); expect(stringToMoves('R')).toEqual([]); expect(() => stringToMoves('R,B1-B3')).toThrow( - 'Cannot process resignation when subsequent moves still exist.', + 'Cannot process resignation marker when subsequent moves still exist.', + ); + }); + + test('stringToMoves handles additional game termination markers', () => { + const moves: Move[] = [ + { from: 'A1', to: 'A2', enPassant: false, promotion: null }, + { from: 'B2', to: 'B3', enPassant: false, promotion: null }, + ]; + const base = movesToString(moves); + expect(stringToMoves(`${base},#`)).toEqual(moves); + expect(stringToMoves(`${base},D`)).toEqual(moves); + expect(stringToMoves(`${base},T`)).toEqual(moves); + + expect(stringToMoves('#')).toEqual([]); + expect(stringToMoves('D')).toEqual([]); + expect(stringToMoves('T')).toEqual([]); + + expect(() => stringToMoves('#,A1-A2')).toThrow( + 'Cannot process checkmate marker when subsequent moves still exist.', + ); + expect(() => stringToMoves('D,A1-A2')).toThrow( + 'Cannot process draw marker when subsequent moves still exist.', + ); + expect(() => stringToMoves('T,A1-A2')).toThrow( + 'Cannot process timeout marker when subsequent moves still exist.', ); }); diff --git a/src/utils.ts b/src/utils.ts index aec6c5a..72e48f6 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,33 @@ import { Board } from './board'; import type { Color, Move, Piece } from './types'; export const RESIGNATION_MARKER = 'R'; +export const CHECKMATE_MARKER = '#'; +export const DRAW_MARKER = 'D'; +export const TIMEOUT_MARKER = 'T'; + +export type GameTermination = 'resignation' | 'checkmate' | 'draw' | 'timeout'; + +type GameTerminationMarker = + | typeof RESIGNATION_MARKER + | typeof CHECKMATE_MARKER + | typeof DRAW_MARKER + | typeof TIMEOUT_MARKER; + +const TERMINATION_MARKERS: Record = { + resignation: RESIGNATION_MARKER, + checkmate: CHECKMATE_MARKER, + draw: DRAW_MARKER, + timeout: TIMEOUT_MARKER, +}; + +const TERMINATION_LABELS: Record = { + [RESIGNATION_MARKER]: 'resignation', + [CHECKMATE_MARKER]: 'checkmate', + [DRAW_MARKER]: 'draw', + [TIMEOUT_MARKER]: 'timeout', +}; + +type MovesToStringTerminator = GameTermination | boolean | undefined; export const COLUMN_ARRAY = [ 'A', @@ -190,7 +217,10 @@ export const fenToBoard = (position: string | null): Board => { return converted; }; -export const movesToString = (moves: Move[], resigned = false): string => { +export const movesToString = ( + moves: Move[], + terminator?: MovesToStringTerminator, +): string => { const result: string[] = []; for (const move of moves) { let newString = move.capturedPiece @@ -204,8 +234,14 @@ export const movesToString = (moves: Move[], resigned = false): string => { } result.push(newString); } - if (resigned) { - result.push(RESIGNATION_MARKER); + const normalizedTerminator = + typeof terminator === 'boolean' + ? terminator + ? 'resignation' + : undefined + : terminator; + if (normalizedTerminator) { + result.push(TERMINATION_MARKERS[normalizedTerminator]); } return result.join(','); }; @@ -221,10 +257,10 @@ export const stringToMoves = (movesStr: string): Move[] => { const result: Move[] = []; for (let i = 0; i < moves.length; i++) { const move = moves[i]; - if (move === RESIGNATION_MARKER) { + if (move in TERMINATION_LABELS) { if (i !== moves.length - 1) { throw new Error( - 'Cannot process resignation when subsequent moves still exist.', + `Cannot process ${TERMINATION_LABELS[move as GameTerminationMarker]} marker when subsequent moves still exist.`, ); } break;