feat(abilities): add disasters & pandemics lever (v0.41.0, #63)
CI / build + unit + e2e (pull_request) Successful in 1m55s
CI / build + unit + e2e (pull_request) Successful in 1m55s
Add the 9th modular LLM ability: a turn-transient `TurnResult.disasters`
lever letting the LLM declare natural/man-made disasters (earthquake /
flood / volcanic / wildfire / famine / plague / epidemic) on a nation or
a region, applying a DETERMINISTIC (type, severity) stat dip (no hidden
randomness). Like the hybrid layer it owns no GameState field, needs no
db.ts change, and leaves no durable list — an ongoing plague/epidemic is
simply re-declared each turn it persists.
- shared/protocol.ts: DisasterType / DisasterSeverity / DisasterEvent /
DisastersOps, and `TurnResult.disasters` (between `ideology` and
`markers`; required, default `{ events: [] }`).
- server/abilities/disasters.ts: the ability module (validate / apply /
schemaLine / constraint / isMeaningful / inert stub / empty payload; no
stateFields). `DISASTER_EFFECTS` is the deterministic (type, severity)
dip table (population never touched). Target resolution mirrors hybrid
(a region key hits its SOLE owner; 0/2+ owners dropped) but has NO
`from` field, so there is no self-target drop. Dips clamp to 0-100,
mirror to the target's regions (regionStats), and re-derive the target
nation's stats (same 0-region / no-entry guards).
- Registered last in ABILITIES (after ideology). System-prompt goldens
regenerated (one schema line + one constraint bullet each); the
user.txt golden stays byte-identical (payload() is {}).
- New server/abilities/disasters.test.ts (33 tests); registry.test.ts
updated to the 9-ability contract.
- Secondary cleanups from the v0.40 refactor: AGENTS.md (abilities/ in
the Layout tree + a "how to add a new ability" bullet + a disasters
bullet), delete the orphaned validateTakeovers JSDoc, fold both inline
`as unknown` casts into the top-level ./abilities import, and de-dup
`str` (now imported from ./abilities/shared).
Version bumped 0.40.0 -> 0.41.0.
This commit is contained in:
@@ -0,0 +1,445 @@
|
||||
// Unit tests for the DISASTERS & PANDEMICS ability (v0.41.0, #63) — the
|
||||
// modular, turn-transient LLM-ability that applies a deterministic
|
||||
// (type, severity) stat dip to a nation or a single region. 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 disaster, so the apply path
|
||||
// is exercised here, not by the e2e suite.
|
||||
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { AbilityCtx } from './types';
|
||||
import {
|
||||
DISASTER_EFFECTS,
|
||||
DISASTER_SEVERITIES,
|
||||
DISASTER_TYPES,
|
||||
applyDisasterEvent,
|
||||
disasters,
|
||||
isDisasterSeverity,
|
||||
isDisasterType,
|
||||
validateDisasterEvents,
|
||||
} from './disasters';
|
||||
|
||||
import type { DisasterEvent, DisastersOps } from '../../shared/protocol';
|
||||
import type { GameState, Nation, NationStats, RegionStats } 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, neutral relations. */
|
||||
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) and per-region
|
||||
* stats. Every other required field is at its fresh-game baseline, so a test
|
||||
* can focus purely on the disaster dip.
|
||||
*/
|
||||
function mkState(
|
||||
nations: Record<string, Nation>,
|
||||
regionStats: Record<string, RegionStats>,
|
||||
): GameState {
|
||||
return {
|
||||
id: 'g1',
|
||||
presetId: 'test',
|
||||
presetTitle: 'Test',
|
||||
playerNationId: Object.keys(nations)[0] ?? 'uk',
|
||||
turn: 1,
|
||||
date: '1939-09-01',
|
||||
nations,
|
||||
events: [],
|
||||
pending: [],
|
||||
version: 1,
|
||||
unowned: [],
|
||||
pendingNationOps: [],
|
||||
markers: [],
|
||||
regionStats,
|
||||
wars: [],
|
||||
trades: [],
|
||||
embargoes: [],
|
||||
industry: [],
|
||||
};
|
||||
}
|
||||
|
||||
/** A minimal AbilityCtx for the ability's `apply` (no manifests loaded). */
|
||||
function mkCtx(state: GameState, validRegionKeys?: ReadonlySet<string>): AbilityCtx {
|
||||
return {
|
||||
previous: state,
|
||||
actingTurn: state.turn,
|
||||
...(validRegionKeys ? { validRegionKeys } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Type / severity constants + predicates
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('DISASTER_TYPES / DISASTER_SEVERITIES', () => {
|
||||
it('exposes exactly the seven types and three severities', () => {
|
||||
expect([...DISASTER_TYPES]).toEqual([
|
||||
'earthquake',
|
||||
'flood',
|
||||
'volcanic',
|
||||
'wildfire',
|
||||
'famine',
|
||||
'plague',
|
||||
'epidemic',
|
||||
]);
|
||||
expect([...DISASTER_SEVERITIES]).toEqual(['minor', 'moderate', 'severe']);
|
||||
});
|
||||
|
||||
it('every (type, severity) pair dips at least one stat, never population', () => {
|
||||
for (const t of DISASTER_TYPES) {
|
||||
for (const s of DISASTER_SEVERITIES) {
|
||||
const dips = DISASTER_EFFECTS[t][s];
|
||||
expect(dips).toBeDefined();
|
||||
const keys = Object.keys(dips);
|
||||
expect(keys.length).toBeGreaterThan(0);
|
||||
expect(keys).not.toContain('population');
|
||||
for (const k of keys) expect(dips[k as keyof typeof dips]).toBeLessThan(0);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('isDisasterType / isDisasterSeverity', () => {
|
||||
it('accepts each known type and rejects unknown / non-strings', () => {
|
||||
for (const t of DISASTER_TYPES) expect(isDisasterType(t)).toBe(true);
|
||||
expect(isDisasterType('tsunami')).toBe(false);
|
||||
expect(isDisasterType('Earthquake')).toBe(false); // case-sensitive
|
||||
expect(isDisasterType('')).toBe(false);
|
||||
expect(isDisasterType(42)).toBe(false);
|
||||
expect(isDisasterType(null)).toBe(false);
|
||||
expect(isDisasterType(undefined)).toBe(false);
|
||||
});
|
||||
|
||||
it('accepts each known severity and rejects unknown / non-strings', () => {
|
||||
for (const s of DISASTER_SEVERITIES) expect(isDisasterSeverity(s)).toBe(true);
|
||||
expect(isDisasterSeverity('extreme')).toBe(false);
|
||||
expect(isDisasterSeverity('Minor')).toBe(false); // case-sensitive
|
||||
expect(isDisasterSeverity('')).toBe(false);
|
||||
expect(isDisasterSeverity(3)).toBe(false);
|
||||
expect(isDisasterSeverity(undefined)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// validateDisasterEvents
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('validateDisasterEvents', () => {
|
||||
const valid: DisasterEvent = { type: 'earthquake', target: 'uk', severity: 'severe' };
|
||||
|
||||
it('accepts the { events: [...] } shape', () => {
|
||||
expect(validateDisasterEvents({ events: [valid] })).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('accepts a bare array (lenient about the wrapper)', () => {
|
||||
expect(validateDisasterEvents([valid])).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('drops an event with an unknown type, keeping the valid one', () => {
|
||||
const out = validateDisasterEvents({
|
||||
events: [
|
||||
{ type: 'tsunami', target: 'uk', severity: 'minor' },
|
||||
valid,
|
||||
],
|
||||
});
|
||||
expect(out).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('drops an event with a missing type', () => {
|
||||
const out = validateDisasterEvents({
|
||||
events: [{ target: 'uk', severity: 'minor' }, valid],
|
||||
});
|
||||
expect(out).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('drops an event with an unknown severity, keeping the valid one', () => {
|
||||
const out = validateDisasterEvents({
|
||||
events: [
|
||||
{ type: 'plague', target: 'uk', severity: 'extreme' },
|
||||
valid,
|
||||
],
|
||||
});
|
||||
expect(out).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('drops an event with a missing severity', () => {
|
||||
const out = validateDisasterEvents({
|
||||
events: [{ type: 'plague', target: 'uk' }, valid],
|
||||
});
|
||||
expect(out).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('drops an event with an empty / whitespace target', () => {
|
||||
const out = validateDisasterEvents({
|
||||
events: [
|
||||
{ type: 'plague', target: ' ', severity: 'minor' },
|
||||
{ type: 'plague', target: '', severity: 'minor' },
|
||||
valid,
|
||||
],
|
||||
});
|
||||
expect(out).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('caps a non-empty detail at 300 chars and keeps a short one', () => {
|
||||
const long = 'x'.repeat(500);
|
||||
const out = validateDisasterEvents({
|
||||
events: [
|
||||
{ type: 'plague', target: 'uk', severity: 'minor', detail: long },
|
||||
{ type: 'flood', target: 'de', severity: 'minor', detail: 'a short note' },
|
||||
],
|
||||
});
|
||||
expect(out).toEqual([
|
||||
{ type: 'plague', target: 'uk', severity: 'minor', detail: 'x'.repeat(300) },
|
||||
{ type: 'flood', target: 'de', severity: 'minor', detail: 'a short note' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('omits the detail key entirely when detail is empty / missing', () => {
|
||||
const out = validateDisasterEvents({
|
||||
events: [
|
||||
{ type: 'flood', target: 'uk', severity: 'minor', detail: '' },
|
||||
{ type: 'flood', target: 'de', severity: 'minor' },
|
||||
],
|
||||
});
|
||||
expect(out).toEqual([
|
||||
{ type: 'flood', target: 'uk', severity: 'minor' },
|
||||
{ type: 'flood', target: 'de', severity: 'minor' },
|
||||
]);
|
||||
expect(out[0]).not.toHaveProperty('detail');
|
||||
});
|
||||
|
||||
it('drops non-object event entries', () => {
|
||||
const out = validateDisasterEvents({ events: ['not-an-object', 7, null, valid] });
|
||||
expect(out).toEqual([valid]);
|
||||
});
|
||||
|
||||
it('yields { events: [] } (here, []) for a missing / malformed top-level value', () => {
|
||||
expect(validateDisasterEvents(undefined)).toEqual([]);
|
||||
expect(validateDisasterEvents(null)).toEqual([]);
|
||||
expect(validateDisasterEvents('nope')).toEqual([]);
|
||||
expect(validateDisasterEvents(5)).toEqual([]);
|
||||
expect(validateDisasterEvents({})).toEqual([]);
|
||||
expect(validateDisasterEvents({ events: 'not-an-array' })).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// applyDisasterEvent — the deterministic dip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('applyDisasterEvent', () => {
|
||||
it('a nation target: dips the nation AND every region it owns, then re-derives (identical values pass through)', () => {
|
||||
// uk owns three identical regions; all stats identical → the re-derived
|
||||
// aggregate equals the dipped values exactly.
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1', 'g2', 'g3']) },
|
||||
{ g1: RS(50, 50, 50, 50), g2: RS(50, 50, 50, 50), g3: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'earthquake', target: 'uk', severity: 'moderate' });
|
||||
// earthquake moderate = stability -4, economy -4 (military / population untouched)
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 46, 46, 50));
|
||||
for (const k of ['g1', 'g2', 'g3']) expect(state.regionStats[k]).toEqual(RS(50, 46, 46, 50));
|
||||
});
|
||||
|
||||
it('a region target: dips ONLY the named region (the sibling region is untouched)', () => {
|
||||
// uk owns g1 and g2 (identical). Target the region key g1 → only g1 dips.
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1', 'g2']) },
|
||||
{ g1: RS(50, 50, 50, 50), g2: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'wildfire', target: 'g1', severity: 'severe' });
|
||||
// wildfire severe = economy -7 only
|
||||
expect(state.regionStats.g1).toEqual(RS(50, 43, 50, 50));
|
||||
expect(state.regionStats.g2).toEqual(RS(50, 50, 50, 50)); // untouched
|
||||
// re-derive (equal weights, min-biased median) → the nation takes the dipped value
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 43, 50, 50));
|
||||
});
|
||||
|
||||
it('a region target resolved by a validKeys-gated key hits its sole owner', () => {
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(
|
||||
state,
|
||||
{ type: 'famine', target: 'g1', severity: 'moderate' },
|
||||
undefined,
|
||||
new Set(['g1']),
|
||||
);
|
||||
// famine moderate = economy -4, stability -4
|
||||
expect(state.regionStats.g1).toEqual(RS(50, 46, 46, 50));
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 46, 46, 50));
|
||||
});
|
||||
|
||||
it('clamps every dip at 0 (never negative)', () => {
|
||||
// low stability + military; a severe plague (stability -9, military -6) floors both.
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(3, 50, 3, 50), ['g1']) },
|
||||
{ g1: RS(3, 50, 3, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'plague', target: 'uk', severity: 'severe' });
|
||||
expect(state.nations.uk.stats).toEqual(NS(0, 50, 0, 50));
|
||||
expect(state.regionStats.g1).toEqual(RS(0, 50, 0, 50));
|
||||
});
|
||||
|
||||
it('a 0-owner region is untargetable (the event is dropped, nothing dips)', () => {
|
||||
// 'x1' is owned by nobody; no validKeys supplied, so the owners check drops it.
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'plague', target: 'x1', severity: 'severe' });
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 50, 50, 50)); // untouched
|
||||
expect(state.regionStats.g1).toEqual(RS(50, 50, 50, 50));
|
||||
});
|
||||
|
||||
it('a region with 2+ owners is untargetable (the event is dropped, neither dips)', () => {
|
||||
// corrupt double-ownership of 'y1'
|
||||
const state = mkState(
|
||||
{
|
||||
uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['y1']),
|
||||
de: mkNation('de', 'DE', NS(60, 60, 60, 60), ['y1']),
|
||||
},
|
||||
{ y1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'plague', target: 'y1', severity: 'severe' });
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 50, 50, 50)); // untouched
|
||||
expect(state.nations.de.stats).toEqual(NS(60, 60, 60, 60)); // untouched
|
||||
});
|
||||
|
||||
it('an unknown region key (gated by validKeys) is dropped', () => {
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
// 'zzz' is not in the manifest → dropped before the owners check.
|
||||
applyDisasterEvent(state, { type: 'plague', target: 'zzz', severity: 'severe' }, undefined, new Set(['g1']));
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 50, 50, 50));
|
||||
});
|
||||
|
||||
it('an unknown nation reference is dropped (no throw, no dip)', () => {
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'plague', target: 'ghost', severity: 'severe' });
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 50, 50, 50));
|
||||
expect(state.regionStats.g1).toEqual(RS(50, 50, 50, 50));
|
||||
});
|
||||
|
||||
it('an ambiguous nation reference (two share the name) is dropped', () => {
|
||||
const state = mkState(
|
||||
{
|
||||
a: mkNation('a', 'Same', NS(50, 50, 50, 50), ['g1']),
|
||||
b: mkNation('b', 'Same', NS(70, 70, 70, 70), ['d1']),
|
||||
},
|
||||
{ g1: RS(50, 50, 50, 50), d1: RS(70, 70, 70, 70) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'plague', target: 'Same', severity: 'severe' });
|
||||
expect(state.nations.a.stats).toEqual(NS(50, 50, 50, 50)); // untouched
|
||||
expect(state.nations.b.stats).toEqual(NS(70, 70, 70, 70)); // untouched
|
||||
});
|
||||
|
||||
it('resolves a nation by (case-insensitive) name, not just id', () => {
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'United Kingdom', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'epidemic', target: 'united kingdom', severity: 'moderate' });
|
||||
// epidemic moderate = stability -4, military -2
|
||||
expect(state.nations.uk.stats).toEqual(NS(48, 50, 46, 50));
|
||||
expect(state.regionStats.g1).toEqual(RS(48, 50, 46, 50));
|
||||
});
|
||||
|
||||
it('a 0-region nation keeps its directly-dipped stats (no re-derive to zero)', () => {
|
||||
const iso = mkNation('iso', 'Iso', NS(50, 50, 50, 50), []);
|
||||
const state = mkState({ iso }, {});
|
||||
applyDisasterEvent(state, { type: 'epidemic', target: 'iso', severity: 'moderate' });
|
||||
// epidemic moderate = stability -4, military -2 — the direct dip persists
|
||||
expect(state.nations.iso.stats).toEqual(NS(48, 50, 46, 50));
|
||||
expect(state.regionStats).toEqual({});
|
||||
});
|
||||
|
||||
it('a region with no regionStats entry is skipped (the nation still dips + re-derives from its other regions)', () => {
|
||||
// uk owns g1 (has stats) and g2 (no entry). Target the nation → the dip
|
||||
// mirrors to g1 only (g2 has no entry to dip); re-derive uses g1 alone.
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1', 'g2']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
applyDisasterEvent(state, { type: 'earthquake', target: 'uk', severity: 'moderate' });
|
||||
expect(state.regionStats.g1).toEqual(RS(50, 46, 46, 50));
|
||||
expect(state.regionStats).not.toHaveProperty('g2'); // never created
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 46, 46, 50));
|
||||
});
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// The Ability object
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
describe('disasters (Ability)', () => {
|
||||
it('owns the "disasters" field and declares NO durable state fields (turn-transient)', () => {
|
||||
expect(disasters.field).toBe('disasters');
|
||||
expect(disasters.stateFields).toBeUndefined();
|
||||
});
|
||||
|
||||
it('default() and stub() are fresh, empty { events: [] } values', () => {
|
||||
expect(disasters.default()).toEqual({ events: [] });
|
||||
expect(disasters.stub()).toEqual({ events: [] });
|
||||
// fresh on every call (never a shared mutable reference)
|
||||
expect(disasters.default()).not.toBe(disasters.default());
|
||||
expect(disasters.default().events).not.toBe(disasters.default().events);
|
||||
expect(disasters.stub()).not.toBe(disasters.stub());
|
||||
});
|
||||
|
||||
it('isMeaningful is true iff there is at least one event', () => {
|
||||
expect(disasters.isMeaningful({ events: [] })).toBe(false);
|
||||
expect(disasters.isMeaningful({ events: [{ type: 'plague', target: 'uk', severity: 'minor' }] })).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('payload() is always empty (the layer is turn-transient)', () => {
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
expect(disasters.payload({ state })).toEqual({});
|
||||
});
|
||||
|
||||
it('validate() wraps the low-level validator into the { events } field shape', () => {
|
||||
const valid = { type: 'flood', target: 'uk', severity: 'minor' } as DisasterEvent;
|
||||
expect(disasters.validate({ events: [valid] })).toEqual({ events: [valid] });
|
||||
expect(disasters.validate(undefined)).toEqual({ events: [] });
|
||||
expect(disasters.validate([valid])).toEqual({ events: [valid] }); // bare array leniency
|
||||
});
|
||||
|
||||
it('apply() wires ctx.geoOf + ctx.validRegionKeys through to the dip', () => {
|
||||
const state = mkState(
|
||||
{ uk: mkNation('uk', 'UK', NS(50, 50, 50, 50), ['g1']) },
|
||||
{ g1: RS(50, 50, 50, 50) },
|
||||
);
|
||||
const ctx = mkCtx(state, new Set(['g1']));
|
||||
const data: DisastersOps = { events: [{ type: 'volcanic', target: 'g1', severity: 'moderate' }] };
|
||||
disasters.apply(state, data, ctx);
|
||||
// volcanic moderate = economy -4, stability -3
|
||||
expect(state.regionStats.g1).toEqual(RS(50, 46, 47, 50));
|
||||
expect(state.nations.uk.stats).toEqual(NS(50, 46, 47, 50));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user