Files
core2026/app/game-engine/test/scenario903GeneralPoolComposition.test.ts
T
Hide_D 85591c68ad fix: Ref 게임 로직과 시나리오 풀 호환을 보정
월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
2026-08-23 16:26:14 +00:00

167 lines
6.1 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest';
import { parseScenarioGeneralPoolCandidate, readScenarioGeneralPoolClaim, type City } from '@sammo-ts/logic';
import { loadGeneralPoolEntries } from '../src/scenario/generalPoolLoader.js';
import { loadScenarioDefinitionById } from '../src/scenario/scenarioLoader.js';
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
import { createCreateManyNpcHandler } from '../src/turn/monthlyCreateManyNpcAction.js';
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
import { buildCommandEnv } from '../src/turn/reservedTurnCommands.js';
import type { TurnGeneralPoolEntry, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
const turnTime = new Date('0180-12-01T00:00:00.000Z');
const city: City = {
id: 1,
name: '테스트성',
nationId: 0,
level: 4,
state: 0,
population: 10_000,
populationMax: 20_000,
agriculture: 1_000,
agricultureMax: 2_000,
commerce: 1_000,
commerceMax: 2_000,
security: 1_000,
securityMax: 2_000,
supplyState: 1,
frontState: 0,
defence: 1_000,
defenceMax: 2_000,
wall: 1_000,
wallMax: 2_000,
meta: {},
};
describe('scenario 903 general-pool composition', () => {
it('feeds the tracked U30 pool into the first 100-NPC event without losing candidate fields', async () => {
const [scenario, seeds] = await Promise.all([
loadScenarioDefinitionById(903),
loadGeneralPoolEntries('SPoolUnderU30'),
]);
expect(scenario.config.map.targetGeneralPool).toBe('SPoolUnderU30');
const createEvent = scenario.events.find(
(event): event is unknown[] =>
Array.isArray(event) &&
event[0] === 'month' &&
event.some((action) => Array.isArray(action) && action[0] === 'CreateManyNPC')
);
expect(createEvent).toEqual([
'month',
1_000,
['Date', '==', null, 12],
['CreateManyNPC', 100, 0],
['DeleteEvent'],
]);
const createAction = createEvent?.find(
(action): action is unknown[] => Array.isArray(action) && action[0] === 'CreateManyNPC'
);
expect(createAction).toBeDefined();
const generalPoolEntries: TurnGeneralPoolEntry[] = seeds.map((seed, index) => ({
id: index + 1,
uniqueName: seed.uniqueName,
ownerUserId: null,
generalId: null,
reservedUntil: null,
reservedUntilTick: null,
candidate: parseScenarioGeneralPoolCandidate({ id: index + 1, ...seed }),
}));
const map = {
id: 'scenario-903-pool-composition',
name: 'scenario 903 pool composition',
cities: [],
defaults: { trust: 50, trade: 100, supplyState: 1, frontState: 0 },
};
const state: TurnWorldState = {
id: 1,
currentYear: 180,
currentMonth: 12,
tickSeconds: 600,
lastTurnTime: turnTime,
meta: { hiddenSeed: 'scenario-903-pool-composition' },
};
const snapshot: TurnWorldSnapshot = {
scenarioConfig: scenario.config,
scenarioMeta: {
title: scenario.title,
startYear: scenario.startYear,
life: scenario.life,
fiction: scenario.fiction,
history: scenario.history,
ignoreDefaultEvents: scenario.ignoreDefaultEvents,
},
map,
unitSet: { id: 'test', name: 'test', crewTypes: [] },
generals: [],
cities: [city],
nations: [],
troops: [],
diplomacy: [],
events: [],
initialEvents: [],
generalPoolEntries,
};
const world = new InMemoryTurnWorld(state, snapshot, {
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
});
const reservedTurns = new InMemoryReservedTurnStore(
{
generalTurn: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn() },
nationTurn: { findMany: vi.fn(), deleteMany: vi.fn(), createMany: vi.fn() },
} as never,
{ maxGeneralTurns: 30, maxNationTurns: 12 }
);
const handler = createCreateManyNpcHandler({
getWorld: () => world,
reservedTurns,
env: buildCommandEnv(scenario.config),
});
await handler(
createAction!.slice(1),
{
year: 180,
month: 12,
startyear: 180,
currentEventID: 1,
turnTime,
},
{
id: 1,
targetCode: 'month',
priority: 1_000,
condition: true,
action: [],
meta: {},
}
);
const candidatesById = new Map(generalPoolEntries.map((entry) => [entry.id, entry.candidate]));
const created = world.peekDirtyState().createdGenerals;
expect(created).toHaveLength(100);
const claims = created.map((general) => readScenarioGeneralPoolClaim(general.meta));
expect(new Set(claims.map((claim) => claim?.poolEntryId)).size).toBe(100);
for (const [index, general] of created.entries()) {
const claim = claims[index];
expect(claim).not.toBeNull();
const candidate = candidatesById.get(claim!.poolEntryId)!;
expect(general).toMatchObject({
name: `ⓜ${candidate.name}`,
stats: candidate.stats,
picture: candidate.picture,
imageServer: candidate.imageServer,
role: { specialDomestic: candidate.specialDomestic },
meta: {
dex1: candidate.dex?.[0],
dex2: candidate.dex?.[1],
dex3: candidate.dex?.[2],
dex4: candidate.dex?.[3],
dex5: candidate.dex?.[4],
},
});
}
});
});