feat: game lobby service
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user