feat: game lobby service
This commit is contained in:
@@ -0,0 +1,196 @@
|
||||
# Flows
|
||||
|
||||
This document collects the eight platform flows that span Game Lobby plus
|
||||
its synchronous and asynchronous neighbours. Narrative descriptions of the
|
||||
rules these flows enforce live in `../README.md`; the diagrams here focus on
|
||||
the message order across the boundary.
|
||||
|
||||
## Public Game Application
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Gateway
|
||||
participant Lobby as Lobby publichttp
|
||||
participant UserSvc as User Service
|
||||
participant Redis
|
||||
participant Stream as notification:intents
|
||||
|
||||
User->>Gateway: lobby.application.submit(game_id, race_name)
|
||||
Gateway->>Lobby: POST /api/v1/lobby/games/{id}/applications + X-User-ID
|
||||
Lobby->>UserSvc: GetEligibility(user_id)
|
||||
UserSvc-->>Lobby: snapshot (entitlement, sanctions)
|
||||
Lobby->>Redis: persist Application(submitted) + indexes
|
||||
Lobby->>Stream: lobby.application.submitted (admin recipients)
|
||||
Lobby-->>Gateway: 200 ApplicationRecord
|
||||
```
|
||||
|
||||
Approval and rejection follow the same pattern, mutating the application
|
||||
status to `approved`/`rejected` and emitting
|
||||
`lobby.membership.approved`/`lobby.membership.rejected` to the applicant.
|
||||
|
||||
## Private Game Invite
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Owner
|
||||
participant Invitee
|
||||
participant Lobby
|
||||
participant Redis
|
||||
participant Stream as notification:intents
|
||||
|
||||
Owner->>Lobby: lobby.invite.create(invitee_user_id)
|
||||
Lobby->>Redis: persist Invite(created)
|
||||
Lobby->>Stream: lobby.invite.created (recipient: invitee)
|
||||
|
||||
Invitee->>Lobby: lobby.invite.redeem(race_name)
|
||||
Lobby->>Lobby: User Service guard for inviter and invitee
|
||||
Lobby->>Redis: RND.Reserve + Membership(active) + Invite(redeemed)
|
||||
Lobby->>Stream: lobby.invite.redeemed (recipient: owner)
|
||||
```
|
||||
|
||||
The owner-facing decline and revoke transitions persist the invite status
|
||||
update and produce no notification in v1.
|
||||
|
||||
## Enrollment Automation
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Tick as Worker tick
|
||||
participant Lobby
|
||||
participant Redis
|
||||
participant Stream as notification:intents
|
||||
|
||||
Tick->>Lobby: enrollment automation cycle
|
||||
Lobby->>Redis: load enrollment_open games + roster sizes
|
||||
alt deadline reached or gap exhausted
|
||||
Lobby->>Redis: status enrollment_open → ready_to_start (CAS)
|
||||
Lobby->>Redis: pending invites → expired
|
||||
Lobby->>Stream: lobby.invite.expired (per expired invite)
|
||||
else still within window
|
||||
Lobby-->>Tick: no-op
|
||||
end
|
||||
```
|
||||
|
||||
Manual `lobby.game.ready_to_start` from owner or admin runs the same close
|
||||
pipeline synchronously without waiting for the next tick.
|
||||
|
||||
## Game Start (happy path)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Actor as Owner or Admin
|
||||
participant Lobby
|
||||
participant Redis
|
||||
participant RT as Runtime Manager
|
||||
participant GM as Game Master
|
||||
|
||||
Actor->>Lobby: lobby.game.start
|
||||
Lobby->>Redis: status ready_to_start → starting (CAS)
|
||||
Lobby->>Redis: XADD runtime:start_jobs
|
||||
RT->>Redis: XADD runtime:job_results (success + container metadata)
|
||||
Lobby->>Redis: persist runtime_binding on game record
|
||||
Lobby->>GM: POST /internal/games/{id}/register-runtime
|
||||
GM-->>Lobby: 200 OK
|
||||
Lobby->>Redis: status starting → running; set started_at
|
||||
```
|
||||
|
||||
If runtime metadata persistence fails, Lobby publishes a stop-job to remove
|
||||
the orphan container before flipping the game to `start_failed`.
|
||||
|
||||
## Game Start (GM unavailable)
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Lobby
|
||||
participant Redis
|
||||
participant GM as Game Master
|
||||
participant Stream as notification:intents
|
||||
|
||||
Lobby->>GM: POST /internal/games/{id}/register-runtime
|
||||
GM-->>Lobby: timeout / 5xx
|
||||
Lobby->>Redis: status starting → paused (CAS)
|
||||
Lobby->>Stream: lobby.runtime_paused_after_start (admin)
|
||||
Note over Lobby,GM: Container stays alive; admin restarts GM<br/>and issues lobby.game.resume.
|
||||
```
|
||||
|
||||
## Game Finish + Capability Evaluation
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant GM as Game Master
|
||||
participant Stream as gm:lobby_events
|
||||
participant Lobby
|
||||
participant Redis
|
||||
participant Intents as notification:intents
|
||||
|
||||
GM->>Stream: XADD runtime_snapshot_update (player_turn_stats)
|
||||
Lobby->>Redis: UpdateMax for each member's stats aggregate
|
||||
GM->>Stream: XADD game_finished
|
||||
Lobby->>Redis: status running/paused → finished; finished_at = event_ts
|
||||
Lobby->>Redis: capability evaluator runs per active membership
|
||||
alt member capable
|
||||
Lobby->>Redis: RND.MarkPendingRegistration(eligible_until = finished_at + 30d)
|
||||
Lobby->>Intents: lobby.race_name.registration_eligible (recipient: user)
|
||||
else not capable
|
||||
Lobby->>Redis: RND.ReleaseReservation
|
||||
Lobby->>Intents: lobby.race_name.registration_denied (optional)
|
||||
end
|
||||
Lobby->>Redis: ReleaseReservation for removed/blocked memberships
|
||||
Lobby->>Redis: delete per-game stats aggregate
|
||||
```
|
||||
|
||||
The evaluation guard `lobby:capability_evaluation:done:<game_id>` makes a
|
||||
replayed `game_finished` event a no-op.
|
||||
|
||||
## Race Name Registration
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant Lobby
|
||||
participant UserSvc as User Service
|
||||
participant RND as Race Name Directory
|
||||
participant Stream as notification:intents
|
||||
|
||||
User->>Lobby: lobby.race_name.register(race_name)
|
||||
Lobby->>UserSvc: GetEligibility (sanctions, max_registered_race_names)
|
||||
UserSvc-->>Lobby: snapshot
|
||||
Lobby->>RND: Register(game_id, user_id, race_name)
|
||||
RND-->>Lobby: ok / ErrPendingExpired / ErrQuotaExceeded
|
||||
alt success
|
||||
Lobby->>Stream: lobby.race_name.registered (recipient: user)
|
||||
Lobby-->>User: 200 RegisteredRaceName
|
||||
else precondition failure
|
||||
Lobby-->>User: 422 DomainPreconditionError
|
||||
end
|
||||
```
|
||||
|
||||
Registration consumes one tariff slot keyed by `(canonical_key, user_id)`;
|
||||
tariff downgrade never revokes existing registrations.
|
||||
|
||||
## Cascade Release on User Lifecycle Event
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant US as User Service
|
||||
participant Stream as user:lifecycle_events
|
||||
participant Lobby
|
||||
participant RT as Runtime Manager
|
||||
participant Intents as notification:intents
|
||||
|
||||
US->>Stream: XADD permanent_blocked or deleted
|
||||
Lobby->>Stream: XREAD (consumer)
|
||||
Lobby->>Lobby: RND.ReleaseAllByUser
|
||||
Lobby->>Lobby: memberships → blocked + lobby.membership.blocked per private game
|
||||
Lobby->>Lobby: applications → rejected
|
||||
Lobby->>Lobby: invites (addressed and inviter-side) → revoked
|
||||
Lobby->>Lobby: owned non-terminal games → cancelled (external_block trigger)
|
||||
Lobby->>RT: XADD runtime:stop_jobs for in-flight owned games
|
||||
Lobby->>Intents: lobby.membership.blocked per affected membership
|
||||
Lobby->>Stream: advance offset
|
||||
```
|
||||
|
||||
Every step is idempotent at the store layer (`ErrConflict` from a CAS is
|
||||
treated as «already done»); the consumer only advances the offset once the
|
||||
handler returns nil.
|
||||
Reference in New Issue
Block a user