v0.27.0 (#44): LLM-placed named map point markers
CI / build + unit + e2e (pull_request) Successful in 2m36s
CI / build + unit + e2e (pull_request) Successful in 2m36s
- shared: 8 MarkerTypes + MARKER_TYPE_INFO (label/color/glyph); MapMarker;
GameState.markers (required, [] at init); TurnResult.markers {add,remove};
MarkerSpec/MarkerInfo; FNV-1a 4-char markerId; MAX_MARKERS=400.
- data: new public/region-centroids.json (1,800 anchors = largest-part centroid
+ spread ~0.1x min-bbox) built by new idempotent scripts/build-region-centroids.mjs.
- server: regions.ts centroid loader (public/ then dist/); engine applyTurnResult
marker ops (remove -> owner-recompute -> add, jitter, cap, never throws);
prompt documents the contract; buildUserPrompt carries markers; stub marker-free.
- client: WorldMap symbol layer rendered as canvas per-type icons (glyph CDN dead);
hover tooltip shows name; click focuses owning nation.
- tests: 168 -> 189 (centroids script, regions centroids, db legacy, engine markers).
This commit is contained in:
+276
-17
@@ -1,6 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import type { GameState, Preset, RoundItem } from '../shared/types';
|
||||
import { makeEvent, type NationOp, type TurnResult } from '../shared/protocol';
|
||||
import type { GameState, MarkerType, Preset, RoundItem } from '../shared/types';
|
||||
import { makeEvent, markerId, MAX_MARKERS, type MarkerSpec, type NationOp, type TurnResult } from '../shared/protocol';
|
||||
import type { LlmClient, ToolDefinition, WireMessage } from './llm';
|
||||
import { makeStubLlmClient } from './stub';
|
||||
import {
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
removeRoundItem,
|
||||
resolveNationRef,
|
||||
runTurn,
|
||||
validateMarkerOps,
|
||||
validateNationOps,
|
||||
validateTurnResult,
|
||||
} from './engine';
|
||||
@@ -87,6 +88,7 @@ function baseState(): GameState {
|
||||
version: 1,
|
||||
unowned: [],
|
||||
pendingNationOps: [],
|
||||
markers: [],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -524,7 +526,7 @@ describe('validateNationOps (v0.23 #16 P4a)', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'France', to: 'French Republic' }],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'France', to: 'French Republic' }], markers: { add: [], remove: [] },
|
||||
});
|
||||
expect(withOps).not.toBeNull();
|
||||
expect(withOps!.nationOps).toEqual([{ kind: 'rename', nationRef: 'France', to: 'French Republic' }]);
|
||||
@@ -545,7 +547,7 @@ describe('applyTurnResult (nation-op proposals, v0.23 #16 P4a)', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'de', to: 'Reich' }],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'de', to: 'Reich' }], markers: { add: [], remove: [] },
|
||||
});
|
||||
expect(next.nations.de.name).toBe('DE'); // still the old name
|
||||
expect(next.pendingNationOps).toEqual([{ kind: 'rename', nationRef: 'de', to: 'Reich' }]);
|
||||
@@ -565,7 +567,7 @@ describe('applyTurnResult (nation-op proposals, v0.23 #16 P4a)', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'us', to: 'USA' }], // new proposal this turn
|
||||
nationOps: [{ kind: 'rename', nationRef: 'us', to: 'USA' }], markers: { add: [], remove: [] }, // new proposal this turn
|
||||
});
|
||||
expect(next.pendingNationOps).toEqual([
|
||||
{ kind: 'rename', nationRef: 'de', to: 'Reich' }, // the old one survived
|
||||
@@ -583,7 +585,7 @@ describe('applyTurnResult (nation-op proposals, v0.23 #16 P4a)', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'de', to: 'Reich' }],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'de', to: 'Reich' }], markers: { add: [], remove: [] },
|
||||
});
|
||||
expect(next.pendingNationOps).toEqual([{ kind: 'rename', nationRef: 'de', to: 'Reich' }]);
|
||||
});
|
||||
@@ -1039,7 +1041,7 @@ describe('applyTurnResult', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
nationOps: [], markers: { add: [], remove: [] },
|
||||
};
|
||||
applyTurnResult(state, result);
|
||||
expect(JSON.stringify(state)).toBe(snapshot);
|
||||
@@ -1055,7 +1057,7 @@ describe('applyTurnResult', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
nationOps: [], markers: { add: [], remove: [] },
|
||||
};
|
||||
const next = applyTurnResult(state, result);
|
||||
expect(next.pending).toEqual([]);
|
||||
@@ -1077,7 +1079,7 @@ describe('applyTurnResult', () => {
|
||||
// is not a real key at all. Both are proposed as a gain by DE.
|
||||
territoryChanges: [{ nation: 'de', gain: ['wales-bbbb', 'not-a-region-xyz'], lose: [] }],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
nationOps: [], markers: { add: [], remove: [] },
|
||||
};
|
||||
const next = applyTurnResult(state, result, new Set(['wales-bbbb']));
|
||||
// the valid key was gained (taken from its sole previous owner); the bogus one was ignored
|
||||
@@ -1102,7 +1104,7 @@ describe('applyTurnResult', () => {
|
||||
// released (not in state.unowned), so it does not enter GameState.unowned.
|
||||
territoryChanges: [{ nation: 'de', gain: ['POL', 'wales-bbbb'], lose: [] }],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
nationOps: [], markers: { add: [], remove: [] },
|
||||
};
|
||||
const next = applyTurnResult(state, result); // no validKeys -> nothing filtered
|
||||
expect(next.nations.de.territory).toContain('wales-bbbb');
|
||||
@@ -1119,7 +1121,7 @@ describe('applyTurnResult (sole-ownership invariants, v0.21 #16 P2)', () => {
|
||||
stateChanges: [],
|
||||
relations: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
nationOps: [], markers: { add: [], remove: [] },
|
||||
};
|
||||
|
||||
it('a double claim (two different gainers, one key) is rejected: the key stays with its original owner', () => {
|
||||
@@ -1208,7 +1210,7 @@ describe('applyTurnResult (no man\'s land, v0.22 #16 P3)', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
nationOps: [], markers: { add: [], remove: [] },
|
||||
};
|
||||
|
||||
it('a release removes the key from its sole owner and marks it in GameState.unowned', () => {
|
||||
@@ -1323,7 +1325,7 @@ describe('applyTurnResult (no man\'s land, v0.22 #16 P3)', () => {
|
||||
|
||||
it('a lone release is meaningful (it survives the retry/nudge path)', () => {
|
||||
expect(
|
||||
isMeaningful({ date: 'x', events: [], stateChanges: [], relations: [], territoryChanges: [], unowned: ['GBR'], nationOps: [] }),
|
||||
isMeaningful({ date: 'x', events: [], stateChanges: [], relations: [], territoryChanges: [], unowned: ['GBR'], nationOps: [], markers: { add: [], remove: [] } }),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
@@ -1630,9 +1632,9 @@ describe('buildSystemPrompt (tool mode, v0.21 #16 P2)', () => {
|
||||
|
||||
describe('isMeaningful', () => {
|
||||
it('is true with an event, false when everything is empty', () => {
|
||||
expect(isMeaningful({ date: 'x', events: [{ nation: 'uk', type: 'military', title: 't', text: 'b', outcome: 'neutral' }], stateChanges: [], relations: [], territoryChanges: [], unowned: [], nationOps: [] })).toBe(true);
|
||||
expect(isMeaningful({ date: 'x', events: [], stateChanges: [{ nation: 'uk', stat: 'military', op: 'set', value: 5 }], relations: [], territoryChanges: [], unowned: [], nationOps: [] })).toBe(true);
|
||||
expect(isMeaningful({ date: 'x', events: [], stateChanges: [], relations: [], territoryChanges: [], unowned: [], nationOps: [] })).toBe(false);
|
||||
expect(isMeaningful({ date: 'x', events: [{ nation: 'uk', type: 'military', title: 't', text: 'b', outcome: 'neutral' }], stateChanges: [], relations: [], territoryChanges: [], unowned: [], nationOps: [], markers: { add: [], remove: [] } })).toBe(true);
|
||||
expect(isMeaningful({ date: 'x', events: [], stateChanges: [{ nation: 'uk', stat: 'military', op: 'set', value: 5 }], relations: [], territoryChanges: [], unowned: [], nationOps: [], markers: { add: [], remove: [] } })).toBe(true);
|
||||
expect(isMeaningful({ date: 'x', events: [], stateChanges: [], relations: [], territoryChanges: [], unowned: [], nationOps: [], markers: { add: [], remove: [] } })).toBe(false);
|
||||
});
|
||||
|
||||
it('(v0.23 #16 P4a) a turn that only proposes a nation-op is meaningful (it survives the retry/nudge path)', () => {
|
||||
@@ -1644,8 +1646,265 @@ describe('isMeaningful', () => {
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'de', to: 'Reich' }],
|
||||
nationOps: [{ kind: 'rename', nationRef: 'de', to: 'Reich' }], markers: { add: [], remove: [] },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it('(v0.27 #44) a turn that only adds a marker is meaningful (it survives the retry/nudge path)', () => {
|
||||
expect(
|
||||
isMeaningful({
|
||||
date: 'x',
|
||||
events: [],
|
||||
stateChanges: [],
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
markers: { add: [{ type: 'city', name: 'Lyon', regionKey: 'rhone-rhne' }], remove: [] },
|
||||
}),
|
||||
).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('markers (point placement, v0.27 #44)', () => {
|
||||
// a deterministic centroid for one region key, undefined for everything else
|
||||
const centroidOf = (key: string) =>
|
||||
key === 'moselle-rygy' ? { lat: 48, lon: 6, spread: 1 } : undefined;
|
||||
|
||||
// build a MarkerSpec from a (possibly invalid, for the guard tests) type string
|
||||
const mk = (type: string, name: string, regionKey: string): MarkerSpec =>
|
||||
({ type: type as MarkerType, name, regionKey });
|
||||
|
||||
it('adds a valid marker with a stable id, a jittered position within the spread, the region owner, and the placed turn', () => {
|
||||
const state = baseState();
|
||||
state.turn = 4; // placedTurn should equal the resolved turn
|
||||
state.nations.uk.territory = ['GBR', 'moselle-rygy']; // the region is owned by uk
|
||||
const result: TurnResult = {
|
||||
date: '1939-09-03',
|
||||
events: [],
|
||||
stateChanges: [],
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
markers: { add: [mk('city', 'Strasbourg', 'moselle-rygy')], remove: [] },
|
||||
};
|
||||
const next = applyTurnResult(state, result, undefined, centroidOf);
|
||||
expect(next.markers).toHaveLength(1);
|
||||
const m = next.markers[0];
|
||||
expect(m.id).toBe(markerId('city', 'Strasbourg', 'moselle-rygy'));
|
||||
expect(m.type).toBe('city');
|
||||
expect(m.name).toBe('Strasbourg');
|
||||
expect(m.regionKey).toBe('moselle-rygy');
|
||||
expect(m.ownerNationId).toBe('uk'); // the region's sole owner
|
||||
expect(m.placedTurn).toBe(4);
|
||||
// the position is the centroid jittered within ±spread/2
|
||||
expect(m.lat).toBeGreaterThanOrEqual(47.5);
|
||||
expect(m.lat).toBeLessThanOrEqual(48.5);
|
||||
expect(m.lon).toBeGreaterThanOrEqual(5.5);
|
||||
expect(m.lon).toBeLessThanOrEqual(6.5);
|
||||
});
|
||||
|
||||
it('is idempotent: re-adding the same (type, name, regionKey) does not duplicate the marker', () => {
|
||||
const state = baseState();
|
||||
state.nations.uk.territory = ['moselle-rygy'];
|
||||
const add = [mk('city', 'Strasbourg', 'moselle-rygy')];
|
||||
const first = applyTurnResult(state, {
|
||||
date: '1939-09-03', events: [], stateChanges: [], relations: [], territoryChanges: [],
|
||||
unowned: [], nationOps: [], markers: { add, remove: [] },
|
||||
}, undefined, centroidOf);
|
||||
expect(first.markers).toHaveLength(1);
|
||||
// a second turn proposes the exact same marker again
|
||||
const second = applyTurnResult(first, {
|
||||
date: '1939-09-05', events: [], stateChanges: [], relations: [], territoryChanges: [],
|
||||
unowned: [], nationOps: [], markers: { add, remove: [] },
|
||||
}, undefined, centroidOf);
|
||||
expect(second.markers).toHaveLength(1);
|
||||
expect(second.markers[0].id).toBe(first.markers[0].id); // stable id, no second copy
|
||||
});
|
||||
|
||||
it('applyTurnResult drops a new add whose region is unknown or has no centroid, but keeps a valid one', () => {
|
||||
const state = baseState();
|
||||
state.nations.uk.territory = ['moselle-rygy'];
|
||||
// validKeys knows two keys; the centroid resolves only the first.
|
||||
const validKeys = new Set(['moselle-rygy', 'other-key']);
|
||||
const centroidOf2 = (k: string) =>
|
||||
k === 'moselle-rygy' ? { lat: 48, lon: 6, spread: 1 } : undefined;
|
||||
const result: TurnResult = {
|
||||
date: '1939-09-03',
|
||||
events: [],
|
||||
stateChanges: [],
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
markers: {
|
||||
add: [
|
||||
{ type: 'city', name: 'Good', regionKey: 'moselle-rygy' }, // valid + centroid → kept
|
||||
{ type: 'city', name: 'NoCent', regionKey: 'other-key' }, // in validKeys, no centroid → dropped
|
||||
{ type: 'city', name: 'Unknown', regionKey: 'zzz-not-a-key' }, // not in validKeys → dropped
|
||||
],
|
||||
remove: [],
|
||||
},
|
||||
};
|
||||
const next = applyTurnResult(state, result, validKeys, centroidOf2);
|
||||
expect(next.markers).toHaveLength(1);
|
||||
expect(next.markers[0].name).toBe('Good');
|
||||
});
|
||||
|
||||
it('caps the total marker count at MAX_MARKERS, dropping the newest add that exceeds the cap', () => {
|
||||
const state = baseState();
|
||||
// pre-seed the state with MAX_MARKERS - 1 distinct markers
|
||||
const seeded: typeof state.markers = [];
|
||||
for (let i = 0; i < MAX_MARKERS - 1; i++) {
|
||||
const k = `seed-${i}`;
|
||||
seeded.push({
|
||||
id: markerId('city', `S${i}`, k),
|
||||
type: 'city',
|
||||
name: `S${i}`,
|
||||
regionKey: k,
|
||||
lat: 0, lon: 0,
|
||||
ownerNationId: null,
|
||||
placedTurn: 1,
|
||||
});
|
||||
}
|
||||
state.markers = seeded;
|
||||
// a permissive centroid: every region key resolves
|
||||
const anyCentroid = () => ({ lat: 0, lon: 0, spread: 0 });
|
||||
const result: TurnResult = {
|
||||
date: '1939-09-03',
|
||||
events: [],
|
||||
stateChanges: [],
|
||||
relations: [],
|
||||
territoryChanges: [],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
markers: {
|
||||
add: [
|
||||
{ type: 'city', name: 'NewA', regionKey: 'new-a' },
|
||||
{ type: 'city', name: 'NewB', regionKey: 'new-b' },
|
||||
],
|
||||
remove: [],
|
||||
},
|
||||
};
|
||||
const next = applyTurnResult(state, result, undefined, anyCentroid);
|
||||
expect(next.markers).toHaveLength(MAX_MARKERS); // capped, not MAX_MARKERS + 1
|
||||
expect(next.markers.some((m) => m.name === 'NewA')).toBe(true); // the 1st new one kept
|
||||
expect(next.markers.some((m) => m.name === 'NewB')).toBe(false); // the 2nd (over cap) dropped
|
||||
});
|
||||
|
||||
it('removes a marker by id; removing an absent id is a no-op', () => {
|
||||
const state = baseState();
|
||||
state.nations.uk.territory = ['moselle-rygy'];
|
||||
const idA = markerId('city', 'Strasbourg', 'moselle-rygy');
|
||||
const idB = markerId('port', 'Dunkirk', 'moselle-rygy');
|
||||
state.markers = [
|
||||
{ id: idA, type: 'city', name: 'Strasbourg', regionKey: 'moselle-rygy', lat: 48, lon: 6, ownerNationId: 'uk', placedTurn: 1 },
|
||||
{ id: idB, type: 'port', name: 'Dunkirk', regionKey: 'moselle-rygy', lat: 48, lon: 6, ownerNationId: 'uk', placedTurn: 1 },
|
||||
];
|
||||
const next = applyTurnResult(state, {
|
||||
date: '1939-09-03', events: [], stateChanges: [], relations: [], territoryChanges: [],
|
||||
unowned: [], nationOps: [], markers: { add: [], remove: [idA, 'zzzz-not-there'] },
|
||||
}, undefined, centroidOf);
|
||||
expect(next.markers).toHaveLength(1);
|
||||
expect(next.markers[0].id).toBe(idB); // only A removed; the bogus id was a no-op
|
||||
});
|
||||
|
||||
it('a marker owner follows its region: released to no man\'s land → null; re-claimed → the new owner', () => {
|
||||
const state = baseState();
|
||||
state.nations.uk.territory = ['moselle-rygy'];
|
||||
const idA = markerId('city', 'Strasbourg', 'moselle-rygy');
|
||||
state.markers = [
|
||||
{ id: idA, type: 'city', name: 'Strasbourg', regionKey: 'moselle-rygy', lat: 48, lon: 6, ownerNationId: 'uk', placedTurn: 1 },
|
||||
];
|
||||
// 1) uk releases the region → the marker becomes ownerless
|
||||
const released = applyTurnResult(state, {
|
||||
date: '1939-09-03', events: [], stateChanges: [], relations: [], territoryChanges: [],
|
||||
unowned: ['moselle-rygy'], nationOps: [], markers: { add: [], remove: [] },
|
||||
}, undefined, centroidOf);
|
||||
expect(released.markers[0].ownerNationId).toBeNull();
|
||||
// 2) de re-claims the region → the marker's owner follows to de
|
||||
const reclaimed = applyTurnResult(released, {
|
||||
date: '1939-09-05', events: [], stateChanges: [], relations: [],
|
||||
territoryChanges: [{ nation: 'de', gain: ['moselle-rygy'], lose: [] }],
|
||||
unowned: [], nationOps: [], markers: { add: [], remove: [] },
|
||||
}, undefined, centroidOf);
|
||||
expect(reclaimed.markers[0].ownerNationId).toBe('de');
|
||||
});
|
||||
|
||||
it('the user prompt carries the existing markers when provided (and omits the field otherwise)', () => {
|
||||
const state = baseState();
|
||||
const info = {
|
||||
id: markerId('city', 'Strasbourg', 'moselle-rygy'),
|
||||
type: 'city' as MarkerType,
|
||||
name: 'Strasbourg',
|
||||
regionKey: 'moselle-rygy',
|
||||
owner: 'uk',
|
||||
};
|
||||
const payload = JSON.parse(
|
||||
buildUserPrompt(state, [], undefined, undefined, undefined, [info]),
|
||||
) as { markers?: Array<Record<string, unknown>> };
|
||||
expect(payload.markers).toHaveLength(1);
|
||||
expect(payload.markers![0].name).toBe('Strasbourg');
|
||||
expect(payload.markers![0].owner).toBe('uk');
|
||||
// a bare call (no markers arg) omits the field entirely
|
||||
const bare = JSON.parse(buildUserPrompt(state, [])) as { markers?: unknown };
|
||||
expect(bare.markers).toBeUndefined();
|
||||
});
|
||||
|
||||
it('the system prompt documents the markers contract (field + placement rule + a type)', () => {
|
||||
const prompt = buildSystemPrompt(preset);
|
||||
expect(prompt).toMatch(/named points on the map/i);
|
||||
expect(prompt).toMatch(/"markers"/);
|
||||
expect(prompt).toMatch(/"add"/);
|
||||
expect(prompt).toMatch(/"remove"/);
|
||||
expect(prompt).toMatch(/gathering/);
|
||||
expect(prompt).toMatch(/no man's land/); // owner recompute on release
|
||||
});
|
||||
|
||||
it('validateMarkerOps cleans a raw markers field: drops bad types/empty names, dedupes, trims removes', () => {
|
||||
const raw = {
|
||||
add: [
|
||||
{ type: 'city', name: 'X', regionKey: 'a' },
|
||||
{ type: 'not-a-type', name: 'Y', regionKey: 'a' }, // invalid type -> dropped
|
||||
{ type: 'city', name: ' ', regionKey: 'a' }, // empty name -> dropped
|
||||
{ type: 'city', name: 'X', regionKey: 'a' }, // duplicate (same id) -> deduped
|
||||
{ type: 'port', name: 'Z', regionKey: 'b' }, // valid
|
||||
],
|
||||
remove: ['id-1', 42, '', ' id-2 ', 'id-1'], // number/empty dropped, trimmed, deduped
|
||||
};
|
||||
const out = validateMarkerOps(raw);
|
||||
expect(out.add.map((m) => m.name).sort()).toEqual(['X', 'Z']);
|
||||
expect(out.remove).toEqual(['id-1', 'id-2']);
|
||||
});
|
||||
|
||||
it('validateMarkerOps returns empty ops for a non-object raw value (a string, an array, null)', () => {
|
||||
for (const bad of ['oops', [{ type: 'city', name: 'X', regionKey: 'a' }], null, undefined]) {
|
||||
expect(validateMarkerOps(bad)).toEqual({ add: [], remove: [] });
|
||||
}
|
||||
});
|
||||
|
||||
it('runTurn wires the centroid through: a marker added over the LLM path is placed (owner = region owner)', async () => {
|
||||
const result = JSON.stringify({
|
||||
date: '1939-09-03',
|
||||
events: [],
|
||||
stateChanges: [],
|
||||
relations: [],
|
||||
territoryChanges: [{ nation: 'uk', gain: ['moselle-rygy'], lose: [] }],
|
||||
unowned: [],
|
||||
nationOps: [],
|
||||
markers: { add: [{ type: 'city', name: 'Strasbourg', regionKey: 'moselle-rygy' }], remove: [] },
|
||||
});
|
||||
const { state } = await runTurn({
|
||||
llm: fakeLlm([result]),
|
||||
preset,
|
||||
state: baseState(),
|
||||
items: [],
|
||||
centroidOf,
|
||||
});
|
||||
expect(state.markers).toHaveLength(1);
|
||||
expect(state.markers[0].name).toBe('Strasbourg');
|
||||
expect(state.markers[0].ownerNationId).toBe('uk'); // uk gained the region this turn
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user