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
+27 -1
View File
@@ -16,6 +16,7 @@ import type {
RegionResources,
RegionStats,
RoundItem,
SeaZoneClaim,
StatKey,
TradeAgreement,
} from '../shared/types';
@@ -104,6 +105,14 @@ export {
MIGRATION_SOURCE_EFFECTS,
MIGRATION_DESTINATION_EFFECTS,
LARGE_INFLUX_STABILITY_DIP,
validateSeaOps,
applySeaControl,
applySeaZoneBonuses,
SEA_ZONE_BONUS,
SEA_ZONES,
SEA_ZONE_IDS,
isSeaZoneId,
seaZoneName,
} from './abilities';
// --- The LLM's getNeighbours tool (v0.21, #16 P2: contiguous region transfers) ---
@@ -376,6 +385,14 @@ export function buildUserPrompt(
* key on any nation) when undefined.
*/
nuclear?: Record<string, boolean>,
/**
* (v0.43.0, #65) The game's current sea-zone control (each a `{ zone, by }`
* pair of the fixed sea zones and their controlling nation id). Emitted
* verbatim (even when empty) so the model knows which of the 12 fixed sea
* zones each nation currently controls and can re-claim or contest them via
* `TurnResult.sea`. Omitted entirely when undefined.
*/
seaZones?: SeaZoneClaim[],
): string {
const player = state.nations[state.playerNationId];
const playerName = player?.name ?? state.playerNationId;
@@ -485,6 +502,7 @@ export function buildUserPrompt(
embargoes,
nationResources,
industryProjects,
seaZones,
};
Object.assign(payload, ABILITIES[0].payload(upCtx)); // takeovers → takeoverRisks
if (noMansLand !== undefined) payload.unowned = noMansLand;
@@ -493,6 +511,7 @@ export function buildUserPrompt(
Object.assign(payload, ABILITIES[2].payload(upCtx)); // hybrid → relations
Object.assign(payload, ABILITIES[3].payload(upCtx)); // economy → trades, embargoes
Object.assign(payload, ABILITIES[4].payload(upCtx)); // industry → resources, industry
Object.assign(payload, ABILITIES[10].payload(upCtx)); // sea → seaZones (always emitted)
return JSON.stringify(payload, null, 2);
}
@@ -633,6 +652,7 @@ export function validateTurnResult(
ideology: { changes: [] },
disasters: { events: [] },
migration: { ops: [] },
sea: { control: [] },
markers: { add: [], remove: [] },
};
@@ -723,7 +743,7 @@ export function validateTurnResult(
// list for the player to confirm/reject.
result.nationOps = validateNationOps(o.nationOps);
// The 9 per-feature LLM abilities (v0.40.0, #67): each one's validate call
// The 11 per-feature LLM abilities (v0.40.0, #67): each one's validate call
// is owned by its Ability module in the ABILITIES registry. The field's raw
// value is passed through a.validate() which returns the validated field shape.
for (const a of ABILITIES) {
@@ -917,6 +937,9 @@ export function applyTurnResult(
// (v0.36, #59) active industry projects; a legacy/corrupt clone without the
// field starts empty (the turn then only ever adds to it).
next.industry = next.industry ?? [];
// (v0.43.0, #65) sea-zone control; a legacy/corrupt clone without the field
// starts empty (the turn then upserts this turn's claims into it).
next.seaZones = next.seaZones ?? [];
const actingTurn = state.turn; // this action resolves the current turn
next.turn = state.turn + 1; // then we advance to the next turn to play
next.date = isDateLike(result.date) ? result.date : addDays(state.date, 2);
@@ -1594,6 +1617,9 @@ export async function runTurn(opts: {
state.industry ?? [],
// (v0.37.0, #60) per-nation nuclear capability (flag or rocket marker).
nuclearCapability,
// (v0.43.0, #65) the current sea-zone control (always emitted — even when
// empty — so the model knows who controls which of the fixed sea zones).
state.seaZones ?? [],
);
const nudge =
'Your previous output was invalid. Respond with ONLY the JSON object described above for this batch — no prose, no fences.';