merge: add general alternative command parity
This commit is contained in:
@@ -857,7 +857,8 @@ export const createReservedTurnHandler = async (options: {
|
||||
fallbackDefinition: GeneralActionDefinition,
|
||||
command: ReservedTurnEntry,
|
||||
applyNextTurnAt: boolean,
|
||||
alternativeDepth = 0
|
||||
alternativeDepth = 0,
|
||||
sharedActionRng?: RandUtil
|
||||
): { nextTurnAt?: Date; actionKey: string; usedFallback: boolean; blockedReason?: string } => {
|
||||
const resolvedDefinition = resolveDefinition(command.action, definitionMap, fallbackDefinition);
|
||||
const rawArgs = extractArgsRecord(command.args);
|
||||
@@ -954,11 +955,12 @@ export const createReservedTurnHandler = async (options: {
|
||||
uniqueConfig,
|
||||
getAdditionalOccupiedUniqueItemKeys: options.getAdditionalOccupiedUniqueItemKeys,
|
||||
});
|
||||
let actionRng = sharedActionRng ?? buildRng(actionKey);
|
||||
let baseContext: ActionContextBase = {
|
||||
general: currentGeneral,
|
||||
city: currentCity,
|
||||
nation: currentNation,
|
||||
rng: buildRng(actionKey),
|
||||
rng: actionRng,
|
||||
uniqueLottery,
|
||||
};
|
||||
let specificContext = buildActionContext(
|
||||
@@ -985,11 +987,12 @@ export const createReservedTurnHandler = async (options: {
|
||||
usedFallback = true;
|
||||
blockedReason = '예약된 명령을 실행하지 못했습니다.';
|
||||
logs.push(createActionLog('예약된 명령을 실행하지 못했습니다.'));
|
||||
actionRng = sharedActionRng ?? buildRng(actionKey);
|
||||
baseContext = {
|
||||
general: currentGeneral,
|
||||
city: currentCity,
|
||||
nation: currentNation,
|
||||
rng: buildRng(actionKey),
|
||||
rng: actionRng,
|
||||
};
|
||||
specificContext = baseContext;
|
||||
}
|
||||
@@ -1287,7 +1290,8 @@ export const createReservedTurnHandler = async (options: {
|
||||
args: extractArgsRecord(resolution.alternative.args),
|
||||
},
|
||||
applyNextTurnAt,
|
||||
alternativeDepth + 1
|
||||
alternativeDepth + 1,
|
||||
actionRng
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,31 @@ export const resolveStartYear = (world: ActionContextWorldState, scenarioMeta?:
|
||||
return world.currentYear;
|
||||
};
|
||||
|
||||
const readFiniteInteger = (value: unknown, fallback: number): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return Math.floor(value);
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
export const resolveInitYearMonth = (
|
||||
world: ActionContextWorldState,
|
||||
scenarioMeta?: ScenarioMeta
|
||||
): { year: number; month: number } => {
|
||||
const meta = asRecord(world.meta);
|
||||
const startYear = resolveStartYear(world, scenarioMeta);
|
||||
return {
|
||||
year: readFiniteInteger(meta.initYear, startYear),
|
||||
month: readFiniteInteger(meta.initMonth, 1),
|
||||
};
|
||||
};
|
||||
|
||||
export const resolveTurnTermMinutes = (world: ActionContextWorldState): number =>
|
||||
Math.max(1, Math.round(world.tickSeconds / 60));
|
||||
|
||||
|
||||
@@ -25,6 +25,7 @@ import { z } from 'zod';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { resolveInitYearMonth } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
@@ -223,34 +224,18 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStartYear = (currentYear: number, raw: unknown): number => {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
||||
return Math.floor(raw);
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
const parsed = Number(raw);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
}
|
||||
return currentYear;
|
||||
};
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
const nationId = base.nation?.id ?? base.general.nationId;
|
||||
const currentYear = options.world.currentYear;
|
||||
const currentMonth = options.world.currentMonth;
|
||||
|
||||
const startYear = resolveStartYear(currentYear, options.scenarioMeta?.startYear);
|
||||
const initYear = startYear;
|
||||
const initMonth = 1;
|
||||
const init = resolveInitYearMonth(options.world, options.scenarioMeta);
|
||||
|
||||
return {
|
||||
...base,
|
||||
allCities: options.worldRef?.listCities() ?? [],
|
||||
nationGenerals: options.worldRef?.listGenerals().filter((general) => general.nationId === nationId) ?? [],
|
||||
currentYearMonth: currentYear * 12 + currentMonth - 1,
|
||||
initYearMonth: initYear * 12 + initMonth - 1,
|
||||
initYearMonth: init.year * 12 + init.month - 1,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { LogCategory, LogFormat, LogScope } from '@sammo-ts/logic/logging/types.js';
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { resolveInitYearMonth } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
|
||||
@@ -145,33 +146,19 @@ export class ActionDefinition<
|
||||
}
|
||||
}
|
||||
|
||||
const resolveStartYear = (currentYear: number, raw: unknown): number => {
|
||||
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
||||
return Math.floor(raw);
|
||||
}
|
||||
if (typeof raw === 'string') {
|
||||
const parsed = Number(raw);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
}
|
||||
return currentYear;
|
||||
};
|
||||
|
||||
export const actionContextBuilder: ActionContextBuilder = (base, options) => {
|
||||
const nationId = base.nation?.id ?? base.general.nationId;
|
||||
const worldRef = options.worldRef;
|
||||
const currentYear = options.world.currentYear;
|
||||
const currentMonth = options.world.currentMonth;
|
||||
const initYear = resolveStartYear(currentYear, options.scenarioMeta?.startYear);
|
||||
const initMonth = 1;
|
||||
const init = resolveInitYearMonth(options.world, options.scenarioMeta);
|
||||
|
||||
return {
|
||||
...base,
|
||||
nationGenerals: worldRef?.listGenerals().filter((general) => general.nationId === nationId) ?? [],
|
||||
nationCities: worldRef?.listCities().filter((city) => city.nationId === nationId) ?? [],
|
||||
currentYearMonth: currentYear * 12 + currentMonth - 1,
|
||||
initYearMonth: initYear * 12 + initMonth - 1,
|
||||
initYearMonth: init.year * 12 + init.month - 1,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -160,8 +160,11 @@ const createCommandProfile = (request: TurnCommandFixtureRequest): TurnCommandPr
|
||||
if (!GENERAL_TURN_COMMAND_KEYS.includes(request.action as (typeof GENERAL_TURN_COMMAND_KEYS)[number])) {
|
||||
throw new Error(`Unknown general command: ${request.action}`);
|
||||
}
|
||||
const generalActions = [request.action, '휴식', 'che_인재탐색', 'che_해산', 'che_이동'] as Array<
|
||||
(typeof GENERAL_TURN_COMMAND_KEYS)[number]
|
||||
>;
|
||||
return {
|
||||
general: [request.action as (typeof GENERAL_TURN_COMMAND_KEYS)[number], '휴식'],
|
||||
general: [...new Set(generalActions)],
|
||||
nation: ['휴식'],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -717,6 +717,117 @@ integration('general sabotage injury boundary matrix', () => {
|
||||
);
|
||||
});
|
||||
|
||||
integration('general command alternative matrix', () => {
|
||||
const expectAlternativeParity = async (
|
||||
request: TurnCommandFixtureRequest,
|
||||
expectedActionKey: string,
|
||||
expectedLogText: string
|
||||
): Promise<void> => {
|
||||
const reference = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
);
|
||||
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||
|
||||
expect(reference.execution.outcome).toMatchObject({ completed: false });
|
||||
expect(core.execution.outcome).toMatchObject({
|
||||
requestedAction: request.action,
|
||||
actionKey: expectedActionKey,
|
||||
usedFallback: false,
|
||||
});
|
||||
expect(
|
||||
reference.after.logs.some((entry) => typeof entry.text === 'string' && entry.text.includes(expectedLogText))
|
||||
).toBe(true);
|
||||
expect(
|
||||
core.after.logs.some((entry) => typeof entry.text === 'string' && entry.text.includes(expectedLogText))
|
||||
).toBe(true);
|
||||
expect(core.rng).toEqual(reference.rng);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
};
|
||||
|
||||
it('che_해산 continues into che_인재탐색 with the legacy RNG and state delta', async () => {
|
||||
const request = buildRequest(
|
||||
'che_해산',
|
||||
undefined,
|
||||
{},
|
||||
{
|
||||
world: { initYear: 190, initMonth: 1 },
|
||||
nations: { 1: { level: 0, capitalCityId: 0, typeCode: 'None' } },
|
||||
}
|
||||
);
|
||||
request.setup!.world!.hiddenSeed = 'general-alternative-disband-search';
|
||||
await expectAlternativeParity(request, 'che_인재탐색', '다음 턴부터 해산할 수 있습니다.');
|
||||
}, 120_000);
|
||||
|
||||
it('che_랜덤임관 continues into che_인재탐색 when no eligible nation exists', async () => {
|
||||
const request = buildRequest(
|
||||
'che_랜덤임관',
|
||||
undefined,
|
||||
{ nationId: 0, officerLevel: 0 },
|
||||
{
|
||||
generals: {
|
||||
2: { npcState: 5 },
|
||||
3: { npcState: 5 },
|
||||
},
|
||||
}
|
||||
);
|
||||
request.setup!.world!.hiddenSeed = 'general-alternative-random-appointment-search';
|
||||
await expectAlternativeParity(request, 'che_인재탐색', '임관 가능한 국가가 없습니다.');
|
||||
}, 120_000);
|
||||
|
||||
it('che_무작위건국 continues into che_인재탐색 during the initial month', async () => {
|
||||
const request = buildRequest(
|
||||
'che_무작위건국',
|
||||
{ nationName: '신국', nationType: 'che_도적', colorType: 1 },
|
||||
{},
|
||||
{
|
||||
world: { startYear: 190, initYear: 190, initMonth: 1 },
|
||||
nations: { 1: { level: 0, capitalCityId: 0, typeCode: 'None' } },
|
||||
}
|
||||
);
|
||||
request.setup!.world!.hiddenSeed = 'general-alternative-random-founding-search';
|
||||
await expectAlternativeParity(request, 'che_인재탐색', '다음 턴부터 건국할 수 있습니다.');
|
||||
}, 120_000);
|
||||
|
||||
it('che_무작위건국 continues into che_해산 when no founding city exists', async () => {
|
||||
const request = buildRequest(
|
||||
'che_무작위건국',
|
||||
{ nationName: '신국', nationType: 'che_도적', colorType: 1 },
|
||||
{},
|
||||
{
|
||||
world: { initYear: 180, initMonth: 1, year: 181 },
|
||||
nations: { 1: { level: 0, capitalCityId: 0, typeCode: 'None' } },
|
||||
cities: {
|
||||
3: { nationId: 1, level: 4 },
|
||||
70: { nationId: 0, level: 4 },
|
||||
},
|
||||
randomFoundingCandidateCityIds: [3],
|
||||
}
|
||||
);
|
||||
request.setup!.world!.hiddenSeed = 'general-alternative-random-founding-disband';
|
||||
await expectAlternativeParity(request, 'che_해산', '건국할 수 있는 도시가 없습니다.');
|
||||
}, 120_000);
|
||||
|
||||
it('che_출병 continues into che_이동 when the destination belongs to the actor nation', async () => {
|
||||
const request = buildRequest(
|
||||
'che_출병',
|
||||
{ destCityID: 70 },
|
||||
{},
|
||||
{
|
||||
world: { startYear: 180, year: 185 },
|
||||
cities: { 70: { nationId: 1, supplyState: 1, frontState: 0 } },
|
||||
generals: { 2: { nationId: 1 } },
|
||||
}
|
||||
);
|
||||
request.setup!.world!.hiddenSeed = 'general-alternative-sortie-move';
|
||||
await expectAlternativeParity(request, 'che_이동', '본국입니다.');
|
||||
}, 120_000);
|
||||
});
|
||||
|
||||
type GeneralConstraintCase = {
|
||||
name: string;
|
||||
action: string;
|
||||
|
||||
Reference in New Issue
Block a user