feat: game lobby service
This commit is contained in:
+151
-9
@@ -71,6 +71,11 @@ For downstream business services, the current default trusted transport is
|
||||
strict REST/JSON. Gateway may therefore authenticate and verify one external
|
||||
FlatBuffers command, then transcode it to one trusted downstream REST call.
|
||||
|
||||
When forwarding an authenticated command to a downstream service, `Edge Gateway`
|
||||
enriches the REST call with the `X-User-ID` header carrying the verified platform
|
||||
user identifier. Downstream services derive the acting user identity exclusively
|
||||
from this header and must never accept identity claims from request body fields.
|
||||
|
||||
The public auth contract is:
|
||||
|
||||
* `send-email-code(email) -> challenge_id`
|
||||
@@ -142,6 +147,8 @@ flowchart LR
|
||||
Lobby --> Runtime
|
||||
Lobby --> Redis
|
||||
|
||||
User --> Lobby
|
||||
|
||||
GM --> Lobby
|
||||
GM --> Runtime
|
||||
GM --> Redis
|
||||
@@ -242,10 +249,20 @@ business data.
|
||||
It is the source of truth for:
|
||||
|
||||
* `user_id` of regular platform users;
|
||||
* regular-user profile fields and editable user settings;
|
||||
* current tariff/entitlement state;
|
||||
* user-specific limits and platform sanctions;
|
||||
* latest effective `declared_country`.
|
||||
* `user_name` — immutable auto-generated unique platform handle in
|
||||
`player-<suffix>` form; never used as foreign key in other models;
|
||||
* `display_name` — mutable free-text user-editable label validated through
|
||||
`pkg/util/string.go:ValidateTypeName`; not required to be unique; default
|
||||
empty for new accounts;
|
||||
* editable user settings (`preferred_language`, `time_zone`);
|
||||
* current tariff/entitlement state including `max_registered_race_names`;
|
||||
* user-specific limits and platform sanctions (including
|
||||
`permanent_block` and `max_registered_race_names` override limits);
|
||||
* latest effective `declared_country`;
|
||||
* soft-delete state via `DeleteUser`.
|
||||
|
||||
`User Service` does not own in-game `race_name` values; those live in
|
||||
`Game Lobby` Race Name Directory.
|
||||
|
||||
System-administrator identity remains outside this service and belongs to the
|
||||
later `Admin Service`. Trusted administrative reads and mutations against
|
||||
@@ -270,6 +287,16 @@ Architectural rules fixed for this service:
|
||||
of scope.
|
||||
* `User Service` stores only the current effective `declared_country`; review
|
||||
workflow and history belong to `Geo Profile Service`.
|
||||
* `User Service` does not own in-game `race_name` values. All in-game name
|
||||
state (registered, reserved, pending registration) lives in the Game Lobby
|
||||
Race Name Directory. The only identity strings owned by `User Service` are
|
||||
`user_name` (immutable) and `display_name` (mutable, non-unique).
|
||||
* `permanent_block` is a dedicated sanction code that collapses every
|
||||
`can_*` eligibility marker to false and triggers RND cascade release via
|
||||
the `user:lifecycle_events` stream.
|
||||
* `DeleteUser` is a trusted internal endpoint that soft-deletes the account,
|
||||
rejects all subsequent operations with `subject_not_found`, and triggers
|
||||
the same RND cascade release.
|
||||
* During the current auth-registration rollout, `Auth / Session Service`
|
||||
passes a preferred-language candidate derived from public
|
||||
`Accept-Language`, falling back to `en` when no supported value is
|
||||
@@ -349,7 +376,7 @@ Its job is to:
|
||||
|
||||
System administrators can view and operate on all games, including private ones.
|
||||
|
||||
## 7. Game Lobby Service
|
||||
## 7. [Game Lobby Service](lobby/README.md)
|
||||
|
||||
`Game Lobby` owns platform-level metadata and lifecycle of game sessions as platform entities.
|
||||
|
||||
@@ -379,6 +406,11 @@ It also stores a denormalized runtime snapshot for convenience, at least:
|
||||
* `runtime_status`;
|
||||
* `engine_health_summary`.
|
||||
|
||||
Additionally, `Game Lobby` aggregates per-member game statistics from
|
||||
`player_turn_stats` carried on each `runtime_snapshot_update` event: current
|
||||
and running-max of `planets`, `population`, and `ships_built`. The aggregate
|
||||
is retained from game start until capability evaluation at `game_finished`.
|
||||
|
||||
This prevents user-facing list/read flows from fan-out requests into `Game Master`.
|
||||
|
||||
### Lobby status model
|
||||
@@ -387,9 +419,9 @@ Minimum platform-level status set:
|
||||
|
||||
* `draft`
|
||||
* `enrollment_open`
|
||||
* `enrollment_closed`
|
||||
* `ready_to_start`
|
||||
* `starting`
|
||||
* `start_failed`
|
||||
* `running`
|
||||
* `paused`
|
||||
* `finished`
|
||||
@@ -397,6 +429,32 @@ Minimum platform-level status set:
|
||||
|
||||
`Lobby.paused` is a business/platform pause, distinct from engine/runtime failure states.
|
||||
|
||||
`start_failed` indicates that the runtime container could not be started or that
|
||||
metadata persistence failed after a successful container start.
|
||||
From `start_failed` an admin or owner may retry (→ `ready_to_start`) or cancel (→ `cancelled`).
|
||||
|
||||
### Enrollment rules
|
||||
|
||||
Each game stores three enrollment configuration fields set at creation:
|
||||
|
||||
* `min_players` — minimum approved participants required before the game may start.
|
||||
* `max_players` — target roster size that activates the gap admission window.
|
||||
* `start_gap_hours` — hours to keep enrollment open after `max_players` is reached.
|
||||
* `start_gap_players` — additional players admitted during the gap window.
|
||||
* `enrollment_ends_at` — UTC Unix timestamp at which enrollment closes automatically.
|
||||
|
||||
Transition from `enrollment_open` to `ready_to_start` occurs via one of three paths:
|
||||
|
||||
1. **Manual**: an admin (public game) or owner (private game) issues a close-enrollment
|
||||
command when `approved_count >= min_players`.
|
||||
2. **Deadline**: `enrollment_ends_at` is reached and `approved_count >= min_players`.
|
||||
3. **Gap exhaustion**: `approved_count >= max_players` activates a gap window of
|
||||
`start_gap_hours` during which up to `start_gap_players` additional participants
|
||||
may join; the transition fires when the gap window expires or
|
||||
`approved_count >= max_players + start_gap_players`.
|
||||
|
||||
All pending invites transition to `expired` when the game moves to `ready_to_start`.
|
||||
|
||||
### Membership rules
|
||||
|
||||
* `User Service` owns users of the platform as identities.
|
||||
@@ -417,11 +475,65 @@ Private games:
|
||||
* can be created only by eligible paid users;
|
||||
* visible only to their owner and to invited users whose invitation is bound
|
||||
to a concrete `user_id` and later accepted;
|
||||
* joining uses a user-bound invite plus owner approval;
|
||||
* joining uses a user-bound invite; accepting the invite immediately creates active
|
||||
membership without a separate owner-approval step;
|
||||
* invite lifecycle belongs entirely to `Game Lobby`.
|
||||
|
||||
Private-party owners get a limited owner-admin capability set, not full system admin power.
|
||||
|
||||
### Race Name Directory
|
||||
|
||||
`Race Name Directory` (RND) is the platform source of truth for in-game player
|
||||
names (`race_name`). It is owned by `Game Lobby` in v1 and is scheduled to move
|
||||
to a dedicated `Race Name Service` later without changing the domain or
|
||||
service-layer logic.
|
||||
|
||||
RND owns three levels of state per name:
|
||||
|
||||
- **registered** — platform-unique permanent names owned by one regular user.
|
||||
A registered name cannot be transferred, released, or renamed; the only path
|
||||
back to availability is `permanent_block` or `DeleteUser` on the owning
|
||||
account. The number of registered names a user can hold is bounded by the
|
||||
current tariff (`max_registered_race_names` in the `User Service` eligibility
|
||||
snapshot): `free=1`, `paid_monthly=2`, `paid_yearly=6`,
|
||||
`paid_lifetime=unlimited`. Tariff downgrade never revokes existing
|
||||
registrations; it only constrains new ones.
|
||||
- **reservation** — per-game binding created when a participant joins a game
|
||||
through application approval or invite redeem. The reservation key is
|
||||
`(game_id, canonical_key)`. One user may hold the same name simultaneously
|
||||
across multiple active games. A reservation survives until the game
|
||||
finishes, then either becomes a `pending_registration` (see below) or is
|
||||
released.
|
||||
- **pending_registration** — a reservation that survived a capable finish and
|
||||
is now waiting up to 30 days for the owner to upgrade it into a registered
|
||||
name via `lobby.race_name.register`. Expiration releases the binding.
|
||||
|
||||
**Canonical key** — RND uses a canonical key (lowercase + frozen
|
||||
confusable-pair policy) to enforce uniqueness. A name is considered taken for
|
||||
another user when any `registered`, active `reservation`, or
|
||||
`pending_registration` with a different `user_id` exists under the same
|
||||
canonical key. The confusable-pair policy lives in Lobby
|
||||
(`lobby/internal/domain/racename/policy.go`).
|
||||
|
||||
**Capability gating** — at `game_finished` `Game Lobby` evaluates per-member
|
||||
capability: `capable = max_planets > initial_planets AND max_population >
|
||||
initial_population`, computed from the `player_turn_stats` stream published by
|
||||
`Game Master`. Capable reservations transition to `pending_registration` with
|
||||
`eligible_until = finished_at + 30 days`; non-capable reservations are
|
||||
released immediately.
|
||||
|
||||
**Registration** — a user initiates registration via `lobby.race_name.register`
|
||||
inside the 30-day window. Registration succeeds only when the user is still
|
||||
eligible (no `permanent_block`, tariff slot available) and the pending entry
|
||||
is still within its window. Expired pending entries are released by a
|
||||
background worker.
|
||||
|
||||
**Cascade release** — `User Service` publishes
|
||||
`user.lifecycle.permanent_blocked` and `user.lifecycle.deleted` events to
|
||||
`user:lifecycle_events`. `Game Lobby` consumes this stream and calls
|
||||
`RND.ReleaseAllByUser(user_id)` atomically with membership/application/invite
|
||||
cancellations for the affected user.
|
||||
|
||||
## 8. Game Master
|
||||
|
||||
`Game Master` owns runtime and operational metadata of already running games.
|
||||
@@ -486,6 +598,24 @@ It triggers turn generation according to the game schedule.
|
||||
|
||||
If a manual “force next turn” is executed, the next scheduled turn slot must be skipped so that players still get at least one full normal schedule interval before the following generated turn.
|
||||
|
||||
### Runtime snapshot publishing
|
||||
|
||||
`Game Master` publishes runtime updates to the `gm:lobby_events` Redis Stream
|
||||
consumed by `Game Lobby`. Events include:
|
||||
|
||||
* `runtime_snapshot_update` — carries the current `current_turn`,
|
||||
`runtime_status`, `engine_health_summary`, and a `player_turn_stats` array
|
||||
with one entry per active member (`user_id`, `planets`, `population`,
|
||||
`ships_built`). `Game Lobby` maintains a per-game per-user stats aggregate
|
||||
from these events for capability evaluation at game finish.
|
||||
* `game_finished` — carries the final snapshot values and triggers the
|
||||
platform status transition plus Race Name Directory capability evaluation
|
||||
inside `Game Lobby`.
|
||||
|
||||
`Game Master` does not retain the aggregate; it only publishes the per-turn
|
||||
observation. `Game Lobby` is responsible for holding initial values and
|
||||
running maxima across the lifetime of the game.
|
||||
|
||||
### Runtime/engine finish flow
|
||||
|
||||
When the engine determines that a game is finished:
|
||||
@@ -595,6 +725,11 @@ When introduced, it will:
|
||||
|
||||
`User Service` remains the source of truth for current entitlement used by the rest of the platform.
|
||||
|
||||
Billing-driven tariff changes alter only the headroom for *new* registered
|
||||
race names: tariff downgrade never revokes already registered names. The
|
||||
affected ceiling is materialized as `max_registered_race_names` in the
|
||||
eligibility snapshot consumed by `Game Lobby`.
|
||||
|
||||
## Data Ownership Summary
|
||||
|
||||
```mermaid
|
||||
@@ -608,9 +743,9 @@ flowchart TD
|
||||
N["Notification Service"]
|
||||
M["Mail Service"]
|
||||
|
||||
U -->|"regular users, profile/settings, tariffs, limits, sanctions, current declared_country"| X1["Platform user identity"]
|
||||
U -->|"regular users, user_name/display_name, settings, tariffs, limits, sanctions, declared_country, soft-delete"| X1["Platform user identity"]
|
||||
A -->|"challenges, device sessions, revoke/block state"| X2["Auth/session state"]
|
||||
L -->|"game metadata, invites, applications, membership, roster"| X3["Platform game records"]
|
||||
L -->|"game metadata, invites, applications, membership, roster, race names (registered/reservations/pending)"| X3["Platform game records"]
|
||||
G -->|"runtime state, current turn, engine health, engine mapping, engine version registry"| X4["Running-game state"]
|
||||
R -->|"container execution and technical runtime control"| X5["Container runtime"]
|
||||
P -->|"observed country, usual_connection_country, review state, declared_country history"| X6["Geo state"]
|
||||
@@ -646,6 +781,13 @@ The platform uses one simple rule:
|
||||
* `Lobby -> Runtime Manager` runtime jobs;
|
||||
* `Game Master -> Runtime Manager` runtime jobs;
|
||||
* all event-bus propagation;
|
||||
* `Game Master -> Game Lobby` runtime snapshot updates (including
|
||||
`player_turn_stats` for capability aggregation) and game-finish events
|
||||
through a dedicated Redis Stream consumed by `Game Lobby`;
|
||||
* `User Service -> Game Lobby` user lifecycle events
|
||||
(`user.lifecycle.permanent_blocked`, `user.lifecycle.deleted`) through the
|
||||
`user:lifecycle_events` Redis Stream, consumed by `Game Lobby` to cascade
|
||||
RND release and membership/application/invite cancellation;
|
||||
* `Game Master -> Notification Service` notification intents through
|
||||
`notification:intents`;
|
||||
* `Game Lobby -> Notification Service` notification intents through
|
||||
|
||||
Reference in New Issue
Block a user