feat: #66 contested regions / frontlines — 12th ability (v0.44.0)
CI / build + unit + e2e (pull_request) Successful in 2m10s
CI / build + unit + e2e (pull_request) Successful in 2m10s
A third region state (contested frontline): a region in no nation's territory and not in unowned, opened/resolved by the LLM, tied to an active war. Sole-ownership invariant preserved. Durable GameState.contested; contested-inert stub; map legend stays at 7.
This commit is contained in:
@@ -104,7 +104,7 @@ history/
|
||||
│ │ ├── <name>.ts # one module per ability: wars / takeovers / leaders / nuclear /
|
||||
│ │ │ # ideology / industry / economy / hybrid / disasters (v0.41.0 #63) /
|
||||
│ │ │ # migration (v0.42.0 #64)
|
||||
│ │ ├── *.test.ts # per-ability unit tests (registry.test.ts pins the 10-ability contract)
|
||||
│ │ ├── *.test.ts # per-ability unit tests (registry.test.ts pins the 12-ability contract)
|
||||
│ │ └── goldens/ # pinned prompt goldens (system-no-tool / system-tool / user)
|
||||
│ └── *.test.ts # unit tests (vitest)
|
||||
└── src/
|
||||
@@ -583,6 +583,52 @@ history/
|
||||
12 named zones**, sole-control semantics, contest-by-higher-military, and a durable
|
||||
`seaZones` list — a cleaner, more deterministic model that the LLM can reason about without
|
||||
inventing zone names each turn.
|
||||
- **Contested regions / frontlines — a `TurnResult.contested` lever opening/resolving war frontlines: a THIRD region state in no nation's `territory` and not in `unowned` (v0.44.0, #66).**
|
||||
`TurnResult.contested = ContestedOps = { open: ContestOp[]; resolve: string[] }` (required,
|
||||
default `{ open: [], resolve: [] }`); `ContestOp = { regionKey, parties }` where `regionKey` is
|
||||
a region key or resolvable name and `parties` are TWO OR MORE nation refs (ids or names, via
|
||||
the shared `resolveNationRef`). Like the `sea` / `industry` layers this is **durable**: it owns
|
||||
the `GameState.contested: ContestedRegion[]` field (`ContestedRegion = { regionKey, parties }`
|
||||
— `parties` store the RESOLVED nation ids; required, `[]` at creation; a legacy SQLite row
|
||||
without the field loads as `[]` — via the generic `ABILITIES` `stateFields` loop in `db.ts`,
|
||||
no `db.ts` code change needed). `applyContested` (run LAST in the `ABILITIES` loop, after
|
||||
territory / wars / economy / sea — it sees the final post-claim ownership) runs **RESOLVE
|
||||
first, then OPEN**: a `resolve` removes a currently-contested key and **releases it to no
|
||||
man's land** (into `unowned`, out of any territory, `regionStats` deleted) — UNLESS a
|
||||
concurrent winning claim by one of the contesting **parties** already re-owned it this turn
|
||||
(the only claim the territory step permits on a contested key), in which case it stays owned
|
||||
and keeps (or re-inits) its `regionStats`; an `open` makes a region a frontline only when it
|
||||
is **contestable** — a **sole owner** at war (a shared **active** `War`) with at least one
|
||||
party, or a **no-man's-land** region (0 owners AND in `unowned`) whose two parties are at war
|
||||
with each other — a 2+-owner key or an owner not at war with any party is **dropped**
|
||||
(an armistice/ended war does not count); on a successful open the key is **stripped from its
|
||||
owner's `territory` (or from `unowned`)**, its `regionStats` entry deleted, and a
|
||||
`{ regionKey, parties }` entry added to `next.contested` (**unique by `regionKey`** — a
|
||||
re-open replaces a kept entry), with the stripped owner's derived stats re-derived (same
|
||||
0-region / no-entry guards). The **sacrosanct sole-ownership invariant is never broken**: a
|
||||
contested key is in **no** territory (never two) and is **never** in `unowned` while
|
||||
contested (the two states are mutually exclusive). The **territory step in `engine.ts` is
|
||||
guarded**: a normal `gain` on a currently-contested key is **dropped** unless this same turn
|
||||
resolves the contest AND the gainer is one of the contesting parties (a party re-claim) — a
|
||||
still-contested key is never a valid normal-claim target. The **map (client-only, like
|
||||
no-man's-land)**: `WorldMap` renders a contested key in the distinct visible
|
||||
`CONTESTED_FILL` / `CONTESTED_STROKE` (orange "hot" frontline) with a `"Contested: A vs B"`
|
||||
hover sub-line (names from `GameState.contested`; a removed party degrades to a dash) — the
|
||||
contested check runs **first** (before unowned / owned); the server `RegionMap` stays
|
||||
owned-vs-absent (it never knows about contested). **The map legend stays at 7 items** (it is
|
||||
derived from the `RegionMap` owners only — a contested key is absent from the `RegionMap`, so
|
||||
no new legend entry; e2e legend assertions are unaffected). `isMeaningful` is
|
||||
`open.length > 0 || resolve.length > 0`; the system prompt documents the field + constraints
|
||||
(at most 1–2 contested-region changes per turn, each narrated in `events`); the user payload
|
||||
carries the current frontlines (region key + party **names**) **only when non-empty** —
|
||||
`payload()` → `{}` on a fresh / no-frontline world (unlike `sea`, which always emits). The
|
||||
**stub never** opens or resolves a frontline (`{ open: [], resolve: [] }`), so e2e (CI,
|
||||
`LLM_STUB=1`) is unaffected and the path is unit-tested only
|
||||
(`server/abilities/contested.test.ts`, 52 tests, each asserting the sole-ownership invariants).
|
||||
**Design note (deviation from the issue body):** the Gitea #66 body sketched a **two-owner**
|
||||
`disputed` / `fronts` design that "relaxes sole-ownership"; the implemented, approved design
|
||||
is this **third region state** (a contested key in NO territory, sole-ownership preserved)
|
||||
with `open` / `resolve` ops and a minimum of two contesting parties.
|
||||
- **Map click → dossier target (owner resolved from the RegionMap, by region key).**
|
||||
Clicking a sub-region does NOT open a popup — it switches the dossier to that sub-region's
|
||||
owning nation (`focusNation(ownerId, {name})` in `App.tsx`, which also records which region was
|
||||
|
||||
Reference in New Issue
Block a user