feat: #65 sea control — 11th ability (v0.43.0)
CI / build + unit + e2e (pull_request) Successful in 2m33s
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:
@@ -707,6 +707,82 @@ export interface MigrationOps {
|
||||
ops: MigrationOp[];
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// (v0.43.0, #65) Sea zones & sea control — the naval layer, fully parallel to
|
||||
// but INDEPENDENT of land. The world has a fixed set of 12 named SEA ZONES
|
||||
// ({@link SEA_ZONES}); each is under the SOLE CONTROL of at most one nation.
|
||||
// Controlling a sea zone never moves land (`territory` / `unowned` are
|
||||
// untouched) and moving land never touches a sea zone. `SeaZoneClaim` (the
|
||||
// durable per-zone record, `{ zone, by }`) lives in `shared/types.ts` like
|
||||
// `Trade` / `Embargo` / `War` / `IndustryProject`; `GameState.seaZones` is a
|
||||
// list of them, unique per zone (`[]` at creation; a legacy SQLite row
|
||||
// without the field loads as `[]` — additive, never wiped). Each turn the LLM
|
||||
// declares `TurnResult.sea.control` ops — one op names the zone and the
|
||||
// nation that (now) controls it. The engine resolves `by` (id first, then a
|
||||
// case-insensitive unique name) and UPSERTS the zone's claim (replace in
|
||||
// place if the zone is already claimed, append if new). If two or more ops
|
||||
// this turn claim the SAME zone for DIFFERENT resolved nations, the zone is
|
||||
// CONTESTED: the higher current `stats.military` wins (a tie goes to the op
|
||||
// listed earlier in the `control` array) — deterministic, no hidden
|
||||
// randomness; an unresolvable `by` is dropped (never thrown). Every committed
|
||||
// turn — including a turn with an empty `control` — each existing claim's
|
||||
// controller earns a small DETERMINISTIC bonus (`SEA_ZONE_BONUS = 1` military
|
||||
// + 1 economy, clamped to 0–100, mirrored to the controller's regions and
|
||||
// re-derived with the same 0-region / no-entry guards as the economy /
|
||||
// migration layers); a claim whose nation no longer exists is skipped (the
|
||||
// claim is kept, never thrown). The layer is DURABLE (unlike the turn-
|
||||
// transient hybrid / disasters / migration layers): `GameState.seaZones`
|
||||
// persists across turns and in SQLite. The user payload carries the current
|
||||
// `seaZones` list so the model knows who controls which zone.
|
||||
|
||||
/** The 12 named sea zones the world model knows (v0.43.0, #65). */
|
||||
export const SEA_ZONES = [
|
||||
{ id: 'north-atlantic', name: 'North Atlantic' },
|
||||
{ id: 'south-atlantic', name: 'South Atlantic' },
|
||||
{ id: 'pacific', name: 'Pacific Ocean' },
|
||||
{ id: 'indian', name: 'Indian Ocean' },
|
||||
{ id: 'mediterranean', name: 'Mediterranean Sea' },
|
||||
{ id: 'north-sea', name: 'North Sea' },
|
||||
{ id: 'baltic', name: 'Baltic Sea' },
|
||||
{ id: 'arctic', name: 'Arctic Ocean' },
|
||||
{ id: 'black-sea', name: 'Black Sea' },
|
||||
{ id: 'gulf-mexico', name: 'Gulf of Mexico' },
|
||||
{ id: 'red-sea', name: 'Red Sea' },
|
||||
{ id: 'sargasso', name: 'Sargasso Sea' },
|
||||
] as const;
|
||||
|
||||
/** The set of valid sea-zone ids (v0.43.0, #65). */
|
||||
export const SEA_ZONE_IDS: ReadonlySet<string> = new Set(SEA_ZONES.map((z) => z.id));
|
||||
|
||||
/** Whether `id` is one of the fixed sea-zone ids (v0.43.0, #65). */
|
||||
export function isSeaZoneId(id: string): boolean {
|
||||
return SEA_ZONE_IDS.has(id);
|
||||
}
|
||||
|
||||
/** The display name of a sea-zone id; an unknown id is returned as-is (v0.43.0, #65). */
|
||||
export function seaZoneName(id: string): string {
|
||||
return SEA_ZONES.find((z) => z.id === id)?.name ?? id;
|
||||
}
|
||||
|
||||
/**
|
||||
* A single sea-control op (v0.43.0, #65): `zone` is one of the fixed
|
||||
* {@link SEA_ZONES} ids and `by` is the nation that (now) controls it (by id
|
||||
* or name). One op SETS or REPLACES the zone's controller; two or more ops on
|
||||
* the same zone for different nations are CONTESTED — the higher current
|
||||
* `stats.military` wins (a tie: the earlier-listed op).
|
||||
*/
|
||||
export interface SeaControlOp {
|
||||
/** The sea zone (one of {@link SEA_ZONE_IDS}). */
|
||||
zone: string;
|
||||
/** The controlling nation (by id or name). */
|
||||
by: string;
|
||||
}
|
||||
|
||||
/** The sea-control block of a `TurnResult`. Required; default `{ control: [] }`. */
|
||||
export interface SeaOps {
|
||||
control: SeaControlOp[];
|
||||
}
|
||||
|
||||
/** The exact JSON object the world-simulator LLM must return for a turn. */
|
||||
export interface TurnResult {
|
||||
date: string;
|
||||
@@ -899,6 +975,23 @@ export interface TurnResult {
|
||||
* `{ ops: [] }`.
|
||||
*/
|
||||
migration: MigrationOps;
|
||||
/**
|
||||
* (v0.43.0, #65) Sea-zone control ops this turn — the naval layer, fully
|
||||
* parallel to but INDEPENDENT of land: each op names one of the 12 fixed
|
||||
* sea zones (`SEA_ZONES`) and the nation that now controls it. The engine
|
||||
* resolves `by` (id first, then a case-insensitive unique name) and
|
||||
* upserts the zone's claim in `GameState.seaZones` (a single op sets or
|
||||
* replaces the controller; two or more ops on the same zone for different
|
||||
* nations are CONTESTED — the higher current `stats.military` wins, a tie
|
||||
* goes to the op listed earlier in `control`; an unresolvable `by` is
|
||||
* dropped, never thrown). Every committed turn, each existing claim's
|
||||
* controller earns a small deterministic military/economy bonus
|
||||
* (`SEA_ZONE_BONUS`). Sea control never moves land: `territory` and
|
||||
* `unowned` are untouched. At most 1–2 per turn, each narrated in `events`
|
||||
* (a `military` or `diplomatic` event). Required; default
|
||||
* `{ control: [] }`.
|
||||
*/
|
||||
sea: SeaOps;
|
||||
/**
|
||||
* (v0.27, #44) Map-marker ops this turn. `add` places new named markers
|
||||
* (the engine positions each at its region's centroid + a small
|
||||
|
||||
Reference in New Issue
Block a user