feat: #66 contested regions / frontlines — 12th ability (v0.44.0)
CI / build + unit + e2e (pull_request) Successful in 2m10s
CI / build + unit + e2e (pull_request) Successful in 2m10s
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.
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
markerId,
|
||||
MAX_MARKERS,
|
||||
nationForPrompt,
|
||||
type ContestedRegion,
|
||||
type MarkerInfo,
|
||||
type MarkerSpec,
|
||||
type NationOp,
|
||||
@@ -113,6 +114,11 @@ export {
|
||||
SEA_ZONE_IDS,
|
||||
isSeaZoneId,
|
||||
seaZoneName,
|
||||
validateContestedOps,
|
||||
applyContested,
|
||||
CONTESTED_FILL,
|
||||
CONTESTED_STROKE,
|
||||
contestedTooltipLabel,
|
||||
} from './abilities';
|
||||
|
||||
// --- The LLM's getNeighbours tool (v0.21, #16 P2: contiguous region transfers) ---
|
||||
@@ -393,6 +399,14 @@ export function buildUserPrompt(
|
||||
* `TurnResult.sea`. Omitted entirely when undefined.
|
||||
*/
|
||||
seaZones?: SeaZoneClaim[],
|
||||
/**
|
||||
* (v0.44.0, #66) The game's current contested regions (each a `{ regionKey,
|
||||
* parties }` pair — a war frontline in no nation's territory and not in
|
||||
* `unowned`). Emitted verbatim (even when empty) so the model knows which
|
||||
* regions are frontlines (and can `resolve` them or see their parties);
|
||||
* omitted entirely when undefined.
|
||||
*/
|
||||
contested?: ContestedRegion[],
|
||||
): string {
|
||||
const player = state.nations[state.playerNationId];
|
||||
const playerName = player?.name ?? state.playerNationId;
|
||||
@@ -503,6 +517,7 @@ export function buildUserPrompt(
|
||||
nationResources,
|
||||
industryProjects,
|
||||
seaZones,
|
||||
contested,
|
||||
};
|
||||
Object.assign(payload, ABILITIES[0].payload(upCtx)); // takeovers → takeoverRisks
|
||||
if (noMansLand !== undefined) payload.unowned = noMansLand;
|
||||
@@ -512,6 +527,7 @@ export function buildUserPrompt(
|
||||
Object.assign(payload, ABILITIES[3].payload(upCtx)); // economy → trades, embargoes
|
||||
Object.assign(payload, ABILITIES[4].payload(upCtx)); // industry → resources, industry
|
||||
Object.assign(payload, ABILITIES[10].payload(upCtx)); // sea → seaZones (always emitted)
|
||||
Object.assign(payload, ABILITIES[11].payload(upCtx)); // contested → contested (only when non-empty)
|
||||
return JSON.stringify(payload, null, 2);
|
||||
}
|
||||
|
||||
@@ -653,6 +669,7 @@ export function validateTurnResult(
|
||||
disasters: { events: [] },
|
||||
migration: { ops: [] },
|
||||
sea: { control: [] },
|
||||
contested: { open: [], resolve: [] },
|
||||
markers: { add: [], remove: [] },
|
||||
};
|
||||
|
||||
@@ -940,6 +957,9 @@ export function applyTurnResult(
|
||||
// (v0.43.0, #65) sea-zone control; a legacy/corrupt clone without the field
|
||||
// starts empty (the turn then upserts this turn's claims into it).
|
||||
next.seaZones = next.seaZones ?? [];
|
||||
// (v0.44.0, #66) contested regions (war frontlines); a legacy/corrupt clone
|
||||
// without the field starts empty (the turn then opens/resolves contests).
|
||||
next.contested = next.contested ?? [];
|
||||
const actingTurn = state.turn; // this action resolves the current turn
|
||||
next.turn = state.turn + 1; // then we advance to the next turn to play
|
||||
next.date = isDateLike(result.date) ? result.date : addDays(state.date, 2);
|
||||
@@ -1035,12 +1055,34 @@ export function applyTurnResult(
|
||||
}
|
||||
set.add(gainer);
|
||||
}
|
||||
// (v0.44.0, #66) Contested-region guard for the claims step below: a region
|
||||
// that is currently a war frontline (in `state.contested`) sits in no
|
||||
// nation's territory, so a normal `gain` would otherwise treat it as a free
|
||||
// re-claim. A contested key may only be claimed by a normal claim when this
|
||||
// same turn resolves the contest AND the gainer is one of the contesting
|
||||
// parties (a party re-claim); otherwise the frontline stays contested for
|
||||
// the contested layer (later in the turn) to decide its fate.
|
||||
const contestedBefore = new Set((state.contested ?? []).map((c) => c.regionKey));
|
||||
const resolveThisTurn = new Set(result.contested.resolve);
|
||||
const contestedParties = new Map(
|
||||
(state.contested ?? []).map((c) => [c.regionKey, c.parties]),
|
||||
);
|
||||
for (const [key, claiming] of claimants) {
|
||||
if (claiming.size !== 1) continue; // double claim: the key stays as it was
|
||||
const gainer = [...claiming][0];
|
||||
const owner = ownerOf.get(key); // undefined: no owner; '': 2+ owners (corrupt)
|
||||
if (owner === '') continue; // pre-existing double ownership: never touched
|
||||
if (owner !== undefined && owner === gainer) continue; // self-gain: no-op
|
||||
// (v0.44.0, #66) a currently-contested frontline is not a normal claim
|
||||
// target: a claim on it only proceeds when this turn resolves the contest
|
||||
// and the gainer is one of the contesting parties (a party re-claim);
|
||||
// otherwise the frontline stays contested for the contested layer below.
|
||||
if (
|
||||
contestedBefore.has(key) &&
|
||||
(!resolveThisTurn.has(key) || !contestedParties.get(key)?.includes(gainer))
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
// (v0.30, #50) MILITARY HARD GATE: a CONTESTED conquest (the key has a
|
||||
// different sole owner) only proceeds when the attacker's derived military
|
||||
// is not MORE than CONQUEST_MILITARY_MARGIN below the defender's. A
|
||||
@@ -1620,6 +1662,10 @@ export async function runTurn(opts: {
|
||||
// (v0.43.0, #65) the current sea-zone control (always emitted — even when
|
||||
// empty — so the model knows who controls which of the fixed sea zones).
|
||||
state.seaZones ?? [],
|
||||
// (v0.44.0, #66) the current contested regions (war frontlines) — emitted
|
||||
// verbatim (even when empty) so the model knows which regions are
|
||||
// frontlines and can resolve them.
|
||||
state.contested ?? [],
|
||||
);
|
||||
const nudge =
|
||||
'Your previous output was invalid. Respond with ONLY the JSON object described above for this batch — no prose, no fences.';
|
||||
|
||||
Reference in New Issue
Block a user