Files
core2026/packages/logic/test/actions/turn/nationCapitalCommandLogParity.test.ts
T
Hide_D c63a49bd07 fix: 커맨드 차등 생명주기와 로그 그래프를 보강
장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다.

실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
2026-08-23 21:48:29 +00:00

313 lines
11 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { City, General, GeneralTriggerState, Nation } from '../../../src/domain/entities.js';
import {
resolveGeneralAction,
type GeneralActionResolveInputContext,
type GeneralActionResolver,
} from '../../../src/actions/engine.js';
import { ActionDefinition as MoveCapitalAction } from '../../../src/actions/turn/nation/che_천도.js';
import { ActionDefinition as ExpandCityAction } from '../../../src/actions/turn/nation/che_증축.js';
import { ActionDefinition as ReduceCityAction } from '../../../src/actions/turn/nation/che_감축.js';
import { ActionDefinition as RandomMoveCapitalAction } from '../../../src/actions/turn/nation/che_무작위수도이전.js';
import { ActionDefinition as ScorchedEarthAction } from '../../../src/actions/turn/nation/che_초토화.js';
import type { TurnCommandEnv } from '../../../src/actions/turn/commandEnv.js';
import { orderLegacyActionLoggerFlush } from '../../../src/logging/actionLogger.js';
import { LogCategory, LogFormat, LogScope, type LogEntryDraft } from '../../../src/logging/types.js';
import type { TurnSchedule } from '../../../src/turn/calendar.js';
import type { MapDefinition } from '../../../src/world/types.js';
const ENV: TurnCommandEnv = {
develCost: 100,
trainDelta: 30,
atmosDelta: 30,
maxTrainByCommand: 100,
maxAtmosByCommand: 100,
sabotageDefaultProb: 0.5,
sabotageProbCoefByStat: 0.1,
sabotageDefenceCoefByGeneralCount: 0.1,
sabotageDamageMin: 10,
sabotageDamageMax: 30,
openingPartYear: 3,
maxGeneral: 500,
defaultNpcGold: 1_000,
defaultNpcRice: 1_000,
defaultCrewTypeId: 1_100,
defaultSpecialDomestic: null,
defaultSpecialWar: null,
initialNationGenLimit: 10,
maxTechLevel: 12,
baseGold: 1_000,
baseRice: 2_000,
maxResourceActionAmount: 10_000,
};
const schedule: TurnSchedule = { entries: [{ startMinute: 0, tickMinutes: 60 }] };
const rng = {
nextFloat1: () => 0,
nextBool: () => false,
nextInt: () => 0,
};
const makeGeneral = (id: number, name = '운영자'): General => ({
id,
name,
nationId: 1,
cityId: 1,
troopId: 0,
stats: { leadership: 80, strength: 70, intelligence: 60 },
experience: 1_000,
dedication: 1_000,
officerLevel: id === 1 ? 12 : 1,
role: {
personality: null,
specialDomestic: null,
specialWar: null,
items: { horse: null, weapon: null, book: null, item: null },
},
injury: 0,
gold: 1_000,
rice: 1_000,
crew: 100,
crewTypeId: 1_100,
train: 100,
atmos: 100,
age: 30,
npcState: 0,
triggerState: { flags: {}, counters: {}, modifiers: {}, meta: {} },
meta: { killturn: 24, betray: 0 },
});
const makeNation = (): Nation => ({
id: 1,
name: '위',
color: '#111111',
capitalCityId: 1,
chiefGeneralId: 1,
gold: 1_000_000,
rice: 1_000_000,
power: 1_000,
level: 1,
typeCode: 'che_명가',
meta: { capset: 0, can_무작위수도이전: 1, surlimit: 0 },
});
const makeCity = (id: number, name: string, nationId: number): City => ({
id,
name,
nationId,
level: 5,
state: 0,
population: 200_000,
populationMax: 300_000,
agriculture: 3_000,
agricultureMax: 4_000,
commerce: 3_000,
commerceMax: 4_000,
security: 3_000,
securityMax: 4_000,
supplyState: 1,
frontState: 0,
defence: 3_000,
defenceMax: 4_000,
wall: 3_000,
wallMax: 4_000,
conflict: {},
meta: { trust: 80, trade: 100 },
});
const mapStats = {
population: 200_000,
agriculture: 3_000,
commerce: 3_000,
security: 3_000,
defence: 3_000,
wall: 3_000,
};
const map: MapDefinition = {
id: 'nation-capital-log-parity',
name: '국가 명령 로그',
cities: [
{
id: 1,
name: '허창',
level: 5,
region: 1,
position: { x: 0, y: 0 },
connections: [2],
initial: mapStats,
max: mapStats,
},
{
id: 2,
name: '낙양',
level: 5,
region: 1,
position: { x: 1, y: 0 },
connections: [1],
initial: mapStats,
max: mapStats,
},
],
};
const resolveLogs = <Args>(
resolver: GeneralActionResolver<GeneralTriggerState, Args>,
context: GeneralActionResolveInputContext & Record<string, unknown>,
args: Args
): LogEntryDraft[] =>
orderLegacyActionLoggerFlush(
resolveGeneralAction(resolver, context, { now: new Date('2026-08-23T00:00:00.000Z'), schedule }, args).logs
);
const projectLogs = (logs: readonly LogEntryDraft[]) =>
logs.map((log) => [
log.scope,
log.category,
log.generalId ?? null,
log.nationId ?? null,
log.format,
log.legacyFlushGroup ?? 0,
log.text,
]);
const expectedActorLoggerFlush = (params: {
actorId?: number;
nationId?: number;
generalHistory: string;
generalAction: string;
nationHistory: string;
globalHistory: string;
globalSummary: string;
}) => [
[LogScope.GENERAL, LogCategory.HISTORY, params.actorId ?? 1, null, LogFormat.YEAR_MONTH, 0, params.generalHistory],
[LogScope.GENERAL, LogCategory.ACTION, params.actorId ?? 1, null, LogFormat.MONTH, 0, params.generalAction],
[LogScope.NATION, LogCategory.HISTORY, null, params.nationId ?? 1, LogFormat.YEAR_MONTH, 0, params.nationHistory],
[LogScope.SYSTEM, LogCategory.HISTORY, null, null, LogFormat.YEAR_MONTH, 0, params.globalHistory],
[LogScope.SYSTEM, LogCategory.SUMMARY, null, null, LogFormat.MONTH, 0, params.globalSummary],
];
describe('nation capital command Ref ActionLogger parity', () => {
it('che_천도', () => {
const general = makeGeneral(1);
const nation = makeNation();
const capitalCity = makeCity(1, '허창', 1);
const destCity = makeCity(2, '낙양', 1);
const logs = resolveLogs(
new MoveCapitalAction(ENV),
{ general, city: capitalCity, nation, destCity, map, nationCities: [capitalCity, destCity], rng },
{ destCityID: destCity.id }
);
expect(projectLogs(logs)).toEqual(
expectedActorLoggerFlush({
generalHistory: '<G><b>낙양</b></>으로 <M>천도</>명령',
generalAction: '<G><b>낙양</b></>으로 천도했습니다.',
nationHistory: '<Y>운영자</>가 <G><b>낙양</b></>으로 <M>천도</> 명령',
globalHistory: '<S><b>【천도】</b></><D><b>위</b></>가 <G><b>낙양</b></>으로 <M>천도</>하였습니다.',
globalSummary: '<Y>운영자</>가 <G><b>낙양</b></>으로 <M>천도</>를 명령하였습니다.',
})
);
});
it.each([
{
action: '증축',
resolver: new ExpandCityAction(ENV),
globalHistoryPrefix: '<C><b>【증축】</b></>',
},
{
action: '감축',
resolver: new ReduceCityAction(ENV),
globalHistoryPrefix: '<M><b>【감축】</b></>',
},
])('che_$action', ({ action, resolver, globalHistoryPrefix }) => {
const general = makeGeneral(1);
const nation = makeNation();
const capitalCity = makeCity(1, '낙양', 1);
const logs = resolveLogs(resolver, { general, city: capitalCity, nation, capitalCity, rng }, {});
expect(projectLogs(logs)).toEqual(
expectedActorLoggerFlush({
generalHistory: `<G><b>낙양</b></>을 <M>${action}</>`,
generalAction: `<G><b>낙양</b></>을 ${action}했습니다.`,
nationHistory: `<Y>운영자</>가 <G><b>낙양</b></>을 <M>${action}</>`,
globalHistory: `${globalHistoryPrefix}<D><b>위</b></>가 <G><b>낙양</b></>을 <M>${action}</>하였습니다.`,
globalSummary: `<Y>운영자</>가 <G><b>낙양</b></>을 <M>${action}</>하였습니다.`,
})
);
});
it('che_무작위수도이전', () => {
const general = makeGeneral(1);
const follower = makeGeneral(2, '부하');
const nation = makeNation();
const capitalCity = makeCity(1, '허창', 1);
const destCity = makeCity(2, '낙양', 0);
const logs = resolveLogs(
new RandomMoveCapitalAction(),
{
general,
city: capitalCity,
nation,
neutralCandidateCities: [destCity],
nationGenerals: [general, follower],
oldCapitalCity: capitalCity,
rng,
},
{}
);
expect(projectLogs(logs)).toEqual([
[
LogScope.GENERAL,
LogCategory.ACTION,
follower.id,
null,
LogFormat.PLAIN,
-1,
'국가 수도를 <G><b>낙양</b></>으로 옮겼습니다.',
],
...expectedActorLoggerFlush({
generalHistory: '<G><b>낙양</b></>으로 <M>무작위 수도 이전</>',
generalAction: '<G><b>낙양</b></>으로 국가를 옮겼습니다.',
nationHistory: '<Y>운영자</>가 <G><b>낙양</b></>으로 <M>무작위 수도 이전</>',
globalHistory:
'<S><b>【무작위 수도 이전】</b></><D><b>위</b></>가 <G><b>낙양</b></>으로 <M>수도 이전</>하였습니다.',
globalSummary: '<Y>운영자</>가 <G><b>낙양</b></>으로 <M>수도 이전</>하였습니다.',
}),
]);
});
it('che_초토화', () => {
const general = makeGeneral(1);
const follower = makeGeneral(2, '부하');
const nation = makeNation();
const capitalCity = makeCity(1, '허창', 1);
const destCity = makeCity(2, '낙양', 1);
const logs = resolveLogs(
new ScorchedEarthAction(),
{
general,
city: capitalCity,
nation,
destCity,
destNation: nation,
friendlyGenerals: [general, follower],
rng,
},
{ destCityId: destCity.id }
);
expect(projectLogs(logs)).toEqual(
expectedActorLoggerFlush({
generalHistory: '<G><b>낙양</b></>을 <M>초토화</> 명령',
generalAction: '<G><b>낙양</b></>을 초토화했습니다.',
nationHistory: '<Y>운영자</>가 <G><b>낙양</b></>을 <M>초토화</> 명령',
globalHistory: '<S><b>【초토화】</b></><D><b>위</b></>가 <G><b>낙양</b></>을 <M>초토화</>하였습니다.',
globalSummary: '<Y>운영자</>가 <G><b>낙양</b></>을 <M>초토화</>하였습니다.',
})
);
});
});