From f59fe782c84745ba6ca4683a0caf72dcaa38da8b Mon Sep 17 00:00:00 2001 From: hided62 Date: Mon, 24 Aug 2026 12:35:57 +0000 Subject: [PATCH] =?UTF-8?q?test:=20=EC=9E=90=EB=8F=99=20=EC=84=A0=ED=8F=AC?= =?UTF-8?q?=EC=9D=98=20=EC=9C=A0=EC=A0=80=EA=B5=AD=20=EA=B5=AD=EB=A0=A5=20?= =?UTF-8?q?=EC=9A=B0=EC=84=A0=EB=8F=84=EB=A5=BC=20=EA=B3=A0=EC=A0=95?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ref 월간 국력 산식의 유저 장수 통솔 기여와 자동 선포 역가중 연결을 대칭 역할 교환 회귀로 검증한다. 선포 단계에 중복 배율을 추가하지 않도록 계약도 명시한다. --- .../src/turn/monthlyNationStatsHandler.ts | 5 ++ .../generalAiLegacyDecisionParity.test.ts | 24 +++--- .../test/monthlyNationStatsHandler.test.ts | 77 +++++++++++++++++++ 3 files changed, 94 insertions(+), 12 deletions(-) diff --git a/app/game-engine/src/turn/monthlyNationStatsHandler.ts b/app/game-engine/src/turn/monthlyNationStatsHandler.ts index 323b0bc9..c2a18e26 100644 --- a/app/game-engine/src/turn/monthlyNationStatsHandler.ts +++ b/app/game-engine/src/turn/monthlyNationStatsHandler.ts @@ -82,6 +82,11 @@ const calculateNationPower = ( const killCrew = readNumber(meta.rank_killcrew_person); const deathCrew = readNumber(meta.rank_deathcrew_person); const ratio = (killCrew + 1000) / (deathCrew + 1000); + // Ref boosts the leadership contribution of user generals here. Its + // automatic declaration AI later applies 1 / sqrt(nation.power + 1), + // so this monthly value is also the indirect lower target priority for + // nations with users; the declaration step must not add another ruler + // type multiplier on top of it. const npcMultiplier = general.npcState < 2 ? 1.2 : 1; const leadership = general.stats.leadership; const leaderCore = leadership >= 40 ? leadership : 0; diff --git a/app/game-engine/test/generalAiLegacyDecisionParity.test.ts b/app/game-engine/test/generalAiLegacyDecisionParity.test.ts index c82813a0..3a182c01 100644 --- a/app/game-engine/test/generalAiLegacyDecisionParity.test.ts +++ b/app/game-engine/test/generalAiLegacyDecisionParity.test.ts @@ -420,11 +420,7 @@ const makePromotionGeneral = (overrides: Partial): TurnGeneral => ( meta: { ...baseGeneral().meta, belong: 1, ...overrides.meta }, }); -const makeDeclarationAi = ( - targetNations: Nation[], - targetRulers: General[], - rng: ScriptedRng -): GeneralAI => { +const makeDeclarationAi = (targetNations: Nation[], targetRulers: General[], rng: ScriptedRng): GeneralAI => { const actorNation = { ...baseNation(), meta: { ...baseNation().meta, tech: 2_000 }, @@ -458,8 +454,7 @@ const makeDeclarationAi = ( listGenerals: () => [actor, ...targetRulers], listCities: () => cities, getNationById: (id: number) => [actorNation, ...targetNations].find((nation) => nation.id === id) ?? null, - getGeneralById: (id: number) => - [actor, ...targetRulers].find((general) => general.id === id) ?? null, + getGeneralById: (id: number) => [actor, ...targetRulers].find((general) => general.id === id) ?? null, getCityById: (id: number) => cities.find((city) => city.id === id) ?? null, }, map: { @@ -862,14 +857,14 @@ describe('legacy NPC AI final-decision parity', () => { expect(rng.choices).toEqual([0]); }); - it('keeps active user and NPC nations equally eligible while excluding the user wandering nation', () => { + it('uses monthly power to give an active user nation lower declaration weight while excluding its wandering nation', () => { const targets = [ { id: 2, name: '유저방랑군', level: 0, npcState: 0 }, { id: 3, name: '정식유저국', level: 1, npcState: 0 }, { id: 4, name: '정식NPC국', level: 1, npcState: 2 }, ]; const nations = targets.map( - ({ id, name, level }) => + ({ id, name, level, npcState }) => ({ ...baseNation(), id, @@ -877,7 +872,11 @@ describe('legacy NPC AI final-decision parity', () => { capitalCityId: id, chiefGeneralId: id, level, - power: 100, + // Ref monthly power for otherwise identical fixtures is + // 64 with a user general and 62 with an NPC general. The + // declaration AI reads that stored result; it does not + // inspect the target ruler type directly. + power: npcState < 2 ? 64 : 62, }) satisfies Nation ); const rulers = targets.map( @@ -901,10 +900,11 @@ describe('legacy NPC AI final-decision parity', () => { }); expect(userTargetRng.weightedChoices).toEqual([ { - '3': 1 / Math.sqrt(101), - '4': 1 / Math.sqrt(101), + '3': 1 / Math.sqrt(65), + '4': 1 / Math.sqrt(63), }, ]); + expect(userTargetRng.weightedChoices[0]?.['3']).toBeLessThan(userTargetRng.weightedChoices[0]?.['4'] ?? 0); const npcTargetRng = makeRng([true], [1]); const npcTargetAi = makeDeclarationAi(nations, rulers, npcTargetRng); diff --git a/app/game-engine/test/monthlyNationStatsHandler.test.ts b/app/game-engine/test/monthlyNationStatsHandler.test.ts index 404996c6..7f8f746b 100644 --- a/app/game-engine/test/monthlyNationStatsHandler.test.ts +++ b/app/game-engine/test/monthlyNationStatsHandler.test.ts @@ -243,4 +243,81 @@ describe('monthly nation statistics boundary', () => { }, }); }); + + it('raises stored power for an otherwise identical user general before declaration weighting', async () => { + const run = async (firstNpcState: number, secondNpcState: number): Promise => { + const state: TurnWorldState = { + id: 1, + currentYear: 193, + currentMonth: 1, + tickSeconds: 600, + lastTurnTime: new Date('0193-01-01T00:00:00.000Z'), + meta: { hiddenSeed: 'monthly-post-nation-stats-fixture' }, + }; + const symmetricGeneral = (id: number, nationId: number, npcState: number): TurnGeneral => + buildGeneral({ + id, + nationId, + npcState, + gold: 1_000, + rice: 2_000, + crew: 100, + leadership: 50, + strength: 40, + intelligence: 30, + experience: 100, + dedication: 200, + dexterity: [1_000, 0, 0, 0, 0], + killCrew: 100, + deathCrew: 0, + }); + const snapshot: TurnWorldSnapshot = { + scenarioConfig: { + stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 }, + iconPath: '', + map: {}, + const: {}, + environment: { mapName: 'test', unitSet: 'default' }, + }, + map: { id: 'test', name: 'test', cities: [] }, + diplomacy: [], + events: [], + initialEvents: [], + generals: [symmetricGeneral(1, 1, firstNpcState), symmetricGeneral(2, 2, secondNpcState)], + cities: [buildCity(1, 1, 1), buildCity(2, 2, 1)], + nations: [ + buildNation(1, { + gold: 10_000, + rice: 20_000, + power: 7, + level: 2, + meta: { tech: 100, gennum: 1 }, + }), + buildNation(2, { + gold: 10_000, + rice: 20_000, + power: 8, + level: 2, + meta: { tech: 100, gennum: 1 }, + }), + ], + troops: [], + }; + let world: InMemoryTurnWorld | null = null; + world = new InMemoryTurnWorld(state, snapshot, { + schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + autoAdvanceDiplomacyMonth: false, + calendarHandler: createMonthlyNationStatsHandler({ getWorld: () => world }), + }); + + await world.advanceMonth(new Date('0193-02-01T00:00:00.000Z')); + + return [world.getNationById(1)?.power ?? -1, world.getNationById(2)?.power ?? -1]; + }; + + // Disposable Ref MariaDB produces the same role-swap pairs. Comparing + // each nation ID across the swap removes its separate monthly RNG roll. + expect(await run(0, 2)).toEqual([64, 62]); + expect(await run(2, 0)).toEqual([62, 64]); + }); });