feat(offline): randomise the hotseat seating (random first mover)
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Has been skipped
CI / integration (pull_request) Has been skipped
CI / ui (pull_request) Successful in 1m7s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m42s

The 2-4 hotseat players now start in a random seating order (so who goes
first, and the turn order, is random) — matching the online games. The
engine starts at seat 0, so shuffling the seats at creation picks the
starter; the shuffle is seed-driven (a distinct derivation from the tile
bag), so replay reproduces it. Scoped to hotseat — vs_ai stays human-first.
- lib/roster.ts: shuffleSeeded (unit-tested); NewGame shuffles buildSeats
  with the game seed before create.
- e2e: pin a seed that keeps the roster order so the lock assertions stay
  deterministic.
This commit is contained in:
Ilia Denisov
2026-07-07 16:20:53 +02:00
parent 80cc2a76e8
commit a7f483792f
4 changed files with 53 additions and 8 deletions
+8 -5
View File
@@ -13,7 +13,7 @@
import { t, type MessageKey } from '../lib/i18n/index.svelte';
import { validDisplayName } from '../lib/profileValidation';
import { verifyPin, type PinLock } from '../lib/pin';
import { commitName, rosterReady, buildSeats, type RosterRow } from '../lib/roster';
import { commitName, rosterReady, buildSeats, shuffleSeeded, type RosterRow } from '../lib/roster';
import type { AccountRef, Variant } from '../lib/model';
import {
availableVariants,
@@ -255,16 +255,19 @@
return;
}
const id = newLocalGameId();
const seed = randomSeed();
// Randomise the seating so the FIRST mover is random (the engine starts at seat 0, so shuffling
// the seats picks a random starter and turn order). Seed-driven, so replay is consistent.
// Snapshot the roster + PINs to plain objects: the rows/pins are $state proxies, and a proxy
// fails structured-clone when the record is persisted to IndexedDB (silently, since the store
// is best-effort) — so the game would vanish on reload. See lib/localgame/store.
// fails structured-clone when the record is persisted to IndexedDB (silently, best-effort store)
// — so the game would vanish on reload. See lib/localgame/store.
await localSource.create({
id,
variant: inviteVariant,
dictVersion: version,
seed: randomSeed(),
seed,
multipleWords: multipleWordsForRequest(inviteVariant, multipleWords),
seats: $state.snapshot(buildSeats(rows)),
seats: $state.snapshot(shuffleSeeded(buildSeats(rows), seed)),
hotseat: true,
hostPin: hostPin ? $state.snapshot(hostPin) : undefined,
});