feat: #80 + #81 alliances & space program — 13th & 14th abilities (v0.45.0)
CI / build + unit + e2e (pull_request) Successful in 2m20s
CI / build + unit + e2e (pull_request) Successful in 2m20s
#80: TurnResult.alliances — form/break durable mutual-defense pacts (GameState.alliances; pair relation → ally/neutral, an active war overrides). #81: TurnResult.space — launch/deorbit satellites (GameState.satellites; +1 stability per active satellite per turn). Both appended last to the ABILITIES registry (entries 13 & 14, after contested); durable in SQLite via the stateFields loop, mirrored to regionStats, stats re-derived. System + user prompt goldens updated; 738 unit tests passing.
This commit is contained in:
@@ -2,6 +2,17 @@
|
||||
|
||||
All notable changes to **History** are listed here, newest first.
|
||||
|
||||
## v0.45.0 — 2026-10-17 08:35
|
||||
- **#80: Alliances / mutual-defense treaties — a `TurnResult.alliances` lever letting the LLM form (`form`) and break (`break`) durable, symmetric pacts between two distinct nations.** A `form` op creates a pact and sets the pair's relation to `ally` (unless an active war overrides — the pact still forms but `war` stays in force); a `break` op removes the pact and sets the pair back to `neutral` (again unless an active war still binds them). A pair holds at most one pact (re-forming is a no-op). Pacts live in `GameState.alliances` and the user payload **always** carries them (even when empty). It lands as the **13th ability** of the v0.40.0 modular system, in a new `server/abilities/alliances.ts` module, **registered last** in the `ABILITIES` registry (before `space`). Like `sea` / `wars` / `industry` it is **durable**: `GameState.alliances` persists across turns and in SQLite. Alliances never move borders (sole ownership is untouched).
|
||||
- **#81: Space program / satellites — a `TurnResult.space` lever letting the LLM launch (`launch`) satellites over regions it owns and deorbit (`deorbit`) active ones by id.** Each satellite is one of four types (`recon`, `comms`, `navigation`, `weather`), gets a fresh stable id (`s1`, `s2`, …), and is stamped with the turn and date. Every active satellite grants its owner +1 stability per committed turn (`SATELLITE_STABILITY_BONUS`), mirrored to every region the owner holds and re-derived — a nation with several satellites stacks the bonus. Launches/deorbits never move borders (sole ownership is untouched). It lands as the **14th ability** of the v0.40.0 modular system, in a new `server/abilities/space.ts` module, **registered last** in the `ABILITIES` registry (after `alliances`). Like `sea` / `wars` / `industry` it is **durable**: `GameState.satellites` persists across turns and in SQLite, and the user payload **always** carries the current satellite list.
|
||||
- **Types (`shared/protocol.ts`).** New `Alliance` (`{ id, parties, formedTurn, formedDate }`), `AllianceFormOp`, `AllianceBreakOp`, `AllianceOps` (`{ form, break }`); `SatelliteType` (`recon` | `comms` | `navigation` | `weather`), `SATELLITE_TYPES`, `SatelliteLaunchOp`, `SatelliteDeorbitOp`, `SpaceOps` (`{ launch, deorbit }`), `Satellite` (`{ id, by, regionKey, type, launchedTurn, launchedDate }`); `TurnResult.alliances` (required, between `contested` and `space`) and `TurnResult.space` (required, between `alliances` and `markers`).
|
||||
- **Types (`shared/types.ts`).** `GameState.alliances: Alliance[]` (after `contested`) and `GameState.satellites: Satellite[]` (after `alliances`) — both required, `[]` at creation, migrated on legacy rows via the generic `ABILITIES` `stateFields` loop in `db.ts` (no `db.ts` code change needed).
|
||||
- **Ability modules.** `server/abilities/alliances.ts` exports `validateAlliancesOps`, `applyAlliances`, `freshAllianceId`, and the `alliances: Ability<'alliances'>` const (`stateFields: ['alliances']`, payload always emits `{ alliances }`). `server/abilities/space.ts` exports `validateSpaceOps`, `applySpaceControl`, `applySatelliteBonuses`, `SATELLITE_STABILITY_BONUS`, `SATELLITE_TYPES`, `isSatelliteType`, `freshSatelliteId`, and the `space: Ability<'space'>` const (`stateFields: ['satellites']`, payload always emits `{ satellites }`).
|
||||
- **Engine (`server/engine.ts`).** `buildUserPrompt` merges `ABILITIES[12].payload()` (alliances → `alliances`) and `ABILITIES[13].payload()` (space → `satellites`) after the contested payload merge (both always emitted, like `sea`).
|
||||
- **Registry & prompt.** `alliances` and `space` appended **last** to `ABILITIES` (existing order untouched — entries 13 and 14); the system prompt gains both schema lines and constraint bullets; the user prompt gains `"alliances"` and `"satellites"` keys. The two system-prompt **goldens** and `user.txt` are regenerated.
|
||||
- **Tests.** New `server/abilities/alliances.test.ts` (26 tests) and `server/abilities/space.test.ts` (30 tests), mirroring `sea.test.ts` style: `default()` / `stub()` freshness, `validate` (valid/invalid/field caps/malformed top-level), `apply` (form creates pact + `ally` relation; break removes + `neutral`; idempotent re-form; break-then-re-form reuses id; war overrides neutral/ally; unresolvable / ambiguous refs dropped; never touches `territory` / `unowned`), `isMeaningful`, `payload` (always emits), `stateFields`, and `applySatelliteBonuses` (+1 stability per satellite, clamped, mirrored, re-derived, stacks). `registry.test.ts` updated for the **14-ability** contract (order, distinct fields, `fullDefaultTurn()`, `meaningfulFor`, durable `stateFields` now 8 entries, composes 15 / `extended[14]`).
|
||||
- **Version** bumped `0.44.0 → 0.45.0`.
|
||||
|
||||
## v0.44.0 — 2026-10-16 15:10
|
||||
- **#66: Contested regions / frontlines — a `TurnResult.contested` lever letting the LLM declare a region a **contested** war frontline (a **third region state**: in **no** nation's `territory` and **not** in `unowned` / no-man's-land) and resolve existing frontlines.** It lands as the **12th ability** of the v0.40.0 modular system, in a new `server/abilities/contested.ts` module, **registered last** in the `ABILITIES` registry (after `sea`). Like the `sea` and `industry` layers it is **durable**: `GameState.contested` persists across turns and in SQLite. It is a **pure territory-state** layer — the contested key simply *leaves* ownership (sole ownership is never broken: a key is in at most one `territory`), it is never placed into two territories, and it never touches borders directly.
|
||||
- **Types (`shared/protocol.ts`).** New `ContestOp` (`{ regionKey; parties }` — `regionKey` is a region key or resolvable name, `parties` are two or more nation refs (ids or names)), `ContestedOps` (`{ open: ContestOp[]; resolve: string[] }`), and `ContestedRegion` (`{ regionKey; parties }` — `parties` store the **resolved** nation ids); `TurnResult.contested` is **required**, placed between `sea` and `markers`, default `{ open: [], resolve: [] }`. New `CONTESTED_FILL = '#f97316'` / `CONTESTED_STROKE` (the distinct visible frontline fill/stroke for the map — a reddish/orange "hot" frontline on the dark basemap, clearly distinct from a nation color and from the no-man's-land grey) and `contestedTooltipLabel(parties)` (a short `"Contested: A vs B"` hover sub-line; a missing party name degrades to a dash, never crashes). `ContestedRegion` is imported into `shared/types.ts`; `GameState.contested: ContestedRegion[]` is **required** (`[]` at creation; a legacy SQLite row without the field loads as `[]` — additive, never wiped, via the generic `ABILITIES` `stateFields` loop in `db.ts` — no `db.ts` code change needed).
|
||||
|
||||
Reference in New Issue
Block a user