feat(game): canonical gameId in POST /api/v1/admin/init
Tests · Go / test (push) Successful in 1m57s
Tests · Integration / integration (pull_request) Successful in 1m48s
Tests · Go / test (pull_request) Successful in 2m0s

Engine no longer mints its own game UUID. The orchestrator (backend)
generates the game UUID at game-create time and passes it in the
admin/init request body as the required `gameId` field, so the value
that names the engine container and host bind-mount directory also
ends up inside the engine's state.json.

The engine rejects the zero UUID with 400 and any init that conflicts
with an existing state.json with 409 (a second init on the same gameId
is also a conflict; full idempotency is not part of the contract).

Updates rest.InitRequest, openapi.yaml (schema + 409 response),
controller.GenerateGame/NewGame/buildGameOnMap signatures, the engine
HTTP handler/executor, the backend runtime worker, and the relevant
unit and contract tests. Documentation in game/README.md,
docs/ARCHITECTURE.md, backend/README.md, and backend/docs/{runtime,flows}.md
is updated in the same patch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-29 13:13:31 +02:00
parent 4a7bf0be61
commit 15d35f6f1f
20 changed files with 270 additions and 37 deletions
+29 -2
View File
@@ -2,8 +2,10 @@ package controller
import (
"errors"
"fmt"
"time"
e "galaxy/error"
"galaxy/game/internal/model/game"
"github.com/google/uuid"
@@ -87,7 +89,16 @@ type Ctrl interface {
PlanetRouteRemove(actor, loadType string, origin uint) error
}
func GenerateGame(configure func(*Param), races []string) (s game.State, err error) {
// GenerateGame initialises a fresh game in storage under the supplied
// canonical gameID. The orchestrator must allocate gameID before the
// engine container is started and pass it here as the request body of
// POST /api/v1/admin/init. A zero UUID is rejected with
// ErrGameInitNilUUID; an attempt to init on top of an existing
// state.json is rejected with ErrGameAlreadyInit.
func GenerateGame(configure func(*Param), gameID uuid.UUID, races []string) (s game.State, err error) {
if gameID == uuid.Nil {
return game.State{}, ErrGameInitNilUUID
}
ec, err := NewRepoController(configure)
if err != nil {
return game.State{}, err
@@ -102,10 +113,26 @@ func GenerateGame(configure func(*Param), races []string) (s game.State, err err
}
}()
_, err = NewGame(ec.Repo, races)
if existing, loadErr := ec.Repo.LoadState(); loadErr == nil {
err = fmt.Errorf("%w: stored gameId=%s, requested=%s", ErrGameAlreadyInit, existing.ID, gameID)
return
} else if !isGameNotInitialized(loadErr) {
err = fmt.Errorf("check existing state: %w", loadErr)
return
}
_, err = NewGame(ec.Repo, gameID, races)
return
}
// isGameNotInitialized reports whether err is the engine's canonical
// "no state.json on disk" signal returned by Repo.LoadState on a
// fresh storage directory.
func isGameNotInitialized(err error) bool {
var ge *e.GenericError
return errors.As(err, &ge) && ge.Code == e.ErrGameNotInitialized
}
func GenerateTurn(configure func(*Param)) (err error) {
ec, err := NewRepoController(configure)
if err != nil {