From 1ccda1f0fbc10170eff2b6aa97e42837fa2279e2 Mon Sep 17 00:00:00 2001 From: hided62 Date: Wed, 26 Aug 2026 15:00:29 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20NPC=20=EC=A0=84=EC=9F=81=20=ED=8F=AC?= =?UTF-8?q?=EC=83=81=20=EB=B3=91=EB=A0=A5=20=EB=8B=A8=EC=9C=84=20=EA=B3=84?= =?UTF-8?q?=EC=82=B0=20=EB=B3=B5=EC=9B=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref costWithTech의 병력 100명 단위를 기본 NPC 및 사용자 전쟁 포상 기준에 반영한다. 어려움 이민족 초기 자원이 포상 필요 상태로 판정되는 회귀 테스트를 추가한다. --- app/game-engine/src/turn/ai/policies.ts | 10 +++--- .../test/npcPolicyLifecycle.test.ts | 32 +++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) 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(