feat: enhance NPC war declaration tests with logging and unification handling
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
import { vi } from 'vitest';
|
import { vi } from 'vitest';
|
||||||
import type { MapDefinition, TurnSchedule } from '@sammo-ts/logic';
|
import type { LogEntryDraft, MapDefinition, TurnSchedule } from '@sammo-ts/logic';
|
||||||
import type { TurnWorldSnapshot, TurnWorldState } from '../../src/turn/types.js';
|
import type { TurnWorldSnapshot, TurnWorldState } from '../../src/turn/types.js';
|
||||||
import type { InMemoryTurnWorld, GeneralTurnHandler } from '../../src/turn/inMemoryWorld.js';
|
import type { InMemoryTurnWorld, GeneralTurnHandler } from '../../src/turn/inMemoryWorld.js';
|
||||||
|
import type { TurnCalendarHandler } from '../../src/turn/inMemoryWorld.js';
|
||||||
import { InMemoryTurnWorld as InMemoryTurnWorldClass } from '../../src/turn/inMemoryWorld.js';
|
import { InMemoryTurnWorld as InMemoryTurnWorldClass } from '../../src/turn/inMemoryWorld.js';
|
||||||
import { InMemoryReservedTurnStore } from '../../src/turn/reservedTurnStore.js';
|
import { InMemoryReservedTurnStore } from '../../src/turn/reservedTurnStore.js';
|
||||||
import { createReservedTurnHandler } from '../../src/turn/reservedTurnHandler.js';
|
import { createReservedTurnHandler } from '../../src/turn/reservedTurnHandler.js';
|
||||||
@@ -70,6 +71,8 @@ export type TurnTestHarnessOptions = {
|
|||||||
worldRef?: { current: InMemoryTurnWorld | null };
|
worldRef?: { current: InMemoryTurnWorld | null };
|
||||||
onActionResolved?: Parameters<typeof createReservedTurnHandler>[0]['onActionResolved'];
|
onActionResolved?: Parameters<typeof createReservedTurnHandler>[0]['onActionResolved'];
|
||||||
wrapGeneralTurnHandler?: (handler: GeneralTurnHandler) => GeneralTurnHandler;
|
wrapGeneralTurnHandler?: (handler: GeneralTurnHandler) => GeneralTurnHandler;
|
||||||
|
extraCalendarHandlers?: TurnCalendarHandler[];
|
||||||
|
collectLogs?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultRunOptions = {
|
const defaultRunOptions = {
|
||||||
@@ -116,7 +119,12 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
map: options.map,
|
map: options.map,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarHandler = composeCalendarHandlers(incomeHandler, npcTaxHandler, frontStateHandler);
|
const calendarHandler = composeCalendarHandlers(
|
||||||
|
incomeHandler,
|
||||||
|
npcTaxHandler,
|
||||||
|
frontStateHandler,
|
||||||
|
...(options.extraCalendarHandlers ?? [])
|
||||||
|
);
|
||||||
|
|
||||||
const world = new InMemoryTurnWorldClass(options.state, options.snapshot, {
|
const world = new InMemoryTurnWorldClass(options.state, options.snapshot, {
|
||||||
schedule: options.schedule,
|
schedule: options.schedule,
|
||||||
@@ -130,6 +138,18 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
afterExecuteGeneral: options.turnProcessorOptions?.afterExecuteGeneral,
|
afterExecuteGeneral: options.turnProcessorOptions?.afterExecuteGeneral,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const collectedLogs: LogEntryDraft[] = [];
|
||||||
|
|
||||||
|
const collectWorldLogs = () => {
|
||||||
|
if (!options.collectLogs) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { logs } = world.consumeDirtyState();
|
||||||
|
if (logs.length > 0) {
|
||||||
|
collectedLogs.push(...logs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const runOneTick = async (runOptions: TurnHarnessRunOptions = {}) => {
|
const runOneTick = async (runOptions: TurnHarnessRunOptions = {}) => {
|
||||||
const minutes = runOptions.minutes ?? options.turnProcessorOptions?.tickMinutes ?? 10;
|
const minutes = runOptions.minutes ?? options.turnProcessorOptions?.tickMinutes ?? 10;
|
||||||
const target = addMinutes(world.getState().lastTurnTime, minutes);
|
const target = addMinutes(world.getState().lastTurnTime, minutes);
|
||||||
@@ -138,6 +158,7 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
maxGenerals: runOptions.maxGenerals ?? defaultRunOptions.maxGenerals,
|
maxGenerals: runOptions.maxGenerals ?? defaultRunOptions.maxGenerals,
|
||||||
catchUpCap: runOptions.catchUpCap ?? defaultRunOptions.catchUpCap,
|
catchUpCap: runOptions.catchUpCap ?? defaultRunOptions.catchUpCap,
|
||||||
});
|
});
|
||||||
|
collectWorldLogs();
|
||||||
};
|
};
|
||||||
|
|
||||||
const runUntil = async (
|
const runUntil = async (
|
||||||
@@ -163,6 +184,7 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
|
|||||||
processor,
|
processor,
|
||||||
runOneTick,
|
runOneTick,
|
||||||
runUntil,
|
runUntil,
|
||||||
|
getCollectedLogs: () => [...collectedLogs],
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { TurnSchedule, UnitSetDefinition } from '@sammo-ts/logic';
|
import type { LogEntryDraft, TurnSchedule, UnitSetDefinition } from '@sammo-ts/logic';
|
||||||
import { DIPLOMACY_STATE } from '@sammo-ts/logic';
|
import { DIPLOMACY_STATE, LogCategory, LogFormat, LogScope } from '@sammo-ts/logic';
|
||||||
import type { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
import type { InMemoryTurnWorld, TurnCalendarHandler } from '../src/turn/inMemoryWorld.js';
|
||||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||||
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
||||||
import { createTurnTestHarness, createWorldDebugger } from './helpers/turnTestHarness.js';
|
import { createTurnTestHarness, createWorldDebugger } from './helpers/turnTestHarness.js';
|
||||||
@@ -62,6 +62,14 @@ const addMonths = (year: number, month: number, delta: number): { year: number;
|
|||||||
return { year: nextYear, month: nextMonth };
|
return { year: nextYear, month: nextMonth };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const buildUnificationLog = (nationName: string): LogEntryDraft => ({
|
||||||
|
scope: LogScope.SYSTEM,
|
||||||
|
category: LogCategory.HISTORY,
|
||||||
|
format: LogFormat.YEAR_MONTH,
|
||||||
|
text: `<C>●</><Y><b>【통일】</b></><D><b>${nationName}</b></>이 전토를 통일하였습니다.`,
|
||||||
|
meta: {},
|
||||||
|
});
|
||||||
|
|
||||||
describe('NPC 선전포고 조건 테스트', () => {
|
describe('NPC 선전포고 조건 테스트', () => {
|
||||||
it('183년 5월 시작 후 3개월 이내 선전포고가 발생해야 한다', async () => {
|
it('183년 5월 시작 후 3개월 이내 선전포고가 발생해야 한다', async () => {
|
||||||
const cities = buildLargeTestCities().map(maxCityStats);
|
const cities = buildLargeTestCities().map(maxCityStats);
|
||||||
@@ -126,7 +134,9 @@ describe('NPC 선전포고 조건 테스트', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
pushNationGenerals(1, cityA1.id);
|
pushNationGenerals(1, cityA1.id);
|
||||||
pushNationGenerals(2, cityB1.id);
|
for (let i = 0; i < 3; i += 1) {
|
||||||
|
pushNationGenerals(2, cityB1.id);
|
||||||
|
}
|
||||||
|
|
||||||
const snapshot: TurnWorldSnapshot = {
|
const snapshot: TurnWorldSnapshot = {
|
||||||
generals: generals as any,
|
generals: generals as any,
|
||||||
@@ -201,12 +211,42 @@ describe('NPC 선전포고 조건 테스트', () => {
|
|||||||
let declaredAt: { year: number; month: number; nationId: number } | null = null;
|
let declaredAt: { year: number; month: number; nationId: number } | null = null;
|
||||||
const dispatchCounts = new Map<string, number>();
|
const dispatchCounts = new Map<string, number>();
|
||||||
|
|
||||||
const { runUntil } = await createTurnTestHarness({
|
const unificationHandler: TurnCalendarHandler = {
|
||||||
|
onMonthChanged: () => {
|
||||||
|
const world = worldRef.current;
|
||||||
|
if (!world) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const meta = world.getState().meta as Record<string, unknown>;
|
||||||
|
if (typeof meta.isUnited === 'number' && meta.isUnited !== 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const cities = world.listCities();
|
||||||
|
const activeNations = world
|
||||||
|
.listNations()
|
||||||
|
.filter((nation) => nation.level > 0)
|
||||||
|
.filter((nation) => cities.some((city) => city.nationId === nation.id));
|
||||||
|
if (activeNations.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const winner = activeNations[0];
|
||||||
|
const ownedCount = cities.filter((city) => city.nationId === winner.id).length;
|
||||||
|
if (ownedCount !== cities.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
world.updateWorldMeta({ isUnited: 2 });
|
||||||
|
world.pushLog(buildUnificationLog(winner.name));
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const { runUntil, getCollectedLogs } = await createTurnTestHarness({
|
||||||
snapshot,
|
snapshot,
|
||||||
state,
|
state,
|
||||||
schedule,
|
schedule,
|
||||||
map: LARGE_TEST_MAP,
|
map: LARGE_TEST_MAP,
|
||||||
worldRef,
|
worldRef,
|
||||||
|
extraCalendarHandlers: [unificationHandler],
|
||||||
|
collectLogs: true,
|
||||||
onActionResolved: (payload) => {
|
onActionResolved: (payload) => {
|
||||||
if (payload.kind === 'general' && payload.actionKey === 'che_출병') {
|
if (payload.kind === 'general' && payload.actionKey === 'che_출병') {
|
||||||
const world = worldRef.current;
|
const world = worldRef.current;
|
||||||
@@ -246,6 +286,9 @@ describe('NPC 선전포고 조건 테스트', () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const countCities = (nationId: number, currentWorld: InMemoryTurnWorld) =>
|
||||||
|
currentWorld.listCities().filter((city) => city.nationId === nationId).length;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await runUntil(
|
await runUntil(
|
||||||
(current) =>
|
(current) =>
|
||||||
@@ -329,6 +372,62 @@ describe('NPC 선전포고 조건 테스트', () => {
|
|||||||
expect(warEntry).not.toBeNull();
|
expect(warEntry).not.toBeNull();
|
||||||
expect(warEntry?.state).toBe(DIPLOMACY_STATE.WAR);
|
expect(warEntry?.state).toBe(DIPLOMACY_STATE.WAR);
|
||||||
|
|
||||||
|
let prevNation1Cities = countCities(1, world);
|
||||||
|
let prevNation2Cities = countCities(2, world);
|
||||||
|
let guard = 0;
|
||||||
|
|
||||||
|
while (prevNation1Cities > 0 && guard < 120) {
|
||||||
|
const next = addMonths(world.getState().currentYear, world.getState().currentMonth, 1);
|
||||||
|
await runUntil((current) =>
|
||||||
|
current.currentYear > next.year ||
|
||||||
|
(current.currentYear === next.year && current.currentMonth >= next.month)
|
||||||
|
);
|
||||||
|
|
||||||
|
const warLoopEntry = findDiplomacyEntry(world);
|
||||||
|
if (!warLoopEntry) {
|
||||||
|
debug.dumpWatched('개전 이후 외교 상태 누락');
|
||||||
|
throw new Error('missing diplomacy entry');
|
||||||
|
}
|
||||||
|
expect(warLoopEntry.state).toBe(DIPLOMACY_STATE.WAR);
|
||||||
|
|
||||||
|
const nowNation1Cities = countCities(1, world);
|
||||||
|
const nowNation2Cities = countCities(2, world);
|
||||||
|
|
||||||
|
expect(nowNation1Cities).toBeLessThanOrEqual(prevNation1Cities);
|
||||||
|
expect(nowNation2Cities).toBeGreaterThanOrEqual(prevNation2Cities);
|
||||||
|
|
||||||
|
prevNation1Cities = nowNation1Cities;
|
||||||
|
prevNation2Cities = nowNation2Cities;
|
||||||
|
guard += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prevNation1Cities > 0) {
|
||||||
|
debug.dumpWatched('국가 1 도시 소멸 실패');
|
||||||
|
throw new Error('nation 1 cities not eliminated');
|
||||||
|
}
|
||||||
|
|
||||||
|
const allOwnedByNation2 = world.listCities().every((city) => city.nationId === 2);
|
||||||
|
expect(allOwnedByNation2).toBe(true);
|
||||||
|
|
||||||
|
const unifyCheckTarget = addMonths(world.getState().currentYear, world.getState().currentMonth, 1);
|
||||||
|
await runUntil((current) =>
|
||||||
|
current.currentYear > unifyCheckTarget.year ||
|
||||||
|
(current.currentYear === unifyCheckTarget.year && current.currentMonth >= unifyCheckTarget.month)
|
||||||
|
);
|
||||||
|
|
||||||
|
const worldMeta = world.getState().meta as Record<string, unknown>;
|
||||||
|
if (worldMeta.isUnited !== 2) {
|
||||||
|
debug.dumpWatched('통일 상태 누락');
|
||||||
|
}
|
||||||
|
expect(worldMeta.isUnited).toBe(2);
|
||||||
|
|
||||||
|
const logs = getCollectedLogs();
|
||||||
|
const hasUnificationLog = logs.some((log) => log.text.includes('전토를 통일하였습니다.'));
|
||||||
|
if (!hasUnificationLog) {
|
||||||
|
debug.dumpWatched('통일 로그 누락');
|
||||||
|
}
|
||||||
|
expect(hasUnificationLog).toBe(true);
|
||||||
|
|
||||||
const dispatchWindowEnd = addMonths(warTarget.year, warTarget.month, 2);
|
const dispatchWindowEnd = addMonths(warTarget.year, warTarget.month, 2);
|
||||||
await runUntil((current) =>
|
await runUntil((current) =>
|
||||||
current.currentYear > dispatchWindowEnd.year ||
|
current.currentYear > dispatchWindowEnd.year ||
|
||||||
|
|||||||
Reference in New Issue
Block a user