feat: #65 sea control — 11th ability (v0.43.0)
CI / build + unit + e2e (pull_request) Successful in 2m33s
CI / build + unit + e2e (pull_request) Successful in 2m33s
Adds the sea ability: a fixed set of 12 named sea zones with sole-control semantics, contest-by-higher-military, and a deterministic per-turn +1 military/+1 economy bonus for each claimed zone. Durable (owns GameState.seaZones), additive migration, stub-inert (e2e unaffected). Fixes #65 Design note: Gitea #65 sketched a free-form 'naval' layer; the implemented approved design is this richer 'sea' layer with a fixed zone set.
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 10 abilities; each ability's default() equals the canonical
|
||||
// list of the 11 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
|
||||
@@ -68,6 +68,14 @@ import {
|
||||
validateNuclearOps,
|
||||
validateTakeovers,
|
||||
validateWarOps,
|
||||
validateSeaOps,
|
||||
applySeaControl,
|
||||
applySeaZoneBonuses,
|
||||
SEA_ZONE_BONUS,
|
||||
SEA_ZONES,
|
||||
SEA_ZONE_IDS,
|
||||
isSeaZoneId,
|
||||
seaZoneName,
|
||||
} from '../engine';
|
||||
import { createPresetStore } from '../presets';
|
||||
import { ABILITIES } from './index';
|
||||
@@ -81,6 +89,7 @@ import type {
|
||||
RegionResources,
|
||||
RelationStatus,
|
||||
RoundItem,
|
||||
SeaZoneClaim,
|
||||
TradeAgreement,
|
||||
} from '../../shared/types';
|
||||
import type { MarkerInfo, TurnResult, WarInfo } from '../../shared/protocol';
|
||||
@@ -154,6 +163,8 @@ const state: GameState = {
|
||||
trades: [],
|
||||
embargoes: [],
|
||||
industry: [],
|
||||
// (v0.43.0, #65) no sea-zone control on a fresh state.
|
||||
seaZones: [],
|
||||
};
|
||||
|
||||
// --- buildUserPrompt arguments: every optional branch exercised. ---
|
||||
@@ -190,6 +201,7 @@ 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[] = [];
|
||||
|
||||
/**
|
||||
* Assert the current prompt output is byte-identical to the committed golden —
|
||||
@@ -228,6 +240,7 @@ function fullDefaultTurn(): TurnResult {
|
||||
ideology: { changes: [] },
|
||||
disasters: { events: [] },
|
||||
migration: { ops: [] },
|
||||
sea: { control: [] },
|
||||
markers: { add: [], remove: [] },
|
||||
};
|
||||
}
|
||||
@@ -255,11 +268,13 @@ function meaningfulFor(field: AbilityField): TurnResult[AbilityField] {
|
||||
return { events: [{ type: 'earthquake', target: 'a1', severity: 'minor' }] };
|
||||
case 'migration':
|
||||
return { ops: [{ kind: 'migration', from: 'a1', to: 'b2', scale: 'small' }] };
|
||||
case 'sea':
|
||||
return { control: [{ zone: 'pacific', by: 'a1' }] };
|
||||
}
|
||||
}
|
||||
|
||||
describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
it('is the canonical 10-ability order (takeovers → migration)', () => {
|
||||
it('is the canonical 11-ability order (takeovers → sea)', () => {
|
||||
expect(ABILITIES.map((a) => a.field)).toEqual([
|
||||
'takeovers',
|
||||
'wars',
|
||||
@@ -271,13 +286,14 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
'ideology',
|
||||
'disasters',
|
||||
'migration',
|
||||
'sea',
|
||||
]);
|
||||
});
|
||||
|
||||
it('lists 10 distinct fields, each a key of TurnResult', () => {
|
||||
it('lists 11 distinct fields, each a key of TurnResult', () => {
|
||||
const fields = ABILITIES.map((a) => a.field);
|
||||
expect(fields).toHaveLength(10);
|
||||
expect(new Set(fields).size).toBe(10);
|
||||
expect(fields).toHaveLength(11);
|
||||
expect(new Set(fields).size).toBe(11);
|
||||
const all = fullDefaultTurn();
|
||||
for (const f of fields) expect(f in all).toBe(true);
|
||||
});
|
||||
@@ -307,6 +323,7 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
nationResources,
|
||||
industryProjects,
|
||||
nuclear,
|
||||
seaZones,
|
||||
),
|
||||
);
|
||||
});
|
||||
@@ -336,7 +353,7 @@ 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', 'trades', 'wars']);
|
||||
expect([...owned].sort()).toEqual(['embargoes', 'industry', 'seaZones', 'trades', 'wars']);
|
||||
});
|
||||
|
||||
it('composes: a hypothetical extra ability can be appended and every uniform loop still runs', () => {
|
||||
@@ -361,9 +378,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(11);
|
||||
expect(extended.map((a) => a.constraint())).toHaveLength(11);
|
||||
expect(extended[10].schemaLine()).toBe(' "custom": [],');
|
||||
expect(extended.map((a) => a.schemaLine())).toHaveLength(12);
|
||||
expect(extended.map((a) => a.constraint())).toHaveLength(12);
|
||||
expect(extended[11].schemaLine()).toBe(' "custom": [],');
|
||||
});
|
||||
|
||||
it('the engine still re-exports every name its callers import from it', () => {
|
||||
@@ -413,6 +430,15 @@ describe('ABILITIES registry (v0.40.0, #67)', () => {
|
||||
MIGRATION_SOURCE_EFFECTS,
|
||||
MIGRATION_DESTINATION_EFFECTS,
|
||||
LARGE_INFLUX_STABILITY_DIP,
|
||||
// sea (v0.43.0, #65)
|
||||
validateSeaOps,
|
||||
applySeaControl,
|
||||
applySeaZoneBonuses,
|
||||
SEA_ZONE_BONUS,
|
||||
SEA_ZONES,
|
||||
SEA_ZONE_IDS,
|
||||
isSeaZoneId,
|
||||
seaZoneName,
|
||||
};
|
||||
for (const [name, value] of Object.entries(reexported)) {
|
||||
expect(value, `engine should re-export ${name}`).toBeDefined();
|
||||
|
||||
Reference in New Issue
Block a user