diff --git a/app/game-engine/src/turn/ai/policies.ts b/app/game-engine/src/turn/ai/policies.ts index ea669aad..7968bd69 100644 --- a/app/game-engine/src/turn/ai/policies.ts +++ b/app/game-engine/src/turn/ai/policies.ts @@ -314,8 +314,10 @@ 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, env.maxTechLevel) * stat.npcMax : 0; - const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.npcMax : 0; + // Ref costWithTech() receives a troop count, not a leadership value. + // The policy funds four complete recruitments at the NPC stat cap. + const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.npcMax * 100 : 0; + const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.npcMax * 100 : 0; if (this.reqNpcWarGold === 0) { this.reqNpcWarGold = roundTo(baseGold * 4, -2); } @@ -326,8 +328,8 @@ export class AutorunNationPolicy { if (this.reqHumanWarUrgentGold === 0 || this.reqHumanWarUrgentRice === 0) { const crewType = findCrewTypeById(unitSet, env.defaultCrewTypeId); - const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.max : 0; - const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.max : 0; + const baseGold = crewType ? crewType.cost * getTechCost(tech, env.maxTechLevel) * stat.max * 100 : 0; + const baseRice = crewType ? crewType.rice * getTechCost(tech, env.maxTechLevel) * stat.max * 100 : 0; if (this.reqHumanWarUrgentGold === 0) { this.reqHumanWarUrgentGold = roundTo(baseGold * 6, -2); } diff --git a/app/game-engine/test/npcPolicyLifecycle.test.ts b/app/game-engine/test/npcPolicyLifecycle.test.ts index 009eb8e6..98272554 100644 --- a/app/game-engine/test/npcPolicyLifecycle.test.ts +++ b/app/game-engine/test/npcPolicyLifecycle.test.ts @@ -176,6 +176,32 @@ const unitSet: UnitSetDefinition = { }; describe('NPC policy lifecycle', () => { + it('keeps hard-invader starting resources below the Ref-compatible NPC war funding target', () => { + const hardInvaderNation = { + ...snapshot.nations[0]!, + name: 'ⓞ강족', + meta: { ...snapshot.nations[0]!.meta, tech: 15_000 }, + }; + const hardInvaderScenario = structuredClone(snapshot.scenarioConfig); + hardInvaderScenario.stat.npcMax = 80; + const hardInvaderGeneral = { ...general, npcState: 9, gold: 99_999, rice: 99_999 }; + const policy = new AutorunNationPolicy({ + general: hardInvaderGeneral, + aiOptions: null, + nationPolicy: null, + serverPolicy: null, + nation: hardInvaderNation, + env: { ...commandEnv, maxTechLevel: 12 }, + scenarioConfig: hardInvaderScenario, + unitSet, + }); + + expect(policy.reqNpcWarGold).toBe(806_400); + expect(policy.reqNpcWarRice).toBe(806_400); + expect(hardInvaderGeneral.gold).toBeLessThan(policy.reqNpcWarGold); + expect(hardInvaderGeneral.rice).toBeLessThan(policy.reqNpcWarRice); + }); + it('applies CAS-protected semantic policy changes and the next AI instance consumes them without scheduler changes', () => { const world = new InMemoryTurnWorld(state, snapshot, { schedule }); const first = applyNpcPolicyMutation({ @@ -254,8 +280,10 @@ describe('NPC policy lifecycle', () => { expect(policy.reqNationGold).toBe(4_321); expect(policy.priority).toEqual(['천도']); expect(policy.reqNpcDevelGold).toBe(540); - expect(policy.reqNpcWarGold).toBe(3_900); - expect(policy.reqNpcWarRice).toBe(3_900); + expect(policy.reqNpcWarGold).toBe(391_500); + expect(policy.reqNpcWarRice).toBe(391_500); + expect(policy.reqHumanWarUrgentGold).toBe(626_400); + expect(policy.reqHumanWarUrgentRice).toBe(626_400); const beforeConflict = structuredClone(world.getNationById(1)?.meta); expect(