Implements Feature #66 — Contested regions / frontlines as the 12th modular LLM ability (v0.44.0): a third region state — a war frontline that is in no nation's territory and not in unowned / no-man's-land. The LLM can open a region as a contested frontline (its sole owner is at war with a party, or it is no-man's-land whose parties are at war) and resolve an existing one (released to no man's land — unless a party's concurrent claim re-owns it this turn).
Design notes
Deviation from the issue body (#66). The Gitea #66 body sketched a two-owner design (disputed regions with fronts, explicitly "relaxing sole-ownership"). The implemented, approved design is a third region state: a contested key is in no nation's territory and not in unowned, with open / resolve ops and a minimum of two contesting parties tied to an active war. This keeps the sacrosanct sole-ownership invariant intact (a key is never in two territories) while still modelling a war frontline.
Map legend stays at 7. The client derives the legend from the server RegionMap owners only; a contested key is absent from that RegionMap (it stays owned-vs-absent), so it adds no legend entry — the existing e2e legend assertions (7 items) are unaffected. The contested fill / tooltip is rendered client-side (like no-man's-land), with the contested check running before unowned/owned.
Guarantees
No engine randomness — the LLM declares; the engine applies fixed, deterministic rules.
No new runtime deps; shared/ stays runtime-dep-free; no @turf/@types/geojson in any .ts.
DurableGameState.contested — legacy SQLite rows load as [] via the generic ABILITIESstateFields loop in db.ts (no db.ts code change).
The stub is contested-inert ({ open: [], resolve: [] }), so e2e (CI, LLM_STUB=1) is unaffected; the apply path is unit-tested only (52 new tests in server/abilities/contested.test.ts, each asserting the sole-ownership invariants).
All existing unit tests green (additive): 682 passed (20 files).
Verification
npm run typecheck + npm run lint + npm test (682/682) + npm run build — all green locally. E2e is the CI source of truth (runs on this PR).
## Summary
Implements **Feature #66 — Contested regions / frontlines** as the **12th modular LLM ability** (v0.44.0): a **third region state** — a war **frontline** that is in **no** nation's `territory` and **not** in `unowned` / no-man's-land. The LLM can **`open`** a region as a contested frontline (its sole owner is at war with a party, or it is no-man's-land whose parties are at war) and **`resolve`** an existing one (released to no man's land — unless a party's concurrent claim re-owns it this turn).
## Design notes
- **Deviation from the issue body (#66).** The Gitea #66 body sketched a *two-owner* design (`disputed` regions with `fronts`, explicitly "relaxing sole-ownership"). The implemented, approved design is a **third region state**: a contested key is in **no** nation's `territory` and **not** in `unowned`, with `open` / `resolve` ops and a **minimum of two** contesting parties tied to an **active** war. This keeps the sacrosanct **sole-ownership invariant** intact (a key is never in two territories) while still modelling a war frontline.
- **Map legend stays at 7.** The client derives the legend from the server `RegionMap` owners only; a contested key is **absent** from that `RegionMap` (it stays owned-vs-absent), so it adds no legend entry — the existing e2e legend assertions (7 items) are unaffected. The contested fill / tooltip is rendered client-side (like no-man's-land), with the contested check running **before** unowned/owned.
## Guarantees
- **No engine randomness** — the LLM declares; the engine applies fixed, deterministic rules.
- **No new runtime deps**; `shared/` stays runtime-dep-free; no `@turf`/`@types/geojson` in any `.ts`.
- **Durable** `GameState.contested` — legacy SQLite rows load as `[]` via the generic `ABILITIES` `stateFields` loop in `db.ts` (no `db.ts` code change).
- The **stub is contested-inert** (`{ open: [], resolve: [] }`), so **e2e (CI, `LLM_STUB=1`) is unaffected**; the apply path is unit-tested only (52 new tests in `server/abilities/contested.test.ts`, each asserting the sole-ownership invariants).
- All existing unit tests green (additive): **682 passed** (20 files).
## Verification
`npm run typecheck` + `npm run lint` + `npm test` (682/682) + `npm run build` — all green locally. E2e is the CI source of truth (runs on this PR).
## Checklist
- [x] Changelog entry (`CHANGELOG.md` → v0.44.0)
- [x] Version bump (`package.json` → 0.44.0)
- [x] Unit tests (52 new + all existing green)
- [x] Prompt goldens regenerated (system + user)
- [x] No production deploy (manual, post-merge)
Fixes #66
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.
kamilkrzywda
scheduled this pull request to auto merge when all checks succeed 2026-10-16 17:26:32 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
Implements Feature #66 — Contested regions / frontlines as the 12th modular LLM ability (v0.44.0): a third region state — a war frontline that is in no nation's
territoryand not inunowned/ no-man's-land. The LLM canopena region as a contested frontline (its sole owner is at war with a party, or it is no-man's-land whose parties are at war) andresolvean existing one (released to no man's land — unless a party's concurrent claim re-owns it this turn).Design notes
disputedregions withfronts, explicitly "relaxing sole-ownership"). The implemented, approved design is a third region state: a contested key is in no nation'sterritoryand not inunowned, withopen/resolveops and a minimum of two contesting parties tied to an active war. This keeps the sacrosanct sole-ownership invariant intact (a key is never in two territories) while still modelling a war frontline.RegionMapowners only; a contested key is absent from thatRegionMap(it stays owned-vs-absent), so it adds no legend entry — the existing e2e legend assertions (7 items) are unaffected. The contested fill / tooltip is rendered client-side (like no-man's-land), with the contested check running before unowned/owned.Guarantees
shared/stays runtime-dep-free; no@turf/@types/geojsonin any.ts.GameState.contested— legacy SQLite rows load as[]via the genericABILITIESstateFieldsloop indb.ts(nodb.tscode change).{ open: [], resolve: [] }), so e2e (CI,LLM_STUB=1) is unaffected; the apply path is unit-tested only (52 new tests inserver/abilities/contested.test.ts, each asserting the sole-ownership invariants).Verification
npm run typecheck+npm run lint+npm test(682/682) +npm run build— all green locally. E2e is the CI source of truth (runs on this PR).Checklist
CHANGELOG.md→ v0.44.0)package.json→ 0.44.0)Fixes #66