fix(game): #59 — per-command rejection on PUT /api/v1/order
Tests · Go / test (push) Successful in 2m2s
Tests · Go / test (pull_request) Successful in 3m3s
Tests · Integration / integration (pull_request) Successful in 1m40s

Validation of a player's order now applies every command against a
transient game-state snapshot and records the per-command outcome
(cmdApplied, cmdErrorCode) in each command's meta. The order is
persisted even when some commands are rejected, and the response is
202 + UserGamesOrder so clients can surface the partial failure
without the chain collapsing into "downstream service is unavailable".

Pkg/error consts are reshelved onto three explicit ranges with a
package doc and helpers (IsInternalCode/IsInputCode/IsGameStateCode):
1xxx internal/server (500/501), 2xxx structural input (400), 3xxx
game-state per-command rejection (400 when escaping HTTP, otherwise
recorded as cmdErrorCode). Two pre-existing typos fixed mechanically
(ErrBeakGroupNumberNotEnough -> ErrBreakGroupNumberNotEnough,
ErrRaceExinct -> ErrRaceExtinct) along with all callsites.

Engine errorResponse maps *GenericError by shelf rather than mapping
everything to 500. The Quit-not-last structural check in
Controller.ValidateOrder is preserved and its type assertion fixed
(was a value assertion against a pointer-typed command, so the check
silently never fired).

Backend, gateway and UI are unchanged — they were already correct on
the 202 path; only the engine collapsing per-command rejection into
500 was needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ilia Denisov
2026-05-29 09:36:29 +02:00
parent ce1dc19a29
commit af30846091
22 changed files with 517 additions and 110 deletions
+45 -10
View File
@@ -16,9 +16,19 @@ info:
`504 Gateway Timeout`
- `501 Not Implemented` is returned without a body when the game has not
been initialized
- validation errors return `400` with `{"error": "message"}`
- game-engine errors return `500` with `{"generic_error": "message", "code": integer}`
- other internal errors return `500` with `{"error": "message"}`
- request-binding validation errors return `400` with `{"error": "message"}`
- structural input errors and game-state rejections that escape to
HTTP return `400` with `{"generic_error": "message", "code": integer}`;
the `code` carries the engine's `GenericError` code (see
`pkg/error/generic.go` — shelf `2xxx` for structural input and
`3xxx` for game-state rejection)
- on `PUT /api/v1/order`, game-state rejections do not become HTTP
errors; the engine returns `202 Accepted` with the full
`UserGamesOrder` body and reports the failure on the offending
command via `cmdApplied=false` and `cmdErrorCode=<integer>`
- internal engine failures return `500` with
`{"generic_error": "message", "code": integer}` (code on shelf `1xxx`)
or `{"error": "message"}` for unclassified failures
servers:
- url: http://localhost:8080
description: Default local listener for Game Service.
@@ -161,10 +171,22 @@ paths:
operationId: validateOrder
summary: Validate and store a player order without executing it
description: |
Validates and stores the game commands structurally without executing them.
On success returns `202 Accepted` with the stored order, including the
engine-assigned `updatedAt` timestamp used by clients to detect stale
submissions.
Validates and stores the game commands without executing them. The
engine applies each command in submission order against a transient
view of the game state, records the per-command outcome on the
command's meta, and persists the resulting `UserGamesOrder` so
clients can reload the same per-command verdict via
`GET /api/v1/order`.
On success returns `202 Accepted` with the stored order; the
engine-assigned `updatedAt` timestamp is used by clients to detect
stale submissions. Game-state rejections (e.g. a "produce ship of
class X" command after class X was removed) are reported per
command via `cmdApplied=false` and `cmdErrorCode=<integer>` inside
the same `202` response — they do **not** become a `400` or `500`
on the whole order. Order-level structural rejections (e.g. a
`quit` command that is not the last command in the order) return
`400`.
requestBody:
required: true
content:
@@ -173,7 +195,10 @@ paths:
$ref: "#/components/schemas/CommandRequest"
responses:
"202":
description: Order is structurally valid and stored.
description: |
Order is stored. Each entry of `cmd` carries `cmdApplied` and
`cmdErrorCode` describing the per-command outcome; the order
is considered stored even when some commands were rejected.
content:
application/json:
schema:
@@ -481,10 +506,20 @@ components:
description: Unique command identifier (RFC 4122 UUID).
cmdApplied:
type: boolean
description: Set in command-result responses; true when the command was applied.
description: |
Per-command outcome. Set by the engine in every response that
carries a stored or freshly-validated order (`PUT /api/v1/order`
and `GET /api/v1/order`): `true` when the command was applied
against the engine state during validation or turn generation,
`false` when it was rejected (see `cmdErrorCode`). Omitted on
requests.
cmdErrorCode:
type: integer
description: Set in command-result responses; non-zero when the command was rejected.
description: |
Per-command error code. Set by the engine alongside `cmdApplied`:
`0` when the command was applied, a non-zero `GenericError`
code (shelves `2xxx`/`3xxx` in `pkg/error/generic.go`) when
the command was rejected. Omitted on requests.
CommandType:
type: string
description: Discriminator identifying the game command variant carried in a `cmd` element.