merge: match population move city constraints
This commit is contained in:
@@ -40,7 +40,7 @@ const AMOUNT_LIMIT = 100000;
|
||||
const MIN_AVAILABLE_RECRUIT_POP = 30000;
|
||||
const LEGACY_NUMERIC_PATTERN = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?$/;
|
||||
|
||||
const normalizeAmount = (value: unknown): unknown => {
|
||||
const normalizeInteger = (value: unknown): unknown => {
|
||||
let numericValue = value;
|
||||
if (typeof value === 'string') {
|
||||
const trimmed = value.trim();
|
||||
@@ -52,14 +52,15 @@ const normalizeAmount = (value: unknown): unknown => {
|
||||
if (typeof numericValue !== 'number' || !Number.isFinite(numericValue)) {
|
||||
return value;
|
||||
}
|
||||
return Math.min(Math.trunc(numericValue), AMOUNT_LIMIT);
|
||||
return Math.trunc(numericValue);
|
||||
};
|
||||
const normalizeAmount = (value: unknown): unknown => {
|
||||
const normalized = normalizeInteger(value);
|
||||
return typeof normalized === 'number' ? Math.min(normalized, AMOUNT_LIMIT) : normalized;
|
||||
};
|
||||
|
||||
const ARGS_SCHEMA = z.object({
|
||||
destCityId: z.preprocess(
|
||||
(value) => (typeof value === 'number' ? Math.floor(value) : value),
|
||||
z.number().int().positive()
|
||||
),
|
||||
destCityId: z.preprocess(normalizeInteger, z.number().int().positive()),
|
||||
amount: z.preprocess(normalizeAmount, z.number().int().min(0).max(AMOUNT_LIMIT)),
|
||||
});
|
||||
export type PopulationMoveArgs = z.infer<typeof ARGS_SCHEMA>;
|
||||
|
||||
@@ -185,6 +185,25 @@ const buildRequest = (
|
||||
trade: 100,
|
||||
...fixturePatches.cities?.[70],
|
||||
},
|
||||
...Object.entries(fixturePatches.cities ?? {})
|
||||
.filter(([id]) => !['3', '70'].includes(id))
|
||||
.map(([id, patch]) => ({
|
||||
id: Number(id),
|
||||
nationId: 0,
|
||||
population: 100_000,
|
||||
agriculture: 1_000,
|
||||
commerce: 1_000,
|
||||
security: 1_000,
|
||||
defence: 1_000,
|
||||
wall: 1_000,
|
||||
supplyState: 1,
|
||||
frontState: 0,
|
||||
state: 0,
|
||||
term: 0,
|
||||
trust: 80,
|
||||
trade: 100,
|
||||
...patch,
|
||||
})),
|
||||
],
|
||||
generals: [
|
||||
{ ...general(1, 1, 3, 12), ...fixturePatches.generals?.[1] },
|
||||
@@ -227,7 +246,13 @@ const buildRequest = (
|
||||
},
|
||||
observe: {
|
||||
generalIds: [1, 2, 3, 4, 5, 6],
|
||||
cityIds: [3, 70],
|
||||
cityIds: [
|
||||
3,
|
||||
70,
|
||||
...Object.keys(fixturePatches.cities ?? {})
|
||||
.map(Number)
|
||||
.filter((id) => id !== 3 && id !== 70),
|
||||
],
|
||||
nationIds: [1, 2],
|
||||
logAfterId: 0,
|
||||
messageAfterId: 0,
|
||||
@@ -1326,3 +1351,107 @@ integration('nation population move value and resource boundaries', () => {
|
||||
120_000
|
||||
);
|
||||
});
|
||||
|
||||
const populationMoveConstraintCases: Array<{
|
||||
name: string;
|
||||
destCityId: unknown;
|
||||
fixturePatches?: FixturePatches;
|
||||
completed?: boolean;
|
||||
coreResolution?: boolean;
|
||||
}> = [
|
||||
{
|
||||
name: 'rejects a missing destination city',
|
||||
destCityId: 9999,
|
||||
},
|
||||
{
|
||||
name: 'rejects the source city as destination',
|
||||
destCityId: 3,
|
||||
},
|
||||
{
|
||||
name: 'accepts a numeric string destination city ID',
|
||||
destCityId: '70',
|
||||
completed: true,
|
||||
},
|
||||
{
|
||||
name: 'rejects a source city below the minimum population',
|
||||
destCityId: 70,
|
||||
fixturePatches: { cities: { 3: { population: 30_099 } } },
|
||||
},
|
||||
{
|
||||
name: 'rejects a source city occupied by another nation',
|
||||
destCityId: 70,
|
||||
fixturePatches: { cities: { 3: { nationId: 2 } } },
|
||||
},
|
||||
{
|
||||
name: 'rejects a destination city occupied by another nation',
|
||||
destCityId: 70,
|
||||
fixturePatches: { cities: { 70: { nationId: 2 } } },
|
||||
},
|
||||
{
|
||||
name: 'rejects a non-adjacent destination city',
|
||||
destCityId: 73,
|
||||
fixturePatches: { cities: { 73: { nationId: 1, supplyState: 1 } } },
|
||||
},
|
||||
{
|
||||
name: 'rejects an actor below chief rank',
|
||||
destCityId: 70,
|
||||
fixturePatches: { generals: { 1: { officerLevel: 4, officerCityId: 0 } } },
|
||||
coreResolution: false,
|
||||
},
|
||||
{
|
||||
name: 'rejects an unsupplied source city',
|
||||
destCityId: 70,
|
||||
fixturePatches: { cities: { 3: { supplyState: 0 } } },
|
||||
},
|
||||
{
|
||||
name: 'rejects an unsupplied destination city',
|
||||
destCityId: 70,
|
||||
fixturePatches: { cities: { 70: { supplyState: 0 } } },
|
||||
},
|
||||
];
|
||||
|
||||
integration('nation population move target and city constraints', () => {
|
||||
it.each(populationMoveConstraintCases)(
|
||||
'$name matches legacy fallback, RNG, and semantic delta',
|
||||
async ({ destCityId, fixturePatches, completed = false, coreResolution = true }) => {
|
||||
const request = buildRequest(
|
||||
'cr_인구이동',
|
||||
{ destCityID: destCityId, amount: 1_000 },
|
||||
{
|
||||
...fixturePatches,
|
||||
cities: {
|
||||
...fixturePatches?.cities,
|
||||
70: {
|
||||
nationId: 1,
|
||||
supplyState: 1,
|
||||
...fixturePatches?.cities?.[70],
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
const reference = runReferenceTurnCommandTraceRequest(
|
||||
workspaceRoot!,
|
||||
request as unknown as Record<string, unknown>
|
||||
);
|
||||
const core = await runCoreTurnCommandTrace(request, reference.before);
|
||||
|
||||
expect(reference.execution.outcome).toMatchObject({ completed });
|
||||
if (coreResolution) {
|
||||
expect(core.execution.outcome).toMatchObject({
|
||||
requestedAction: 'cr_인구이동',
|
||||
actionKey: completed ? 'cr_인구이동' : '휴식',
|
||||
usedFallback: !completed,
|
||||
});
|
||||
} else {
|
||||
expect(core.execution.outcome).toBeUndefined();
|
||||
}
|
||||
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