merge: match random capital move semantics
This commit is contained in:
@@ -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 } : {}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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 } : {}),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user