feat: NPC 일반 내정 턴 테스트 추가 및 예약 턴 핸들러 구현

This commit is contained in:
2026-01-24 09:11:15 +00:00
parent f280ed0c23
commit c9ab8e425a
@@ -0,0 +1,299 @@
import { describe, expect, it, vi } from 'vitest';
import type { TurnSchedule } from '@sammo-ts/logic';
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
const mockDate = new Date('0189-01-01T00:00:00Z');
const MINIMAL_MAP = {
id: 'npc_domestic_map',
name: 'NPC내정맵',
cities: [
{
id: 1,
name: '소성A',
level: 1,
region: 1,
position: { x: 10, y: 10 },
connections: [2],
max: {
population: 20000,
agriculture: 2000,
commerce: 2000,
security: 2000,
defence: 1000,
wall: 1000,
},
initial: {
population: 10000,
agriculture: 1000,
commerce: 1000,
security: 1000,
defence: 500,
wall: 500,
},
},
{
id: 2,
name: '중성B',
level: 1,
region: 2,
position: { x: 20, y: 10 },
connections: [1],
max: {
population: 20000,
agriculture: 2000,
commerce: 2000,
security: 2000,
defence: 1000,
wall: 1000,
},
initial: {
population: 10000,
agriculture: 1000,
commerce: 1000,
security: 1000,
defence: 500,
wall: 500,
},
},
],
defaults: { trust: 50, trade: 100, supplyState: 1, frontState: 0 },
};
const createMockPrisma = (initialGeneralRows: any[] = []) => {
let generalRows = [...initialGeneralRows];
return {
generalTurn: {
findMany: vi.fn(async ({ where } = {}) => {
if (where?.generalId) {
return generalRows
.filter((row) => row.generalId === where.generalId)
.sort((a, b) => a.turnIdx - b.turnIdx);
}
return generalRows;
}),
deleteMany: vi.fn(async ({ where } = {}) => {
if (where?.generalId) {
generalRows = generalRows.filter((row) => row.generalId !== where.generalId);
}
return { count: 0 };
}),
createMany: vi.fn(async ({ data }) => {
if (Array.isArray(data)) {
generalRows.push(...data);
}
return { count: data.length };
}),
},
nationTurn: {
findMany: vi.fn(async () => []),
deleteMany: vi.fn(async () => ({ count: 0 })),
createMany: vi.fn(async () => ({ count: 0 })),
},
};
};
const addMinutes = (time: Date, minutes: number): Date => new Date(time.getTime() + minutes * 60_000);
describe('NPC 일반 내정 턴', () => {
it('예약 턴이 없어도 내정 수치가 증가한다', async () => {
const generals: TurnGeneral[] = [
{
id: 1,
name: 'NPC_무장',
nationId: 1,
cityId: 1,
troopId: 0,
stats: { leadership: 75, strength: 75, intelligence: 15 },
turnTime: mockDate,
role: {
items: { horse: null, weapon: null, book: null, item: null },
personality: null,
specialDomestic: null,
specialWar: null,
},
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
meta: { killturn: 999 },
officerLevel: 4,
experience: 0,
dedication: 0,
injury: 0,
gold: 0,
rice: 0,
crew: 0,
crewTypeId: 0,
train: 0,
atmos: 0,
age: 30,
npcState: 2,
},
];
const cities = [
{
id: 1,
name: '소성A',
nationId: 1,
level: 1,
state: 0,
population: 10000,
populationMax: 20000,
agriculture: 1000,
agricultureMax: 2000,
commerce: 1000,
commerceMax: 2000,
security: 1000,
securityMax: 2000,
supplyState: 1,
frontState: 0,
defence: 500,
defenceMax: 1000,
wall: 500,
wallMax: 1000,
meta: { trust: 98 },
},
{
id: 2,
name: '중성B',
nationId: 2,
level: 1,
state: 0,
population: 12000,
populationMax: 20000,
agriculture: 1200,
agricultureMax: 2000,
commerce: 1200,
commerceMax: 2000,
security: 1200,
securityMax: 2000,
supplyState: 1,
frontState: 0,
defence: 600,
defenceMax: 1000,
wall: 600,
wallMax: 1000,
meta: { trust: 98 },
},
];
const nations = [
{
id: 1,
name: 'NPC국가',
color: '#FF0000',
capitalCityId: 1,
chiefGeneralId: 1,
gold: 50000,
rice: 50000,
power: 0,
level: 1,
typeCode: 'che_def',
meta: {},
},
{
id: 2,
name: '경쟁국',
color: '#0000FF',
capitalCityId: 2,
chiefGeneralId: null,
gold: 50000,
rice: 50000,
power: 0,
level: 1,
typeCode: 'che_def',
meta: {},
},
];
const snapshot: TurnWorldSnapshot = {
generals: generals as any,
cities: cities as any,
nations: nations as any,
troops: [],
diplomacy: [],
events: [],
initialEvents: [],
map: MINIMAL_MAP as any,
scenarioConfig: {
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
iconPath: '',
map: {},
const: {},
environment: { mapName: 'npc_domestic_map', unitSet: 'default' },
},
scenarioMeta: {
startYear: 189,
} as any,
unitSet: {} as any,
};
const state: TurnWorldState = {
id: 1,
currentYear: 189,
currentMonth: 1,
tickSeconds: 600,
lastTurnTime: mockDate,
meta: { seed: 1 },
};
const schedule: TurnSchedule = {
entries: [{ startMinute: 0, tickMinutes: 10 }],
};
const mockPrisma = createMockPrisma();
const reservedTurnStore = new InMemoryReservedTurnStore(mockPrisma as any, {
maxGeneralTurns: 10,
maxNationTurns: 10,
});
await reservedTurnStore.loadAll();
const wrapper = { world: null as InMemoryTurnWorld | null };
const handler = await createReservedTurnHandler({
reservedTurns: reservedTurnStore,
scenarioConfig: snapshot.scenarioConfig,
scenarioMeta: snapshot.scenarioMeta,
map: MINIMAL_MAP as any,
unitSet: snapshot.unitSet,
getWorld: () => wrapper.world,
});
const world = new InMemoryTurnWorld(state, snapshot, {
schedule,
generalTurnHandler: handler,
});
wrapper.world = world;
const beforeCity = world.getCityById(1)!;
const beforeStats = {
population: beforeCity.population,
agriculture: beforeCity.agriculture,
commerce: beforeCity.commerce,
security: beforeCity.security,
defence: beforeCity.defence,
wall: beforeCity.wall,
};
const processor = new InMemoryTurnProcessor(world, { tickMinutes: 10 });
await processor.run(addMinutes(mockDate, 10), {
budgetMs: 1000,
maxGenerals: 10,
catchUpCap: 1,
});
const afterCity = world.getCityById(1)!;
const increased =
afterCity.population > beforeStats.population ||
afterCity.agriculture > beforeStats.agriculture ||
afterCity.commerce > beforeStats.commerce ||
afterCity.security > beforeStats.security ||
afterCity.defence > beforeStats.defence ||
afterCity.wall > beforeStats.wall;
expect(increased).toBe(true);
expect(world.getGeneralById(1)!.turnTime.getTime()).toBeGreaterThan(mockDate.getTime());
});
});