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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user