fix(offline): unlock finished hotseat games + raise entry budget to 120
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 1m5s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m47s
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 1m5s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m47s
- source: a finished hotseat game is never locked (its final rack is shown for review, not an Unlock button); guard locked on !isOver. - bundle-size: raise the app-entry budget 115 -> 120 KB for the hotseat UI (PIN pad, roster, host menu), which lives in the always-loaded New Game / Game / Lobby screens; the offline engine + PIN hashing stay lazy.
This commit is contained in:
@@ -23,10 +23,13 @@ const DIST = 'dist';
|
|||||||
// wiring, to 112 for the PWA install feature, to 113 for the offline-mode wiring, to 114 for
|
// wiring, to 112 for the PWA install feature, to 113 for the offline-mode wiring, to 114 for
|
||||||
// the offline auto-detect (the cold-start reachability check and the "no connection" dialog live in
|
// the offline auto-detect (the cold-start reachability check and the "no connection" dialog live in
|
||||||
// the boot path), then to 115 for the vs_ai idle-hint gate — its monotonic clock, lock badge and
|
// the boot path), then to 115 for the vs_ai idle-hint gate — its monotonic clock, lock badge and
|
||||||
// countdown toast live in the always-loaded game screen (Game.svelte). The heavy parts — the dict
|
// countdown toast live in the always-loaded game screen (Game.svelte) — and to 120 for the offline
|
||||||
// loader, the move generator and the preload orchestration — still stay in lazy chunks. Scoped CSS
|
// pass-and-play (hotseat) mode: the on-screen PIN pad, the creation roster and the in-game host menu
|
||||||
// lands in the CSS chunk, not this JS budget.
|
// live in the always-loaded New Game / Game / Lobby screens (the offline engine and the tiny PIN
|
||||||
const BUDGET = { app: 115, shared: 30, landing: 5 };
|
// hashing stay in lazy chunks / are negligible). The heavy parts — the dict loader, the move
|
||||||
|
// generator and the preload orchestration — still stay in lazy chunks. Scoped CSS lands in the CSS
|
||||||
|
// chunk, not this JS budget.
|
||||||
|
const BUDGET = { app: 120, shared: 30, landing: 5 };
|
||||||
|
|
||||||
// gzipped returns the gzipped byte size of a built asset, or 0 when the reference is not a
|
// gzipped returns the gzipped byte size of a built asset, or 0 when the reference is not a
|
||||||
// local file (e.g. the Telegram SDK loaded from a CDN) or is missing.
|
// local file (e.g. the Telegram SDK loaded from a CDN) or is missing.
|
||||||
|
|||||||
@@ -120,13 +120,14 @@ describe('LocalSource hotseat', () => {
|
|||||||
expect(await src.verifyHostPin('local:h7', '0000')).toBe(false);
|
expect(await src.verifyHostPin('local:h7', '0000')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('persists a naturally finished game (scoreless run) as finished', async () => {
|
it('persists a naturally finished game (scoreless run) as finished, and unlocked', async () => {
|
||||||
const src = await hotseat('local:h8', [
|
const src = await hotseat('local:h8', [
|
||||||
{ kind: 'human', name: 'A' },
|
{ kind: 'human', name: 'A', pin: await newLock('1111') },
|
||||||
{ kind: 'human', name: 'B' },
|
{ kind: 'human', name: 'B', pin: await newLock('2222') },
|
||||||
]);
|
]);
|
||||||
for (let i = 0; i < 6; i++) await src.pass('local:h8');
|
for (let i = 0; i < 6; i++) await src.pass('local:h8');
|
||||||
const st = await src.gameState('local:h8');
|
const st = await src.gameState('local:h8');
|
||||||
expect(st.game.status).toBe('finished');
|
expect(st.game.status).toBe('finished');
|
||||||
|
expect(st.locked).toBeFalsy(); // a finished game shows its final rack, never an unlock button
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -418,7 +418,8 @@ export class LocalSource implements GameLoopSource {
|
|||||||
private stateView(entry: Live): StateView {
|
private stateView(entry: Live): StateView {
|
||||||
const hotseat = !!entry.record.hotseat;
|
const hotseat = !!entry.record.hotseat;
|
||||||
const seat = hotseat ? entry.game.currentPlayer : this.humanSeat(entry);
|
const seat = hotseat ? entry.game.currentPlayer : this.humanSeat(entry);
|
||||||
const locked = hotseat && !!entry.record.seats[seat]?.pin && entry.unlockedSeat !== seat;
|
// A finished game is never locked — its final rack is shown for review, not an unlock button.
|
||||||
|
const locked = !entry.game.isOver && hotseat && !!entry.record.seats[seat]?.pin && entry.unlockedSeat !== seat;
|
||||||
const rack = locked ? [] : entry.game.handOf(seat).map((t) => indexToLetter(entry.record.variant, t));
|
const rack = locked ? [] : entry.game.handOf(seat).map((t) => indexToLetter(entry.record.variant, t));
|
||||||
return {
|
return {
|
||||||
game: this.gameView(entry),
|
game: this.gameView(entry),
|
||||||
|
|||||||
Reference in New Issue
Block a user