fix: Ref 게임 로직과 시나리오 풀 호환을 보정

월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
This commit is contained in:
2026-08-23 16:26:14 +00:00
parent bf6b7be7b0
commit 85591c68ad
114 changed files with 13327 additions and 901 deletions
@@ -33,6 +33,7 @@ export const resolveConstraintEnv = (
develCost: env.develCost,
openingPartYear: env.openingPartYear,
minAvailableRecruitPop: env.minAvailableRecruitPop,
maxTechLevel: env.maxTechLevel,
...(Number.isFinite(killturn) ? { killturn } : {}),
};
};
@@ -115,7 +115,8 @@ export const resolveLegacyAiStatsWithModules = (
modules: TurnCommandEnv['generalActionModules'],
worldRef: AiWorldView | null,
world: TurnWorldState,
startYear: number
startYear: number,
maxTechLevel = 12
) => {
const maxLevel = Math.max(1, maxStatLevel);
const clampStat = (value: number): number => Math.max(0, Math.min(value, maxLevel));
@@ -138,6 +139,7 @@ export const resolveLegacyAiStatsWithModules = (
month: world.currentMonth,
startYear,
},
maxTechLevel,
};
const rawStat = (statName: 'leadership' | 'strength' | 'intelligence'): number => general.stats[statName];
const calculate = (
@@ -699,7 +701,8 @@ export class GeneralAI {
this.commandEnv.generalActionModules,
this.worldRef,
this.world,
this.startYear
this.startYear,
this.commandEnv.maxTechLevel
);
}
return resolveLegacyAiStats(general, this.nation, this.commandEnv.maxStatLevel ?? LEGACY_DEFAULT_MAX_LEVEL);
@@ -726,6 +729,7 @@ export class GeneralAI {
month: this.world.currentMonth,
startYear: this.startYear,
},
maxTechLevel: this.commandEnv.maxTechLevel,
},
'징집인구',
'score',
@@ -70,13 +70,14 @@ export const do금쌀구매 = (ai: GeneralAI) => {
}
: {}),
time: { year: ai.world.currentYear, month: ai.world.currentMonth, startYear: ai.startYear },
maxTechLevel: ai.commandEnv.maxTechLevel,
};
const recruitment = new RecruitmentCommandResolver(ai.commandEnv.generalActionModules ?? [], ai.commandEnv);
const goldCost = crewType
? recruitment.getCost(recruitContext, crewType.id, crewAmount, crewType).gold *
(ai.generalPolicy.can('모병') ? 2 : 1)
: 0;
const riceCost = crewType ? (crewType.rice * getTechCost(tech) * crewAmount) / 100 : 0;
const riceCost = crewType ? (crewType.rice * getTechCost(tech, ai.commandEnv.maxTechLevel) * crewAmount) / 100 : 0;
trace('recruit-cost', {
crewTypeId: crewType?.id ?? null,
crewCost: crewType?.cost ?? null,
@@ -87,6 +87,7 @@ export const do징병 = (ai: GeneralAI) => {
month: ai.world.currentMonth,
startYear: ai.startYear,
},
maxTechLevel: ai.commandEnv.maxTechLevel,
};
const recruitment = new RecruitmentCommandResolver(ai.commandEnv.generalActionModules ?? [], ai.commandEnv);
// The cached AI stat follows the scenario/global classification cap, while
@@ -163,11 +164,17 @@ export const do징병 = (ai: GeneralAI) => {
return null;
}
let picked = ai.rng.choiceUsingWeightPair(
candidates.map((crew) => [crew, getCrewTypePickScore(crew, tech, warConfig.armPerPhase)])
candidates.map((crew) => [
crew,
getCrewTypePickScore(crew, tech, warConfig.armPerPhase, ai.commandEnv.maxTechLevel),
])
);
trace('crew-type', {
armType,
candidates: candidates.map((crew) => [crew.id, getCrewTypePickScore(crew, tech, warConfig.armPerPhase)]),
candidates: candidates.map((crew) => [
crew.id,
getCrewTypePickScore(crew, tech, warConfig.armPerPhase, ai.commandEnv.maxTechLevel),
]),
picked: picked.id,
});
if (ai.generalPolicy.can('고급병종')) {
@@ -203,7 +210,7 @@ export const do징병 = (ai: GeneralAI) => {
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));
let riceCost = (picked.rice * getTechCost(tech) * expectedCrewLoss) / 100;
let riceCost = (picked.rice * getTechCost(tech, ai.commandEnv.maxTechLevel) * expectedCrewLoss) / 100;
const remainingGold = ai.general.gold - fullLeadership * 3;
const remainingRice = ai.general.rice - fullLeadership * 4;
@@ -56,7 +56,8 @@ const getCrewGoldCost = (ai: GeneralAI, general: TurnGeneral, baseMultiplier: nu
// Keeping that operation order is observable at exact resource boundaries
// (for example 3036 versus 3036.0000000000005).
return (
(((crewType?.cost ?? 0) * getTechCost(tech) * getFullLeadership(ai, general)) / 100) *
(((crewType?.cost ?? 0) * getTechCost(tech, ai.commandEnv.maxTechLevel) * getFullLeadership(ai, general)) /
100) *
100 *
baseMultiplier *
finalMultiplier
+5 -8
View File
@@ -77,10 +77,7 @@ const USER_RULER_ACTION_FEATURE: Readonly<Record<string, UserRulerAutomationFeat
};
/** NPC는 기존 계약을 유지하고, 사용자 군주는 명시적으로 고른 업무만 위임한다. */
export const canUseRulerAutomation = (
general: TurnGeneral,
feature: UserRulerAutomationFeature
): boolean => {
export const canUseRulerAutomation = (general: TurnGeneral, feature: UserRulerAutomationFeature): boolean => {
if (general.npcState >= 2) {
return true;
}
@@ -316,8 +313,8 @@ export class AutorunNationPolicy {
const stat = scenarioConfig.stat;
if (this.reqNpcWarGold === 0 || this.reqNpcWarRice === 0) {
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
const baseGold = crewType ? crewType.cost * getTechCost(tech) * stat.npcMax : 0;
const baseRice = crewType ? crewType.rice * getTechCost(tech) * stat.npcMax : 0;
const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.npcMax : 0;
const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.npcMax : 0;
if (this.reqNpcWarGold === 0) {
this.reqNpcWarGold = roundTo(baseGold * 4, -2);
}
@@ -328,8 +325,8 @@ export class AutorunNationPolicy {
if (this.reqHumanWarUrgentGold === 0 || this.reqHumanWarUrgentRice === 0) {
const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId);
const baseGold = crewType ? crewType.cost * getTechCost(tech) * stat.max : 0;
const baseRice = crewType ? crewType.rice * getTechCost(tech) * stat.max : 0;
const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.max : 0;
const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.max : 0;
if (this.reqHumanWarUrgentGold === 0) {
this.reqHumanWarUrgentGold = roundTo(baseGold * 6, -2);
}