167 lines
5.2 KiB
Go
167 lines
5.2 KiB
Go
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
|
|
}
|