fix: match scenario 2601 monthly seed progression

This commit is contained in:
2026-08-03 18:56:23 +00:00
parent 2f17584075
commit 58b9a230a7
92 changed files with 3696 additions and 587 deletions
@@ -7,6 +7,11 @@ import type { InMemoryTurnWorld, TurnCalendarHandler } from './inMemoryWorld.js'
const MAX_AVAILABLE_WAR_SETTING_COUNT = 10;
const MONTHLY_AVAILABLE_WAR_SETTING_INCREMENT = 2;
export const roundLegacyNationPowerValue = (value: number): number => {
const stabilized = Number(value.toPrecision(15));
return stabilized >= 0 ? Math.floor(stabilized + 0.5) : Math.ceil(stabilized - 0.5);
};
const readNumber = (value: unknown, fallback = 0): number => {
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
@@ -32,15 +37,16 @@ const calculateNationPower = (
): {
power: number;
totalCrew: number;
trace: Record<string, number>;
} => {
const nation = world.getNationById(nationId);
if (!nation) {
return { power: 0, totalCrew: 0 };
return { power: 0, totalCrew: 0, trace: {} };
}
const generals = world.listGenerals().filter((general) => general.nationId === nationId);
const suppliedCities = world.listCities().filter((city) => city.nationId === nationId && city.supplyState === 1);
const generalResources = generals.reduce((sum, general) => sum + general.gold + general.rice, 0);
const resourcePower = Math.round((nation.gold + nation.rice + generalResources) / 100);
const resourcePower = roundLegacyNationPowerValue((nation.gold + nation.rice + generalResources) / 100);
const techPower = readNumber(asRecord(nation.meta).tech);
let cityPower = 0;
@@ -62,7 +68,7 @@ const calculateNationPower = (
city.defenceMax,
0
);
cityPower = maximum > 0 ? Math.round((population * current) / maximum / 100) : 0;
cityPower = maximum > 0 ? roundLegacyNationPowerValue((population * current) / maximum / 100) : 0;
}
let generalPower = 0;
@@ -90,16 +96,29 @@ const calculateNationPower = (
totalCrew += general.crew;
}
const power = Math.round(
const power = roundLegacyNationPowerValue(
(resourcePower +
techPower +
cityPower +
generalPower +
Math.round(dexterityPower / 1000) +
Math.round(experiencePower / 100)) /
roundLegacyNationPowerValue(dexterityPower / 1000) +
roundLegacyNationPowerValue(experiencePower / 100)) /
10
);
return { power, totalCrew };
return {
power,
totalCrew,
trace: {
resourcePower,
techPower,
cityPower,
generalPower,
dexterityPowerRaw: dexterityPower / 1000,
dexterityPower: roundLegacyNationPowerValue(dexterityPower / 1000),
experiencePowerRaw: experiencePower / 100,
experiencePower: roundLegacyNationPowerValue(experiencePower / 100),
},
};
};
const updateNationPower = (world: InMemoryTurnWorld, rng: RandUtil): number => {
@@ -110,10 +129,25 @@ const updateNationPower = (world: InMemoryTurnWorld, rng: RandUtil): number => {
citiesByNation.set(city.nationId, names);
}
const nations = world.listNations().sort((left, right) => left.id - right.id);
// Core keeps an internal nation 0 record, while Ref's nation table/query
// starts at 1. It must not consume a monthly RNG draw or receive power.
const nations = world
.listNations()
.filter((nation) => nation.id > 0)
.sort((left, right) => left.id - right.id);
for (const nation of nations) {
const calculated = calculateNationPower(world, nation.id);
const power = Math.round(calculated.power * rng.nextRange(0.95, 1.05));
const multiplier = rng.nextRange(0.95, 1.05);
const power = roundLegacyNationPowerValue(calculated.power * multiplier);
const traceIds = (process.env.SEED_PARITY_TRACE_NATION_POWER_IDS ?? '')
.split(',')
.map((value) => Number(value.trim()))
.filter(Number.isFinite);
if (traceIds.includes(nation.id)) {
process.stdout.write(
`NATION_POWER_TRACE ${JSON.stringify({ nationId: nation.id, ...calculated.trace, basePower: calculated.power, multiplier, power })}\n`
);
}
const meta = asRecord(nation.meta);
const previousMax = asRecord(meta.max_power);
const previousCities = Array.isArray(previousMax.maxCities)