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

252 lines
7.0 KiB
Go

package game
// Status identifies one platform-level lifecycle state of a game record.
type Status string
const (
// StatusDraft reports that the record was created but enrollment is
// not yet open.
StatusDraft Status = "draft"
// StatusEnrollmentOpen reports that applications (public game) or
// invite redemptions (private game) are being accepted.
StatusEnrollmentOpen Status = "enrollment_open"
// StatusReadyToStart reports that enrollment closed and the start
// command is accepted.
StatusReadyToStart Status = "ready_to_start"
// StatusStarting reports that a start job has been submitted to
// Runtime Manager and Lobby is waiting for the result.
StatusStarting Status = "starting"
// StatusStartFailed reports that the container start or metadata
// persistence step failed.
StatusStartFailed Status = "start_failed"
// StatusRunning reports that the engine container is live and normal
// gameplay has begun.
StatusRunning Status = "running"
// StatusPaused reports a platform-level pause; the engine container
// may still be alive.
StatusPaused Status = "paused"
// StatusFinished reports that the game ended; the record is terminal.
StatusFinished Status = "finished"
// StatusCancelled reports that the game was cancelled before start;
// the record is terminal.
StatusCancelled Status = "cancelled"
)
// IsKnown reports whether status belongs to the frozen Lobby status
// vocabulary.
func (status Status) IsKnown() bool {
switch status {
case StatusDraft,
StatusEnrollmentOpen,
StatusReadyToStart,
StatusStarting,
StatusStartFailed,
StatusRunning,
StatusPaused,
StatusFinished,
StatusCancelled:
return true
default:
return false
}
}
// AllStatuses returns the frozen list of every Lobby status value. The
// slice order is stable across calls and matches the README §Status
// vocabulary listing.
func AllStatuses() []Status {
return []Status{
StatusDraft,
StatusEnrollmentOpen,
StatusReadyToStart,
StatusStarting,
StatusStartFailed,
StatusRunning,
StatusPaused,
StatusFinished,
StatusCancelled,
}
}
// IsTerminal reports whether status can no longer accept lifecycle
// transitions.
func (status Status) IsTerminal() bool {
switch status {
case StatusFinished, StatusCancelled:
return true
default:
return false
}
}
// Trigger identifies what caused a status transition. The value is used by
// the domain transition gate and by later observability layers.
type Trigger string
const (
// TriggerCommand reports an explicit owner or admin command such as
// start, pause, resume, cancel, open-enrollment, or retry-start.
TriggerCommand Trigger = "command"
// TriggerManual reports that an admin or owner manually closed
// enrollment while min_players was already satisfied.
TriggerManual Trigger = "manual"
// TriggerDeadline reports that the enrollment automation worker
// detected enrollment_ends_at had passed with min_players satisfied.
TriggerDeadline Trigger = "deadline"
// TriggerGap reports that the enrollment automation worker detected
// the gap window exhausted its time or player budget.
TriggerGap Trigger = "gap"
// TriggerRuntimeEvent reports that the transition was caused by a
// Runtime Manager job result or a Game Master runtime event.
TriggerRuntimeEvent Trigger = "runtime_event"
// TriggerExternalBlock reports that the transition was caused by the
// user-lifecycle cascade reacting to a permanent_block or DeleteUser
// event on the game owner. The trigger is the only path that
// cancels a game from in-flight statuses
// (`starting`, `running`, `paused`).
TriggerExternalBlock Trigger = "external_block"
)
// IsKnown reports whether trigger belongs to the frozen trigger
// vocabulary.
func (trigger Trigger) IsKnown() bool {
switch trigger {
case TriggerCommand,
TriggerManual,
TriggerDeadline,
TriggerGap,
TriggerRuntimeEvent,
TriggerExternalBlock:
return true
default:
return false
}
}
// transitionKey stores one `(from, to)` pair in the allowed-transitions
// table.
type transitionKey struct {
from Status
to Status
}
// allowedTransitions maps each permitted `(from, to)` pair to the set of
// triggers that may drive it. The table mirrors the status transition
// table frozen in lobby/README.md.
var allowedTransitions = map[transitionKey]map[Trigger]struct{}{
{StatusDraft, StatusEnrollmentOpen}: {
TriggerCommand: {},
},
{StatusEnrollmentOpen, StatusReadyToStart}: {
TriggerManual: {},
TriggerDeadline: {},
TriggerGap: {},
},
{StatusReadyToStart, StatusStarting}: {
TriggerCommand: {},
},
{StatusStarting, StatusRunning}: {
TriggerRuntimeEvent: {},
},
{StatusStarting, StatusPaused}: {
TriggerRuntimeEvent: {},
},
{StatusStarting, StatusStartFailed}: {
TriggerRuntimeEvent: {},
},
{StatusStartFailed, StatusReadyToStart}: {
TriggerCommand: {},
},
{StatusRunning, StatusPaused}: {
TriggerCommand: {},
},
{StatusRunning, StatusFinished}: {
TriggerRuntimeEvent: {},
},
{StatusPaused, StatusRunning}: {
TriggerCommand: {},
},
{StatusPaused, StatusFinished}: {
TriggerRuntimeEvent: {},
},
{StatusDraft, StatusCancelled}: {
TriggerCommand: {},
TriggerExternalBlock: {},
},
{StatusEnrollmentOpen, StatusCancelled}: {
TriggerCommand: {},
TriggerExternalBlock: {},
},
{StatusReadyToStart, StatusCancelled}: {
TriggerCommand: {},
TriggerExternalBlock: {},
},
{StatusStartFailed, StatusCancelled}: {
TriggerCommand: {},
TriggerExternalBlock: {},
},
{StatusStarting, StatusCancelled}: {
TriggerExternalBlock: {},
},
{StatusRunning, StatusCancelled}: {
TriggerExternalBlock: {},
},
{StatusPaused, StatusCancelled}: {
TriggerExternalBlock: {},
},
}
// AllowedTransitions returns a copy of the `(from, to) -> {triggers}` table
// used by Transition. The returned map is safe to mutate; callers should
// not rely on iteration order.
func AllowedTransitions() map[Status]map[Status][]Trigger {
result := make(map[Status]map[Status][]Trigger, len(allowedTransitions))
for key, triggers := range allowedTransitions {
inner, ok := result[key.from]
if !ok {
inner = make(map[Status][]Trigger)
result[key.from] = inner
}
copied := make([]Trigger, 0, len(triggers))
for trigger := range triggers {
copied = append(copied, trigger)
}
inner[key.to] = copied
}
return result
}
// Transition reports whether from may transition to next under trigger.
// The function returns nil when the triplet is permitted, and an
// *InvalidTransitionError wrapping ErrInvalidTransition otherwise. It does
// not touch any store and is safe to call from any layer.
func Transition(from Status, next Status, trigger Trigger) error {
if !from.IsKnown() || !next.IsKnown() || !trigger.IsKnown() {
return &InvalidTransitionError{From: from, To: next, Trigger: trigger}
}
triggers, ok := allowedTransitions[transitionKey{from: from, to: next}]
if !ok {
return &InvalidTransitionError{From: from, To: next, Trigger: trigger}
}
if _, ok := triggers[trigger]; !ok {
return &InvalidTransitionError{From: from, To: next, Trigger: trigger}
}
return nil
}