fix: preserve nation and recruit AI boundaries
This commit is contained in:
@@ -5,8 +5,8 @@ import {
|
||||
isCrewTypeAvailable,
|
||||
} from '@sammo-ts/logic/world/unitSet.js';
|
||||
import { buildWarConfig } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
|
||||
import { CommandResolver as RecruitmentCommandResolver } from '@sammo-ts/logic/actions/turn/general/che_징병.js';
|
||||
import type { CrewTypeDefinition, General, WarArmTypes } from '@sammo-ts/logic';
|
||||
import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js';
|
||||
|
||||
import type { GeneralAI } from '../core.js';
|
||||
import { asRecord, readMetaNumber, roundTo } from '../../aiUtils.js';
|
||||
@@ -106,19 +106,20 @@ export const do징병 = (ai: GeneralAI) => {
|
||||
}
|
||||
const armTypeWeights = forcedArmType > 0 ? [] : buildRecruitArmTypeWeights(ai.general, warConfig.armTypes);
|
||||
let armTypeDraw: number | null = null;
|
||||
const armType = forcedArmType > 0
|
||||
? forcedArmType
|
||||
: traceEnabled
|
||||
? (() => {
|
||||
armTypeDraw = ai.rng.nextFloat1();
|
||||
let cursor = armTypeDraw * armTypeWeights.reduce((sum, [, weight]) => sum + Math.max(0, weight), 0);
|
||||
for (const [candidate, weight] of armTypeWeights) {
|
||||
if (cursor <= weight) return candidate;
|
||||
cursor -= Math.max(0, weight);
|
||||
}
|
||||
return armTypeWeights.at(-1)![0];
|
||||
})()
|
||||
: ai.rng.choiceUsingWeightPair(armTypeWeights);
|
||||
const armType =
|
||||
forcedArmType > 0
|
||||
? forcedArmType
|
||||
: traceEnabled
|
||||
? (() => {
|
||||
armTypeDraw = ai.rng.nextFloat1();
|
||||
let cursor = armTypeDraw * armTypeWeights.reduce((sum, [, weight]) => sum + Math.max(0, weight), 0);
|
||||
for (const [candidate, weight] of armTypeWeights) {
|
||||
if (cursor <= weight) return candidate;
|
||||
cursor -= Math.max(0, weight);
|
||||
}
|
||||
return armTypeWeights.at(-1)![0];
|
||||
})()
|
||||
: ai.rng.choiceUsingWeightPair(armTypeWeights);
|
||||
trace('arm-type', { forcedArmType, armType, armTypeDraw, armTypeWeights });
|
||||
|
||||
const candidates = (ai.unitSet?.crewTypes ?? [])
|
||||
@@ -170,39 +171,31 @@ export const do징병 = (ai: GeneralAI) => {
|
||||
const crewTypeId = picked.id;
|
||||
|
||||
let crewAmount = crewAmountBase;
|
||||
const rawGoldCost = (picked.cost * getTechCost(tech) * crewAmount) / 100;
|
||||
// Ref asks the concrete che_징병 command for getCost() before deciding
|
||||
// whether to halve the requested crew. That path includes personality,
|
||||
// traits, items, and the final integer rounding; using the raw unit price
|
||||
// makes che_출세 (+20% cost) recruit a full stack incorrectly.
|
||||
const actionPipeline = new GeneralActionPipeline(ai.commandEnv.generalActionModules ?? []);
|
||||
const goldCost = Math.round(
|
||||
actionPipeline.onCalcDomestic(
|
||||
{
|
||||
general: ai.general,
|
||||
nation,
|
||||
...(ai.worldRef
|
||||
? {
|
||||
worldView: {
|
||||
listGenerals: () => ai.worldRef!.listGenerals(),
|
||||
listGeneralsByCity: (cityId: number) =>
|
||||
ai.worldRef!.listGenerals().filter((candidate) => candidate.cityId === cityId),
|
||||
listNations: () => ai.worldRef!.listNations(),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
time: {
|
||||
year: ai.world.currentYear,
|
||||
month: ai.world.currentMonth,
|
||||
startYear: ai.startYear,
|
||||
},
|
||||
},
|
||||
'징병',
|
||||
'cost',
|
||||
rawGoldCost,
|
||||
{ armType: picked.armType }
|
||||
)
|
||||
);
|
||||
// whether to halve the requested crew. In particular, that command caps
|
||||
// the charge at the actually refillable amount when the selected type is
|
||||
// already equipped, then applies traits/items and legacy rounding.
|
||||
const recruitContext = {
|
||||
general: ai.general,
|
||||
nation,
|
||||
...(ai.worldRef
|
||||
? {
|
||||
worldView: {
|
||||
listGenerals: () => ai.worldRef!.listGenerals(),
|
||||
listGeneralsByCity: (cityId: number) =>
|
||||
ai.worldRef!.listGenerals().filter((candidate) => candidate.cityId === cityId),
|
||||
listNations: () => ai.worldRef!.listNations(),
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
time: {
|
||||
year: ai.world.currentYear,
|
||||
month: ai.world.currentMonth,
|
||||
startYear: ai.startYear,
|
||||
},
|
||||
};
|
||||
const recruitment = new RecruitmentCommandResolver(ai.commandEnv.generalActionModules ?? [], ai.commandEnv);
|
||||
const goldCost = recruitment.getCost(recruitContext, crewTypeId, crewAmount, picked).gold;
|
||||
const killCrew = readMetaNumber(generalMeta, 'rank_killcrew', readMetaNumber(generalMeta, 'killcrew', 0));
|
||||
const deathCrew = readMetaNumber(generalMeta, 'rank_deathcrew', readMetaNumber(generalMeta, 'deathcrew', 0));
|
||||
const expectedCrewLoss = Math.floor((crewAmount * killCrew * 1.2) / Math.max(deathCrew, 1));
|
||||
|
||||
@@ -277,7 +277,7 @@ const normalizeGeneralMetaDatabaseIntegers = (meta: TurnGeneral['meta']): TurnGe
|
||||
// Keeping fractional action results in memory until the monthly flush changes
|
||||
// later aggregation (notably nation power), even if the eventual DB rows look
|
||||
// identical after they are rounded.
|
||||
const normalizeGeneralDatabaseIntegers = (general: TurnGeneral): TurnGeneral => ({
|
||||
export const normalizeGeneralDatabaseIntegers = (general: TurnGeneral): TurnGeneral => ({
|
||||
...general,
|
||||
nationId: toLegacyDatabaseInt(general.nationId),
|
||||
cityId: toLegacyDatabaseInt(general.cityId),
|
||||
|
||||
@@ -44,6 +44,7 @@ import { asRecord, JosaUtil, LEGACY_RANK_DATA_TYPES, LiteHashDRBG, RandUtil } fr
|
||||
import type { ConstraintContext, StateView } from '@sammo-ts/logic';
|
||||
|
||||
import type { GeneralTurnHandler, GeneralTurnResult } from './inMemoryWorld.js';
|
||||
import { normalizeGeneralDatabaseIntegers } from './inMemoryWorld.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { TurnDiplomacy, TurnGeneral, TurnWorldState } from './types.js';
|
||||
import type { ReservedTurnEntry } from './reservedTurnStore.js';
|
||||
@@ -1691,6 +1692,12 @@ export const createReservedTurnHandler = async (options: {
|
||||
nationAiState = ai.getDebugState();
|
||||
}
|
||||
const nationResult = runAction('nation', nationDefinitions, nationFallback, nationCommand, false);
|
||||
// Ref persists a completed nation command before it chooses and
|
||||
// executes the general command for the same turn. Preserve that
|
||||
// MariaDB INT boundary so fractional rewards cannot leak into the
|
||||
// following command or its AI refresh.
|
||||
currentGeneral = normalizeGeneralDatabaseIntegers(currentGeneral);
|
||||
worldOverlay?.syncGeneral(currentGeneral);
|
||||
if (
|
||||
worldView &&
|
||||
(process.env.CORE_AI_TRACE_GENERAL_IDS?.split(',') ?? []).includes(String(currentGeneral.id))
|
||||
|
||||
Reference in New Issue
Block a user