Implement crew type execution model

This commit is contained in:
2026-07-25 05:16:11 +00:00
parent 2824e5af13
commit c5da507df9
24 changed files with 1165 additions and 48 deletions
@@ -1,9 +1,44 @@
import { getTechCost, isCrewTypeAvailable } from '@sammo-ts/logic/world/unitSet.js';
import {
findCrewTypeById,
getCrewTypePickScore,
getTechCost,
isCrewTypeAvailable,
} from '@sammo-ts/logic/world/unitSet.js';
import { buildWarConfig } from '@sammo-ts/logic/actions/turn/actionContextHelpers.js';
import type { CrewTypeDefinition, General, WarArmTypes } from '@sammo-ts/logic';
import type { GeneralAI } from '../core.js';
import { asRecord, readMetaNumber, roundTo } from '../../aiUtils.js';
import { t통솔장 } from './helpers.js';
export const buildRecruitArmTypeWeights = (general: General, armTypes: WarArmTypes): Array<[number, number]> => {
const meta = asRecord(general.meta);
const fullStrength = readMetaNumber(meta, 'fullStrength', general.stats.strength);
const fullIntelligence = readMetaNumber(meta, 'fullIntelligence', general.stats.intelligence);
const weights: Array<[number, number]> = [];
if (fullStrength > fullIntelligence * 0.9) {
for (const armType of [armTypes.footman, armTypes.archer, armTypes.cavalry]) {
if (armType === undefined) {
continue;
}
weights.push([armType, Math.sqrt(readMetaNumber(meta, `dex${armType}`, 0) + 500) * fullStrength]);
}
}
if (fullIntelligence > fullStrength * 0.9 && armTypes.wizard !== undefined) {
weights.push([
armTypes.wizard,
Math.sqrt(readMetaNumber(meta, `dex${armTypes.wizard}`, 0) + 500) * fullIntelligence * 3,
]);
}
return weights;
};
const getRequiredTech = (crewType: CrewTypeDefinition): number | null => {
const requirement = crewType.requirements.find((entry) => entry.type === 'ReqTech');
return requirement?.type === 'ReqTech' && typeof requirement.tech === 'number' ? requirement.tech : null;
};
export const do징병 = (ai: GeneralAI) => {
const city = ai.city;
const nation = ai.nation;
@@ -37,9 +72,12 @@ export const do징병 = (ai: GeneralAI) => {
const tech = readMetaNumber(asRecord(nation.meta), 'tech', 0);
const crewAmountBase = ai.general.stats.leadership * 100;
const warConfig = buildWarConfig(ai.scenarioConfig, ai.unitSet);
const forcedArmType = readMetaNumber(asRecord(ai.general.meta), 'armType', 0);
const armType =
readMetaNumber(asRecord(ai.general.meta), 'armType', 0) ||
(ai.general.stats.strength >= ai.general.stats.intelligence * 0.9 ? 1 : 4);
forcedArmType > 0
? forcedArmType
: ai.rng.choiceUsingWeightPair(buildRecruitArmTypeWeights(ai.general, warConfig.armTypes));
const candidates = (ai.unitSet?.crewTypes ?? [])
.filter((crew) => crew.armType === armType)
@@ -56,7 +94,31 @@ export const do징병 = (ai: GeneralAI) => {
if (candidates.length === 0) {
return null;
}
const picked = ai.rng.choiceUsingWeightPair(candidates.map((crew) => [crew, Math.max(1, crew.cost)]));
let picked = ai.rng.choiceUsingWeightPair(
candidates.map((crew) => [crew, getCrewTypePickScore(crew, tech, warConfig.armPerPhase)])
);
if (ai.generalPolicy.can('고급병종')) {
const currentCrewType = findCrewTypeById(ai.unitSet, ai.general.crewTypeId);
if (
currentCrewType &&
isCrewTypeAvailable(ai.unitSet, currentCrewType.id, {
general: ai.general,
nation,
map: ai.map,
cities: ai.worldRef?.listCities() ?? [],
currentYear: ai.world.currentYear,
startYear: ai.startYear,
})
) {
const requiredTech = getRequiredTech(currentCrewType);
if (
requiredTech !== null &&
(requiredTech >= 2000 || (currentCrewType.armType !== armType && requiredTech >= 1000))
) {
picked = currentCrewType;
}
}
}
const crewTypeId = picked.id;
let crewAmount = crewAmountBase;
@@ -15,6 +15,8 @@ import {
ITEM_KEYS,
loadItemModules,
createInheritBuffModules,
compileCrewTypeCatalog,
createCrewTypeWarTriggerRegistry,
} from '@sammo-ts/logic';
import { asRecord } from '@sammo-ts/common';
@@ -70,6 +72,7 @@ export const buildCommandEnv = (config: ScenarioConfig, unitSet?: UnitSetDefinit
const constValues = asRecord(config.const);
return {
...(unitSet ? { unitSet } : {}),
develCost: resolveNumber(constValues, ['develCost', 'develcost', 'develrate'], 0),
minAvailableRecruitPop: resolveNumber(constValues, ['minAvailableRecruitPop'], 30000),
trainDelta: resolveNumber(constValues, ['trainDelta'], DEFAULT_TRAIN_DELTA),
@@ -96,11 +99,7 @@ export const buildCommandEnv = (config: ScenarioConfig, unitSet?: UnitSetDefinit
),
defaultSpecialDomestic: resolveOptionalString(constValues, ['defaultSpecialDomestic']),
defaultSpecialWar: resolveOptionalString(constValues, ['defaultSpecialWar']),
initialNationGenLimit: resolveNumber(
constValues,
['initialNationGenLimit'],
DEFAULT_INITIAL_NATION_GEN_LIMIT
),
initialNationGenLimit: resolveNumber(constValues, ['initialNationGenLimit'], DEFAULT_INITIAL_NATION_GEN_LIMIT),
maxTechLevel: resolveNumber(constValues, ['maxTechLevel'], DEFAULT_MAX_TECH_LEVEL),
baseGold: resolveNumber(constValues, ['baseGold', 'basegold'], DEFAULT_BASE_GOLD),
baseRice: resolveNumber(constValues, ['baseRice', 'baserice'], DEFAULT_BASE_RICE),
@@ -153,10 +152,21 @@ export const buildReservedTurnDefinitions = async (options: {
const itemRegistry = createItemModuleRegistry(itemModules);
const itemActionModules = createItemActionModules(itemRegistry);
const inheritBuffModules = createInheritBuffModules();
options.env.generalActionModules = [...(options.env.generalActionModules ?? []), ...itemActionModules.general];
options.env.warActionModules = [...(options.env.warActionModules ?? []), ...itemActionModules.war];
options.env.generalActionModules.push(inheritBuffModules.general);
options.env.warActionModules.push(inheritBuffModules.war);
const crewTypeCatalog = options.env.unitSet?.crewTypes?.length
? compileCrewTypeCatalog(options.env.unitSet, createCrewTypeWarTriggerRegistry())
: null;
options.env.generalActionModules = [
...(options.env.generalActionModules ?? []),
...(crewTypeCatalog ? [crewTypeCatalog.generalActionModule] : []),
inheritBuffModules.general,
...itemActionModules.general,
];
options.env.warActionModules = [
...(options.env.warActionModules ?? []),
...(crewTypeCatalog ? [crewTypeCatalog.warActionModule] : []),
inheritBuffModules.war,
...itemActionModules.war,
];
const generalSpecs = await loadGeneralTurnCommandSpecs(options.commandProfile.general);
const nationSpecs = await loadNationTurnCommandSpecs(options.commandProfile.nation);