feat: 사용자 군주 자율행동 업무를 개별 설정한다

NPC 군주의 기존 국가 운영은 유지하고 사용자 군주는 외교, 수뇌 임명, 재정 조정, 천도를 기본 비활성화한다.

내 정보 설정과 인증 API, 엔진 gate를 연결하고 자율행동 기간 및 국가턴 master를 함께 검증한다.
This commit is contained in:
2026-08-22 08:28:04 +00:00
parent 957445b0e0
commit 9c588dbd67
14 changed files with 309 additions and 28 deletions
@@ -2,8 +2,9 @@ import { describe, expect, it } from 'vitest';
import type { City, General, Nation } from '@sammo-ts/logic';
import { createRefOrderedActionStack } from '@sammo-ts/logic/actionModules/bundle.js';
import { GeneralAI } from '../src/turn/ai/generalAi.js';
import type { TurnGeneral } from '../src/turn/types.js';
import { GeneralAI, shouldUseNationAi } from '../src/turn/ai/generalAi.js';
import { canUseAutomatedNationAction, canUseRulerAutomation } from '../src/turn/ai/policies.js';
import type { TurnGeneral, TurnWorldState } from '../src/turn/types.js';
import {
calculateRecentWarTurn,
resolveLegacyAiStats,
@@ -672,13 +673,13 @@ describe('legacy NPC user-chief promotion parity', () => {
expect(ai.consumePromotionPatches().generals).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0 }]);
});
it('runs automatic appointments only on the quarterly NPC nation turn', () => {
const run = (currentMonth: number) => {
it('runs automatic appointments for NPC rulers and only opted-in user rulers in quarter months', () => {
const run = (currentMonth: number, npcState = 2, enabled = 0) => {
const ruler = makePromotionGeneral({
id: 1,
officerLevel: 12,
npcState: 2,
meta: { killturn: 100, belong: 2 },
npcState,
meta: { killturn: 100, belong: 2, use_auto_nation_promotion: enabled },
});
const user = makePromotionGeneral({
id: 2,
@@ -710,6 +711,45 @@ describe('legacy NPC user-chief promotion parity', () => {
expect(run(2)).toEqual([]);
expect(run(3)).toEqual([{ generalId: 2, officerLevel: 11, officerCity: 0, permission: 'ambassador' }]);
expect(run(3, 0)).toEqual([]);
expect(run(3, 0, 1)).toEqual([
{ generalId: 2, officerLevel: 11, officerCity: 0, permission: 'ambassador' },
]);
});
it('keeps user-ruler duties individually disabled until each setting is enabled', () => {
const ruler = makePromotionGeneral({ id: 1, officerLevel: 12, npcState: 0, meta: { killturn: 0 } });
expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(false);
expect(canUseAutomatedNationAction(ruler, '천도')).toBe(false);
expect(canUseRulerAutomation(ruler, 'finance')).toBe(false);
ruler.meta = {
...ruler.meta,
use_auto_nation_diplomacy: 1,
use_auto_nation_capital: 1,
use_auto_nation_finance: 1,
};
expect(canUseAutomatedNationAction(ruler, '불가침제의')).toBe(true);
expect(canUseAutomatedNationAction(ruler, '선전포고')).toBe(true);
expect(canUseAutomatedNationAction(ruler, '천도')).toBe(true);
expect(canUseRulerAutomation(ruler, 'finance')).toBe(true);
});
it('honors the existing automatic nation-turn master switch for user chiefs only', () => {
const world = {
currentYear: 190,
currentMonth: 3,
meta: {},
} as TurnWorldState;
const build = (npcState: number, useAutoNationTurn: number) =>
makePromotionGeneral({
npcState,
meta: { killturn: 0, autorun_limit: 19004, use_auto_nation_turn: useAutoNationTurn },
});
expect(shouldUseNationAi(build(0, 0), world)).toBe(false);
expect(shouldUseNationAi(build(0, 1), world)).toBe(true);
expect(shouldUseNationAi(build(2, 0), world)).toBe(true);
});
});
@@ -468,4 +468,52 @@ describe('core monthly event actions at the real month boundary', () => {
expect(lowChiefBill).toBeTypeOf('number');
expect(highChiefBill).toBe(lowChiefBill);
});
it('keeps user-ruler finance unchanged by default and applies it only after opt-in', () => {
const config: TurnWorldSnapshot['scenarioConfig'] = {
stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 50, npcMin: 10, chiefMin: 70 },
iconPath: '',
map: {},
const: { baseGold: 0, baseRice: 0 },
environment: { mapName: map.id, unitSet: 'default' },
};
const buildFinanceWorld = (settings: {
finance: number;
autorunLimit?: number;
nationTurn?: number;
}) => {
const chief = buildGeneral(1, 1, 3);
chief.npcState = 0;
chief.officerLevel = 12;
chief.meta = {
...chief.meta,
use_auto_nation_finance: settings.finance,
...(settings.autorunLimit === undefined ? {} : { autorun_limit: settings.autorunLimit }),
...(settings.nationTurn === undefined ? {} : { use_auto_nation_turn: settings.nationTurn }),
};
const subordinate = buildGeneral(2, 1, 3);
return buildWorld([], () => new Map(), {
currentMonth: 12,
generals: [chief, subordinate],
nations: [buildNation()],
cities: [buildCity()],
});
};
const options = { scenarioConfig: config, commandEnv: buildCommandEnv(config) };
const disabled = buildFinanceWorld({ finance: 0, autorunLimit: 19101 });
const outsideAutorun = buildFinanceWorld({ finance: 1 });
const masterDisabled = buildFinanceWorld({ finance: 1, autorunLimit: 19101, nationTurn: 0 });
const enabled = buildFinanceWorld({ finance: 1, autorunLimit: 19101, nationTurn: 1 });
expect(calculateNpcNationFinance(disabled, disabled.getNationById(1)!, 12, options)).toBeNull();
expect(
calculateNpcNationFinance(outsideAutorun, outsideAutorun.getNationById(1)!, 12, options)
).toBeNull();
expect(
calculateNpcNationFinance(masterDisabled, masterDisabled.getNationById(1)!, 12, options)
).toBeNull();
expect(calculateNpcNationFinance(enabled, enabled.getNationById(1)!, 12, options)).toEqual(
expect.objectContaining({ rate: expect.any(Number), bill: expect.any(Number) })
);
});
});
@@ -215,6 +215,10 @@ describe('my information world commands', () => {
defence_train: 94,
use_treatment: 200,
use_auto_nation_turn: 0,
use_auto_nation_diplomacy: 1,
use_auto_nation_promotion: 1,
use_auto_nation_finance: 1,
use_auto_nation_capital: 1,
},
})
).resolves.toMatchObject({ ok: true });
@@ -227,6 +231,10 @@ describe('my information world commands', () => {
defence_train: 999,
use_treatment: 100,
use_auto_nation_turn: 0,
use_auto_nation_diplomacy: 1,
use_auto_nation_promotion: 1,
use_auto_nation_finance: 1,
use_auto_nation_capital: 1,
myset: 2,
},
});