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