Allow a region to be contested by two factions at once — a frontline: sieges, advancing/retreating front lines, disputed sub-regions. This relaxes the load-bearing sole-ownership invariant (a region key having at most one owner) in a controlled, reversible way.
Mechanism
Introduce a disputed state on a region key: GameState.disputed: { regionKey: string; claimants: [string,string] }[] — a region can have one controlling owner + one contesting claimant (or be genuinely contested with no clear owner). The map renders a contested region distinctly (e.g. a hatched/mixed fill).
TurnResult gains fronts: { contest: { regionKey: string; by: string /* the new claimant */ }[]; resolve: { regionKey: string; to: string }[] } — required, default []s. A contest adds a claimant (region is now disputed); a resolve collapses it back to sole ownership by to.
Hard rule preserved: a region can never have more than 2 owners, and it always has at least a resolving owner; the existing "never orphaned / never duplicated" guarantee is generalized to "at most one controller + one claimant, always resolvable." The canConquer gate still applies to resolving a contest by force.
This is the riskiest change (touches the core invariant) — do it last, carefully, with exhaustive tests.
Builds on: sole-ownership (generalized), territory, the canConquer gate, the choropleth.
Acceptance
e2e safety (critical): stub returns fronts: { contest: [], resolve: [] }, deterministic; additive (legacy games load with disputed: []); must not break the UK-in-wwii-1939 e2e (empty frontier, military 80→85, 7-item legend) or any existing sole-ownership unit/e2e assertion (update them deliberately, not by accident).
Exhaustive unit tests in server/engine.test.ts + server/map.test.ts (contest, resolve, ≤2 owners, no orphan, no duplicate, conquest-resolution gate, contested map fill).
Version bump at PR time. Do last (most invasive).
## What
Allow a **region to be contested** by two factions at once — a **frontline**: sieges, advancing/retreating front lines, disputed sub-regions. This **relaxes the load-bearing sole-ownership invariant** (a region key having at most one owner) in a controlled, reversible way.
## Mechanism
- Introduce a `disputed` state on a region key: `GameState.disputed: { regionKey: string; claimants: [string,string] }[]` — a region can have **one controlling owner + one contesting claimant** (or be genuinely contested with no clear owner). The map renders a contested region distinctly (e.g. a hatched/mixed fill).
- `TurnResult` gains `fronts: { contest: { regionKey: string; by: string /* the new claimant */ }[]; resolve: { regionKey: string; to: string }[] }` — required, default `[]`s. A `contest` adds a claimant (region is now disputed); a `resolve` collapses it back to sole ownership by `to`.
- **Hard rule preserved:** a region can never have **more than 2** owners, and it always has at least a resolving owner; the existing "never orphaned / never duplicated" guarantee is generalized to "at most one controller + one claimant, always resolvable." The `canConquer` gate still applies to resolving a contest by force.
- This is the **riskiest** change (touches the core invariant) — do it last, carefully, with exhaustive tests.
- Builds on: sole-ownership (generalized), territory, the `canConquer` gate, the choropleth.
## Acceptance
- **e2e safety (critical):** stub returns `fronts: { contest: [], resolve: [] }`, deterministic; additive (legacy games load with `disputed: []`); must **not** break the UK-in-`wwii-1939` e2e (empty frontier, military 80→85, 7-item legend) or any existing sole-ownership unit/e2e assertion (update them deliberately, not by accident).
- **Exhaustive** unit tests in `server/engine.test.ts` + `server/map.test.ts` (contest, resolve, ≤2 owners, no orphan, no duplicate, conquest-resolution gate, contested map fill).
- Version bump at PR time. Do **last** (most invasive).
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
Allow a region to be contested by two factions at once — a frontline: sieges, advancing/retreating front lines, disputed sub-regions. This relaxes the load-bearing sole-ownership invariant (a region key having at most one owner) in a controlled, reversible way.
Mechanism
disputedstate on a region key:GameState.disputed: { regionKey: string; claimants: [string,string] }[]— a region can have one controlling owner + one contesting claimant (or be genuinely contested with no clear owner). The map renders a contested region distinctly (e.g. a hatched/mixed fill).TurnResultgainsfronts: { contest: { regionKey: string; by: string /* the new claimant */ }[]; resolve: { regionKey: string; to: string }[] }— required, default[]s. Acontestadds a claimant (region is now disputed); aresolvecollapses it back to sole ownership byto.canConquergate still applies to resolving a contest by force.canConquergate, the choropleth.Acceptance
fronts: { contest: [], resolve: [] }, deterministic; additive (legacy games load withdisputed: []); must not break the UK-in-wwii-1939e2e (empty frontier, military 80→85, 7-item legend) or any existing sole-ownership unit/e2e assertion (update them deliberately, not by accident).server/engine.test.ts+server/map.test.ts(contest, resolve, ≤2 owners, no orphan, no duplicate, conquest-resolution gate, contested map fill).