Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 37 additions & 1 deletion src/test/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down Expand Up @@ -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.',
);
});

Expand Down
46 changes: 41 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<GameTermination, GameTerminationMarker> = {
resignation: RESIGNATION_MARKER,
checkmate: CHECKMATE_MARKER,
draw: DRAW_MARKER,
timeout: TIMEOUT_MARKER,
};

const TERMINATION_LABELS: Record<GameTerminationMarker, string> = {
[RESIGNATION_MARKER]: 'resignation',
[CHECKMATE_MARKER]: 'checkmate',
[DRAW_MARKER]: 'draw',
[TIMEOUT_MARKER]: 'timeout',
};

type MovesToStringTerminator = GameTermination | boolean | undefined;

export const COLUMN_ARRAY = [
'A',
Expand Down Expand Up @@ -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
Expand All @@ -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(',');
};
Expand All @@ -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;
Expand Down