Files
galaxy-game/lobby/internal/adapters/redisstate/codecs_gameturnstats.go
T
2026-04-25 23:20:55 +02:00

88 lines
3.2 KiB
Go

package redisstate
import (
"encoding/json"
"fmt"
"galaxy/lobby/internal/ports"
)
// playerStatsRecord stores the strict Redis JSON shape used for one
// per-game per-user stats aggregate. The shape mirrors the field set
// documented in lobby/README.md §Runtime Snapshot.
type playerStatsRecord struct {
UserID string `json:"user_id"`
InitialPlanets int64 `json:"initial_planets"`
InitialPopulation int64 `json:"initial_population"`
InitialShipsBuilt int64 `json:"initial_ships_built"`
MaxPlanets int64 `json:"max_planets"`
MaxPopulation int64 `json:"max_population"`
MaxShipsBuilt int64 `json:"max_ships_built"`
}
// MarshalPlayerStats encodes aggregate into the strict Redis JSON shape.
// Negative counters are rejected to match the validation surface of
// ports.PlayerObservedStats.Validate.
func MarshalPlayerStats(aggregate ports.PlayerStatsAggregate) ([]byte, error) {
if err := validatePlayerStatsAggregate(aggregate); err != nil {
return nil, fmt.Errorf("marshal player stats aggregate: %w", err)
}
return json.Marshal(playerStatsRecord{
UserID: aggregate.UserID,
InitialPlanets: aggregate.InitialPlanets,
InitialPopulation: aggregate.InitialPopulation,
InitialShipsBuilt: aggregate.InitialShipsBuilt,
MaxPlanets: aggregate.MaxPlanets,
MaxPopulation: aggregate.MaxPopulation,
MaxShipsBuilt: aggregate.MaxShipsBuilt,
})
}
// UnmarshalPlayerStats decodes payload into a PlayerStatsAggregate. The
// returned aggregate is re-validated to guarantee the Redis store never
// surfaces malformed records.
func UnmarshalPlayerStats(payload []byte) (ports.PlayerStatsAggregate, error) {
var stored playerStatsRecord
if err := json.Unmarshal(payload, &stored); err != nil {
return ports.PlayerStatsAggregate{}, fmt.Errorf("unmarshal player stats aggregate: %w", err)
}
aggregate := ports.PlayerStatsAggregate{
UserID: stored.UserID,
InitialPlanets: stored.InitialPlanets,
InitialPopulation: stored.InitialPopulation,
InitialShipsBuilt: stored.InitialShipsBuilt,
MaxPlanets: stored.MaxPlanets,
MaxPopulation: stored.MaxPopulation,
MaxShipsBuilt: stored.MaxShipsBuilt,
}
if err := validatePlayerStatsAggregate(aggregate); err != nil {
return ports.PlayerStatsAggregate{}, fmt.Errorf("unmarshal player stats aggregate: %w", err)
}
return aggregate, nil
}
func validatePlayerStatsAggregate(aggregate ports.PlayerStatsAggregate) error {
if aggregate.UserID == "" {
return fmt.Errorf("user id must not be empty")
}
if aggregate.InitialPlanets < 0 {
return fmt.Errorf("initial planets must not be negative")
}
if aggregate.InitialPopulation < 0 {
return fmt.Errorf("initial population must not be negative")
}
if aggregate.InitialShipsBuilt < 0 {
return fmt.Errorf("initial ships built must not be negative")
}
if aggregate.MaxPlanets < aggregate.InitialPlanets {
return fmt.Errorf("max planets must not be below initial planets")
}
if aggregate.MaxPopulation < aggregate.InitialPopulation {
return fmt.Errorf("max population must not be below initial population")
}
if aggregate.MaxShipsBuilt < aggregate.InitialShipsBuilt {
return fmt.Errorf("max ships built must not be below initial ships built")
}
return nil
}