feat: #65 sea control — 11th ability (v0.43.0)
CI / build + unit + e2e (pull_request) Successful in 2m33s

Adds the sea ability: a fixed set of 12 named sea zones with sole-control
semantics, contest-by-higher-military, and a deterministic per-turn
+1 military/+1 economy bonus for each claimed zone. Durable (owns
GameState.seaZones), additive migration, stub-inert (e2e unaffected).

Fixes #65

Design note: Gitea #65 sketched a free-form 'naval' layer; the implemented
approved design is this richer 'sea' layer with a fixed zone set.
This commit is contained in:
2026-10-16 12:50:22 +00:00
parent 13f91845fd
commit c77e81c45b
24 changed files with 1060 additions and 58 deletions
+22 -2
View File
@@ -100,6 +100,14 @@ export interface StubPayload {
* deterministicTurn), so the industry path is covered by unit tests only.
*/
industry?: Array<{ regionKey: string; type: string }>;
/**
* (v0.43.0, #65) The currently active sea-zone control
* (`{ zone, by }` — fixed sea-zone id + controlling nation id), emitted by
* the engine's user prompt (always, even when empty). The stub ignores this
* field — it never claims, contests or re-claims a sea zone (see
* deterministicTurn), so the sea path is covered by unit tests only.
*/
seaZones?: Array<{ zone: string; by: string }>;
}
function truncate(s: string, n: number): string {
@@ -173,6 +181,18 @@ function truncate(s: string, n: number): string {
* (v0.39.0, #62) The stub likewise NEVER changes a nation's ideology (it
* always returns `ideology: { changes: [] }`) — the ideology path is covered
* by unit tests only, keeping the e2e assertions untouched.
*
* (v0.41.0, #63) The stub likewise NEVER declares a disaster or pandemic (it
* always returns `disasters: { events: [] }`) — the disasters path is covered
* by unit tests only, keeping the e2e assertions untouched.
*
* (v0.42.0, #64) The stub likewise NEVER declares a migration / refugee flow
* (it always returns `migration: { ops: [] }`) — the migration path is
* covered by unit tests only, keeping the e2e assertions untouched.
*
* (v0.43.0, #65) The stub likewise NEVER claims, contests or re-claims a sea
* zone (it always returns `sea: { control: [] }`) — the sea-control path is
* covered by unit tests only, keeping the e2e assertions untouched.
*/
export function deterministicTurn(p: StubPayload): TurnResult {
const state = p.state;
@@ -252,13 +272,13 @@ export function deterministicTurn(p: StubPayload): TurnResult {
nationOps: [], // the stub never proposes nation ops (see doc comment)
};
// The 9 ability fields, filled by the registry in its canonical order. The
// The 11 ability fields, filled by the registry in its canonical order. The
// stub is ability-inert: every ability contributes its own no-op default
// (each ability's `stub()` — see the per-ability doc comments above), so the
// e2e assertions are unaffected. The insertion order here (ABILITIES) is the
// historical key order pinned by the stub's deterministic output
// (takeovers → wars → hybrid → economy → industry → nuclear → leaders →
// ideology → disasters), which must stay byte-identical.
// ideology → disasters → migration → sea), which must stay byte-identical.
const abilities = {} as Record<AbilityField, unknown>;
for (const a of ABILITIES) abilities[a.field] = a.stub();