88 lines
3.2 KiB
Go
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
|
|
}
|