feat(offline): offline lobby lists + creates local vs_ai games
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 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
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 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m39s
In offline mode the lobby now shows only the device-local games and its New-vs-AI entry creates one through the in-browser engine — the visible payoff of the offline mode. - LocalSource.list() reconstructs a lobby GameView per stored local game by replay, exposed through the lazy gamesource proxy; unit-tested via an in-memory store. - Lobby.load() branches on offlineMode: lists local games and skips every gateway call (no online games/invitations/incoming); the Stats tab is disabled offline. - NewGame offline: find() creates a device-local vs_ai game via LocalSource.create using the profile's advertised dict version + a local seed; the friends flow and the random-opponent option are hidden, and the variant picker / Start are enabled offline (were gated on connection). - id.ts: newLocalGameId + randomSeed (tested). Docs: ARCHITECTURE + FUNCTIONAL(+ru) offline-mode section. Deferred to fast-follow: the Settings Friends/Profile/Feedback affordance gating, the flip-to-offline readiness wait, the offline mock e2e (needs mock-dawg support), and the local-hint UI. The offline flow is verified on the test contour — the mock e2e cannot enter offline mode (the toggle is gated to an installed PWA).
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
|
||||
import { LocalGame, type LocalMove } from './engine';
|
||||
import { serializeGame, replayGame, type LocalGameRecord, type Seat as LocalSeat } from './serialize';
|
||||
import { getLocalGame, saveLocalGame } from './store';
|
||||
import { getLocalGame, saveLocalGame, listLocalGames } from './store';
|
||||
import { RULESETS } from './ruleset';
|
||||
import { getDawg } from '../dict';
|
||||
import { LOCAL_ID_PREFIX, isLocalGameId } from './id';
|
||||
@@ -131,6 +131,26 @@ export class LocalSource implements GameLoopSource {
|
||||
return this.stateView(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* list returns a lobby GameView for every stored local game, most-recently-updated first, so the
|
||||
* offline lobby renders them with the same card machinery as the online games. Each is
|
||||
* reconstructed by replay; a record whose dictionary is unavailable (e.g. offline without its
|
||||
* dawg) is skipped, since it cannot be rendered — never throws.
|
||||
*/
|
||||
async list(): Promise<GameView[]> {
|
||||
const records = await listLocalGames();
|
||||
records.sort((a, b) => b.updatedAtUnix - a.updatedAtUnix);
|
||||
const views: GameView[] = [];
|
||||
for (const record of records) {
|
||||
try {
|
||||
views.push(this.gameView(await this.load(record.id)));
|
||||
} catch {
|
||||
/* a record that cannot be replayed (its dawg is not cached offline) is left out */
|
||||
}
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
async gameState(gameId: string): Promise<StateView> {
|
||||
return this.stateView(await this.load(gameId));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user