186 lines
5.7 KiB
Go
186 lines
5.7 KiB
Go
// Package gameturnstatsstub provides an in-memory ports.GameTurnStatsStore
|
|
// implementation for service-level tests. The stub mirrors the behavioural
|
|
// contract of the Redis adapter in redisstate: SaveInitial freezes the
|
|
// initial fields on the first call per user, UpdateMax keeps the max fields
|
|
// monotonically non-decreasing, Load returns the aggregate sorted by user
|
|
// id, and Delete is a no-op when no entries exist for the game.
|
|
//
|
|
// Production code never wires this stub; it is test-only but exposed as a
|
|
// regular (non _test.go) package so downstream service test packages can
|
|
// import it.
|
|
package gameturnstatsstub
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"sort"
|
|
"sync"
|
|
|
|
"galaxy/lobby/internal/domain/common"
|
|
"galaxy/lobby/internal/ports"
|
|
)
|
|
|
|
// Store is a concurrency-safe in-memory implementation of
|
|
// ports.GameTurnStatsStore. The zero value is not usable; call NewStore.
|
|
type Store struct {
|
|
mu sync.Mutex
|
|
records map[common.GameID]map[string]ports.PlayerStatsAggregate
|
|
}
|
|
|
|
// NewStore constructs one empty Store ready for use.
|
|
func NewStore() *Store {
|
|
return &Store{records: make(map[common.GameID]map[string]ports.PlayerStatsAggregate)}
|
|
}
|
|
|
|
// SaveInitial freezes the initial fields for every user in stats. The
|
|
// first call for a user also primes the max fields with the same values.
|
|
// Subsequent calls leave both initial and max fields untouched; the
|
|
// observation is silently ignored.
|
|
func (store *Store) SaveInitial(ctx context.Context, gameID common.GameID, stats []ports.PlayerInitialStats) error {
|
|
if store == nil {
|
|
return errors.New("save initial player stats: nil store")
|
|
}
|
|
if ctx == nil {
|
|
return errors.New("save initial player stats: nil context")
|
|
}
|
|
if err := gameID.Validate(); err != nil {
|
|
return fmt.Errorf("save initial player stats: %w", err)
|
|
}
|
|
for _, line := range stats {
|
|
if err := line.Validate(); err != nil {
|
|
return fmt.Errorf("save initial player stats: %w", err)
|
|
}
|
|
}
|
|
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
bucket := store.records[gameID]
|
|
if bucket == nil {
|
|
bucket = make(map[string]ports.PlayerStatsAggregate)
|
|
store.records[gameID] = bucket
|
|
}
|
|
for _, line := range stats {
|
|
if _, ok := bucket[line.UserID]; ok {
|
|
continue
|
|
}
|
|
bucket[line.UserID] = ports.PlayerStatsAggregate{
|
|
UserID: line.UserID,
|
|
InitialPlanets: line.Planets,
|
|
InitialPopulation: line.Population,
|
|
InitialShipsBuilt: line.ShipsBuilt,
|
|
MaxPlanets: line.Planets,
|
|
MaxPopulation: line.Population,
|
|
MaxShipsBuilt: line.ShipsBuilt,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// UpdateMax updates the max fields by per-component maximum. New users
|
|
// receive an aggregate whose initial fields and max fields both equal the
|
|
// observation, so SaveInitial is not strictly required before UpdateMax.
|
|
func (store *Store) UpdateMax(ctx context.Context, gameID common.GameID, stats []ports.PlayerObservedStats) error {
|
|
if store == nil {
|
|
return errors.New("update max player stats: nil store")
|
|
}
|
|
if ctx == nil {
|
|
return errors.New("update max player stats: nil context")
|
|
}
|
|
if err := gameID.Validate(); err != nil {
|
|
return fmt.Errorf("update max player stats: %w", err)
|
|
}
|
|
for _, line := range stats {
|
|
if err := line.Validate(); err != nil {
|
|
return fmt.Errorf("update max player stats: %w", err)
|
|
}
|
|
}
|
|
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
bucket := store.records[gameID]
|
|
if bucket == nil {
|
|
bucket = make(map[string]ports.PlayerStatsAggregate)
|
|
store.records[gameID] = bucket
|
|
}
|
|
for _, line := range stats {
|
|
entry, ok := bucket[line.UserID]
|
|
if !ok {
|
|
bucket[line.UserID] = ports.PlayerStatsAggregate{
|
|
UserID: line.UserID,
|
|
InitialPlanets: line.Planets,
|
|
InitialPopulation: line.Population,
|
|
InitialShipsBuilt: line.ShipsBuilt,
|
|
MaxPlanets: line.Planets,
|
|
MaxPopulation: line.Population,
|
|
MaxShipsBuilt: line.ShipsBuilt,
|
|
}
|
|
continue
|
|
}
|
|
if line.Planets > entry.MaxPlanets {
|
|
entry.MaxPlanets = line.Planets
|
|
}
|
|
if line.Population > entry.MaxPopulation {
|
|
entry.MaxPopulation = line.Population
|
|
}
|
|
if line.ShipsBuilt > entry.MaxShipsBuilt {
|
|
entry.MaxShipsBuilt = line.ShipsBuilt
|
|
}
|
|
bucket[line.UserID] = entry
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Load returns the GameTurnStatsAggregate stored for gameID with Players
|
|
// sorted by UserID ascending. Calling Load on an unknown gameID returns an
|
|
// aggregate carrying gameID and an empty Players slice.
|
|
func (store *Store) Load(ctx context.Context, gameID common.GameID) (ports.GameTurnStatsAggregate, error) {
|
|
if store == nil {
|
|
return ports.GameTurnStatsAggregate{}, errors.New("load player stats: nil store")
|
|
}
|
|
if ctx == nil {
|
|
return ports.GameTurnStatsAggregate{}, errors.New("load player stats: nil context")
|
|
}
|
|
if err := gameID.Validate(); err != nil {
|
|
return ports.GameTurnStatsAggregate{}, fmt.Errorf("load player stats: %w", err)
|
|
}
|
|
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
bucket := store.records[gameID]
|
|
players := make([]ports.PlayerStatsAggregate, 0, len(bucket))
|
|
for _, entry := range bucket {
|
|
players = append(players, entry)
|
|
}
|
|
sort.Slice(players, func(i, j int) bool {
|
|
return players[i].UserID < players[j].UserID
|
|
})
|
|
return ports.GameTurnStatsAggregate{GameID: gameID, Players: players}, nil
|
|
}
|
|
|
|
// Delete removes every aggregate entry for gameID. It is a no-op when no
|
|
// entries exist.
|
|
func (store *Store) Delete(ctx context.Context, gameID common.GameID) error {
|
|
if store == nil {
|
|
return errors.New("delete player stats: nil store")
|
|
}
|
|
if ctx == nil {
|
|
return errors.New("delete player stats: nil context")
|
|
}
|
|
if err := gameID.Validate(); err != nil {
|
|
return fmt.Errorf("delete player stats: %w", err)
|
|
}
|
|
|
|
store.mu.Lock()
|
|
defer store.mu.Unlock()
|
|
|
|
delete(store.records, gameID)
|
|
return nil
|
|
}
|
|
|
|
// Compile-time interface assertion.
|
|
var _ ports.GameTurnStatsStore = (*Store)(nil)
|