e40adfb0c7
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
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 1m56s
Address review of the active-game limit lock: - Live-event GameViews (opponent_moved, match_found, game_started, ...) did not carry game_kind, so a lobby patch from an event zeroed a game's kind and the client's per-kind count under-counted — a capped kind read as free (a disabled start instead of the lock, and a wrong per-kind result). Thread Kind through notify.GameSummary → the event GameView. - New Game screen refreshes the lobby games on mount so the per-kind count reflects the current set, not a stale cached snapshot. - The guest funnel's login button routes to the profile screen (the account controls), not settings. - Copy: the guest prompt is "sign in to use all the game's features"; the durable notice is "finish your active games to start a new one". Regression tests: the notify opponent-moved payload carries kind; the client lock locks only the reached kind (vs_ai at cap leaves random open).
86 lines
3.3 KiB
TypeScript
86 lines
3.3 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { activeByKind, capFor, isKindLocked } from './gamelimits';
|
|
import { GameKind, type GameView, type GameLimits } from './model';
|
|
|
|
// game builds a minimal GameView for the count: only kind, status and (optionally) hotseat matter.
|
|
function game(kind: number, status: string, extra: Partial<GameView> = {}): GameView {
|
|
return {
|
|
id: 'g',
|
|
variant: 'scrabble_en',
|
|
dictVersion: '',
|
|
status,
|
|
players: 2,
|
|
toMove: 0,
|
|
turnTimeoutSecs: 0,
|
|
multipleWordsPerTurn: true,
|
|
moveCount: 0,
|
|
endReason: '',
|
|
lastActivityUnix: 0,
|
|
vsAi: false,
|
|
unreadChat: false,
|
|
unreadMessages: false,
|
|
kind,
|
|
seats: [],
|
|
...extra,
|
|
};
|
|
}
|
|
|
|
describe('activeByKind', () => {
|
|
it('counts open+active games per kind, skipping finished, hotseat and untagged (kind 0)', () => {
|
|
const by = activeByKind([
|
|
game(GameKind.vsAi, 'active'),
|
|
game(GameKind.random, 'open'),
|
|
game(GameKind.random, 'active'),
|
|
game(GameKind.random, 'finished'), // finished → not counted
|
|
game(GameKind.vsAi, 'active', { hotseat: true }), // local pass-and-play → not counted
|
|
game(0, 'active'), // pre-existing / untagged → not counted
|
|
]);
|
|
expect(by[GameKind.vsAi]).toBe(1);
|
|
expect(by[GameKind.random]).toBe(2);
|
|
});
|
|
});
|
|
|
|
describe('capFor', () => {
|
|
const limits: GameLimits = { vsAi: 1, random: 10, friends: 0 };
|
|
it('maps each kind to its tier cap; unknown kind and absent limits are unlimited (-1)', () => {
|
|
expect(capFor(limits, GameKind.vsAi)).toBe(1);
|
|
expect(capFor(limits, GameKind.random)).toBe(10);
|
|
expect(capFor(limits, GameKind.friends)).toBe(0);
|
|
expect(capFor(limits, 0)).toBe(-1);
|
|
expect(capFor(undefined, GameKind.vsAi)).toBe(-1);
|
|
});
|
|
});
|
|
|
|
describe('isKindLocked', () => {
|
|
const guest: GameLimits = { vsAi: 1, random: 1, friends: 0 };
|
|
const durable: GameLimits = { vsAi: 10, random: 10, friends: 10 };
|
|
|
|
it('locks a guest at the per-kind cap, not another kind', () => {
|
|
const games = [game(GameKind.vsAi, 'active')];
|
|
expect(isKindLocked(games, guest, GameKind.vsAi)).toBe(true);
|
|
expect(isKindLocked(games, guest, GameKind.random)).toBe(false);
|
|
});
|
|
|
|
it('a cap of 0 always locks (a guest cannot start friend games)', () => {
|
|
expect(isKindLocked([], guest, GameKind.friends)).toBe(true);
|
|
});
|
|
|
|
it('an unlimited cap (-1) or absent limits never lock', () => {
|
|
const many = [game(GameKind.random, 'active'), game(GameKind.random, 'active')];
|
|
expect(isKindLocked(many, { vsAi: -1, random: -1, friends: -1 }, GameKind.random)).toBe(false);
|
|
expect(isKindLocked(many, undefined, GameKind.random)).toBe(false);
|
|
});
|
|
|
|
it('a durable account stays well under its higher cap', () => {
|
|
expect(isKindLocked([game(GameKind.random, 'active')], durable, GameKind.random)).toBe(false);
|
|
});
|
|
|
|
it('locks only the reached kind: at the vs_ai cap, random with no games stays open', () => {
|
|
const limits = { vsAi: 3, random: 2, friends: 1 };
|
|
const games = [game(GameKind.vsAi, 'active'), game(GameKind.vsAi, 'active'), game(GameKind.vsAi, 'active')];
|
|
expect(isKindLocked(games, limits, GameKind.vsAi)).toBe(true);
|
|
expect(isKindLocked(games, limits, GameKind.random)).toBe(false);
|
|
expect(isKindLocked(games, limits, GameKind.friends)).toBe(false);
|
|
});
|
|
});
|