feat: gamemaster
This commit is contained in:
@@ -0,0 +1,166 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/gamemaster/internal/domain/runtime"
|
||||
)
|
||||
|
||||
//go:generate go run go.uber.org/mock/mockgen -destination=../adapters/mocks/mock_lobbyeventspublisher.go -package=mocks galaxy/gamemaster/internal/ports LobbyEventsPublisher
|
||||
|
||||
// LobbyEventsPublisher is the producer port for the `gm:lobby_events`
|
||||
// Redis Stream consumed by Game Lobby. Two message shapes share the
|
||||
// stream, discriminated by `event_type` per
|
||||
// `galaxy/gamemaster/api/runtime-events-asyncapi.yaml`:
|
||||
//
|
||||
// - runtime_snapshot_update — every turn generation outcome and every
|
||||
// status / health-summary transition.
|
||||
// - game_finished — the terminal event published once per game when
|
||||
// the engine reports `finished:true`.
|
||||
type LobbyEventsPublisher interface {
|
||||
// PublishSnapshotUpdate appends a `runtime_snapshot_update` message
|
||||
// to the stream. Adapters validate msg through msg.Validate before
|
||||
// touching Redis.
|
||||
PublishSnapshotUpdate(ctx context.Context, msg RuntimeSnapshotUpdate) error
|
||||
|
||||
// PublishGameFinished appends a `game_finished` message to the
|
||||
// stream. Adapters validate msg through msg.Validate before
|
||||
// touching Redis.
|
||||
PublishGameFinished(ctx context.Context, msg GameFinished) error
|
||||
}
|
||||
|
||||
// PlayerTurnStats stores the per-player projection carried on every
|
||||
// `runtime_snapshot_update` and `game_finished` message. The shape is
|
||||
// frozen in the AsyncAPI spec.
|
||||
type PlayerTurnStats struct {
|
||||
// UserID identifies the platform user.
|
||||
UserID string
|
||||
|
||||
// Planets stores the planet count reported for this user on the
|
||||
// most recent turn.
|
||||
Planets int
|
||||
|
||||
// Population stores the population count reported for this user
|
||||
// on the most recent turn.
|
||||
Population int
|
||||
}
|
||||
|
||||
// Validate reports whether stats carries valid per-player projection
|
||||
// values.
|
||||
func (stats PlayerTurnStats) 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")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// RuntimeSnapshotUpdate stores the body of a `runtime_snapshot_update`
|
||||
// message.
|
||||
type RuntimeSnapshotUpdate struct {
|
||||
// GameID identifies the game the snapshot belongs to.
|
||||
GameID string
|
||||
|
||||
// CurrentTurn stores the latest completed turn number.
|
||||
CurrentTurn int
|
||||
|
||||
// RuntimeStatus stores the latest GM-side status of the runtime.
|
||||
RuntimeStatus runtime.Status
|
||||
|
||||
// EngineHealthSummary stores the current health summary string.
|
||||
// Empty when no observation has been processed yet.
|
||||
EngineHealthSummary string
|
||||
|
||||
// PlayerTurnStats stores the per-active-member projection. Empty
|
||||
// when the snapshot is published for a status transition with no
|
||||
// new turn payload.
|
||||
PlayerTurnStats []PlayerTurnStats
|
||||
|
||||
// OccurredAt stores the wall-clock at which the snapshot was
|
||||
// produced. Always UTC.
|
||||
OccurredAt time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether msg satisfies the AsyncAPI-frozen invariants.
|
||||
func (msg RuntimeSnapshotUpdate) Validate() error {
|
||||
if strings.TrimSpace(msg.GameID) == "" {
|
||||
return fmt.Errorf("runtime snapshot update: game id must not be empty")
|
||||
}
|
||||
if msg.CurrentTurn < 0 {
|
||||
return fmt.Errorf("runtime snapshot update: current turn must not be negative")
|
||||
}
|
||||
if !msg.RuntimeStatus.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"runtime snapshot update: runtime status %q is unsupported",
|
||||
msg.RuntimeStatus,
|
||||
)
|
||||
}
|
||||
if msg.OccurredAt.IsZero() {
|
||||
return fmt.Errorf("runtime snapshot update: occurred at must not be zero")
|
||||
}
|
||||
for i, stats := range msg.PlayerTurnStats {
|
||||
if err := stats.Validate(); err != nil {
|
||||
return fmt.Errorf(
|
||||
"runtime snapshot update: player turn stats[%d]: %w",
|
||||
i, err,
|
||||
)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GameFinished stores the body of a `game_finished` message.
|
||||
type GameFinished struct {
|
||||
// GameID identifies the game that finished.
|
||||
GameID string
|
||||
|
||||
// FinalTurnNumber stores the turn number on which the engine
|
||||
// reported `finished:true`.
|
||||
FinalTurnNumber int
|
||||
|
||||
// RuntimeStatus is always runtime.StatusFinished. Carried in the
|
||||
// message body so consumers can apply the same decoder to both
|
||||
// stream shapes.
|
||||
RuntimeStatus runtime.Status
|
||||
|
||||
// PlayerTurnStats stores the final per-player projection used by
|
||||
// Lobby's capability evaluation.
|
||||
PlayerTurnStats []PlayerTurnStats
|
||||
|
||||
// FinishedAt stores the wall-clock at which the engine returned
|
||||
// the finished response. Always UTC.
|
||||
FinishedAt time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether msg satisfies the AsyncAPI-frozen invariants.
|
||||
func (msg GameFinished) Validate() error {
|
||||
if strings.TrimSpace(msg.GameID) == "" {
|
||||
return fmt.Errorf("game finished: game id must not be empty")
|
||||
}
|
||||
if msg.FinalTurnNumber < 0 {
|
||||
return fmt.Errorf("game finished: final turn number must not be negative")
|
||||
}
|
||||
if msg.RuntimeStatus != runtime.StatusFinished {
|
||||
return fmt.Errorf(
|
||||
"game finished: runtime status must be %q, got %q",
|
||||
runtime.StatusFinished, msg.RuntimeStatus,
|
||||
)
|
||||
}
|
||||
if msg.FinishedAt.IsZero() {
|
||||
return fmt.Errorf("game finished: finished at must not be zero")
|
||||
}
|
||||
for i, stats := range msg.PlayerTurnStats {
|
||||
if err := stats.Validate(); err != nil {
|
||||
return fmt.Errorf("game finished: player turn stats[%d]: %w", i, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user