fix: 커맨드 차등 생명주기와 로그 그래프를 보강
장수 55종과 수뇌 35종의 상태, 로그, 메시지, 예약 턴 비교를 닫습니다. 실제 시나리오 프로필과 대표 PostgreSQL 수명주기, 즉시 외교와 출병 회귀를 추가하고 발견된 Ref 로그 및 생성 장수 저장 차이를 교정합니다.
This commit is contained in:
@@ -0,0 +1,312 @@
|
||||
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>초토화</>하였습니다.',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user