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 }