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:
@@ -0,0 +1,494 @@
|
||||
// Unit tests for the SEA (naval / sea control) ability (v0.43.0, #65) — the
|
||||
// modular, DURABLE LLM-ability that gives nations sole control over a fixed
|
||||
// set of 12 named sea zones: the zone is upserted in `GameState.seaZones`
|
||||
// (replace in place, contest by higher current military), and every committed
|
||||
// turn the controller earns a deterministic +1 military / +1 economy bonus
|
||||
// (clamped, mirrored to its regions, re-derived). The layer never touches
|
||||
// `territory` / `unowned` — it is fully parallel to but independent of land.
|
||||
// This is the ability's own test file (the registry's cross-cutting
|
||||
// guarantees — order, distinct fields, the system/user goldens — are covered
|
||||
// by ./registry.test.ts). The stub never proposes a sea op, so the apply path
|
||||
// is exercised here, not by the e2e suite.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
SEA_ZONES,
|
||||
SEA_ZONE_IDS,
|
||||
applySeaControl,
|
||||
applySeaZoneBonuses,
|
||||
isSeaZoneId,
|
||||
sea,
|
||||
seaZoneName,
|
||||
validateSeaOps,
|
||||
} from './sea';
|
||||
|
||||
import type {
|
||||
GameState,
|
||||
Nation,
|
||||
NationStats,
|
||||
RegionStats,
|
||||
SeaZoneClaim,
|
||||
} from '../../shared/types';
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fixture helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/** Build a NationStats from the four canonical indices (military, economy, stability, population). */
|
||||
function NS(military: number, economy: number, stability: number, population: number): NationStats {
|
||||
return { military, economy, stability, population };
|
||||
}
|
||||
|
||||
/** Build a RegionStats from the four canonical indices. */
|
||||
function RS(military: number, economy: number, stability: number, population: number): RegionStats {
|
||||
return { military, economy, stability, population };
|
||||
}
|
||||
|
||||
/** A minimal nation: the given id/name/stats/region-key territory. */
|
||||
function mkNation(
|
||||
id: string,
|
||||
name: string,
|
||||
stats: NationStats,
|
||||
territory: string[],
|
||||
): Nation {
|
||||
return { id, name, type: 'major', stats, territory, relations: {} };
|
||||
}
|
||||
|
||||
/**
|
||||
* A minimal GameState carrying the given nations (keyed by id), per-region
|
||||
* stats, and an optional existing sea-zone list. Every other required field
|
||||
* is at its fresh-game baseline.
|
||||
*/
|
||||
function mkState(
|
||||
nations: Record<string, Nation>,
|
||||
regionStats: Record<string, RegionStats>,
|
||||
seaZones: SeaZoneClaim[] = [],
|
||||
): GameState {
|
||||
return {
|
||||
id: 'g1',
|
||||
presetId: 'test',
|
||||
presetTitle: 'Test',
|
||||
playerNationId: Object.keys(nations)[0] ?? 'a1',
|
||||
turn: 1,
|
||||
date: '1939-09-01',
|
||||
nations,
|
||||
events: [],
|
||||
pending: [],
|
||||
version: 1,
|
||||
unowned: [],
|
||||
pendingNationOps: [],
|
||||
markers: [],
|
||||
regionStats,
|
||||
wars: [],
|
||||
trades: [],
|
||||
embargoes: [],
|
||||
industry: [],
|
||||
seaZones,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// SEA_ZONES / isSeaZoneId / seaZoneName
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('SEA_ZONES / SEA_ZONE_IDS / isSeaZoneId / seaZoneName', () => {
|
||||
it('exposes exactly the 12 fixed sea zones', () => {
|
||||
expect(SEA_ZONES).toHaveLength(12);
|
||||
const ids = SEA_ZONES.map((z) => z.id);
|
||||
expect(ids).toEqual([
|
||||
'north-atlantic',
|
||||
'south-atlantic',
|
||||
'pacific',
|
||||
'indian',
|
||||
'mediterranean',
|
||||
'north-sea',
|
||||
'baltic',
|
||||
'arctic',
|
||||
'black-sea',
|
||||
'gulf-mexico',
|
||||
'red-sea',
|
||||
'sargasso',
|
||||
]);
|
||||
});
|
||||
|
||||
it('SEA_ZONE_IDS is a ReadonlySet of all 12 ids', () => {
|
||||
expect(SEA_ZONE_IDS).toBeInstanceOf(Set);
|
||||
expect(SEA_ZONE_IDS.size).toBe(12);
|
||||
for (const z of SEA_ZONES) expect(SEA_ZONE_IDS.has(z.id)).toBe(true);
|
||||
});
|
||||
|
||||
it('isSeaZoneId accepts each known id and rejects unknown / non-strings', () => {
|
||||
for (const z of SEA_ZONES) expect(isSeaZoneId(z.id)).toBe(true);
|
||||
expect(isSeaZoneId('atlantic-ocean')).toBe(false);
|
||||
expect(isSeaZoneId('')).toBe(false);
|
||||
});
|
||||
|
||||
it('seaZoneName returns the human-readable name; an unknown id is returned raw', () => {
|
||||
expect(seaZoneName('pacific')).toBe('Pacific Ocean');
|
||||
expect(seaZoneName('mediterranean')).toBe('Mediterranean Sea');
|
||||
expect(seaZoneName('nope')).toBe('nope');
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// validateSeaOps
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('validateSeaOps', () => {
|
||||
it('accepts the { control: [...] } object shape and a bare array', () => {
|
||||
const op = { zone: 'pacific', by: 'a1' };
|
||||
expect(validateSeaOps({ control: [op] })).toEqual({ control: [op] });
|
||||
expect(validateSeaOps([op])).toEqual({ control: [op] });
|
||||
});
|
||||
|
||||
it('drops an op with an unknown / missing zone', () => {
|
||||
expect(validateSeaOps([{ zone: 'nope', by: 'a1' }])).toEqual({ control: [] });
|
||||
expect(validateSeaOps([{ by: 'a1' }])).toEqual({ control: [] });
|
||||
expect(validateSeaOps([{ zone: '', by: 'a1' }])).toEqual({ control: [] });
|
||||
});
|
||||
|
||||
it('drops an op with an empty / missing `by` (the controller)', () => {
|
||||
expect(validateSeaOps([{ zone: 'pacific', by: '' }])).toEqual({ control: [] });
|
||||
expect(validateSeaOps([{ zone: 'pacific' }])).toEqual({ control: [] });
|
||||
});
|
||||
|
||||
it('drops a non-object entry', () => {
|
||||
expect(validateSeaOps([42, 'nope', null, { zone: 'pacific', by: 'a1' }])).toEqual({
|
||||
control: [{ zone: 'pacific', by: 'a1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('a missing / malformed top-level value yields { control: [] }', () => {
|
||||
expect(validateSeaOps(undefined)).toEqual({ control: [] });
|
||||
expect(validateSeaOps(null)).toEqual({ control: [] });
|
||||
expect(validateSeaOps('nope')).toEqual({ control: [] });
|
||||
expect(validateSeaOps({ control: 'nope' })).toEqual({ control: [] });
|
||||
});
|
||||
|
||||
it('caps `zone` at 100 and `by` at 200 chars', () => {
|
||||
const longBy = 'f'.repeat(500);
|
||||
const out = validateSeaOps([{ zone: 'pacific', by: longBy }]);
|
||||
expect(out.control).toHaveLength(1);
|
||||
expect(out.control[0].by).toHaveLength(200);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// applySeaControl
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('applySeaControl', () => {
|
||||
it('a single op sets a new zone claim', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(80, 70, 60, 50) },
|
||||
);
|
||||
applySeaControl(state, [{ zone: 'pacific', by: 'a1' }]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
});
|
||||
|
||||
it('replaces an existing claim in place (position preserved)', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']),
|
||||
b2: mkNation('b2', 'Bravo', NS(90, 40, 20, 30), ['b-r1']),
|
||||
},
|
||||
{ 'a-r1': RS(80, 70, 60, 50), 'b-r1': RS(90, 40, 20, 30) },
|
||||
[
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'indian', by: 'b2' },
|
||||
],
|
||||
);
|
||||
// b2 now claims pacific — the pacific claim moves to b2 but stays at index 0
|
||||
applySeaControl(state, [{ zone: 'pacific', by: 'b2' }]);
|
||||
expect(state.seaZones).toEqual([
|
||||
{ zone: 'pacific', by: 'b2' },
|
||||
{ zone: 'indian', by: 'b2' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('appends a new zone after existing claims', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(80, 70, 60, 50) },
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
applySeaControl(state, [{ zone: 'arctic', by: 'a1' }]);
|
||||
expect(state.seaZones).toEqual([
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'arctic', by: 'a1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('a contest: the higher current military wins', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']),
|
||||
b2: mkNation('b2', 'Bravo', NS(90, 40, 20, 30), ['b-r1']),
|
||||
},
|
||||
{ 'a-r1': RS(80, 70, 60, 50), 'b-r1': RS(90, 40, 20, 30) },
|
||||
);
|
||||
// b2 has higher military (90 > 80) → b2 wins
|
||||
applySeaControl(state, [
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'pacific', by: 'b2' },
|
||||
]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'b2' }]);
|
||||
});
|
||||
|
||||
it('a contest tie: the earlier-listed op wins', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']),
|
||||
b2: mkNation('b2', 'Bravo', NS(50, 50, 50, 50), ['b-r1']),
|
||||
},
|
||||
{ 'a-r1': RS(50, 50, 50, 50), 'b-r1': RS(50, 50, 50, 50) },
|
||||
);
|
||||
// equal military (50 == 50) → a1 wins (listed first)
|
||||
applySeaControl(state, [
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'pacific', by: 'b2' },
|
||||
]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
});
|
||||
|
||||
it('two ops for the same nation on the same zone: a single claim (no contest)', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(80, 70, 60, 50) },
|
||||
);
|
||||
applySeaControl(state, [
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
});
|
||||
|
||||
it('resolves `by` by unique name (not just id)', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(80, 70, 60, 50) },
|
||||
);
|
||||
applySeaControl(state, [{ zone: 'pacific', by: 'Alpha' }]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
});
|
||||
|
||||
it('an unresolvable `by` is dropped; if all ops on a zone drop, the zone is untouched', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(80, 70, 60, 50) },
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
// ghost does not exist → dropped; pacific stays as a1
|
||||
applySeaControl(state, [{ zone: 'pacific', by: 'ghost' }]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
|
||||
// a NEW zone with only an unresolvable op → zone not added
|
||||
applySeaControl(state, [{ zone: 'arctic', by: 'ghost' }]);
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
});
|
||||
|
||||
it('an ambiguous name (two nations share it) drops the op', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a1: mkNation('a1', 'Twin', NS(80, 70, 60, 50), ['a-r1']),
|
||||
a2: mkNation('a2', 'Twin', NS(20, 20, 20, 20), ['a-r2']),
|
||||
},
|
||||
{ 'a-r1': RS(80, 70, 60, 50), 'a-r2': RS(20, 20, 20, 20) },
|
||||
);
|
||||
applySeaControl(state, [{ zone: 'pacific', by: 'Twin' }]);
|
||||
// ambiguous → dropped
|
||||
expect(state.seaZones).toEqual([]);
|
||||
});
|
||||
|
||||
it('never touches `territory` or `unowned`', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a1: mkNation('a1', 'Alpha', NS(80, 70, 60, 50), ['a-r1']),
|
||||
b2: mkNation('b2', 'Bravo', NS(90, 40, 20, 30), ['b-r1']),
|
||||
},
|
||||
{ 'a-r1': RS(80, 70, 60, 50), 'b-r1': RS(90, 40, 20, 30) },
|
||||
);
|
||||
applySeaControl(state, [
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'pacific', by: 'b2' },
|
||||
]);
|
||||
expect(state.nations.a1.territory).toEqual(['a-r1']);
|
||||
expect(state.nations.b2.territory).toEqual(['b-r1']);
|
||||
expect(state.unowned).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// applySeaZoneBonuses
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('applySeaZoneBonuses', () => {
|
||||
it('a controller earns +1 military and +1 economy (clamped, mirrored, re-derived)', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(50, 50, 50, 50) },
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
// +1 to both military and economy, mirrored to the region, re-derived
|
||||
// (single region → re-derive gives back the region's value)
|
||||
expect(state.nations.a1.stats).toEqual(NS(51, 51, 50, 50));
|
||||
expect(state.regionStats['a-r1']).toEqual(RS(51, 51, 50, 50));
|
||||
});
|
||||
|
||||
it('clamps to 0 and 100', () => {
|
||||
// military at 99 → 100; economy at 1 → 2
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(99, 1, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(99, 1, 50, 50) },
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
expect(state.nations.a1.stats.military).toBe(100);
|
||||
expect(state.nations.a1.stats.economy).toBe(2);
|
||||
});
|
||||
|
||||
it('a nation controlling several zones stacks the bonus', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(50, 50, 50, 50) },
|
||||
[
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'indian', by: 'a1' },
|
||||
],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
// +2 to both (one per zone)
|
||||
expect(state.nations.a1.stats).toEqual(NS(52, 52, 50, 50));
|
||||
});
|
||||
|
||||
it('a 0-region nation keeps its directly-mutated stats (no re-derive)', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), []) },
|
||||
{},
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
// direct mutation, no re-derive (no regions)
|
||||
expect(state.nations.a1.stats).toEqual(NS(51, 51, 50, 50));
|
||||
});
|
||||
|
||||
it('a stale controller (nation no longer in `nations`) is skipped; the claim is kept', () => {
|
||||
// Build a state where the claim references a nation id that is not in `nations`
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(50, 50, 50, 50) },
|
||||
[{ zone: 'pacific', by: 'ghost' }],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
// ghost is not in nations → skipped; a1 is unaffected
|
||||
expect(state.nations.a1.stats).toEqual(NS(50, 50, 50, 50));
|
||||
// the claim is kept (not removed)
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'ghost' }]);
|
||||
});
|
||||
|
||||
it('an empty seaZones list is a no-op', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(50, 50, 50, 50) },
|
||||
[],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
expect(state.nations.a1.stats).toEqual(NS(50, 50, 50, 50));
|
||||
});
|
||||
|
||||
it('never touches `territory` or `unowned`', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(50, 50, 50, 50) },
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
applySeaZoneBonuses(state);
|
||||
expect(state.nations.a1.territory).toEqual(['a-r1']);
|
||||
expect(state.unowned).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The Ability object contract
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('sea ability', () => {
|
||||
it('owns the "sea" field; default() is a fresh empty { control: [] }', () => {
|
||||
expect(sea.field).toBe('sea');
|
||||
expect(sea.default()).toEqual({ control: [] });
|
||||
expect(sea.default()).not.toBe(sea.default());
|
||||
});
|
||||
|
||||
it('default() is not meaningful; a populated value is', () => {
|
||||
expect(sea.isMeaningful(sea.default())).toBe(false);
|
||||
expect(sea.isMeaningful({ control: [{ zone: 'pacific', by: 'a1' }] })).toBe(true);
|
||||
});
|
||||
|
||||
it('validate() wraps the validator into the SeaOps shape', () => {
|
||||
expect(sea.validate([{ zone: 'pacific', by: 'a1' }])).toEqual({
|
||||
control: [{ zone: 'pacific', by: 'a1' }],
|
||||
});
|
||||
expect(sea.validate(undefined)).toEqual({ control: [] });
|
||||
});
|
||||
|
||||
it('the stub value equals default() (the stub never proposes a sea op)', () => {
|
||||
expect(sea.stub()).toEqual({ control: [] });
|
||||
});
|
||||
|
||||
it('stateFields is [seaZones] (the durable GameState field)', () => {
|
||||
expect(sea.stateFields).toEqual(['seaZones']);
|
||||
});
|
||||
|
||||
it('payload() emits seaZones (even when empty / undefined)', () => {
|
||||
expect(sea.payload({ seaZones: [] } as never)).toEqual({ seaZones: [] });
|
||||
expect(sea.payload({} as never)).toEqual({ seaZones: [] });
|
||||
expect(sea.payload({ seaZones: [{ zone: 'pacific', by: 'a1' }] } as never)).toEqual({
|
||||
seaZones: [{ zone: 'pacific', by: 'a1' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('apply() runs the ops then the per-turn bonus (no throw on a bad ref)', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']),
|
||||
b2: mkNation('b2', 'Bravo', NS(80, 40, 20, 30), ['b-r1']),
|
||||
},
|
||||
{ 'a-r1': RS(50, 50, 50, 50), 'b-r1': RS(80, 40, 20, 30) },
|
||||
);
|
||||
const ctx = { previous: state, actingTurn: state.turn };
|
||||
sea.apply(
|
||||
state,
|
||||
{
|
||||
control: [
|
||||
{ zone: 'pacific', by: 'a1' },
|
||||
{ zone: 'pacific', by: 'b2' },
|
||||
// an unresolvable ref — must be dropped, not throw
|
||||
{ zone: 'arctic', by: 'ghost' },
|
||||
],
|
||||
},
|
||||
ctx,
|
||||
);
|
||||
// contest: b2 (military 80) beats a1 (military 50) → b2 controls pacific
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'b2' }]);
|
||||
// b2 gets the +1/+1 bonus (it controls pacific)
|
||||
expect(state.nations.b2.stats).toEqual(NS(81, 41, 20, 30));
|
||||
// a1 is unaffected
|
||||
expect(state.nations.a1.stats).toEqual(NS(50, 50, 50, 50));
|
||||
});
|
||||
|
||||
it('apply() with an empty control still applies the per-turn bonus to existing claims', () => {
|
||||
const state = mkState(
|
||||
{ a1: mkNation('a1', 'Alpha', NS(50, 50, 50, 50), ['a-r1']) },
|
||||
{ 'a-r1': RS(50, 50, 50, 50) },
|
||||
[{ zone: 'pacific', by: 'a1' }],
|
||||
);
|
||||
const ctx = { previous: state, actingTurn: state.turn };
|
||||
sea.apply(state, { control: [] }, ctx);
|
||||
// no new claims, but the existing claim's controller earns the bonus
|
||||
expect(state.seaZones).toEqual([{ zone: 'pacific', by: 'a1' }]);
|
||||
expect(state.nations.a1.stats).toEqual(NS(51, 51, 50, 50));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user