Merge branch 'main' into feature/general-access-tracking
This commit is contained in:
@@ -219,6 +219,11 @@ minimum/maximum clamps for gift, donation, and rice trade; donation against
|
||||
available and minimum resources; and gift reserve and self-target rejection.
|
||||
They compare normalized last-turn arguments, RNG, fallback, and semantic state
|
||||
deltas.
|
||||
Eleven nation resource cases cover 100-unit rounding and minimum/maximum
|
||||
clamps for award and seizure; award against available gold, the base-rice
|
||||
reserve, and self-target rejection; and seizure against target holdings and
|
||||
self-target rejection. They compare exact target resource deltas, RNG,
|
||||
fallback, and semantic state deltas.
|
||||
Requests that omit the required argument object remain unverified because the
|
||||
reference runner did not terminate within the bounded comparison run.
|
||||
This is not yet a claim that every command-specific
|
||||
|
||||
@@ -16,7 +16,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { normalizeResourceActionAmount } from './resourceAmount.js';
|
||||
import { normalizeResourceActionAmount } from '../resourceAmount.js';
|
||||
|
||||
export interface TradeEnvironment {
|
||||
exchangeFee?: number;
|
||||
|
||||
@@ -20,7 +20,7 @@ import type { TurnCommandEnv } from '@sammo-ts/logic/actions/turn/commandEnv.js'
|
||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { normalizeResourceActionAmount } from './resourceAmount.js';
|
||||
import { normalizeResourceActionAmount } from '../resourceAmount.js';
|
||||
|
||||
const ACTION_NAME = '증여';
|
||||
const ACTION_KEY = 'che_증여';
|
||||
|
||||
@@ -21,7 +21,7 @@ import { defaultActionContextBuilder } from '@sammo-ts/logic/actions/turn/action
|
||||
import { tryApplyUniqueLottery } from '@sammo-ts/logic/rewards/uniqueLottery.js';
|
||||
import type { GeneralTurnCommandSpec } from './index.js';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { normalizeResourceActionAmount } from './resourceAmount.js';
|
||||
import { normalizeResourceActionAmount } from '../resourceAmount.js';
|
||||
|
||||
const ACTION_NAME = '헌납';
|
||||
const ACTION_KEY = 'che_헌납';
|
||||
|
||||
@@ -24,13 +24,11 @@ import type { ActionContextBuilder } from '@sammo-ts/logic/actions/turn/actionCo
|
||||
import { clamp } from 'es-toolkit';
|
||||
import { z } from 'zod';
|
||||
import { parseArgsWithSchema } from '../parseArgs.js';
|
||||
import { normalizeResourceActionAmount } from '../resourceAmount.js';
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
isGold: z.boolean(),
|
||||
amount: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value / 100) * 100 : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
amount: z.number(),
|
||||
destGeneralID: z.number(),
|
||||
});
|
||||
export type SeizureArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
@@ -56,9 +54,13 @@ export class ActionDefinition<
|
||||
if (!data) {
|
||||
return null;
|
||||
}
|
||||
const amount = normalizeResourceActionAmount(data.amount, this.env.maxResourceActionAmount);
|
||||
if (amount === null) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
...data,
|
||||
amount: clamp(data.amount, 100, this.env.maxResourceActionAmount ?? 10000),
|
||||
amount,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -144,10 +144,10 @@ export class ActionResolver<
|
||||
createLogEffect(
|
||||
`<Y>${context.destGeneral.name}</>에게 ${label} <C>${amountText}</>${amountJosa} 수여했습니다.`,
|
||||
{
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
scope: LogScope.GENERAL,
|
||||
category: LogCategory.ACTION,
|
||||
format: LogFormat.MONTH,
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
@@ -199,7 +199,7 @@ export class ActionDefinition<
|
||||
requirements.push({ kind: 'destGeneral', id: ctx.destGeneralId });
|
||||
}
|
||||
|
||||
if (ctx.destGeneralId === ctx.actorId) {
|
||||
if (args.destGeneralId === ctx.actorId) {
|
||||
return [denyWithReason('본인입니다')];
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,8 @@ const configuredWorkspaceRoot = process.env.TURN_DIFFERENTIAL_WORKSPACE_ROOT;
|
||||
const workspaceRoot = configuredWorkspaceRoot ?? findTurnDifferentialWorkspaceRoot(process.cwd());
|
||||
const integration = describe.skipIf(!workspaceRoot || process.env.TURN_DIFFERENTIAL_REFERENCE !== '1');
|
||||
|
||||
const readGold = (row: { gold?: unknown } | undefined): number => (typeof row?.gold === 'number' ? row.gold : 0);
|
||||
|
||||
const ignoredLifecyclePaths = [
|
||||
/^generalTurns/,
|
||||
/^nationTurns/,
|
||||
@@ -469,3 +471,159 @@ integration('nation command success matrix', () => {
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
const nationResourceAmountCases: Array<{
|
||||
name: string;
|
||||
action: 'che_포상' | 'che_몰수';
|
||||
args: Record<string, unknown>;
|
||||
expectedAmount: number;
|
||||
}> = [
|
||||
{
|
||||
name: 'award rounds a half unit up',
|
||||
action: 'che_포상',
|
||||
args: { isGold: true, amount: 150, destGeneralID: 3 },
|
||||
expectedAmount: 200,
|
||||
},
|
||||
{
|
||||
name: 'award clamps below the minimum',
|
||||
action: 'che_포상',
|
||||
args: { isGold: true, amount: 1, destGeneralID: 3 },
|
||||
expectedAmount: 100,
|
||||
},
|
||||
{
|
||||
name: 'award clamps above the maximum',
|
||||
action: 'che_포상',
|
||||
args: { isGold: true, amount: 10_050, destGeneralID: 3 },
|
||||
expectedAmount: 10_000,
|
||||
},
|
||||
{
|
||||
name: 'seizure rounds a half unit up',
|
||||
action: 'che_몰수',
|
||||
args: { isGold: true, amount: 150, destGeneralID: 3 },
|
||||
expectedAmount: 200,
|
||||
},
|
||||
{
|
||||
name: 'seizure clamps below the minimum',
|
||||
action: 'che_몰수',
|
||||
args: { isGold: true, amount: 1, destGeneralID: 3 },
|
||||
expectedAmount: 100,
|
||||
},
|
||||
{
|
||||
name: 'seizure clamps above the maximum',
|
||||
action: 'che_몰수',
|
||||
args: { isGold: true, amount: 10_050, destGeneralID: 3 },
|
||||
expectedAmount: 10_000,
|
||||
},
|
||||
];
|
||||
|
||||
integration('nation command resource amount normalization matrix', () => {
|
||||
it.each(nationResourceAmountCases)(
|
||||
'$name matches legacy rounding and clamp semantics',
|
||||
async ({ action, args, expectedAmount }) => {
|
||||
const request = buildRequest(action, args);
|
||||
const reference = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
);
|
||||
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||
const referenceTargetBefore = reference.before.generals.find((entry) => entry.id === 3);
|
||||
const referenceTargetAfter = reference.after.generals.find((entry) => entry.id === 3);
|
||||
const coreTargetBefore = core.before.generals.find((entry) => entry.id === 3);
|
||||
const coreTargetAfter = core.after.generals.find((entry) => entry.id === 3);
|
||||
const referenceAmount =
|
||||
action === 'che_포상'
|
||||
? readGold(referenceTargetAfter) - readGold(referenceTargetBefore)
|
||||
: readGold(referenceTargetBefore) - readGold(referenceTargetAfter);
|
||||
const coreAmount =
|
||||
action === 'che_포상'
|
||||
? readGold(coreTargetAfter) - readGold(coreTargetBefore)
|
||||
: readGold(coreTargetBefore) - readGold(coreTargetAfter);
|
||||
|
||||
expect(reference.execution.outcome).toMatchObject({ completed: true });
|
||||
expect(core.execution.outcome).toMatchObject({
|
||||
requestedAction: action,
|
||||
actionKey: action,
|
||||
usedFallback: false,
|
||||
});
|
||||
expect(referenceAmount).toBe(expectedAmount);
|
||||
expect(coreAmount).toBe(expectedAmount);
|
||||
expect(core.rng).toEqual(reference.rng);
|
||||
expect(
|
||||
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
|
||||
ignoredPathPatterns: ignoredLifecyclePaths,
|
||||
})
|
||||
).toEqual([]);
|
||||
},
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
const nationResourceBoundaryCases: Array<{
|
||||
name: string;
|
||||
action: 'che_포상' | 'che_몰수';
|
||||
args: Record<string, unknown>;
|
||||
fixturePatches?: FixturePatches;
|
||||
completed: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: 'award is limited to the available nation gold',
|
||||
action: 'che_포상',
|
||||
args: { isGold: true, amount: 10_000, destGeneralID: 3 },
|
||||
fixturePatches: { nations: { 1: { gold: 5_000 } } },
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
name: 'award keeps the legacy base rice reserve',
|
||||
action: 'che_포상',
|
||||
args: { isGold: false, amount: 10_000, destGeneralID: 3 },
|
||||
fixturePatches: { nations: { 1: { rice: 2_100 } } },
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
name: 'award rejects the actor as its target',
|
||||
action: 'che_포상',
|
||||
args: { isGold: true, amount: 100, destGeneralID: 1 },
|
||||
completed: false,
|
||||
},
|
||||
{
|
||||
name: 'seizure is limited to the target general gold',
|
||||
action: 'che_몰수',
|
||||
args: { isGold: true, amount: 1_000, destGeneralID: 3 },
|
||||
fixturePatches: { generals: { 3: { gold: 50 } } },
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
name: 'seizure rejects the actor as its target',
|
||||
action: 'che_몰수',
|
||||
args: { isGold: true, amount: 100, destGeneralID: 1 },
|
||||
completed: false,
|
||||
},
|
||||
];
|
||||
|
||||
integration('nation command resource balance and target boundaries', () => {
|
||||
it.each(nationResourceBoundaryCases)(
|
||||
'$name matches legacy completion, RNG, and state delta',
|
||||
async ({ action, args, fixturePatches, completed }) => {
|
||||
const request = buildRequest(action, args, fixturePatches);
|
||||
const reference = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
);
|
||||
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||
|
||||
expect(reference.execution.outcome).toMatchObject({ completed });
|
||||
expect(core.execution.outcome).toMatchObject({
|
||||
requestedAction: action,
|
||||
actionKey: completed ? action : '휴식',
|
||||
usedFallback: !completed,
|
||||
});
|
||||
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