보급 차단 시점 계약을 회귀 테스트로 고정한다
출병 점령 그 순간에는 보급이 즉시 끊기지 않고(che_출병/aftermath는 점령 도시와 긴급천도 신수도에만 supply=1을 쓴다), 차단(切断)은 월 경계의 UpdateCitySupply(pre_month)에서 일어난다는 Ref 계승 계약을 테스트로 고정한다. 월 경계 연감 스냅샷은 Ref #230(d17b2518) 이후 UpdateCitySupply 뒤에 찍히므로 끝난 달의 연감 지도에는 이미 고립이 반영된다. 10월중 점령 -> 11월 경계 차단 -> 10월 연감 반영 순서를 검증한다.
This commit is contained in:
@@ -0,0 +1,208 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import type { City, MapDefinition, Nation } from '@sammo-ts/logic';
|
||||
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import { composeCalendarHandlers } from '../src/turn/calendarHandlers.js';
|
||||
import { createUpdateCitySupplyHandler } from '../src/turn/monthlyCitySupplyAction.js';
|
||||
import { createMonthlyEventHandler, type MonthlyEventActionHandler } from '../src/turn/monthlyEventHandler.js';
|
||||
import { createYearbookHandler } from '../src/turn/yearbookHandler.js';
|
||||
import type { PendingYearbookSnapshot, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||
|
||||
// Ref 계승 계약 (devel TurnExecutionHelper.php 실행 순서):
|
||||
// runEventHandler(PreMonth) -> UpdateCitySupply (보급 재계산/고립)
|
||||
// preUpdateMonthly() -> LogHistory (방금 끝난 달의 연감 스냅샷)
|
||||
// turnDate() -> 월 advance
|
||||
// 즉 월중 출병 점령은 보급을 즉시 끊지 않고(che_출병/aftermath는 점령 도시와
|
||||
// 천도 신수도에만 supply=1을 쓴다), 보급切断은 다음 달 시작 경계에서 일어난다.
|
||||
// 단 경계 스냅샷은 Ref #230(d17b2518) 이후 UpdateCitySupply "뒤"에 찍히므로
|
||||
// 끝난 달(10월)의 연감 지도에는 이미 끊긴 보급선이 반영된다. Core의
|
||||
// composeCalendarHandlers 순서(monthlyEventHandler -> yearbookHandler)가 이
|
||||
// 계약을 보존하는지를 고정한다.
|
||||
|
||||
const map: MapDefinition = {
|
||||
id: 'supply-cut-order-test',
|
||||
name: 'supply-cut-order-test',
|
||||
cities: [
|
||||
{
|
||||
id: 10,
|
||||
name: '공격기지',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 0, y: 0 },
|
||||
connections: [20],
|
||||
max: { population: 2_000, agriculture: 1_000, commerce: 1_000, security: 1_000, defence: 1_000, wall: 1_000 },
|
||||
initial: { population: 1_000, agriculture: 500, commerce: 500, security: 500, defence: 500, wall: 500 },
|
||||
},
|
||||
{
|
||||
id: 20,
|
||||
name: '중간 보급로',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 1, y: 0 },
|
||||
connections: [10, 21, 22],
|
||||
max: { population: 2_000, agriculture: 1_000, commerce: 1_000, security: 1_000, defence: 1_000, wall: 1_000 },
|
||||
initial: { population: 1_000, agriculture: 500, commerce: 500, security: 500, defence: 500, wall: 500 },
|
||||
},
|
||||
{
|
||||
id: 21,
|
||||
name: '수도',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 2, y: 0 },
|
||||
connections: [20],
|
||||
max: { population: 2_000, agriculture: 1_000, commerce: 1_000, security: 1_000, defence: 1_000, wall: 1_000 },
|
||||
initial: { population: 1_000, agriculture: 500, commerce: 500, security: 500, defence: 500, wall: 500 },
|
||||
},
|
||||
{
|
||||
id: 22,
|
||||
name: '외곽 도시',
|
||||
level: 2,
|
||||
region: 1,
|
||||
position: { x: 3, y: 0 },
|
||||
connections: [20],
|
||||
max: { population: 2_000, agriculture: 1_000, commerce: 1_000, security: 1_000, defence: 1_000, wall: 1_000 },
|
||||
initial: { population: 1_000, agriculture: 500, commerce: 500, security: 500, defence: 500, wall: 500 },
|
||||
},
|
||||
],
|
||||
defaults: { trust: 50, trade: 100, supplyState: 1, frontState: 0 },
|
||||
};
|
||||
|
||||
const buildCity = (id: number, nationId: number): City => ({
|
||||
id,
|
||||
name: map.cities.find((city) => city.id === id)?.name ?? `도시${id}`,
|
||||
nationId,
|
||||
level: 2,
|
||||
state: 0,
|
||||
population: 1_000,
|
||||
populationMax: 2_000,
|
||||
agriculture: 500,
|
||||
agricultureMax: 1_000,
|
||||
commerce: 500,
|
||||
commerceMax: 1_000,
|
||||
security: 500,
|
||||
securityMax: 1_000,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
defence: 500,
|
||||
defenceMax: 1_000,
|
||||
wall: 500,
|
||||
wallMax: 1_000,
|
||||
conflict: {},
|
||||
meta: { trust: 50, trade: 100, officer_set: 0, term: 0 },
|
||||
});
|
||||
|
||||
const buildNation = (id: number, capitalCityId: number): Nation => ({
|
||||
id,
|
||||
name: `국가${id}`,
|
||||
color: '#000000',
|
||||
capitalCityId,
|
||||
chiefGeneralId: null,
|
||||
gold: 1_000,
|
||||
rice: 1_000,
|
||||
power: 0,
|
||||
level: 1,
|
||||
typeCode: 'che_중립',
|
||||
meta: {},
|
||||
});
|
||||
|
||||
type MapCityCompact = [number, number, number, number, number, number];
|
||||
|
||||
const buildWorld = (): { world: InMemoryTurnWorld; snapshots: () => PendingYearbookSnapshot[] } => {
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
currentYear: 191,
|
||||
currentMonth: 10,
|
||||
tickSeconds: 600,
|
||||
lastTurnTime: new Date('0191-10-01T00:00:00.000Z'),
|
||||
meta: {},
|
||||
};
|
||||
const actions = new Map<string, MonthlyEventActionHandler>();
|
||||
const snapshot: TurnWorldSnapshot = {
|
||||
scenarioConfig: {
|
||||
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
|
||||
iconPath: '',
|
||||
map: {},
|
||||
const: {},
|
||||
environment: { mapName: map.id, unitSet: 'default' },
|
||||
},
|
||||
map,
|
||||
generals: [],
|
||||
cities: [buildCity(10, 2), buildCity(20, 1), buildCity(21, 1), buildCity(22, 1)],
|
||||
nations: [buildNation(1, 21), buildNation(2, 10)],
|
||||
troops: [],
|
||||
diplomacy: [],
|
||||
events: [
|
||||
{
|
||||
id: 1,
|
||||
targetCode: 'pre_month',
|
||||
priority: 9_000,
|
||||
condition: true,
|
||||
action: [['UpdateCitySupply']],
|
||||
meta: {},
|
||||
},
|
||||
],
|
||||
initialEvents: [],
|
||||
};
|
||||
let world: InMemoryTurnWorld | null = null;
|
||||
// 프로덕션 turnDaemon과 같은 구성 순서: monthly event(pre_month) -> yearbook.
|
||||
const calendarHandler = composeCalendarHandlers(
|
||||
createMonthlyEventHandler({ getWorld: () => world, startYear: 180, actions }),
|
||||
createYearbookHandler({ profileName: 'test', getWorld: () => world }).handler
|
||||
);
|
||||
world = new InMemoryTurnWorld(state, snapshot, {
|
||||
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||
calendarHandler,
|
||||
});
|
||||
actions.set('UpdateCitySupply', createUpdateCitySupplyHandler({ getWorld: () => world!, map }));
|
||||
return {
|
||||
world: world!,
|
||||
snapshots: () => world!.peekDirtyState().pendingYearbookSnapshots,
|
||||
};
|
||||
};
|
||||
|
||||
describe('10월중 보급로 도시 점령 -> 11월 경계切断 -> 10월 연감 스냅샷 순서', () => {
|
||||
it('keeps supply alive during October and cuts it exactly at the month boundary', async () => {
|
||||
const { world } = buildWorld();
|
||||
|
||||
// che_출병의 aftermath effect가 반영된 10월중 상태: 도시 20이 국가2에
|
||||
// 점령되고 Ref처럼 supplyState=1만 쓴다. 나머지 도시는 무변경.
|
||||
world.updateCity(20, { nationId: 2, supplyState: 1 });
|
||||
|
||||
// 10월 진행중에는 고립될 외곽 도시도 아직 보급 상태다(라이브 지도 기준).
|
||||
expect(world.getCityById(22)?.supplyState).toBe(1);
|
||||
expect(world.getCityById(21)?.supplyState).toBe(1);
|
||||
|
||||
await world.advanceMonth(new Date('0191-11-01T00:00:00.000Z'));
|
||||
|
||||
//切断은 정확히 월 경계에서 일어난다.
|
||||
expect(world.getCityById(22)?.supplyState).toBe(0);
|
||||
expect(world.getCityById(21)?.supplyState).toBe(1);
|
||||
expect(world.getCityById(20)?.supplyState).toBe(1);
|
||||
expect(world.getCityById(10)?.supplyState).toBe(1);
|
||||
});
|
||||
|
||||
it('archives the finished month yearbook map after the boundary supply recalculation (Ref #230 order)', async () => {
|
||||
const { world, snapshots } = buildWorld();
|
||||
world.updateCity(20, { nationId: 2, supplyState: 1 });
|
||||
|
||||
expect(snapshots()).toEqual([]);
|
||||
|
||||
await world.advanceMonth(new Date('0191-11-01T00:00:00.000Z'));
|
||||
|
||||
const pending = snapshots();
|
||||
expect(pending).toHaveLength(1);
|
||||
const october = pending[0]!;
|
||||
expect(october.year).toBe(191);
|
||||
expect(october.month).toBe(10);
|
||||
|
||||
const cityList = (october.map as { cityList: MapCityCompact[] }).cityList;
|
||||
const supplyFlagById = new Map(cityList.map((entry) => [entry[0], entry[5]]));
|
||||
// Ref #230 이후 LogHistory는 UpdateCitySupply 뒤에 실행되므로 끝난 10월의
|
||||
// 연감 지도는 이미 고립을 반영한다. 이 순서가 Core의 계약이다.
|
||||
expect(supplyFlagById.get(22)).toBe(0);
|
||||
expect(supplyFlagById.get(21)).toBe(1);
|
||||
expect(supplyFlagById.get(20)).toBe(1);
|
||||
expect(supplyFlagById.get(10)).toBe(1);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user