Today "at war" is a bare boolean on a Relation. Make war a first-class, named object in GameState so it can carry sides, aims, a front, and a lifecycle — and so alliances can drag nations into wars.
Mechanism
New entity War on GameState: { id: string; sides: string[] /* nation ids */; declaredTurn: number; aims?: string[]; status: 'active'|'armistice'|'capitulation'|'ended'; front?: string[] /* region keys */ }. GameState.wars: War[] (required, [] at creation; a legacy row loads as []).
TurnResult gains wars: { declare: { sides: string[]; aims?: string[] }[]; end: { id: string; outcome: 'armistice'|'capitulation'; terms?: string }[] } — both required, default [] (the required-with-default pattern used by every other field).
The war relation becomes derived: two nations are at war iff they sit on opposite sides of at least one active War. applyTurnResult creates/ends War objects and syncs the derived war relations. A bare war relation with no matching War degrades to an implicit single-war (back-compat).
Integration
A mutual-defense alliance (a relation of type alliance carrying a mutualDefense: true flag) means an attack on one member drags the other into the war (add to the attacker's War.sides).
Capturing a region while a war is active can be narrated as "taking territory in the war"; the front is the set of contested region keys.
Builds on: relations, territory sole-ownership, the canConquer military gate (unchanged).
Acceptance
e2e safety (critical): the stub returns wars: { declare: [], end: [] } and stays deterministic; the change is additive (legacy games load with wars: []); it must not break the UK-in-wwii-1939 e2e (empty frontier → no conquest, military 80→85, 7-item map legend).
Unit tests in server/engine.test.ts (declare/end, derived relation sync, mutual-defense drag-in, legacy bare-war back-compat).
System prompt documents wars; user prompt carries current active wars. Version bump at PR time (next = v0.33.0).
## What
Today "at war" is a bare boolean on a `Relation`. Make war a **first-class, named object** in `GameState` so it can carry sides, aims, a front, and a lifecycle — and so alliances can drag nations into wars.
## Mechanism
- New entity `War` on `GameState`: `{ id: string; sides: string[] /* nation ids */; declaredTurn: number; aims?: string[]; status: 'active'|'armistice'|'capitulation'|'ended'; front?: string[] /* region keys */ }`. `GameState.wars: War[]` (required, `[]` at creation; a legacy row loads as `[]`).
- `TurnResult` gains `wars: { declare: { sides: string[]; aims?: string[] }[]; end: { id: string; outcome: 'armistice'|'capitulation'; terms?: string }[] }` — **both required, default `[]`** (the required-with-default pattern used by every other field).
- The `war` relation becomes **derived**: two nations are at war iff they sit on opposite sides of at least one active `War`. `applyTurnResult` creates/ends `War` objects and syncs the derived `war` relations. A bare `war` relation with no matching `War` degrades to an implicit single-war (back-compat).
## Integration
- A mutual-defense alliance (a `relation` of type `alliance` carrying a `mutualDefense: true` flag) means an attack on one member drags the other into the war (add to the attacker's `War.sides`).
- Capturing a region while a war is active can be narrated as "taking territory in the war"; the `front` is the set of contested region keys.
- Builds on: relations, territory sole-ownership, the `canConquer` military gate (unchanged).
## Acceptance
- **e2e safety (critical):** the stub returns `wars: { declare: [], end: [] }` and stays deterministic; the change is **additive** (legacy games load with `wars: []`); it must **not** break the UK-in-`wwii-1939` e2e (empty frontier → no conquest, military 80→85, 7-item map legend).
- Unit tests in `server/engine.test.ts` (declare/end, derived relation sync, mutual-defense drag-in, legacy bare-war back-compat).
- System prompt documents `wars`; user prompt carries current active wars. Version bump at PR time (next = v0.33.0).
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.
What
Today "at war" is a bare boolean on a
Relation. Make war a first-class, named object inGameStateso it can carry sides, aims, a front, and a lifecycle — and so alliances can drag nations into wars.Mechanism
WaronGameState:{ id: string; sides: string[] /* nation ids */; declaredTurn: number; aims?: string[]; status: 'active'|'armistice'|'capitulation'|'ended'; front?: string[] /* region keys */ }.GameState.wars: War[](required,[]at creation; a legacy row loads as[]).TurnResultgainswars: { declare: { sides: string[]; aims?: string[] }[]; end: { id: string; outcome: 'armistice'|'capitulation'; terms?: string }[] }— both required, default[](the required-with-default pattern used by every other field).warrelation becomes derived: two nations are at war iff they sit on opposite sides of at least one activeWar.applyTurnResultcreates/endsWarobjects and syncs the derivedwarrelations. A barewarrelation with no matchingWardegrades to an implicit single-war (back-compat).Integration
relationof typealliancecarrying amutualDefense: trueflag) means an attack on one member drags the other into the war (add to the attacker'sWar.sides).frontis the set of contested region keys.canConquermilitary gate (unchanged).Acceptance
wars: { declare: [], end: [] }and stays deterministic; the change is additive (legacy games load withwars: []); it must not break the UK-in-wwii-1939e2e (empty frontier → no conquest, military 80→85, 7-item map legend).server/engine.test.ts(declare/end, derived relation sync, mutual-defense drag-in, legacy bare-war back-compat).wars; user prompt carries current active wars. Version bump at PR time (next = v0.33.0).