// Draft (client-side composition) serialization, kept pure for unit tests. The server stores // the JSON opaquely; only the client interprets {rack_order, board_tiles}. The rack // order is a comma-joined permutation of the server rack's indices, in the player's visual // order; the board tiles are the tiles laid but not yet submitted. import type { Tile } from './model'; import type { PendingTile } from './placement'; interface DraftData { rack_order: string; board_tiles: Tile[]; } /** serializeDraft builds the JSON to persist: the rack order and the pending board tiles. */ export function serializeDraft(rackOrder: number[], pending: PendingTile[]): string { const data: DraftData = { rack_order: rackOrder.join(','), board_tiles: pending.map((p) => ({ row: p.row, col: p.col, letter: p.letter, blank: p.blank })), }; return JSON.stringify(data); } /** parseDraft decodes a stored draft, or null when empty or malformed. */ export function parseDraft(json: string): { rackOrder: number[]; tiles: Tile[] } | null { if (!json) return null; try { const d = JSON.parse(json) as Partial; const rackOrder = String(d.rack_order ?? '') .split(',') .filter((s) => s !== '') .map(Number); const tiles = Array.isArray(d.board_tiles) ? d.board_tiles : []; return { rackOrder, tiles }; } catch { return null; } } /** * validRackOrder returns order when it is a permutation of [0, len), else null — so a stale * order (the rack changed since the draft was saved) is ignored and the server order is kept. */ export function validRackOrder(order: number[], len: number): number[] | null { if (order.length !== len) return null; const seen = new Set(); for (const i of order) { if (!Number.isInteger(i) || i < 0 || i >= len || seen.has(i)) return null; seen.add(i); } return order; } /** * liveDraftTiles drops saved tiles whose cell is now occupied on the committed board (the * opponent has since played there) — the position-only reconcile after a refresh. */ export function liveDraftTiles(tiles: Tile[], occupied: (row: number, col: number) => boolean): Tile[] { return tiles.filter((t) => !occupied(t.row, t.col)); }