feat(#56): War as a first-class state — declare/end ops, mutual-defense drag-in, relation sync
CI / build + unit + e2e (pull_request) Successful in 2m32s

Promote the bare 'war' relation to a named War entity in GameState.wars:
- War: { id, sides[], declaredTurn, declaredDate?, aims?, status, front? }
- TurnResult.wars: { declare: WarDeclareOp[], end: WarEndOp[] }
- syncWarRelations: derive 'war' from active wars, fallback to 'hostile' on end
- Mutual-defense drag-in: belligerent allies added to sides (one level)
- Bare 'war' back-compat: never downgraded, never force-created
- Stub: empty wars (e2e unaffected); ~30 new unit tests

v0.33.0
This commit is contained in:
2026-10-15 15:47:06 +00:00
parent 38d3202472
commit b5e2229ee2
15 changed files with 947 additions and 25 deletions
+12
View File
@@ -38,6 +38,13 @@ export interface StubPayload {
* path is covered by unit tests against applyTurnResult, not by the stub.
*/
markers?: Array<{ id: string; type: string; name: string; regionKey: string; owner: string | null }>;
/**
* (v0.33.0, #56) Currently ACTIVE wars (`{ id, sides, aims? }`), emitted by
* the engine's user prompt. The stub ignores this field — it never declares
* or ends a war (see deterministicTurn), so the war path is covered by unit
* tests against applyTurnResult, not by the stub.
*/
wars?: Array<{ id: string; sides: string[]; aims?: string[] }>;
}
function truncate(s: string, n: number): string {
@@ -81,6 +88,10 @@ function truncate(s: string, n: number): string {
* (v0.32.0, #54) The stub likewise NEVER performs a non-military takeover
* (it always returns `takeovers: []`) — the regime-rename path is covered by
* unit tests only, keeping the e2e assertions untouched.
*
* (v0.33.0, #56) The stub likewise NEVER declares or ends a war (it always
* returns `wars: { declare: [], end: [] }`) — the war path is covered by unit
* tests only, keeping the e2e assertions untouched.
*/
export function deterministicTurn(p: StubPayload): TurnResult {
const state = p.state;
@@ -148,6 +159,7 @@ export function deterministicTurn(p: StubPayload): TurnResult {
unowned: [], // the stub never creates no man's land (see doc comment)
nationOps: [], // the stub never proposes nation ops (see doc comment)
takeovers: [], // the stub never performs a takeover (see doc comment)
wars: { declare: [], end: [] }, // the stub never declares/ends wars (see doc comment)
markers: { add: [], remove: [] }, // the stub never touches markers (see doc comment)
};
}