139 lines
5.4 KiB
Go
139 lines
5.4 KiB
Go
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
|
|
}
|