feat(lobby): enter the game immediately and wait for the opponent inside it
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m4s
CI / changes (pull_request) Successful in 1s
CI / unit (pull_request) Successful in 8s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 45s
CI / gate (pull_request) Successful in 1s
CI / deploy (pull_request) Successful in 1m4s
Quick auto-match no longer waits on a separate screen: Enqueue opens a real game seating the caller with an empty opponent seat (new game status 'open') and the player enters it at once. A second human searching the same variant+rule joins that open game; otherwise a background reaper seats a robot after a 90s + random 0-90s wait, pushing a new in-app opponent_joined event that fills the opponent card and re-enables resign and chat in place. Matchmaking state is now the open games in the database (the in-memory pool, lobby.poll and lobby.cancel are gone), serialised by a per-bucket advisory lock. While a game is open the starter may move on their turn, but resign, chat and nudge are refused; the lobby and opponent card show "searching for opponent". Schema edited in the baseline (no prod data): 'open' status, nullable game_players.account_id for the empty seat, and a games.open_deadline_at stamp; jet code regenerated.
This commit is contained in:
@@ -88,6 +88,8 @@ export class MockGateway implements GatewayClient {
|
||||
private readonly profile: Profile = { ...PROFILE };
|
||||
private readonly subs = new Set<(e: PushEvent) => void>();
|
||||
private pendingMatch: string | null = null;
|
||||
// The most recently opened auto-match game still awaiting an opponent, for the e2e join hook.
|
||||
private openGameId: string | null = null;
|
||||
private friends: AccountRef[] = MOCK_FRIENDS.map((f) => ({ ...f }));
|
||||
private incoming: AccountRef[] = MOCK_INCOMING.map((f) => ({ ...f }));
|
||||
private outgoing: AccountRef[] = [];
|
||||
@@ -143,14 +145,16 @@ export class MockGateway implements GatewayClient {
|
||||
|
||||
// --- lobby ---
|
||||
async lobbyEnqueue(variant: Variant, multipleWords: boolean): Promise<MatchResult> {
|
||||
// Simulate a 10s-style robot substitution, sped up: match found shortly.
|
||||
// The player enters an open game immediately and waits inside it; a robot opponent takes
|
||||
// the empty seat shortly (a sped-up version of the backend's 90–180 s wait), pushing
|
||||
// opponent_joined so the game UI restores from the "searching for opponent" state.
|
||||
const id = crypto.randomUUID();
|
||||
const g: MockGame = {
|
||||
view: {
|
||||
id,
|
||||
variant,
|
||||
dictVersion: 'v1',
|
||||
status: 'active',
|
||||
status: 'open',
|
||||
players: 2,
|
||||
toMove: 0,
|
||||
turnTimeoutSecs: 86400,
|
||||
@@ -160,7 +164,7 @@ export class MockGateway implements GatewayClient {
|
||||
lastActivityUnix: Math.floor(Date.now() / 1000),
|
||||
seats: [
|
||||
{ seat: 0, accountId: ME, displayName: 'You', score: 0, hintsUsed: 0, isWinner: false },
|
||||
{ seat: 1, accountId: 'robot', displayName: 'Robo', score: 0, hintsUsed: 0, isWinner: false },
|
||||
{ seat: 1, accountId: '', displayName: '', score: 0, hintsUsed: 0, isWinner: false },
|
||||
],
|
||||
},
|
||||
moves: [],
|
||||
@@ -170,9 +174,28 @@ export class MockGateway implements GatewayClient {
|
||||
chat: [],
|
||||
};
|
||||
this.games.set(id, g);
|
||||
this.pendingMatch = id;
|
||||
setTimeout(() => this.emit({ kind: 'match_found', gameId: id }), 1400);
|
||||
return { matched: false };
|
||||
this.openGameId = id;
|
||||
// The opponent joins on a timer for manual mock play; the e2e triggers it deterministically
|
||||
// through the __mock hook (see lib/gateway.ts).
|
||||
setTimeout(() => this.fillOpponent(id), 3000);
|
||||
return { matched: false, game: structuredClone(g.view) };
|
||||
}
|
||||
|
||||
// fillOpponent seats a robot in an open game's empty seat and pushes opponent_joined — the
|
||||
// mock of a human or robot taking the seat. A no-op once the game is no longer open.
|
||||
private fillOpponent(id: string): void {
|
||||
const game = this.games.get(id);
|
||||
if (!game || game.view.status !== 'open') return;
|
||||
game.view.status = 'active';
|
||||
game.view.seats[1] = { seat: 1, accountId: 'robot', displayName: 'Robo', score: 0, hintsUsed: 0, isWinner: false };
|
||||
this.emit({ kind: 'opponent_joined', gameId: id, state: this.stateOf(game) });
|
||||
}
|
||||
|
||||
// joinPendingOpponent is the e2e hook (exposed as window.__mock.joinOpponent) to attach the
|
||||
// opponent to the most recently opened game on demand, making the waiting → joined transition
|
||||
// deterministic.
|
||||
joinPendingOpponent(): void {
|
||||
if (this.openGameId) this.fillOpponent(this.openGameId);
|
||||
}
|
||||
|
||||
async lobbyPoll(): Promise<MatchResult> {
|
||||
@@ -190,8 +213,8 @@ export class MockGateway implements GatewayClient {
|
||||
}
|
||||
|
||||
// --- game ---
|
||||
async gameState(gameId: string, _includeAlphabet: boolean): Promise<StateView> {
|
||||
const g = this.game(gameId);
|
||||
// stateOf builds a player's StateView from a mock game (the viewer is always ME).
|
||||
private stateOf(g: MockGame): StateView {
|
||||
return {
|
||||
game: structuredClone(g.view),
|
||||
seat: this.mySeat(g),
|
||||
@@ -201,6 +224,10 @@ export class MockGateway implements GatewayClient {
|
||||
};
|
||||
}
|
||||
|
||||
async gameState(gameId: string, _includeAlphabet: boolean): Promise<StateView> {
|
||||
return this.stateOf(this.game(gameId));
|
||||
}
|
||||
|
||||
async gameHistory(gameId: string): Promise<History> {
|
||||
const g = this.game(gameId);
|
||||
return { gameId, moves: structuredClone(g.moves) };
|
||||
|
||||
Reference in New Issue
Block a user