feat: GeneralAI 디버그 상태 추가 및 예약 턴 핸들러에 AI 상태 전달 기능 구현
This commit is contained in:
@@ -141,6 +141,28 @@ export interface GeneralAIOptions {
|
|||||||
nationFallback: GeneralActionDefinition;
|
nationFallback: GeneralActionDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type GeneralAiDebugState = {
|
||||||
|
generalId: number;
|
||||||
|
nationId: number | null;
|
||||||
|
cityId: number | null;
|
||||||
|
yearMonth: number;
|
||||||
|
startYear: number;
|
||||||
|
startYearMonth: number;
|
||||||
|
dipState: number;
|
||||||
|
attackable: boolean;
|
||||||
|
warTargetNation: Record<number, number>;
|
||||||
|
genType: number;
|
||||||
|
lastAttackable: number;
|
||||||
|
frontCities: Array<{ id: number; frontState: number; supplyState: number }>;
|
||||||
|
supplyCities: Array<{ id: number; frontState: number; supplyState: number }>;
|
||||||
|
policy: {
|
||||||
|
minAvailableRecruitPop: number;
|
||||||
|
minNpcWarLeadership: number;
|
||||||
|
minWarCrew: number;
|
||||||
|
cureThreshold: number;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export class GeneralAI {
|
export class GeneralAI {
|
||||||
public readonly general: TurnGeneral;
|
public readonly general: TurnGeneral;
|
||||||
public readonly city?: City;
|
public readonly city?: City;
|
||||||
@@ -423,6 +445,45 @@ export class GeneralAI {
|
|||||||
return neutral ?? this.buildGeneralCandidate(ACTION_REST, {}, 'neutral');
|
return neutral ?? this.buildGeneralCandidate(ACTION_REST, {}, 'neutral');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDebugState(): GeneralAiDebugState {
|
||||||
|
this.updateInstance();
|
||||||
|
this.categorizeNationCities();
|
||||||
|
const yearMonth = joinYearMonth(this.world.currentYear, this.world.currentMonth);
|
||||||
|
const startYearMonth = joinYearMonth(this.startYear + 2, 5);
|
||||||
|
const meta = asRecord(this.nation?.meta ?? {});
|
||||||
|
const lastAttackable = readMetaNumber(meta, 'last_attackable', 0);
|
||||||
|
|
||||||
|
return {
|
||||||
|
generalId: this.general.id,
|
||||||
|
nationId: this.nation?.id ?? null,
|
||||||
|
cityId: this.city?.id ?? null,
|
||||||
|
yearMonth,
|
||||||
|
startYear: this.startYear,
|
||||||
|
startYearMonth,
|
||||||
|
dipState: this.dipState,
|
||||||
|
attackable: this.attackable,
|
||||||
|
warTargetNation: { ...this.warTargetNation },
|
||||||
|
genType: this.genType,
|
||||||
|
lastAttackable,
|
||||||
|
frontCities: Object.values(this.frontCities).map((city) => ({
|
||||||
|
id: city.id,
|
||||||
|
frontState: city.frontState,
|
||||||
|
supplyState: city.supplyState,
|
||||||
|
})),
|
||||||
|
supplyCities: Object.values(this.supplyCities).map((city) => ({
|
||||||
|
id: city.id,
|
||||||
|
frontState: city.frontState,
|
||||||
|
supplyState: city.supplyState,
|
||||||
|
})),
|
||||||
|
policy: {
|
||||||
|
minAvailableRecruitPop: this.aiConst.minAvailableRecruitPop,
|
||||||
|
minNpcWarLeadership: this.nationPolicy.minNpcWarLeadership,
|
||||||
|
minWarCrew: this.nationPolicy.minWarCrew,
|
||||||
|
cureThreshold: this.nationPolicy.cureThreshold,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
buildGeneralCandidate(action: string, args: Record<string, unknown>, reason: string): AiCommandCandidate | null {
|
buildGeneralCandidate(action: string, args: Record<string, unknown>, reason: string): AiCommandCandidate | null {
|
||||||
return this.buildCandidate(this.generalDefinitions, this.generalFallback, action, args, reason);
|
return this.buildCandidate(this.generalDefinitions, this.generalFallback, action, args, reason);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
actionKey: string;
|
actionKey: string;
|
||||||
usedFallback: boolean;
|
usedFallback: boolean;
|
||||||
blockedReason?: string;
|
blockedReason?: string;
|
||||||
|
aiState?: ReturnType<GeneralAI['getDebugState']>;
|
||||||
}) => void;
|
}) => void;
|
||||||
}): Promise<GeneralTurnHandler> => {
|
}): Promise<GeneralTurnHandler> => {
|
||||||
const env = buildCommandEnv(options.scenarioConfig, options.unitSet);
|
const env = buildCommandEnv(options.scenarioConfig, options.unitSet);
|
||||||
@@ -709,6 +710,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
currentGeneral.officerLevel,
|
currentGeneral.officerLevel,
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
let nationAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
|
||||||
if (worldView && shouldUseAi(currentGeneral, context.world)) {
|
if (worldView && shouldUseAi(currentGeneral, context.world)) {
|
||||||
const ai = new GeneralAI({
|
const ai = new GeneralAI({
|
||||||
general: currentGeneral,
|
general: currentGeneral,
|
||||||
@@ -731,6 +733,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
if (candidate) {
|
if (candidate) {
|
||||||
nationCommand = { action: candidate.action, args: candidate.args };
|
nationCommand = { action: candidate.action, args: candidate.args };
|
||||||
}
|
}
|
||||||
|
nationAiState = ai.getDebugState();
|
||||||
}
|
}
|
||||||
const nationResult = runAction(nationDefinitions, nationFallback, nationCommand, false);
|
const nationResult = runAction(nationDefinitions, nationFallback, nationCommand, false);
|
||||||
options.onActionResolved?.({
|
options.onActionResolved?.({
|
||||||
@@ -741,11 +744,13 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
actionKey: nationResult.actionKey,
|
actionKey: nationResult.actionKey,
|
||||||
usedFallback: nationResult.usedFallback,
|
usedFallback: nationResult.usedFallback,
|
||||||
...(nationResult.blockedReason ? { blockedReason: nationResult.blockedReason } : {}),
|
...(nationResult.blockedReason ? { blockedReason: nationResult.blockedReason } : {}),
|
||||||
|
...(nationAiState ? { aiState: nationAiState } : {}),
|
||||||
});
|
});
|
||||||
options.reservedTurns.shiftNationTurns(currentNation.id, currentGeneral.officerLevel, -1);
|
options.reservedTurns.shiftNationTurns(currentNation.id, currentGeneral.officerLevel, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
let generalCommand = options.reservedTurns.getGeneralTurn(currentGeneral.id, 0);
|
let generalCommand = options.reservedTurns.getGeneralTurn(currentGeneral.id, 0);
|
||||||
|
let generalAiState: ReturnType<GeneralAI['getDebugState']> | undefined;
|
||||||
if (worldView && shouldUseAi(currentGeneral, context.world)) {
|
if (worldView && shouldUseAi(currentGeneral, context.world)) {
|
||||||
const ai = new GeneralAI({
|
const ai = new GeneralAI({
|
||||||
general: currentGeneral,
|
general: currentGeneral,
|
||||||
@@ -768,6 +773,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
if (candidate) {
|
if (candidate) {
|
||||||
generalCommand = { action: candidate.action, args: candidate.args };
|
generalCommand = { action: candidate.action, args: candidate.args };
|
||||||
}
|
}
|
||||||
|
generalAiState = ai.getDebugState();
|
||||||
}
|
}
|
||||||
const generalResult = runAction(generalDefinitions, generalFallback, generalCommand, true);
|
const generalResult = runAction(generalDefinitions, generalFallback, generalCommand, true);
|
||||||
options.onActionResolved?.({
|
options.onActionResolved?.({
|
||||||
@@ -778,6 +784,7 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
actionKey: generalResult.actionKey,
|
actionKey: generalResult.actionKey,
|
||||||
usedFallback: generalResult.usedFallback,
|
usedFallback: generalResult.usedFallback,
|
||||||
...(generalResult.blockedReason ? { blockedReason: generalResult.blockedReason } : {}),
|
...(generalResult.blockedReason ? { blockedReason: generalResult.blockedReason } : {}),
|
||||||
|
...(generalAiState ? { aiState: generalAiState } : {}),
|
||||||
});
|
});
|
||||||
const nextTurnAt = generalResult.nextTurnAt;
|
const nextTurnAt = generalResult.nextTurnAt;
|
||||||
options.reservedTurns.shiftGeneralTurns(currentGeneral.id, -1);
|
options.reservedTurns.shiftGeneralTurns(currentGeneral.id, -1);
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ import { describe, expect, it, vi } from 'vitest';
|
|||||||
import type { LogEntryDraft, TurnSchedule, UnitSetDefinition } from '@sammo-ts/logic';
|
import type { LogEntryDraft, TurnSchedule, UnitSetDefinition } from '@sammo-ts/logic';
|
||||||
import { LogCategory } from '@sammo-ts/logic';
|
import { LogCategory } from '@sammo-ts/logic';
|
||||||
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
import type { TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js';
|
||||||
|
import type { GeneralAiDebugState } from '../src/turn/ai/generalAi.js';
|
||||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||||
|
import { joinYearMonth } from '../src/turn/ai/aiUtils.js';
|
||||||
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
|
import { InMemoryReservedTurnStore } from '../src/turn/reservedTurnStore.js';
|
||||||
import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
|
import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
|
||||||
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
|
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
|
||||||
@@ -208,6 +210,7 @@ describe('NPC 대형 시뮬레이션', () => {
|
|||||||
ok: boolean;
|
ok: boolean;
|
||||||
error?: unknown;
|
error?: unknown;
|
||||||
logs: LogEntryDraft[];
|
logs: LogEntryDraft[];
|
||||||
|
aiState?: GeneralAiDebugState;
|
||||||
};
|
};
|
||||||
|
|
||||||
const turnTraces: TurnTrace[] = [];
|
const turnTraces: TurnTrace[] = [];
|
||||||
@@ -232,6 +235,7 @@ describe('NPC 대형 시뮬레이션', () => {
|
|||||||
trace.requestedAction = payload.requestedAction;
|
trace.requestedAction = payload.requestedAction;
|
||||||
trace.usedFallback = payload.usedFallback;
|
trace.usedFallback = payload.usedFallback;
|
||||||
trace.blockedReason = payload.blockedReason;
|
trace.blockedReason = payload.blockedReason;
|
||||||
|
trace.aiState = payload.aiState;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -369,24 +373,6 @@ describe('NPC 대형 시뮬레이션', () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const scheduleNpcRecruitment = () => {
|
|
||||||
const nations = world.listNations().filter((nation) => nation.level >= 1 && nation.capitalCityId);
|
|
||||||
const generals = world.listGenerals();
|
|
||||||
for (const nation of nations) {
|
|
||||||
const targetGenerals = generals.filter((general) => general.nationId === nation.id);
|
|
||||||
for (const general of targetGenerals) {
|
|
||||||
if (general.crew > 0 && general.crewTypeId > 0) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const amount = Math.max(100, Math.floor(general.stats.leadership * 50));
|
|
||||||
world.updateGeneral(general.id, {
|
|
||||||
crew: amount,
|
|
||||||
crewTypeId: unitSet.defaultCrewTypeId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const maybeSnapshotGold = () => {
|
const maybeSnapshotGold = () => {
|
||||||
checkpointGoldByGeneral.clear();
|
checkpointGoldByGeneral.clear();
|
||||||
for (const general of world.listGenerals()) {
|
for (const general of world.listGenerals()) {
|
||||||
@@ -438,6 +424,10 @@ describe('NPC 대형 시뮬레이션', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
const lastAiTrace = [...turnTraces].reverse().find((trace) => trace.aiState);
|
||||||
|
if (lastAiTrace?.aiState) {
|
||||||
|
console.log('[DEBUG] last aiState:', lastAiTrace.aiState);
|
||||||
|
}
|
||||||
dumpTraceSummary('NPC 대형 시뮬레이션 실패');
|
dumpTraceSummary('NPC 대형 시뮬레이션 실패');
|
||||||
const sampleNation = world.listNations().find((nation) => nation.level >= 1 && nation.capitalCityId);
|
const sampleNation = world.listNations().find((nation) => nation.level >= 1 && nation.capitalCityId);
|
||||||
if (sampleNation) {
|
if (sampleNation) {
|
||||||
|
|||||||
Reference in New Issue
Block a user