Add sabotage value boundary parity

This commit is contained in:
2026-07-26 03:59:16 +00:00
parent b27c529a3d
commit d8e94c428c
5 changed files with 115 additions and 10 deletions
@@ -554,6 +554,101 @@ integration('general sabotage probability clamp matrix', () => {
);
});
type SabotageValueBoundaryCase = {
name: string;
action: 'che_화계' | 'che_선동' | 'che_파괴' | 'che_탈취';
stat: 'leadership' | 'strength' | 'intelligence';
fixturePatches: FixturePatches;
};
const sabotageValueBoundaryCases: SabotageValueBoundaryCase[] = [
{
name: 'fire attack does not reduce agriculture or commerce below zero',
action: 'che_화계',
stat: 'intelligence',
fixturePatches: { cities: { 70: { agriculture: 1, commerce: 1 } } },
},
{
name: 'agitation does not reduce security or trust below zero',
action: 'che_선동',
stat: 'leadership',
fixturePatches: { cities: { 70: { security: 1, trust: 1 } } },
},
{
name: 'destruction does not reduce defence or wall below zero',
action: 'che_파괴',
stat: 'strength',
fixturePatches: { cities: { 70: { defence: 1, wall: 1 } } },
},
{
name: 'seizure does not take more than supplied nation resources',
action: 'che_탈취',
stat: 'strength',
fixturePatches: { nations: { 2: { gold: 1, rice: 1 } } },
},
{
name: 'seizure does not reduce unsupplied city resources below zero',
action: 'che_탈취',
stat: 'strength',
fixturePatches: {
cities: { 70: { agriculture: 1, commerce: 1, supplyState: 0 } },
},
},
];
const hasSuccessfulSabotageLog = (logs: Array<Record<string, unknown>>): boolean =>
logs.some((entry) => typeof entry.text === 'string' && entry.text.includes('성공했습니다.'));
integration('general sabotage value boundary matrix', () => {
it.each(sabotageValueBoundaryCases)(
'$name matches the legacy clamped state delta',
async ({ action, stat, fixturePatches }) => {
const request = buildRequest(
action,
{ destCityID: 70 },
{ [stat]: 100 },
{
...fixturePatches,
generals: {
...fixturePatches.generals,
2: { ...fixturePatches.generals?.[2], [stat]: 10 },
},
cities: {
...fixturePatches.cities,
70: {
...fixturePatches.cities?.[70],
security: 0,
securityMax: 2_000,
},
},
}
);
request.setup!.world!.hiddenSeed = 'general-value-0';
const reference = runReferenceTurnCommandTraceRequest(
workspaceRoot!,
request as unknown as Record<string, unknown>
);
const core = await runCoreTurnCommandTrace(request, reference.before);
expect(reference.execution.outcome).toMatchObject({ completed: true });
expect(core.execution.outcome).toMatchObject({
requestedAction: action,
actionKey: action,
usedFallback: false,
});
expect(hasSuccessfulSabotageLog(reference.after.logs)).toBe(true);
expect(hasSuccessfulSabotageLog(core.after.logs)).toBe(true);
expect(core.rng).toEqual(reference.rng);
expect(
compareTurnSnapshotDeltas(reference.before, reference.after, core.before, core.after, {
ignoredPathPatterns: ignoredLifecyclePaths,
})
).toEqual([]);
},
120_000
);
});
type GeneralConstraintCase = {
name: string;
action: string;