diff --git a/src/engine/notation.test.ts b/src/engine/notation.test.ts new file mode 100644 index 0000000..b876093 --- /dev/null +++ b/src/engine/notation.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from 'vitest'; +import { toCoordinate, serialize, deserialize } from './notation'; +import type { HistoryEntry } from './types'; + +describe('notation', () => { + it('renders a move as a coordinate token', () => { + expect(toCoordinate({ from: 'e2', to: 'e4' })).toBe('e2e4'); + expect(toCoordinate({ from: 'e7', to: 'e8', promotion: 'q' })).toBe('e7e8q'); + }); + + it('round-trips a game through serialize/deserialize', () => { + const history: HistoryEntry[] = [ + { player: 'N', from: 'e2', to: 'e4' }, + { player: 'S', from: 'e2', to: 'e4' }, + ]; + const restored = deserialize(serialize(history)); + expect(restored).toEqual(history); + }); + + it('rejects a file that is not a duplicate-chess save', () => { + expect(() => deserialize('{"variant":"chess","version":1,"moves":[]}')).toThrow(); + }); +}); diff --git a/src/engine/notation.ts b/src/engine/notation.ts new file mode 100644 index 0000000..9648e7b --- /dev/null +++ b/src/engine/notation.ts @@ -0,0 +1,25 @@ +import type { SyncMove, HistoryEntry } from './types'; + +/** Coordinate notation, e.g. "e2e4" or "e7e8q". */ +export function toCoordinate(move: SyncMove): string { + return `${move.from}${move.to}${move.promotion ?? ''}`; +} + +export interface SavedGame { + variant: 'duplicate-chess'; + version: 1; + moves: HistoryEntry[]; +} + +export function serialize(history: HistoryEntry[]): string { + const data: SavedGame = { variant: 'duplicate-chess', version: 1, moves: history }; + return JSON.stringify(data, null, 2); +} + +export function deserialize(json: string): HistoryEntry[] { + const data = JSON.parse(json) as Partial; + if (data.variant !== 'duplicate-chess' || !Array.isArray(data.moves)) { + throw new Error('Not a duplicate-chess save file'); + } + return data.moves; +}