feat: 사용자 군주 자율행동 업무를 개별 설정한다
NPC 군주의 기존 국가 운영은 유지하고 사용자 군주는 외교, 수뇌 임명, 재정 조정, 천도를 기본 비활성화한다. 내 정보 설정과 인증 API, 엔진 gate를 연결하고 자율행동 기간 및 국가턴 master를 함께 검증한다.
This commit is contained in:
@@ -22,7 +22,12 @@ import { GeneralActionPipeline } from '@sammo-ts/logic/actionModules/general.js'
|
||||
import type { ReservedTurnEntry } from '../../reservedTurnStore.js';
|
||||
import type { TurnGeneral, TurnWorldState } from '../../types.js';
|
||||
import type { AiCommandCandidate, AiReservedTurnProvider, AiWorldView } from '../types.js';
|
||||
import { AutorunGeneralPolicy, AutorunNationPolicy, AVAILABLE_INSTANT_TURN } from '../policies.js';
|
||||
import {
|
||||
AutorunGeneralPolicy,
|
||||
AutorunNationPolicy,
|
||||
canUseAutomatedNationAction,
|
||||
canUseRulerAutomation,
|
||||
} from '../policies.js';
|
||||
import {
|
||||
asRecord,
|
||||
joinYearMonth,
|
||||
@@ -401,10 +406,10 @@ export class GeneralAI {
|
||||
this.categorizeNationCities();
|
||||
this.categorizeNationGeneral();
|
||||
|
||||
if (this.general.npcState >= 2 && [3, 6, 9, 12].includes(this.world.currentMonth)) {
|
||||
if (this.general.officerLevel === 12) {
|
||||
if ([3, 6, 9, 12].includes(this.world.currentMonth)) {
|
||||
if (this.general.officerLevel === 12 && canUseRulerAutomation(this.general, 'promotion')) {
|
||||
this.chooseNpcPromotion();
|
||||
} else {
|
||||
} else if (this.general.npcState >= 2) {
|
||||
this.chooseNonLordPromotion();
|
||||
}
|
||||
}
|
||||
@@ -420,7 +425,7 @@ export class GeneralAI {
|
||||
if (!this.nationPolicy.can(actionName)) {
|
||||
continue;
|
||||
}
|
||||
if (this.general.npcState < 2 && !AVAILABLE_INSTANT_TURN[actionName]) {
|
||||
if (!canUseAutomatedNationAction(this.general, actionName)) {
|
||||
continue;
|
||||
}
|
||||
const handler = nationActionHandlers[actionName];
|
||||
@@ -1129,7 +1134,7 @@ export class GeneralAI {
|
||||
|
||||
for (let chiefLevel = minChiefLevel; chiefLevel <= 12; chiefLevel += 1) {
|
||||
const chief = this.chiefGenerals[chiefLevel];
|
||||
if (!chief) {
|
||||
if (!chief || chief.id === this.general.id) {
|
||||
continue;
|
||||
}
|
||||
const penalty = asRecord(chief.penalty);
|
||||
@@ -1148,6 +1153,9 @@ export class GeneralAI {
|
||||
|
||||
const minBelong = Math.min(readMetaNumber(asRecord(this.general.meta), 'belong', 0) - 1, 3);
|
||||
const availableUserChiefCount = Object.values(this.userGenerals).filter((candidate) => {
|
||||
if (candidate.id === this.general.id) {
|
||||
return false;
|
||||
}
|
||||
const penalty = asRecord(candidate.penalty);
|
||||
const killturn = readRequiredMetaNumber(asRecord(candidate.meta), 'killturn', `generalId=${candidate.id}`);
|
||||
return (
|
||||
@@ -1158,17 +1166,19 @@ export class GeneralAI {
|
||||
}).length;
|
||||
|
||||
if (userChiefCount === 0 && availableUserChiefCount > 0 && (chiefSet & (1 << 11)) === 0) {
|
||||
const userCandidates = Object.values(this.userGenerals).sort((left, right) => {
|
||||
const leftPenalty = asRecord(left.penalty);
|
||||
const rightPenalty = asRecord(right.penalty);
|
||||
if ((leftPenalty.noChief === true) !== (rightPenalty.noChief === true)) {
|
||||
return leftPenalty.noChief === true ? 1 : -1;
|
||||
}
|
||||
if ((leftPenalty.noAmbassador === true) !== (rightPenalty.noAmbassador === true)) {
|
||||
return leftPenalty.noAmbassador === true ? 1 : -1;
|
||||
}
|
||||
return right.stats.leadership - left.stats.leadership;
|
||||
});
|
||||
const userCandidates = Object.values(this.userGenerals)
|
||||
.filter((candidate) => candidate.id !== this.general.id)
|
||||
.sort((left, right) => {
|
||||
const leftPenalty = asRecord(left.penalty);
|
||||
const rightPenalty = asRecord(right.penalty);
|
||||
if ((leftPenalty.noChief === true) !== (rightPenalty.noChief === true)) {
|
||||
return leftPenalty.noChief === true ? 1 : -1;
|
||||
}
|
||||
if ((leftPenalty.noAmbassador === true) !== (rightPenalty.noAmbassador === true)) {
|
||||
return leftPenalty.noAmbassador === true ? 1 : -1;
|
||||
}
|
||||
return right.stats.leadership - left.stats.leadership;
|
||||
});
|
||||
for (const candidate of userCandidates) {
|
||||
const penalty = asRecord(candidate.penalty);
|
||||
const killturn = readRequiredMetaNumber(
|
||||
@@ -1227,6 +1237,9 @@ export class GeneralAI {
|
||||
}
|
||||
}
|
||||
const nextChief = generals.find((candidate) => {
|
||||
if (candidate.id === this.general.id) {
|
||||
return false;
|
||||
}
|
||||
if ((effectiveOfficerLevel.get(candidate.id) ?? candidate.officerLevel) > 4) {
|
||||
return false;
|
||||
}
|
||||
@@ -1470,4 +1483,15 @@ export const shouldUseAi = (general: TurnGeneral, world: TurnWorldState): boolea
|
||||
return current < limit;
|
||||
};
|
||||
|
||||
export const shouldUseNationAi = (general: TurnGeneral, world: TurnWorldState): boolean => {
|
||||
if (!shouldUseAi(general, world)) {
|
||||
return false;
|
||||
}
|
||||
if (general.npcState >= 2) {
|
||||
return true;
|
||||
}
|
||||
// Ref TurnExecutionHelper는 사용자 수뇌의 국가 AI에만 이 개인 설정을 적용한다.
|
||||
return readMetaNumber(asRecord(general.meta), 'use_auto_nation_turn', 1) !== 0;
|
||||
};
|
||||
|
||||
export type { GeneralAIOptions, GeneralAiDebugState };
|
||||
|
||||
@@ -61,6 +61,43 @@ export const AVAILABLE_INSTANT_TURN: Record<string, boolean> = {
|
||||
NPC전방발령: true,
|
||||
};
|
||||
|
||||
export type UserRulerAutomationFeature = 'diplomacy' | 'promotion' | 'finance' | 'capital';
|
||||
|
||||
const USER_RULER_AUTOMATION_META_KEY = {
|
||||
diplomacy: 'use_auto_nation_diplomacy',
|
||||
promotion: 'use_auto_nation_promotion',
|
||||
finance: 'use_auto_nation_finance',
|
||||
capital: 'use_auto_nation_capital',
|
||||
} as const satisfies Record<UserRulerAutomationFeature, string>;
|
||||
|
||||
const USER_RULER_ACTION_FEATURE: Readonly<Record<string, UserRulerAutomationFeature>> = {
|
||||
불가침제의: 'diplomacy',
|
||||
선전포고: 'diplomacy',
|
||||
천도: 'capital',
|
||||
};
|
||||
|
||||
/** NPC는 기존 계약을 유지하고, 사용자 군주는 명시적으로 고른 업무만 위임한다. */
|
||||
export const canUseRulerAutomation = (
|
||||
general: TurnGeneral,
|
||||
feature: UserRulerAutomationFeature
|
||||
): boolean => {
|
||||
if (general.npcState >= 2) {
|
||||
return true;
|
||||
}
|
||||
if (general.officerLevel !== 12) {
|
||||
return false;
|
||||
}
|
||||
return readMetaNumber(asRecord(general.meta), USER_RULER_AUTOMATION_META_KEY[feature], 0) === 1;
|
||||
};
|
||||
|
||||
export const canUseAutomatedNationAction = (general: TurnGeneral, actionName: string): boolean => {
|
||||
if (general.npcState >= 2 || AVAILABLE_INSTANT_TURN[actionName]) {
|
||||
return true;
|
||||
}
|
||||
const feature = USER_RULER_ACTION_FEATURE[actionName];
|
||||
return feature ? canUseRulerAutomation(general, feature) : false;
|
||||
};
|
||||
|
||||
const buildFlags = (entries: Array<[string, boolean]>): PolicyFlags => Object.fromEntries(entries);
|
||||
|
||||
const applyPriorityOverride = (priority: string[], override: unknown, flags: PolicyFlags): string[] => {
|
||||
|
||||
@@ -132,6 +132,10 @@ const zSetMySetting = z.object({
|
||||
defence_train: z.number().int().optional(),
|
||||
use_treatment: z.number().int().optional(),
|
||||
use_auto_nation_turn: z.number().int().optional(),
|
||||
use_auto_nation_diplomacy: z.number().int().optional(),
|
||||
use_auto_nation_promotion: z.number().int().optional(),
|
||||
use_auto_nation_finance: z.number().int().optional(),
|
||||
use_auto_nation_capital: z.number().int().optional(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -18,7 +18,8 @@ import {
|
||||
|
||||
import type { TurnCalendarContext, TurnCalendarHandler, InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import { readNumber } from './ai/aiUtils.js';
|
||||
import { AutorunNationPolicy } from './ai/policies.js';
|
||||
import { shouldUseNationAi } from './ai/generalAi.js';
|
||||
import { AutorunNationPolicy, canUseRulerAutomation } from './ai/policies.js';
|
||||
|
||||
const calcNationDevelopedRate = (cities: City[]): { pop: number; all: number } => {
|
||||
if (cities.length === 0) {
|
||||
@@ -108,7 +109,9 @@ const resolveNpcMonarch = (nation: Nation, world: InMemoryTurnWorld) => {
|
||||
: world
|
||||
.listGenerals()
|
||||
.find((general) => general.nationId === nation.id && general.officerLevel === 12) ?? null;
|
||||
return chief && chief.npcState >= 2 ? chief : null;
|
||||
return chief && canUseRulerAutomation(chief, 'finance') && shouldUseNationAi(chief, world.getState())
|
||||
? chief
|
||||
: null;
|
||||
};
|
||||
|
||||
type NpcFinanceOptions = {
|
||||
|
||||
@@ -58,7 +58,7 @@ import {
|
||||
import { buildCommandEnv, buildReservedTurnDefinitions } from './reservedTurnCommands.js';
|
||||
import { buildFrontStatePatches } from './frontStateHandler.js';
|
||||
import { buildActionContext } from './reservedTurnActionContext.js';
|
||||
import { GeneralAI, shouldUseAi } from './ai/generalAi.js';
|
||||
import { GeneralAI, shouldUseAi, shouldUseNationAi } from './ai/generalAi.js';
|
||||
import type { AiReservedTurnProvider } from './ai/types.js';
|
||||
import { withCanonicalArgumentAliases } from './ai/aiUtils.js';
|
||||
import { rankMetaKey } from './rankData.js';
|
||||
@@ -1672,7 +1672,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
let nationAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
|
||||
let nationAiDecisionDurationNs = 0n;
|
||||
let nationUsedAi = false;
|
||||
if (worldView && shouldUseAi(currentGeneral, context.world)) {
|
||||
if (worldView && shouldUseNationAi(currentGeneral, context.world)) {
|
||||
nationUsedAi = true;
|
||||
const aiStartedAt = options.onActionProfiled ? process.hrtime.bigint() : 0n;
|
||||
sharedAi = new GeneralAI({
|
||||
|
||||
@@ -1702,7 +1702,17 @@ async function handleSetMySetting(
|
||||
nextMeta.use_treatment = Math.max(10, Math.min(100, settings.use_treatment));
|
||||
}
|
||||
if (settings.use_auto_nation_turn !== undefined) {
|
||||
nextMeta.use_auto_nation_turn = settings.use_auto_nation_turn;
|
||||
nextMeta.use_auto_nation_turn = settings.use_auto_nation_turn === 0 ? 0 : 1;
|
||||
}
|
||||
for (const key of [
|
||||
'use_auto_nation_diplomacy',
|
||||
'use_auto_nation_promotion',
|
||||
'use_auto_nation_finance',
|
||||
'use_auto_nation_capital',
|
||||
] as const) {
|
||||
if (settings[key] !== undefined) {
|
||||
nextMeta[key] = settings[key] === 1 ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
let nextTrain = general.train;
|
||||
|
||||
Reference in New Issue
Block a user