fix command-specific failure log parity
This commit is contained in:
@@ -907,7 +907,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
const constraints = definition.buildConstraints(constraintCtx, actionArgs);
|
||||
const result = evaluateConstraints(constraints, constraintCtx, view);
|
||||
if (result.kind !== 'allow') {
|
||||
const failedCommandName = definition.name;
|
||||
const failedDefinition = definition;
|
||||
definition = fallbackDefinition;
|
||||
actionArgs = definition.parseArgs({}) ?? {};
|
||||
actionKey = definition.key;
|
||||
@@ -915,7 +915,10 @@ export const createReservedTurnHandler = async (options: {
|
||||
const reason = result.kind === 'deny' ? result.reason : '조건을 확인할 수 없습니다.';
|
||||
blockedReason = reason;
|
||||
const meta = result.kind === 'deny' ? { constraintName: result.constraintName } : undefined;
|
||||
logs.push(createActionLog(`${reason} ${failedCommandName} 실패.`, meta));
|
||||
const failureText =
|
||||
failedDefinition.formatConstraintFailure?.(reason, constraintCtx, actionArgs, view) ??
|
||||
`${reason} ${failedDefinition.name} 실패.`;
|
||||
logs.push(createActionLog(failureText, meta));
|
||||
}
|
||||
if (!usedFallback && (kind === 'general' || currentNation)) {
|
||||
const currentYearMonth = joinYearMonth(context.world.currentYear, context.world.currentMonth);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { GeneralActionOutcome, GeneralActionResolveContext } from './engine.js';
|
||||
import type { GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
|
||||
@@ -16,6 +16,12 @@ export interface GeneralActionDefinition<
|
||||
// 커맨드 입력 단계에서 최소 조건만 평가할 때 사용한다.
|
||||
buildMinConstraints?(ctx: ConstraintContext, args: Args): Constraint[];
|
||||
buildConstraints(ctx: ConstraintContext, args: Args): Constraint[];
|
||||
formatConstraintFailure?(
|
||||
reason: string,
|
||||
ctx: ConstraintContext,
|
||||
args: Args,
|
||||
view: StateView
|
||||
): string | null;
|
||||
// NationCommand::addTermStack()/setNextAvailable() 호환 실행 메타데이터.
|
||||
getPreReqTurn?(context: Context, args: Args): number;
|
||||
getPostReqTurn?(context: Context, args: Args): number;
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { JosaUtil } from '@sammo-ts/common';
|
||||
import type { City, General } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
|
||||
export const formatDestCityConstraintFailure = (
|
||||
reason: string,
|
||||
commandName: string,
|
||||
destCityId: number,
|
||||
view: StateView,
|
||||
relation: 'direction' | 'location'
|
||||
): string | null => {
|
||||
const city = view.get({ kind: 'destCity', id: destCityId }) as City | null;
|
||||
if (!city) {
|
||||
return null;
|
||||
}
|
||||
const particle = relation === 'direction' ? JosaUtil.pick(city.name, '로') : '에';
|
||||
return `${reason} <G><b>${city.name}</b></>${particle} ${commandName} 실패.`;
|
||||
};
|
||||
|
||||
export const formatDestGeneralConstraintFailure = (
|
||||
reason: string,
|
||||
commandName: string,
|
||||
destGeneralId: number,
|
||||
view: StateView
|
||||
): string | null => {
|
||||
const general = view.get({ kind: 'destGeneral', id: destGeneralId }) as General | null;
|
||||
if (!general) {
|
||||
return null;
|
||||
}
|
||||
return `${reason} <Y>${general.name}</> ${commandName} 실패.`;
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import { notSameDestCity, nearCity, reqGeneralGold, reqGeneralRice } from '@sammo-ts/logic/constraints/presets.js';
|
||||
import type { GeneralActionDefinition } from '@sammo-ts/logic/actions/definition.js';
|
||||
import type {
|
||||
@@ -18,6 +18,7 @@ import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { normalizeLegacyIntegerArg, parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { formatDestCityConstraintFailure } from '../constraintFailure.js';
|
||||
|
||||
export interface ForcedMoveResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -173,6 +174,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
formatConstraintFailure(
|
||||
reason: string,
|
||||
_ctx: ConstraintContext,
|
||||
args: ForcedMoveArgs,
|
||||
view: StateView
|
||||
): string | null {
|
||||
return formatDestCityConstraintFailure(reason, this.name, args.destCityId, view, 'direction');
|
||||
}
|
||||
|
||||
resolve(context: ForcedMoveResolveContext<TriggerState>, args: ForcedMoveArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { General, GeneralTriggerState } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notSameDestCity,
|
||||
nearCity,
|
||||
@@ -23,6 +23,7 @@ import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js'
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import type { MapDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { formatDestCityConstraintFailure } from '../constraintFailure.js';
|
||||
|
||||
export interface MoveResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -152,6 +153,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
formatConstraintFailure(
|
||||
reason: string,
|
||||
_ctx: ConstraintContext,
|
||||
args: MoveArgs,
|
||||
view: StateView
|
||||
): string | null {
|
||||
return formatDestCityConstraintFailure(reason, this.name, args.destCityId, view, 'direction');
|
||||
}
|
||||
|
||||
resolve(context: MoveResolveContext<TriggerState>, args: MoveArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { City, General, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
notOccupiedDestCity,
|
||||
reqGeneralGold,
|
||||
@@ -22,6 +22,7 @@ import type { ActionContextBase, ActionContextOptions } from '@sammo-ts/logic/ac
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { formatDestCityConstraintFailure } from '../constraintFailure.js';
|
||||
|
||||
export interface SpyResolveContext<
|
||||
TriggerState extends GeneralTriggerState = GeneralTriggerState,
|
||||
@@ -316,6 +317,15 @@ export class ActionDefinition<
|
||||
return [notOccupiedDestCity(), reqGeneralGold(() => cost), reqGeneralRice(() => cost)];
|
||||
}
|
||||
|
||||
formatConstraintFailure(
|
||||
reason: string,
|
||||
_ctx: ConstraintContext,
|
||||
args: SpyArgs,
|
||||
view: StateView
|
||||
): string | null {
|
||||
return formatDestCityConstraintFailure(reason, this.name, args.destCityId, view, 'location');
|
||||
}
|
||||
|
||||
resolve(context: GeneralActionResolveContext<TriggerState>, args: SpyArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
|
||||
@@ -36,6 +36,7 @@ import { increaseMetaNumber, simpleSerialize } from '@sammo-ts/logic/war/utils.j
|
||||
import type { MapDefinition, UnitSetDefinition } from '@sammo-ts/logic/world/types.js';
|
||||
import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionContext.js';
|
||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
import { formatDestCityConstraintFailure } from '../constraintFailure.js';
|
||||
import {
|
||||
buildWarAftermathConfig,
|
||||
buildWarConfig,
|
||||
@@ -304,6 +305,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
formatConstraintFailure(
|
||||
reason: string,
|
||||
_ctx: ConstraintContext,
|
||||
args: DispatchArgs,
|
||||
view: StateView
|
||||
): string | null {
|
||||
return formatDestCityConstraintFailure(reason, this.name, args.destCityId, view, 'direction');
|
||||
}
|
||||
|
||||
resolve(context: DispatchResolveContext<TriggerState>, args: DispatchArgs): GeneralActionOutcome<TriggerState> {
|
||||
void args;
|
||||
const attackerCity = context.city;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RandomGenerator } from '@sammo-ts/common';
|
||||
import type { City, General, GeneralMeta, GeneralTriggerState, Nation } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
disallowDiplomacyBetweenStatus,
|
||||
notBeNeutral,
|
||||
@@ -35,6 +35,7 @@ import { clamp } from 'es-toolkit';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { consumeSuccessfulStrategyItem } from './strategyItemConsumption.js';
|
||||
import { searchDistance } from '@sammo-ts/logic/world/distance.js';
|
||||
import { formatDestCityConstraintFailure } from '../constraintFailure.js';
|
||||
|
||||
export interface FireAttackEnvironment {
|
||||
develCost: number;
|
||||
@@ -436,6 +437,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
formatConstraintFailure(
|
||||
reason: string,
|
||||
_ctx: ConstraintContext,
|
||||
args: FireAttackArgs,
|
||||
view: StateView
|
||||
): string | null {
|
||||
return formatDestCityConstraintFailure(reason, this.name, args.destCityId, view, 'location');
|
||||
}
|
||||
|
||||
resolve(context: FireAttackResolveContext<TriggerState>, args: FireAttackArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { City, General, GeneralMeta, GeneralTriggerState, TriggerValue } from '@sammo-ts/logic/domain/entities.js';
|
||||
import type { Constraint, ConstraintContext } from '@sammo-ts/logic/constraints/types.js';
|
||||
import type { Constraint, ConstraintContext, StateView } from '@sammo-ts/logic/constraints/types.js';
|
||||
import {
|
||||
beChief,
|
||||
existsDestGeneral,
|
||||
@@ -26,6 +26,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import type { NationTurnCommandSpec } from './index.js';
|
||||
import { z } from 'zod';
|
||||
import { normalizeLegacyIntegerArg, parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { formatDestGeneralConstraintFailure } from '../constraintFailure.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destGeneralId: z.preprocess(normalizeLegacyIntegerArg, z.number().int()),
|
||||
@@ -176,6 +177,15 @@ export class ActionDefinition<
|
||||
];
|
||||
}
|
||||
|
||||
formatConstraintFailure(
|
||||
reason: string,
|
||||
_ctx: ConstraintContext,
|
||||
args: AssignmentArgs,
|
||||
view: StateView
|
||||
): string | null {
|
||||
return formatDestGeneralConstraintFailure(reason, this.name, args.destGeneralId, view);
|
||||
}
|
||||
|
||||
resolve(context: AssignmentResolveContext<TriggerState>, args: AssignmentArgs): GeneralActionOutcome<TriggerState> {
|
||||
return this.resolver.resolve(context, args);
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ export const occupiedDestCity = (): Constraint => ({
|
||||
if (destCity.nationId === baseNationId) {
|
||||
return allow();
|
||||
}
|
||||
return { kind: 'deny', reason: '아국이 아닙니다.' };
|
||||
return { kind: 'deny', reason: '대상 도시가 아국이 아닙니다.' };
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -3681,6 +3681,54 @@ const constraintCases: GeneralConstraintCase[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const targetFailureLogCases: Array<{
|
||||
action: 'che_이동' | 'che_강행' | 'che_출병' | 'che_첩보' | 'che_화계';
|
||||
args: Record<string, unknown>;
|
||||
fixturePatches?: FixturePatches;
|
||||
}> = [
|
||||
{ action: 'che_이동', args: { destCityID: 3 } },
|
||||
{ action: 'che_강행', args: { destCityID: 3 } },
|
||||
{
|
||||
action: 'che_출병',
|
||||
args: { destCityID: 3 },
|
||||
fixturePatches: { world: { startYear: 180, year: 185 } },
|
||||
},
|
||||
{ action: 'che_첩보', args: { destCityID: 3 } },
|
||||
{ action: 'che_화계', args: { destCityID: 3 } },
|
||||
];
|
||||
|
||||
integration('general command target-specific constraint failure log parity', () => {
|
||||
it.each(targetFailureLogCases)(
|
||||
'$action preserves the legacy destination name and particle',
|
||||
async ({ action, args, fixturePatches }) => {
|
||||
const request = buildRequest(action, args, {}, fixturePatches);
|
||||
request.setup!.world!.hiddenSeed = `general-target-failure-log-${action}`;
|
||||
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: action,
|
||||
actionKey: '휴식',
|
||||
usedFallback: true,
|
||||
});
|
||||
expect(core.rng).toEqual(reference.rng);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
expect(semanticLogSignatures(core.after.logs)).toEqual(
|
||||
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
|
||||
);
|
||||
},
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
integration('general command full-constraint fallback matrix', () => {
|
||||
it.each(constraintCases)(
|
||||
'$name: $action falls back exactly like legacy',
|
||||
|
||||
@@ -1341,7 +1341,10 @@ integration('nation personnel command boundary parity', () => {
|
||||
})
|
||||
).toEqual([]);
|
||||
if (!completed) {
|
||||
if (originalCommandFailure) {
|
||||
if (
|
||||
originalCommandFailure ||
|
||||
(action === 'che_발령' && args.destGeneralID !== 9)
|
||||
) {
|
||||
expect(semanticLogSignatures(nationCommandLogs(core.after.logs))).toEqual(
|
||||
semanticLogSignatures(addedReferenceLogs(reference.before, reference.after.logs))
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user