merge: match random capital move semantics

This commit is contained in:
2026-07-26 08:17:24 +00:00
7 changed files with 329 additions and 23 deletions
@@ -706,6 +706,7 @@ export const createReservedTurnHandler = async (options: {
requestedAction: string; requestedAction: string;
actionKey: string; actionKey: string;
usedFallback: boolean; usedFallback: boolean;
completed?: boolean;
blockedReason?: string; blockedReason?: string;
aiState?: ReturnType<GeneralAI['getDebugState']>; aiState?: ReturnType<GeneralAI['getDebugState']>;
}) => void; }) => void;
@@ -827,7 +828,13 @@ export const createReservedTurnHandler = async (options: {
applyNextTurnAt: boolean, applyNextTurnAt: boolean,
alternativeDepth = 0, alternativeDepth = 0,
sharedActionRng?: RandUtil sharedActionRng?: RandUtil
): { nextTurnAt?: Date; actionKey: string; usedFallback: boolean; blockedReason?: string } => { ): {
nextTurnAt?: Date;
actionKey: string;
usedFallback: boolean;
completed: boolean;
blockedReason?: string;
} => {
const resolvedDefinition = resolveDefinition(command.action, definitionMap, fallbackDefinition); const resolvedDefinition = resolveDefinition(command.action, definitionMap, fallbackDefinition);
const rawArgs = extractArgsRecord(command.args); const rawArgs = extractArgsRecord(command.args);
const parsedArgs = resolvedDefinition.parseArgs(rawArgs); const parsedArgs = resolvedDefinition.parseArgs(rawArgs);
@@ -1028,7 +1035,7 @@ export const createReservedTurnHandler = async (options: {
executionDefinition.getProgressText?.(actionContext, actionArgs, nextTerm, termMax) ?? executionDefinition.getProgressText?.(actionContext, actionArgs, nextTerm, termMax) ??
`${definition.name} 수행중... (${nextTerm}/${termMax})`; `${definition.name} 수행중... (${nextTerm}/${termMax})`;
logs.push(createActionLog(progressText)); logs.push(createActionLog(progressText));
return { actionKey, usedFallback, blockedReason }; return { actionKey, usedFallback, completed: false, blockedReason };
} }
} }
@@ -1047,7 +1054,7 @@ export const createReservedTurnHandler = async (options: {
currentGeneral = resolution.general as TurnGeneral; currentGeneral = resolution.general as TurnGeneral;
currentCity = resolution.city ?? currentCity; currentCity = resolution.city ?? currentCity;
currentNation = resolution.nation ?? currentNation; currentNation = resolution.nation ?? currentNation;
if (!resolution.alternative && !usedFallback) { if (!resolution.alternative && !usedFallback && resolution.completed) {
currentGeneral = applyLegacyGeneralProgression( currentGeneral = applyLegacyGeneralProgression(
currentGeneral, currentGeneral,
generalBeforeExecution, generalBeforeExecution,
@@ -1059,6 +1066,7 @@ export const createReservedTurnHandler = async (options: {
!resolution.alternative && !resolution.alternative &&
kind === 'nation' && kind === 'nation' &&
!usedFallback && !usedFallback &&
resolution.completed &&
definition.countsAsInheritanceActiveAction definition.countsAsInheritanceActiveAction
) { ) {
const meta = { ...currentGeneral.meta }; const meta = { ...currentGeneral.meta };
@@ -1070,6 +1078,7 @@ export const createReservedTurnHandler = async (options: {
!resolution.alternative && !resolution.alternative &&
kind === 'general' && kind === 'general' &&
!usedFallback && !usedFallback &&
resolution.completed &&
executionDefinition.getInheritanceActiveActionAmount executionDefinition.getInheritanceActiveActionAmount
) { ) {
const amount = executionDefinition.getInheritanceActiveActionAmount(actionContext, actionArgs); const amount = executionDefinition.getInheritanceActiveActionAmount(actionContext, actionArgs);
@@ -1086,7 +1095,7 @@ export const createReservedTurnHandler = async (options: {
(resolution.created.nations as Nation[]).find((n) => n.id === currentGeneral.nationId) ?? (resolution.created.nations as Nation[]).find((n) => n.id === currentGeneral.nationId) ??
currentNation; currentNation;
} }
if (!resolution.alternative && kind === 'general' && !usedFallback) { if (!resolution.alternative && kind === 'general' && !usedFallback && resolution.completed) {
const actionChangedLastTurn = const actionChangedLastTurn =
JSON.stringify(currentGeneral.lastTurn ?? {}) !== lastTurnBeforeExecution; JSON.stringify(currentGeneral.lastTurn ?? {}) !== lastTurnBeforeExecution;
const nextMeta = { ...currentGeneral.meta }; const nextMeta = { ...currentGeneral.meta };
@@ -1107,7 +1116,13 @@ export const createReservedTurnHandler = async (options: {
}, },
}; };
} }
if (!resolution.alternative && kind === 'nation' && !usedFallback && currentNation) { if (
!resolution.alternative &&
kind === 'nation' &&
!usedFallback &&
resolution.completed &&
currentNation
) {
const metaKey = nationLastTurnKey(currentGeneral.officerLevel); const metaKey = nationLastTurnKey(currentGeneral.officerLevel);
const nextMeta: Record<string, unknown> = { const nextMeta: Record<string, unknown> = {
...currentNation.meta, ...currentNation.meta,
@@ -1267,6 +1282,7 @@ export const createReservedTurnHandler = async (options: {
nextTurnAt: applyNextTurnAt ? resolution.nextTurnAt : undefined, nextTurnAt: applyNextTurnAt ? resolution.nextTurnAt : undefined,
actionKey, actionKey,
usedFallback, usedFallback,
completed: resolution.completed,
blockedReason, blockedReason,
}; };
}; };
@@ -1418,6 +1434,7 @@ export const createReservedTurnHandler = async (options: {
requestedAction: nationCommand.action, requestedAction: nationCommand.action,
actionKey: nationResult.actionKey, actionKey: nationResult.actionKey,
usedFallback: nationResult.usedFallback, usedFallback: nationResult.usedFallback,
completed: nationResult.completed,
...(nationResult.blockedReason ? { blockedReason: nationResult.blockedReason } : {}), ...(nationResult.blockedReason ? { blockedReason: nationResult.blockedReason } : {}),
...(nationAiState ? { aiState: nationAiState } : {}), ...(nationAiState ? { aiState: nationAiState } : {}),
}); });
@@ -1464,6 +1481,7 @@ export const createReservedTurnHandler = async (options: {
? { ? {
actionKey: DEFAULT_ACTION, actionKey: DEFAULT_ACTION,
usedFallback: true, usedFallback: true,
completed: false,
blockedReason: '블럭 대상자입니다.', blockedReason: '블럭 대상자입니다.',
} }
: runAction('general', generalDefinitions, generalFallback, generalCommand, true); : runAction('general', generalDefinitions, generalFallback, generalCommand, true);
@@ -1474,6 +1492,7 @@ export const createReservedTurnHandler = async (options: {
requestedAction: generalCommand.action, requestedAction: generalCommand.action,
actionKey: generalResult.actionKey, actionKey: generalResult.actionKey,
usedFallback: generalResult.usedFallback, usedFallback: generalResult.usedFallback,
completed: generalResult.completed,
...(generalResult.blockedReason ? { blockedReason: generalResult.blockedReason } : {}), ...(generalResult.blockedReason ? { blockedReason: generalResult.blockedReason } : {}),
...(generalAiState ? { aiState: generalAiState } : {}), ...(generalAiState ? { aiState: generalAiState } : {}),
}); });
+3
View File
@@ -105,6 +105,7 @@ export type GeneralActionEffect<TriggerState extends GeneralTriggerState = Gener
export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> { export interface GeneralActionOutcome<TriggerState extends GeneralTriggerState = GeneralTriggerState> {
effects: GeneralActionEffect<TriggerState>[]; effects: GeneralActionEffect<TriggerState>[];
completed?: boolean;
alternative?: { alternative?: {
commandKey: string; commandKey: string;
args: unknown; args: unknown;
@@ -120,6 +121,7 @@ export interface GeneralActionResolution {
general: General; general: General;
city?: City; city?: City;
nation?: Nation | null; nation?: Nation | null;
completed: boolean;
nextTurnAt: Date; nextTurnAt: Date;
logs: LogEntryDraft[]; logs: LogEntryDraft[];
effects: GeneralActionEffect[]; effects: GeneralActionEffect[];
@@ -381,6 +383,7 @@ export const resolveGeneralAction = <TriggerState extends GeneralTriggerState =
const resolution: GeneralActionResolution = { const resolution: GeneralActionResolution = {
general: nextWorld.general as General, general: nextWorld.general as General,
nation: nextWorld.nation as Nation | null, nation: nextWorld.nation as Nation | null,
completed: outcome?.completed !== false,
nextTurnAt, nextTurnAt,
logs, logs,
effects: pendingEffects, effects: pendingEffects,
@@ -32,6 +32,7 @@ export interface RandomMoveCapitalResolveContext<
> extends GeneralActionResolveContext<TriggerState> { > extends GeneralActionResolveContext<TriggerState> {
neutralCandidateCities: City[]; neutralCandidateCities: City[];
nationGenerals: General<TriggerState>[]; nationGenerals: General<TriggerState>[];
oldCapitalCity?: City;
} }
const ACTION_NAME = '무작위 수도 이전'; const ACTION_NAME = '무작위 수도 이전';
@@ -81,13 +82,14 @@ export class ActionDefinition<
context: RandomMoveCapitalResolveContext<TriggerState>, context: RandomMoveCapitalResolveContext<TriggerState>,
_args: RandomMoveCapitalArgs _args: RandomMoveCapitalArgs
): GeneralActionOutcome<TriggerState> { ): GeneralActionOutcome<TriggerState> {
const { general, nation, neutralCandidateCities, nationGenerals, rng } = context; const { general, nation, neutralCandidateCities, nationGenerals, oldCapitalCity, rng } = context;
if (!nation) { if (!nation) {
return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] }; return { effects: [createLogEffect('국가 정보가 없습니다.', { scope: LogScope.GENERAL })] };
} }
if (neutralCandidateCities.length === 0) { if (neutralCandidateCities.length === 0) {
return { return {
completed: false,
effects: [ effects: [
createLogEffect(`이동할 수 있는 도시가 없습니다.`, { createLogEffect(`이동할 수 있는 도시가 없습니다.`, {
scope: LogScope.GENERAL, scope: LogScope.GENERAL,
@@ -124,6 +126,7 @@ export class ActionDefinition<
createCityPatchEffect( createCityPatchEffect(
{ {
nationId: nation.id, nationId: nation.id,
conflict: {},
}, },
destCity.id destCity.id
), ),
@@ -132,6 +135,11 @@ export class ActionDefinition<
{ {
nationId: 0, nationId: 0,
frontState: 0, frontState: 0,
conflict: {},
meta: {
...oldCapitalCity?.meta,
officer_set: 0,
},
}, },
oldCityId! oldCityId!
), ),
@@ -213,11 +221,13 @@ export const actionContextBuilder: ActionContextBuilder = (base, options) => {
const allCities = worldRef.listCities(); const allCities = worldRef.listCities();
const neutralCandidateCities = allCities.filter((c) => c.nationId === 0 && c.level >= 5 && c.level <= 6); const neutralCandidateCities = allCities.filter((c) => c.nationId === 0 && c.level >= 5 && c.level <= 6);
const nationGenerals = worldRef.listGenerals().filter((g) => g.nationId === base.general.nationId); const nationGenerals = worldRef.listGenerals().filter((g) => g.nationId === base.general.nationId);
const oldCapitalCity = base.nation?.capitalCityId ? worldRef.getCityById(base.nation.capitalCityId) : undefined;
return { return {
...base, ...base,
neutralCandidateCities, neutralCandidateCities,
nationGenerals, nationGenerals,
...(oldCapitalCity ? { oldCapitalCity } : {}),
}; };
}; };
+2 -7
View File
@@ -39,7 +39,7 @@ export const beOpeningPart = (): Constraint => ({
return { kind: 'deny', reason: '초반 제한 중에는 불가능합니다.' }; return { kind: 'deny', reason: '초반 제한 중에는 불가능합니다.' };
} }
if (relYear + 1 <= openingPartYear) { if (relYear + 1 < openingPartYear) {
return allow(); return allow();
} }
@@ -47,12 +47,7 @@ export const beOpeningPart = (): Constraint => ({
}, },
}); });
export const reqEnvValue = ( export const reqEnvValue = (key: string, comp: CompareOperator, reqVal: unknown, failMessage: string): Constraint => ({
key: string,
comp: CompareOperator,
reqVal: unknown,
failMessage: string
): Constraint => ({
name: 'reqEnvValue', name: 'reqEnvValue',
requires: () => [{ kind: 'env', key }], requires: () => [{ kind: 'env', key }],
test: (_ctx, view) => { test: (_ctx, view) => {
@@ -169,10 +169,12 @@ export const projectCoreDatabaseSnapshot = (rows: {
defenceMax: row.defenceMax, defenceMax: row.defenceMax,
wall: row.wall, wall: row.wall,
wallMax: row.wallMax, wallMax: row.wallMax,
conflict: asRecord(row.conflict),
state: readNumber(meta, 'state'), state: readNumber(meta, 'state'),
term: readNumber(meta, 'term'), term: readNumber(meta, 'term'),
trust: row.trust, trust: row.trust,
trade: row.trade, trade: row.trade,
officerSet: readNumber(meta, 'officer_set'),
}; };
}); });
const nations = rows.nations.map((row) => { const nations = rows.nations.map((row) => {
@@ -18,10 +18,7 @@ import type {
TurnWorldSnapshot, TurnWorldSnapshot,
TurnWorldState, TurnWorldState,
} from '@sammo-ts/game-engine/turn/types.js'; } from '@sammo-ts/game-engine/turn/types.js';
import { import { applyPersistedRankRowsToMeta, buildLegacyComparableRankRows } from '@sammo-ts/game-engine/turn/rankData.js';
applyPersistedRankRowsToMeta,
buildLegacyComparableRankRows,
} from '@sammo-ts/game-engine/turn/rankData.js';
import { import {
canonicalizeTurnCommandArgs, canonicalizeTurnCommandArgs,
@@ -121,6 +118,10 @@ class TracingRng implements RNG {
const asRecord = (value: unknown): Record<string, unknown> => const asRecord = (value: unknown): Record<string, unknown> =>
typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {}; typeof value === 'object' && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
const asNumberRecord = (value: unknown): Record<string, number> =>
Object.fromEntries(
Object.entries(asRecord(value)).filter((entry): entry is [string, number] => typeof entry[1] === 'number')
);
const readNumber = (record: Record<string, unknown>, key: string, fallback = 0): number => { const readNumber = (record: Record<string, unknown>, key: string, fallback = 0): number => {
const value = record[key]; const value = record[key];
@@ -392,12 +393,13 @@ const buildWorldInput = (
id: definition.id, id: definition.id,
name: readString(row, 'name', definition.name), name: readString(row, 'name', definition.name),
nationId: readNumber(row, 'nationId'), nationId: readNumber(row, 'nationId'),
level: level: observedCityRows.has(definition.id)
randomFoundingCandidateCityIds === null ? readNumber(row, 'level', definition.level)
? readNumber(row, 'level', definition.level) : randomFoundingCandidateCityIds === null
: randomFoundingCandidateCityIds.has(definition.id) ? definition.level
? 5 : randomFoundingCandidateCityIds.has(definition.id)
: 4, ? 5
: 4,
state: readNumber(row, 'state'), state: readNumber(row, 'state'),
population: readNumber(row, 'population', definition.initial.population), population: readNumber(row, 'population', definition.initial.population),
populationMax: readNumber(row, 'populationMax', definition.max.population), populationMax: readNumber(row, 'populationMax', definition.max.population),
@@ -413,10 +415,12 @@ const buildWorldInput = (
defenceMax: readNumber(row, 'defenceMax', definition.max.defence), defenceMax: readNumber(row, 'defenceMax', definition.max.defence),
wall: readNumber(row, 'wall', definition.initial.wall), wall: readNumber(row, 'wall', definition.initial.wall),
wallMax: readNumber(row, 'wallMax', definition.max.wall), wallMax: readNumber(row, 'wallMax', definition.max.wall),
conflict: asNumberRecord(row.conflict),
meta: { meta: {
trust: readNumber(row, 'trust', map.defaults?.trust ?? 50), trust: readNumber(row, 'trust', map.defaults?.trust ?? 50),
trade: readNumber(row, 'trade', map.defaults?.trade ?? 100), trade: readNumber(row, 'trade', map.defaults?.trade ?? 100),
term: readNumber(row, 'term'), term: readNumber(row, 'term'),
officer_set: readNumber(row, 'officerSet'),
}, },
}; };
}), }),
@@ -567,10 +571,12 @@ const projectWorld = (
defenceMax: city.defenceMax, defenceMax: city.defenceMax,
wall: toDatabaseInt(city.wall), wall: toDatabaseInt(city.wall),
wallMax: city.wallMax, wallMax: city.wallMax,
conflict: city.conflict ?? {},
state: city.state, state: city.state,
term: readNumber(city.meta, 'term'), term: readNumber(city.meta, 'term'),
trust: readNumber(city.meta, 'trust'), trust: readNumber(city.meta, 'trust'),
trade: readNumber(city.meta, 'trade'), trade: readNumber(city.meta, 'trade'),
officerSet: readNumber(city.meta, 'officer_set'),
})), })),
nations: nations nations: nations
.filter((nation) => selector.nationIds.has(nation.id)) .filter((nation) => selector.nationIds.has(nation.id))
@@ -22,6 +22,7 @@ const readNationMeta = (row: { meta?: unknown } | undefined): Record<string, unk
typeof row?.meta === 'object' && row.meta !== null && !Array.isArray(row.meta) typeof row?.meta === 'object' && row.meta !== null && !Array.isArray(row.meta)
? (row.meta as Record<string, unknown>) ? (row.meta as Record<string, unknown>)
: {}; : {};
const readGeneralMeta = readNationMeta;
const NPC_SEIZURE_MESSAGE_TEXT = '몰수를 하다니... 이것이 윗사람이 할 짓이란 말입니까...'; const NPC_SEIZURE_MESSAGE_TEXT = '몰수를 하다니... 이것이 윗사람이 할 짓이란 말입니까...';
const ignoredLifecyclePaths = [ const ignoredLifecyclePaths = [
@@ -252,6 +253,7 @@ const buildRequest = (
...Object.keys(fixturePatches.cities ?? {}) ...Object.keys(fixturePatches.cities ?? {})
.map(Number) .map(Number)
.filter((id) => id !== 3 && id !== 70), .filter((id) => id !== 3 && id !== 70),
...(fixturePatches.randomFoundingCandidateCityIds ?? []).filter((id) => id !== 3 && id !== 70),
], ],
nationIds: [1, 2], nationIds: [1, 2],
logAfterId: 0, logAfterId: 0,
@@ -2130,3 +2132,272 @@ integration('nation reduce city level, value, recovery, and city constraints', (
).toEqual([]); ).toEqual([]);
}, 120_000); }, 120_000);
}); });
type RandomCapitalOutcome = 'fallback' | 'incomplete' | 'completed';
const randomCapitalCases: Array<{
name: string;
fixturePatches?: FixturePatches;
candidateCityIds: number[];
flag?: number | null;
officerLevel?: number;
outcome: RandomCapitalOutcome;
}> = [
{
name: 'rejects a source city occupied by another nation',
fixturePatches: { cities: { 3: { nationId: 2 } } },
candidateCityIds: [70],
outcome: 'fallback',
},
{
name: 'rejects an unsupplied source city',
fixturePatches: { cities: { 3: { supplyState: 0 } } },
candidateCityIds: [70],
outcome: 'fallback',
},
{
name: 'rejects a non-lord actor',
candidateCityIds: [70],
officerLevel: 11,
outcome: 'fallback',
},
{
name: 'rejects at the opening-part year boundary',
fixturePatches: { world: { year: 182 } },
candidateCityIds: [70],
outcome: 'fallback',
},
{
name: 'rejects a missing remaining-use flag',
candidateCityIds: [70],
flag: null,
outcome: 'fallback',
},
{
name: 'rejects a zero remaining-use flag',
candidateCityIds: [70],
flag: 0,
outcome: 'fallback',
},
{
name: 'stays incomplete when no eligible city exists',
candidateCityIds: [],
outcome: 'incomplete',
},
{
name: 'stays incomplete when the only neutral city is level four',
fixturePatches: { cities: { 70: { level: 4 } } },
candidateCityIds: [70],
outcome: 'incomplete',
},
{
name: 'stays incomplete when the only neutral city is level seven',
fixturePatches: { cities: { 70: { level: 7 } } },
candidateCityIds: [70],
outcome: 'incomplete',
},
{
name: 'allows a single level-five neutral city',
fixturePatches: { cities: { 70: { level: 5 } } },
candidateCityIds: [70],
outcome: 'completed',
},
{
name: 'allows a single level-six neutral city',
fixturePatches: { cities: { 70: { level: 6 } } },
candidateCityIds: [70],
outcome: 'completed',
},
{
name: 'matches legacy ordering and choice across two eligible cities',
fixturePatches: {
cities: {
70: { level: 5 },
71: {
nationId: 0,
level: 6,
conflict: { 2: 30 },
officerSet: 6,
},
},
},
candidateCityIds: [71, 70],
outcome: 'completed',
},
];
integration('nation random capital constraints, candidates, RNG, and city reset effects', () => {
it.each(randomCapitalCases)(
'$name matches legacy completion, destination, city resets, RNG, and semantic delta',
async ({ fixturePatches, candidateCityIds, flag = 1, officerLevel = 12, outcome }) => {
const request = buildRequest('che_무작위수도이전', undefined, {
...fixturePatches,
world: {
year: 181,
...fixturePatches?.world,
},
generals: {
...fixturePatches?.generals,
1: {
...fixturePatches?.generals?.[1],
officerLevel,
},
},
nations: {
...fixturePatches?.nations,
1: {
...fixturePatches?.nations?.[1],
meta: {
...(flag === null ? {} : { can_무작위수도이전: flag }),
},
turnLastByOfficerLevel: {
[officerLevel]: {
command: '무작위 수도 이전',
arg: {},
term: 1,
},
},
},
},
cities: {
...fixturePatches?.cities,
3: {
conflict: { 2: 10 },
officerSet: 7,
...fixturePatches?.cities?.[3],
},
70: {
nationId: 0,
level: candidateCityIds.includes(70) ? 5 : 4,
conflict: { 2: 20 },
officerSet: 5,
...fixturePatches?.cities?.[70],
},
},
randomFoundingCandidateCityIds: candidateCityIds,
});
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
const referenceNationBefore = reference.before.nations.find((entry) => entry.id === 1);
const referenceNationAfter = reference.after.nations.find((entry) => entry.id === 1);
const coreNationBefore = core.before.nations.find((entry) => entry.id === 1);
const coreNationAfter = core.after.nations.find((entry) => entry.id === 1);
const referenceGeneralBefore = reference.before.generals.find((entry) => entry.id === 1);
const referenceGeneralAfter = reference.after.generals.find((entry) => entry.id === 1);
const coreGeneralBefore = core.before.generals.find((entry) => entry.id === 1);
const coreGeneralAfter = core.after.generals.find((entry) => entry.id === 1);
const completed = outcome === 'completed';
const incomplete = outcome === 'incomplete';
expect(reference.execution.outcome).toMatchObject({
// The ref runner infers completion from the previous term=1.
// A no-candidate run returns false without resetting that term,
// so its heuristic reports a false positive for incomplete cases.
completed: outcome !== 'fallback',
});
expect(core.execution.outcome).toMatchObject({
requestedAction: 'che_무작위수도이전',
actionKey: outcome === 'fallback' ? '휴식' : 'che_무작위수도이전',
usedFallback: outcome === 'fallback',
...(outcome === 'fallback' ? {} : { completed }),
});
expect(readNumericField(referenceNationAfter, 'capitalCityId')).toBe(
completed ? readNumericField(coreNationAfter, 'capitalCityId') : 3
);
expect(readNumericField(coreNationAfter, 'capitalCityId')).toBe(
completed ? readNumericField(referenceNationAfter, 'capitalCityId') : 3
);
for (const [before, after] of [
[referenceGeneralBefore, referenceGeneralAfter],
[coreGeneralBefore, coreGeneralAfter],
] as const) {
expect(readNumericField(after, 'experience') - readNumericField(before, 'experience')).toBe(
completed ? 10 : 0
);
expect(readNumericField(after, 'dedication') - readNumericField(before, 'dedication')).toBe(
completed ? 10 : 0
);
}
if (completed) {
const destCityId = readNumericField(referenceNationAfter, 'capitalCityId');
expect(candidateCityIds).toContain(destCityId);
for (const [beforeSnapshot, afterSnapshot] of [
[reference.before, reference.after],
[core.before, core.after],
] as const) {
const oldCity = afterSnapshot.cities.find((entry) => entry.id === 3);
const destCity = afterSnapshot.cities.find((entry) => entry.id === destCityId);
const actor = afterSnapshot.generals.find((entry) => entry.id === 1);
const compatriot = afterSnapshot.generals.find((entry) => entry.id === 3);
const foreignGeneralBefore = beforeSnapshot.generals.find((entry) => entry.id === 2);
const foreignGeneralAfter = afterSnapshot.generals.find((entry) => entry.id === 2);
expect(oldCity).toMatchObject({
nationId: 0,
frontState: 0,
conflict: {},
officerSet: 0,
});
expect(destCity).toMatchObject({
nationId: 1,
conflict: {},
});
expect(readNumericField(actor, 'cityId')).toBe(destCityId);
expect(readNumericField(compatriot, 'cityId')).toBe(destCityId);
expect(readNumericField(foreignGeneralAfter, 'cityId')).toBe(
readNumericField(foreignGeneralBefore, 'cityId')
);
}
expect(readNumericField(readNationMeta(referenceNationAfter), 'can_무작위수도이전')).toBe(0);
expect(readNumericField(readNationMeta(coreNationAfter), 'can_무작위수도이전')).toBe(0);
expect(reference.rng).toHaveLength(1);
expect(reference.rng[0]).toMatchObject({
operation: 'nextInt',
arguments: { maxInclusive: candidateCityIds.length - 1 },
});
} else {
expect(reference.rng).toEqual([]);
expect(readNationMeta(referenceNationAfter).can_무작위수도이전).toBe(
readNationMeta(referenceNationBefore).can_무작위수도이전
);
expect(readNationMeta(coreNationAfter).can_무작위수도이전).toBe(
readNationMeta(coreNationBefore).can_무작위수도이전
);
}
if (incomplete) {
expect(reference.execution.outcome).toMatchObject({
completed: true,
lastTurn: {
command: '무작위 수도 이전',
term: 1,
},
});
expect(readNationMeta(coreNationAfter).turn_last_12).toMatchObject({
command: '무작위 수도 이전',
term: 1,
});
expect(readNumericField(readGeneralMeta(coreGeneralAfter), 'inherit_active_action')).toBe(
readNumericField(readGeneralMeta(coreGeneralBefore), 'inherit_active_action')
);
const noCandidateText = '이동할 수 있는 도시가 없습니다.';
expect(reference.after.logs.slice(reference.before.logs.length).map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining(noCandidateText)])
);
expect(core.after.logs.map((entry) => entry.text)).toEqual(
expect.arrayContaining([expect.stringContaining(noCandidateText)])
);
}
expect(core.rng).toEqual(reference.rng);
expect(
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
ignoredPathPatterns: ignoredLifecyclePaths,
})
).toEqual([]);
},
120_000
);
});