feat: gamemaster
This commit is contained in:
+47
-6
@@ -39,13 +39,54 @@ do not pass per-game limits.
|
||||
## Endpoints
|
||||
|
||||
The contract is the union of `openapi.yaml` and the technical liveness probe
|
||||
described below.
|
||||
described below. Endpoints split into two route classes:
|
||||
|
||||
| Class | Path | Caller | Purpose |
|
||||
| --- | --- | --- | --- |
|
||||
| Admin (GM-only) | `POST /api/v1/admin/init` | `Game Master` | Initialise the engine with the race roster. |
|
||||
| Admin (GM-only) | `GET /api/v1/admin/status` | `Game Master` | Read the full game state. |
|
||||
| Admin (GM-only) | `PUT /api/v1/admin/turn` | `Game Master` | Generate the next turn. |
|
||||
| Admin (GM-only) | `POST /api/v1/admin/race/banish` | `Game Master` | Deactivate a race after a permanent platform removal. |
|
||||
| Player | `PUT /api/v1/command` | `Game Master` (forwarded from `Edge Gateway`) | Execute a batch of player commands. |
|
||||
| Player | `PUT /api/v1/order` | `Game Master` | Validate and store a batch of player orders. |
|
||||
| Player | `GET /api/v1/report` | `Game Master` | Fetch the per-player turn report. |
|
||||
| Probe | `GET /healthz` | `Runtime Manager` | Technical liveness probe. |
|
||||
|
||||
Admin paths are unauthenticated but are routed only from inside the
|
||||
trusted network segment that connects `Game Master` to the engine
|
||||
container. The engine does not enforce caller identity — network-level
|
||||
segmentation is the boundary. Player paths apply the same rule and rely
|
||||
on `Game Master` to forward only verified player payloads.
|
||||
|
||||
### Game endpoints
|
||||
|
||||
Documented in [`openapi.yaml`](openapi.yaml). When the engine has not been
|
||||
initialised through `POST /api/v1/init`, game endpoints respond `501 Not
|
||||
Implemented` to make the uninitialised state unambiguous.
|
||||
initialised through `POST /api/v1/admin/init`, game endpoints respond
|
||||
`501 Not Implemented` to make the uninitialised state unambiguous.
|
||||
|
||||
### `StateResponse.finished`
|
||||
|
||||
`StateResponse` (returned by `GET /api/v1/admin/status` and
|
||||
`PUT /api/v1/admin/turn`) carries a required boolean `finished` field.
|
||||
The engine sets it to `true` exactly once on the turn-generation response
|
||||
that ends the game; otherwise it stays `false`. `Game Master` uses this
|
||||
field as the sole signal to run the platform finish flow. The conditional
|
||||
logic that flips `finished` to `true` lives in the engine's domain code
|
||||
and is owned by the engine maintainers.
|
||||
|
||||
### `POST /api/v1/admin/race/banish`
|
||||
|
||||
Deactivates a race after a permanent platform-level membership removal.
|
||||
`Game Master` calls this endpoint synchronously after a Lobby-driven
|
||||
remove-and-banish flow.
|
||||
|
||||
- Request body: `{ "race_name": "<name>" }`. `race_name` must be
|
||||
non-empty and must match an existing race in the engine's roster.
|
||||
- Successful response: `204 No Content` with an empty body.
|
||||
- Error responses follow the same `400` / `500` envelope shape as the
|
||||
other admin endpoints. The engine-side mechanics of `banish` (what
|
||||
exactly happens to the race's planets, fleets, and pending orders) are
|
||||
owned by the engine maintainers.
|
||||
|
||||
### `GET /healthz`
|
||||
|
||||
@@ -53,9 +94,9 @@ Technical liveness probe used by `Runtime Manager` and operator tooling.
|
||||
|
||||
- Returns `{"status":"ok"}` with HTTP `200` whenever the HTTP server is
|
||||
serving requests, regardless of whether the engine has been initialised
|
||||
through `POST /api/v1/init`.
|
||||
- Carries no game-state semantics. Use `GET /api/v1/status` for game-state
|
||||
inspection.
|
||||
through `POST /api/v1/admin/init`.
|
||||
- Carries no game-state semantics. Use `GET /api/v1/admin/status` for
|
||||
game-state inspection.
|
||||
|
||||
This endpoint exists so that `Runtime Manager` can probe a freshly started
|
||||
container before `init` runs.
|
||||
|
||||
+1
-1
@@ -34,7 +34,7 @@ require (
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 // indirect
|
||||
github.com/oasdiff/yaml v0.0.9 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.9 // indirect
|
||||
github.com/oasdiff/yaml3 v0.0.12 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/perimeterx/marshmallow v1.1.5 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
|
||||
+1
-2
@@ -66,8 +66,7 @@ github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826 h1:RWengNIwukTxcDr9
|
||||
github.com/mohae/deepcopy v0.0.0-20170929034955-c48cc78d4826/go.mod h1:TaXosZuwdSHYgviHp1DAtfrULt5eUgsSMsZf+YrPgl8=
|
||||
github.com/oasdiff/yaml v0.0.9 h1:zQOvd2UKoozsSsAknnWoDJlSK4lC0mpmjfDsfqNwX48=
|
||||
github.com/oasdiff/yaml v0.0.9/go.mod h1:8lvhgJG4xiKPj3HN5lDow4jZHPlx1i7dIwzkdAo6oAM=
|
||||
github.com/oasdiff/yaml3 v0.0.9 h1:rWPrKccrdUm8J0F3sGuU+fuh9+1K/RdJlWF7O/9yw2g=
|
||||
github.com/oasdiff/yaml3 v0.0.9/go.mod h1:y5+oSEHCPT/DGrS++Wc/479ERge0zTFxaF8PbGKcg2o=
|
||||
github.com/oasdiff/yaml3 v0.0.12 h1:75urAtPeDg2/iDEWwzNrLOWxI9N/dCh81nTTJtokt2M=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
|
||||
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||
github.com/perimeterx/marshmallow v1.1.5 h1:a2LALqQ1BlHM8PZblsDdidgv1mWi1DgC2UmX50IvK2s=
|
||||
|
||||
@@ -19,6 +19,15 @@ func (c Controller) RaceID(actor string) (uuid.UUID, error) {
|
||||
return c.Cache.g.Race[ri].ID, nil
|
||||
}
|
||||
|
||||
func (c Controller) RaceBanish(actor string) error {
|
||||
ri, err := c.Cache.validRace(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Cache.g.Race[ri].Extinct = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c Controller) RaceQuit(actor string) error {
|
||||
ri, err := c.Cache.validRace(actor)
|
||||
if err != nil {
|
||||
|
||||
@@ -134,6 +134,14 @@ func ValidateOrder(configure func(*Param), actor string, cmd ...order.DecodableC
|
||||
return ec.validateOrder(actor, cmd...)
|
||||
}
|
||||
|
||||
func BanishRace(configure func(*Param), actor string) error {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return ec.banishRace(actor)
|
||||
}
|
||||
|
||||
func GameState(configure func(*Param)) (s game.State, err error) {
|
||||
ec, err := NewRepoController(configure)
|
||||
if err != nil {
|
||||
@@ -146,10 +154,11 @@ func GameState(configure func(*Param)) (s game.State, err error) {
|
||||
}
|
||||
|
||||
result := &game.State{
|
||||
ID: g.ID,
|
||||
Turn: g.Turn,
|
||||
Stage: g.Stage,
|
||||
Players: make([]game.PlayerState, len(g.Race)),
|
||||
ID: g.ID,
|
||||
Turn: g.Turn,
|
||||
Stage: g.Stage,
|
||||
Finished: g.Finished(),
|
||||
Players: make([]game.PlayerState, len(g.Race)),
|
||||
}
|
||||
|
||||
planetCount := make(map[uuid.UUID]uint)
|
||||
@@ -243,6 +252,16 @@ func (ec *RepoController) executeCommand(consumer func(*Controller) error) (err
|
||||
})
|
||||
}
|
||||
|
||||
func (ec *RepoController) banishRace(actor string) (err error) {
|
||||
return ec.executeLocked(func(c *Controller) error {
|
||||
err = c.RaceBanish(actor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return c.saveState()
|
||||
})
|
||||
}
|
||||
|
||||
func (ec *RepoController) executeSafe(consumer func(uint, *Controller) error) (err error) {
|
||||
g, err := ec.Repo.LoadStateSafe()
|
||||
if err != nil {
|
||||
|
||||
@@ -118,7 +118,7 @@ func (c *Cache) raceTechLevel(ri int, t game.Tech, v float64) {
|
||||
|
||||
func (c *Cache) TurnWipeExtinctRaces() {
|
||||
for i := range c.listRaceActingIdx() {
|
||||
if c.g.Race[i].TTL == 0 {
|
||||
if (c.g.Race[i].Extinct && c.g.Race[i].TTL > 0) || (!c.g.Race[i].Extinct && c.g.Race[i].TTL == 0) {
|
||||
c.wipeRace(i)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,10 +3,11 @@ package game
|
||||
import "github.com/google/uuid"
|
||||
|
||||
type State struct {
|
||||
ID uuid.UUID
|
||||
Turn uint
|
||||
Stage uint
|
||||
Players []PlayerState
|
||||
ID uuid.UUID
|
||||
Turn uint
|
||||
Stage uint
|
||||
Players []PlayerState
|
||||
Finished bool
|
||||
}
|
||||
|
||||
type PlayerState struct {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package router_test
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"galaxy/model/rest"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
const apiBanishPath = "/api/v1/admin/race/banish"
|
||||
|
||||
func TestBanishHappyPath(t *testing.T) {
|
||||
r := setupRouter()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, apiBanishPath, asBody(rest.BanishRequest{RaceName: "Aelinari"}))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusNoContent, w.Code, w.Body)
|
||||
assert.Empty(t, w.Body.String())
|
||||
}
|
||||
|
||||
func TestBanishValidation(t *testing.T) {
|
||||
r := setupRouter()
|
||||
|
||||
for _, tc := range []struct {
|
||||
description string
|
||||
body any
|
||||
}{
|
||||
{"missing race_name", struct{}{}},
|
||||
{"empty race_name", rest.BanishRequest{RaceName: ""}},
|
||||
{"blank race_name", rest.BanishRequest{RaceName: " "}},
|
||||
} {
|
||||
t.Run(tc.description, func(t *testing.T) {
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest(http.MethodPost, apiBanishPath, asBody(tc.body))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"galaxy/model/rest"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func BanishHandler(c *gin.Context, executor CommandExecutor) {
|
||||
var req rest.BanishRequest
|
||||
if errorResponse(c, c.ShouldBindJSON(&req)) {
|
||||
return
|
||||
}
|
||||
|
||||
if errorResponse(c, executor.BanishRace(req.RaceName)) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
@@ -23,6 +23,7 @@ type CommandExecutor interface {
|
||||
GenerateGame([]string) (rest.StateResponse, error)
|
||||
GenerateTurn() (rest.StateResponse, error)
|
||||
GameState() (rest.StateResponse, error)
|
||||
BanishRace(string) error
|
||||
LoadReport(actor string, turn uint) (*report.Report, error)
|
||||
Execute(cmd ...Command) error
|
||||
ValidateOrder(actor string, cmd ...order.DecodableCommand) error
|
||||
@@ -103,16 +104,21 @@ func (e *executor) GameState() (rest.StateResponse, error) {
|
||||
return stateResponse(s), nil
|
||||
}
|
||||
|
||||
func (e *executor) BanishRace(raceName string) error {
|
||||
return controller.BanishRace(e.cfg, raceName)
|
||||
}
|
||||
|
||||
func (e *executor) LoadReport(actor string, turn uint) (*report.Report, error) {
|
||||
return controller.LoadReport(e.cfg, actor, turn)
|
||||
}
|
||||
|
||||
func stateResponse(s game.State) rest.StateResponse {
|
||||
result := &rest.StateResponse{
|
||||
ID: s.ID,
|
||||
Turn: s.Turn,
|
||||
Stage: s.Stage,
|
||||
Players: make([]rest.PlayerState, len(s.Players)),
|
||||
ID: s.ID,
|
||||
Turn: s.Turn,
|
||||
Stage: s.Stage,
|
||||
Finished: s.Finished,
|
||||
Players: make([]rest.PlayerState, len(s.Players)),
|
||||
}
|
||||
for i := range s.Players {
|
||||
result.Players[i].ID = s.Players[i].ID
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
// HealthzHandler is the technical liveness probe used by Runtime Manager
|
||||
// and operator tooling. It returns 200 with {"status":"ok"} regardless
|
||||
// of whether the engine has been initialised through POST /api/v1/init.
|
||||
// of whether the engine has been initialised through POST /api/v1/admin/init.
|
||||
func HealthzHandler(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "ok"})
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestInit(t *testing.T) {
|
||||
payload := generateInitRequest(10)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
|
||||
@@ -42,7 +42,7 @@ func TestInitValidators(t *testing.T) {
|
||||
payload := generateInitRequest(9)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusBadRequest, w.Code, w.Body)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestGetReport(t *testing.T) {
|
||||
payload := generateInitRequest(10)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
|
||||
|
||||
@@ -67,12 +67,15 @@ func setupRouter(executor handler.CommandExecutor) *gin.Engine {
|
||||
|
||||
groupV1 := r.Group("/api/v1")
|
||||
|
||||
groupV1.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) })
|
||||
groupV1.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) })
|
||||
groupAdmin := groupV1.Group("/admin")
|
||||
groupAdmin.GET("/status", func(ctx *gin.Context) { handler.StatusHandler(ctx, executor) })
|
||||
groupAdmin.POST("/init", func(ctx *gin.Context) { handler.InitHandler(ctx, executor) })
|
||||
groupAdmin.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) })
|
||||
groupAdmin.POST("/race/banish", func(ctx *gin.Context) { handler.BanishHandler(ctx, executor) })
|
||||
|
||||
groupV1.GET("/report", func(ctx *gin.Context) { handler.ReportHandler(ctx, executor) })
|
||||
groupV1.PUT("/command", LimitMiddleware(1), func(ctx *gin.Context) { handler.CommandHandler(ctx, executor) })
|
||||
groupV1.PUT("/order", func(ctx *gin.Context) { handler.OrderHandler(ctx, executor) })
|
||||
groupV1.PUT("/turn", func(ctx *gin.Context) { handler.TurnHandler(ctx, executor) })
|
||||
|
||||
return r
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ func (e *dummyExecutor) GenerateTurn() (rest.StateResponse, error) {
|
||||
return rest.StateResponse{}, nil
|
||||
}
|
||||
|
||||
func (e *dummyExecutor) BanishRace(raceName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *dummyExecutor) GameState() (rest.StateResponse, error) {
|
||||
return rest.StateResponse{}, nil
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ func TestGetStatus(t *testing.T) {
|
||||
payload := generateInitRequest(10)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
|
||||
@@ -37,7 +37,7 @@ func TestGetStatus(t *testing.T) {
|
||||
assert.NotEqual(t, uuid.Nil, uuid.MustParse(initResponse.ID.String()))
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/api/v1/status", nil)
|
||||
req, _ = http.NewRequest("GET", "/api/v1/admin/status", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, w.Body)
|
||||
@@ -47,6 +47,7 @@ func TestGetStatus(t *testing.T) {
|
||||
assert.Equal(t, initResponse.ID, stateResponse.ID)
|
||||
assert.Equal(t, uint(0), stateResponse.Turn)
|
||||
assert.Equal(t, uint(0), stateResponse.Stage)
|
||||
assert.False(t, stateResponse.Finished)
|
||||
assert.Len(t, stateResponse.Players, 10)
|
||||
for i := range stateResponse.Players {
|
||||
assert.NoError(t, uuid.Validate(stateResponse.Players[i].ID.String()))
|
||||
|
||||
@@ -29,7 +29,7 @@ func TestGetTurn(t *testing.T) {
|
||||
payload := generateInitRequest(10)
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
req, _ := http.NewRequest("POST", "/api/v1/init", asBody(payload))
|
||||
req, _ := http.NewRequest("POST", "/api/v1/admin/init", asBody(payload))
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusCreated, w.Code, w.Body)
|
||||
@@ -50,7 +50,7 @@ func TestGetTurn(t *testing.T) {
|
||||
// generate next turn
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("PUT", "/api/v1/turn", nil)
|
||||
req, _ = http.NewRequest("PUT", "/api/v1/admin/turn", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, w.Body)
|
||||
@@ -72,7 +72,7 @@ func TestGetTurn(t *testing.T) {
|
||||
// validate status
|
||||
|
||||
w = httptest.NewRecorder()
|
||||
req, _ = http.NewRequest("GET", "/api/v1/status", nil)
|
||||
req, _ = http.NewRequest("GET", "/api/v1/admin/status", nil)
|
||||
r.ServeHTTP(w, req)
|
||||
|
||||
assert.Equal(t, http.StatusOK, w.Code, w.Body)
|
||||
|
||||
+63
-13
@@ -30,16 +30,17 @@ tags:
|
||||
- name: Health
|
||||
description: Technical liveness probes used by Runtime Manager and operator tooling.
|
||||
paths:
|
||||
/api/v1/status:
|
||||
/api/v1/admin/status:
|
||||
get:
|
||||
tags:
|
||||
- GameLifecycle
|
||||
operationId: getGameStatus
|
||||
operationId: adminGetGameStatus
|
||||
summary: Get the current game state
|
||||
description: |
|
||||
Returns the current game state including turn number, stage, and a
|
||||
summary of all players. Returns `501` if the game has not yet been
|
||||
initialized.
|
||||
initialized. Routed only from the trusted network segment that
|
||||
connects `Game Master` to the engine container.
|
||||
responses:
|
||||
"200":
|
||||
description: Current game state.
|
||||
@@ -51,15 +52,17 @@ paths:
|
||||
description: Game has not been initialized yet.
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/init:
|
||||
/api/v1/admin/init:
|
||||
post:
|
||||
tags:
|
||||
- GameLifecycle
|
||||
operationId: initGame
|
||||
operationId: adminInitGame
|
||||
summary: Initialize a new game
|
||||
description: |
|
||||
Generates a new game instance with the supplied list of races.
|
||||
Requires at least 10 race entries.
|
||||
Requires at least 10 race entries. Routed only from the trusted
|
||||
network segment that connects `Game Master` to the engine
|
||||
container.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
@@ -77,6 +80,30 @@ paths:
|
||||
$ref: "#/components/responses/ValidationError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/admin/race/banish:
|
||||
post:
|
||||
tags:
|
||||
- GameLifecycle
|
||||
operationId: adminBanishRace
|
||||
summary: Deactivate a race after a permanent platform-level removal
|
||||
description: |
|
||||
Deactivates the named race in the running engine. Called by `Game
|
||||
Master` after a Lobby-driven permanent membership removal. Routed
|
||||
only from the trusted network segment that connects `Game Master`
|
||||
to the engine container.
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/BanishRequest"
|
||||
responses:
|
||||
"204":
|
||||
description: Race deactivated; no response body.
|
||||
"400":
|
||||
$ref: "#/components/responses/ValidationError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/report:
|
||||
get:
|
||||
tags:
|
||||
@@ -148,15 +175,16 @@ paths:
|
||||
$ref: "#/components/responses/ValidationError"
|
||||
"500":
|
||||
$ref: "#/components/responses/InternalError"
|
||||
/api/v1/turn:
|
||||
/api/v1/admin/turn:
|
||||
put:
|
||||
tags:
|
||||
- GameLifecycle
|
||||
operationId: generateTurn
|
||||
operationId: adminGenerateTurn
|
||||
summary: Advance the game to the next turn
|
||||
description: |
|
||||
Processes the current turn and generates the next one. Returns the
|
||||
updated game state.
|
||||
updated game state. Routed only from the trusted network segment
|
||||
that connects `Game Master` to the engine container.
|
||||
responses:
|
||||
"200":
|
||||
description: Updated game state after turn generation.
|
||||
@@ -175,10 +203,10 @@ paths:
|
||||
description: |
|
||||
Returns `{"status":"ok"}` with HTTP `200` whenever the HTTP server
|
||||
is serving requests, regardless of whether the engine has been
|
||||
initialised through `POST /api/v1/init`. Used by `Runtime Manager`
|
||||
to probe a freshly started container before `init` runs. Carries
|
||||
no game-state semantics; use `GET /api/v1/status` for game-state
|
||||
inspection.
|
||||
initialised through `POST /api/v1/admin/init`. Used by `Runtime
|
||||
Manager` to probe a freshly started container before `init` runs.
|
||||
Carries no game-state semantics; use `GET /api/v1/admin/status`
|
||||
for game-state inspection.
|
||||
responses:
|
||||
"200":
|
||||
description: Engine HTTP server is up.
|
||||
@@ -225,6 +253,7 @@ components:
|
||||
- turn
|
||||
- stage
|
||||
- player
|
||||
- finished
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
@@ -243,6 +272,15 @@ components:
|
||||
description: Summary state for each player participating in the game.
|
||||
items:
|
||||
$ref: "#/components/schemas/PlayerState"
|
||||
finished:
|
||||
type: boolean
|
||||
description: |
|
||||
True exactly once on the turn-generation response that ends the
|
||||
game; otherwise false. Server default: false. `Game Master`
|
||||
uses this flag as the sole signal to run the platform finish
|
||||
flow. The conditional logic that flips it to true lives in
|
||||
the engine's domain code and is owned by the engine
|
||||
maintainers.
|
||||
PlayerState:
|
||||
type: object
|
||||
description: Brief player state returned as part of the game state response.
|
||||
@@ -292,6 +330,18 @@ components:
|
||||
type: string
|
||||
description: Name of the race. Must be non-blank and satisfy the entity-name format.
|
||||
minLength: 1
|
||||
BanishRequest:
|
||||
type: object
|
||||
description: |
|
||||
Request body for the admin banish endpoint. `race_name` must
|
||||
identify an existing race in the engine roster.
|
||||
required:
|
||||
- race_name
|
||||
properties:
|
||||
race_name:
|
||||
type: string
|
||||
description: Name of the race to banish. Must be non-blank.
|
||||
minLength: 1
|
||||
CommandRequest:
|
||||
type: object
|
||||
description: |
|
||||
|
||||
@@ -31,15 +31,15 @@ func TestGameOpenAPISpecFreezesResponseSchemas(t *testing.T) {
|
||||
wantRef string
|
||||
}{
|
||||
{
|
||||
name: "get game status",
|
||||
path: "/api/v1/status",
|
||||
name: "admin get game status",
|
||||
path: "/api/v1/admin/status",
|
||||
method: http.MethodGet,
|
||||
status: http.StatusOK,
|
||||
wantRef: "#/components/schemas/StateResponse",
|
||||
},
|
||||
{
|
||||
name: "init game",
|
||||
path: "/api/v1/init",
|
||||
name: "admin init game",
|
||||
path: "/api/v1/admin/init",
|
||||
method: http.MethodPost,
|
||||
status: http.StatusCreated,
|
||||
wantRef: "#/components/schemas/StateResponse",
|
||||
@@ -52,8 +52,8 @@ func TestGameOpenAPISpecFreezesResponseSchemas(t *testing.T) {
|
||||
wantRef: "#/components/schemas/Report",
|
||||
},
|
||||
{
|
||||
name: "generate turn",
|
||||
path: "/api/v1/turn",
|
||||
name: "admin generate turn",
|
||||
path: "/api/v1/admin/turn",
|
||||
method: http.MethodPut,
|
||||
status: http.StatusOK,
|
||||
wantRef: "#/components/schemas/StateResponse",
|
||||
@@ -81,7 +81,7 @@ func TestGameOpenAPISpecFreezesInitRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadOpenAPISpec(t)
|
||||
operation := getOpenAPIOperation(t, doc, "/api/v1/init", http.MethodPost)
|
||||
operation := getOpenAPIOperation(t, doc, "/api/v1/admin/init", http.MethodPost)
|
||||
|
||||
assertSchemaRef(t, requestSchemaRef(t, operation), "#/components/schemas/InitRequest", "init request schema")
|
||||
|
||||
@@ -93,6 +93,68 @@ func TestGameOpenAPISpecFreezesInitRequest(t *testing.T) {
|
||||
require.Equal(t, uint64(10), racesSchema.Value.MinItems, "InitRequest.races minItems must be 10")
|
||||
}
|
||||
|
||||
func TestGameOpenAPISpecFreezesAdminOperationIDs(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadOpenAPISpec(t)
|
||||
|
||||
tests := []struct {
|
||||
path string
|
||||
method string
|
||||
opID string
|
||||
}{
|
||||
{"/api/v1/admin/init", http.MethodPost, "adminInitGame"},
|
||||
{"/api/v1/admin/status", http.MethodGet, "adminGetGameStatus"},
|
||||
{"/api/v1/admin/turn", http.MethodPut, "adminGenerateTurn"},
|
||||
{"/api/v1/admin/race/banish", http.MethodPost, "adminBanishRace"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.opID, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
operation := getOpenAPIOperation(t, doc, tt.path, tt.method)
|
||||
require.Equal(t, tt.opID, operation.OperationID, "operation id for %s %s", tt.method, tt.path)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestGameOpenAPISpecFreezesBanishRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadOpenAPISpec(t)
|
||||
operation := getOpenAPIOperation(t, doc, "/api/v1/admin/race/banish", http.MethodPost)
|
||||
|
||||
assertSchemaRef(t, requestSchemaRef(t, operation), "#/components/schemas/BanishRequest", "banish request schema")
|
||||
|
||||
if operation.Responses == nil {
|
||||
require.FailNow(t, "banish operation is missing responses")
|
||||
}
|
||||
noContent := operation.Responses.Status(http.StatusNoContent)
|
||||
require.NotNil(t, noContent, "banish operation must declare 204 response")
|
||||
require.NotNil(t, noContent.Value, "banish 204 response must have a value")
|
||||
|
||||
schema := componentSchemaRef(t, doc, "BanishRequest")
|
||||
assertRequiredFields(t, schema, "race_name")
|
||||
|
||||
raceNameSchema := schema.Value.Properties["race_name"]
|
||||
require.NotNil(t, raceNameSchema, "BanishRequest.race_name schema must exist")
|
||||
require.Equal(t, uint64(1), raceNameSchema.Value.MinLength, "BanishRequest.race_name minLength must be 1")
|
||||
}
|
||||
|
||||
func TestGameOpenAPISpecFreezesStateResponseFinished(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
doc := loadOpenAPISpec(t)
|
||||
schema := componentSchemaRef(t, doc, "StateResponse")
|
||||
|
||||
assertRequiredFields(t, schema, "id", "turn", "stage", "player", "finished")
|
||||
|
||||
finishedSchema := schema.Value.Properties["finished"]
|
||||
require.NotNil(t, finishedSchema, "StateResponse.finished schema must exist")
|
||||
require.True(t, finishedSchema.Value.Type.Is("boolean"), "StateResponse.finished must be boolean")
|
||||
}
|
||||
|
||||
func TestGameOpenAPISpecFreezesCommandRequest(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user