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:
@@ -12,7 +12,7 @@
|
||||
// by the disasters feature (its payload is always empty).
|
||||
//
|
||||
// 2. REGISTRY CONTRACT — the ABILITIES registry is the canonical ordered
|
||||
// list of the 11 abilities; each ability's default() equals the canonical
|
||||
// list of the 12 abilities; each ability's default() equals the canonical
|
||||
// TurnResult default for its field, is a FRESH value (never a shared
|
||||
// reference), its default is not "meaningful" while a populated value is,
|
||||
// and the registry composes (a hypothetical extra ability can be appended
|
||||
@@ -76,6 +76,12 @@ import {
|
||||
SEA_ZONE_IDS,
|
||||
isSeaZoneId,
|
||||
seaZoneName,
|
||||
// contested (v0.44.0, #66)
|
||||
validateContestedOps,
|
||||
applyContested,
|
||||
CONTESTED_FILL,
|
||||
CONTESTED_STROKE,
|
||||
contestedTooltipLabel,
|
||||
} from '../engine';
|
||||
import { createPresetStore } from '../presets';
|
||||
import { ABILITIES } from './index';
|
||||
@@ -92,7 +98,7 @@ import type {
|
||||
SeaZoneClaim,
|
||||
TradeAgreement,
|
||||
} from '../../shared/types';
|
||||
import type { MarkerInfo, TurnResult, WarInfo } from '../../shared/protocol';
|
||||
import type { ContestedRegion, MarkerInfo, TurnResult, WarInfo } from '../../shared/protocol';
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const goldensDir = path.join(here, 'goldens');
|
||||
@@ -165,6 +171,8 @@ const state: GameState = {
|
||||
industry: [],
|
||||
// (v0.43.0, #65) no sea-zone control on a fresh state.
|
||||
seaZones: [],
|
||||
// (v0.44.0, #66) no contested regions on a fresh state.
|
||||
contested: [],
|
||||
};
|
||||
|
||||
// --- buildUserPrompt arguments: every optional branch exercised. ---
|
||||
@@ -202,6 +210,10 @@ const nationResources: Record<string, RegionResources> = {
|
||||
const industryProjects: IndustryProject[] = [{ regionKey: 'a-r1', type: 'industry' }];
|
||||
const nuclear: Record<string, boolean> = { a1: true, b2: false, c3: false };
|
||||
const seaZones: SeaZoneClaim[] = [];
|
||||
// (v0.44.0, #66) a non-empty contested sample so the golden's user payload
|
||||
// includes the `contested` key (region key + party NAMES, resolved from the
|
||||
// state's nations). `b-r1` is owned by b2; the parties are a1 + b2.
|
||||
const contested: ContestedRegion[] = [{ regionKey: 'b-r1', parties: ['a1', 'b2'] }];
|
||||
|
||||
/**
|
||||
* Assert the current prompt output is byte-identical to the committed golden —
|
||||
@@ -241,6 +253,7 @@ function fullDefaultTurn(): TurnResult {
|
||||
disasters: { events: [] },
|
||||
migration: { ops: [] },
|
||||
sea: { control: [] },
|
||||
contested: { open: [], resolve: [] },
|
||||
markers: { add: [], remove: [] },
|
||||
};
|
||||
}
|
||||
@@ -270,11 +283,13 @@ function meaningfulFor(field: AbilityField): TurnResult[AbilityField] {
|
||||
return { ops: [{ kind: 'migration', from: 'a1', to: 'b2', scale: 'small' }] };
|
||||
case 'sea':
|
||||
return { control: [{ zone: 'pacific', by: 'a1' }] };
|
||||
case 'contested':
|
||||
return { open: [{ regionKey: 'b-r1', parties: ['a1', 'b2'] }], resolve: [] };
|
||||
}
|
||||
}
|
||||
|
||||
describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
it('is the canonical 11-ability order (takeovers → sea)', () => {
|
||||
it('is the canonical 12-ability order (takeovers → contested)', () => {
|
||||
expect(ABILITIES.map((a) => a.field)).toEqual([
|
||||
'takeovers',
|
||||
'wars',
|
||||
@@ -287,13 +302,14 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
'disasters',
|
||||
'migration',
|
||||
'sea',
|
||||
'contested',
|
||||
]);
|
||||
});
|
||||
|
||||
it('lists 11 distinct fields, each a key of TurnResult', () => {
|
||||
it('lists 12 distinct fields, each a key of TurnResult', () => {
|
||||
const fields = ABILITIES.map((a) => a.field);
|
||||
expect(fields).toHaveLength(11);
|
||||
expect(new Set(fields).size).toBe(11);
|
||||
expect(fields).toHaveLength(12);
|
||||
expect(new Set(fields).size).toBe(12);
|
||||
const all = fullDefaultTurn();
|
||||
for (const f of fields) expect(f in all).toBe(true);
|
||||
});
|
||||
@@ -324,6 +340,7 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
industryProjects,
|
||||
nuclear,
|
||||
seaZones,
|
||||
contested,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -353,13 +370,13 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
it('declares exactly the durable GameState fields the db migration must default', () => {
|
||||
const owned: string[] = [];
|
||||
for (const a of ABILITIES) if (a.stateFields) owned.push(...a.stateFields);
|
||||
expect([...owned].sort()).toEqual(['embargoes', 'industry', 'seaZones', 'trades', 'wars']);
|
||||
expect([...owned].sort()).toEqual(['contested', 'embargoes', 'industry', 'seaZones', 'trades', 'wars']);
|
||||
});
|
||||
|
||||
it('composes: a hypothetical extra ability can be appended and every uniform loop still runs', () => {
|
||||
// The registry is a plain ordered array the engine iterates uniformly. A
|
||||
// structural (field-name-agnostic) loop over the abilities — schema line,
|
||||
// constraint bullet — must keep working when an 11th ability is appended.
|
||||
// constraint bullet — must keep working when a 12th ability is appended.
|
||||
// This documents the extension point without widening the closed
|
||||
// AbilityField union.
|
||||
interface Loopable {
|
||||
@@ -378,9 +395,9 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
'custom',
|
||||
]);
|
||||
// every uniform loop runs over all N abilities, the appended one included:
|
||||
expect(extended.map((a) => a.schemaLine())).toHaveLength(12);
|
||||
expect(extended.map((a) => a.constraint())).toHaveLength(12);
|
||||
expect(extended[11].schemaLine()).toBe(' "custom": [],');
|
||||
expect(extended.map((a) => a.schemaLine())).toHaveLength(13);
|
||||
expect(extended.map((a) => a.constraint())).toHaveLength(13);
|
||||
expect(extended[12].schemaLine()).toBe(' "custom": [],');
|
||||
});
|
||||
|
||||
it('the engine still re-exports every name its callers import from it', () => {
|
||||
@@ -439,6 +456,12 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
SEA_ZONE_IDS,
|
||||
isSeaZoneId,
|
||||
seaZoneName,
|
||||
// contested (v0.44.0, #66)
|
||||
validateContestedOps,
|
||||
applyContested,
|
||||
CONTESTED_FILL,
|
||||
CONTESTED_STROKE,
|
||||
contestedTooltipLabel,
|
||||
};
|
||||
for (const [name, value] of Object.entries(reexported)) {
|
||||
expect(value, `engine should re-export ${name}`).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user