// Pure helpers for the offline hotseat (pass-and-play) creation roster: keeping a last-valid name as // a row is edited, and turning the finished roster into engine seats. Kept out of the component so it // is unit-testable; the form itself is exercised by the e2e layer. import { validDisplayName } from './profileValidation'; import type { PinLock } from './pin'; import type { Seat } from './localgame/serialize'; /** RosterRow is one editable player row: the live input, the last valid name kept when the input * goes invalid, and the optional per-seat PIN lock. */ export interface RosterRow { name: string; committed: string; pin?: PinLock; } /** commitName returns the new last-valid name: the trimmed input when it is a valid display name, * otherwise prevCommitted unchanged — so editing a set name into an invalid one keeps the old one. */ export function commitName(input: string, prevCommitted: string): string { return validDisplayName(input) ? input.trim() : prevCommitted; } /** rosterReady reports whether the roster can start a game: 2-4 rows, each with a committed name. */ export function rosterReady(rows: RosterRow[]): boolean { return rows.length >= 2 && rows.length <= 4 && rows.every((r) => r.committed !== ''); } /** buildSeats turns committed roster rows into engine seats — all human, account-less (local * players sharing a device), carrying each row's optional PIN. */ export function buildSeats(rows: RosterRow[]): Seat[] { return rows.map((r): Seat => ({ kind: 'human', name: r.committed, pin: r.pin })); } /** * shuffleSeeded returns a deterministic shuffle of items driven by seed — a Fisher-Yates over a small * in-house PRNG (a distinct seed derivation from the tile bag, so the seating is not correlated with * the tile draws). Used to randomise the hotseat seating at game start, so the first mover is random; * being seed-driven keeps it reproducible for replay and tests. */ export function shuffleSeeded(items: readonly T[], seed: bigint): T[] { const out = [...items]; let a = (Number(BigInt.asUintN(32, seed ^ 0x9e3779b9n)) >>> 0) || 1; const rand = (): number => { a = (a + 0x6d2b79f5) | 0; let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; for (let i = out.length - 1; i > 0; i--) { const j = Math.floor(rand() * (i + 1)); [out[i], out[j]] = [out[j], out[i]]; } return out; }