feat: game lobby service
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
)
|
||||
|
||||
// PlayerObservedStats stores one decoded `player_turn_stats` element from a
|
||||
// Game Master event. The fields mirror the wire format documented in
|
||||
// lobby/README.md §Runtime Snapshot. Counts are non-negative cumulative
|
||||
// totals reported by the engine for the named user at the time the event
|
||||
// was emitted.
|
||||
type PlayerObservedStats struct {
|
||||
// UserID identifies the platform user the stats line belongs to.
|
||||
UserID string
|
||||
|
||||
// Planets stores the cumulative number of planets controlled by the
|
||||
// user at observation time.
|
||||
Planets int64
|
||||
|
||||
// Population stores the cumulative population owned by the user at
|
||||
// observation time.
|
||||
Population int64
|
||||
|
||||
// ShipsBuilt stores the cumulative ships-built counter for the user
|
||||
// at observation time.
|
||||
ShipsBuilt int64
|
||||
}
|
||||
|
||||
// Validate reports whether stats contain the structural invariants required
|
||||
// by GameTurnStatsStore methods.
|
||||
func (stats PlayerObservedStats) Validate() error {
|
||||
if strings.TrimSpace(stats.UserID) == "" {
|
||||
return fmt.Errorf("player turn stats: user id must not be empty")
|
||||
}
|
||||
if stats.Planets < 0 {
|
||||
return fmt.Errorf("player turn stats: planets must not be negative")
|
||||
}
|
||||
if stats.Population < 0 {
|
||||
return fmt.Errorf("player turn stats: population must not be negative")
|
||||
}
|
||||
if stats.ShipsBuilt < 0 {
|
||||
return fmt.Errorf("player turn stats: ships built must not be negative")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// PlayerInitialStats is an alias for PlayerObservedStats. Game Lobby
|
||||
// Service freezes the first observation per user as that user's initial
|
||||
// values; the wire format is identical, only the persistence semantics
|
||||
// differ.
|
||||
type PlayerInitialStats = PlayerObservedStats
|
||||
|
||||
// PlayerStatsAggregate stores the per-user aggregate maintained by
|
||||
// GameTurnStatsStore for one game. Initial fields are frozen at the first
|
||||
// SaveInitial call for the user; max fields are updated by per-component
|
||||
// maximum on each UpdateMax call.
|
||||
type PlayerStatsAggregate struct {
|
||||
// UserID identifies the platform user the aggregate belongs to.
|
||||
UserID string
|
||||
|
||||
// InitialPlanets stores the user's planets count from the first
|
||||
// observation, frozen for the lifetime of the game.
|
||||
InitialPlanets int64
|
||||
|
||||
// InitialPopulation stores the user's population from the first
|
||||
// observation, frozen for the lifetime of the game.
|
||||
InitialPopulation int64
|
||||
|
||||
// InitialShipsBuilt stores the user's ships-built counter from the
|
||||
// first observation, frozen for the lifetime of the game.
|
||||
InitialShipsBuilt int64
|
||||
|
||||
// MaxPlanets stores the running maximum planets count observed for
|
||||
// the user across the lifetime of the game.
|
||||
MaxPlanets int64
|
||||
|
||||
// MaxPopulation stores the running maximum population observed for
|
||||
// the user across the lifetime of the game.
|
||||
MaxPopulation int64
|
||||
|
||||
// MaxShipsBuilt stores the running maximum ships-built counter
|
||||
// observed for the user across the lifetime of the game.
|
||||
MaxShipsBuilt int64
|
||||
}
|
||||
|
||||
// GameTurnStatsAggregate stores every PlayerStatsAggregate for one game.
|
||||
// The slice is empty when no SaveInitial call has succeeded yet.
|
||||
type GameTurnStatsAggregate struct {
|
||||
// GameID identifies the game the aggregate belongs to.
|
||||
GameID common.GameID
|
||||
|
||||
// Players stores the per-user aggregates ordered by UserID ascending.
|
||||
Players []PlayerStatsAggregate
|
||||
}
|
||||
|
||||
// GameTurnStatsStore stores per-game per-user initial and running-maximum
|
||||
// stats derived from Game Master `runtime_snapshot_update` events. The
|
||||
// aggregate is read once by capability evaluation at game finish and then
|
||||
// deleted (see lobby/README.md §Runtime Snapshot).
|
||||
//
|
||||
// Adapters must keep the initial fields frozen against later SaveInitial
|
||||
// calls and must keep the max fields monotonically non-decreasing on every
|
||||
// UpdateMax call. All methods are safe to call concurrently for the same
|
||||
// gameID.
|
||||
type GameTurnStatsStore interface {
|
||||
// SaveInitial freezes the initial fields for every user in stats.
|
||||
// Subsequent calls for the same (gameID, user_id) tuple do not
|
||||
// overwrite the stored initial values; they return nil so workers
|
||||
// may call SaveInitial on every received event without bookkeeping.
|
||||
// The first call for a user also primes the max fields with the same
|
||||
// values so a single observation is reflected by both the initial
|
||||
// and the running maximum.
|
||||
SaveInitial(ctx context.Context, gameID common.GameID, stats []PlayerInitialStats) error
|
||||
|
||||
// UpdateMax updates the max fields for every user in stats by
|
||||
// per-component maximum. Existing aggregate entries below the new
|
||||
// observation are raised; existing entries at or above the new
|
||||
// observation are left unchanged. Calling UpdateMax for a user that
|
||||
// has no aggregate entry yet creates one whose initial fields and
|
||||
// max fields both equal the observation, so callers may safely call
|
||||
// UpdateMax without first calling SaveInitial.
|
||||
UpdateMax(ctx context.Context, gameID common.GameID, stats []PlayerObservedStats) error
|
||||
|
||||
// Load returns the GameTurnStatsAggregate stored for gameID. The
|
||||
// aggregate is empty (Players length zero) when no observation has
|
||||
// been recorded yet; the GameID is always populated. The returned
|
||||
// Players slice is ordered by UserID ascending so capability
|
||||
// evaluation produces deterministic side-effect order on replay.
|
||||
Load(ctx context.Context, gameID common.GameID) (GameTurnStatsAggregate, error)
|
||||
|
||||
// Delete removes every aggregate entry for gameID. The call is a
|
||||
// no-op when no entries exist; no error is returned in that case.
|
||||
Delete(ctx context.Context, gameID common.GameID) error
|
||||
}
|
||||
Reference in New Issue
Block a user