feat: 메타 데이터에서 필수 killturn 값을 읽어오는 로직 추가 및 관련 함수 수정

This commit is contained in:
2026-01-25 05:26:40 +00:00
parent e89e758494
commit 340cadb8ce
6 changed files with 88 additions and 14 deletions
+15
View File
@@ -34,6 +34,21 @@ export const readString = (value: unknown, fallback = ''): string => (typeof val
export const readMetaNumber = (meta: Record<string, unknown>, key: string, fallback = 0): number =>
readNumber(meta[key], fallback);
export const readRequiredMetaNumber = (meta: Record<string, unknown>, key: string, context?: string): number => {
const value = meta[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
const suffix = context ? ` (${context})` : '';
throw new Error(`meta.${key} is required${suffix}.`);
};
export const readMetaString = (meta: Record<string, unknown>, key: string, fallback = ''): string =>
readString(meta[key], fallback);
+10 -2
View File
@@ -18,7 +18,15 @@ 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 { asRecord, joinYearMonth, readMetaNumber, readNumber, roundTo, valueFit } from '../aiUtils.js';
import {
asRecord,
joinYearMonth,
readMetaNumber,
readNumber,
readRequiredMetaNumber,
roundTo,
valueFit,
} from '../aiUtils.js';
import { searchAllDistanceByNationList } from '../distance.js';
import { generalActionHandlers } from '../generalAiGeneralActions.js';
import { nationActionHandlers } from '../generalAiNationActions.js';
@@ -554,7 +562,7 @@ export class GeneralAI {
continue;
}
const killturn = readMetaNumber(asRecord(candidate.meta), 'killturn', 999);
const killturn = readRequiredMetaNumber(asRecord(candidate.meta), 'killturn', `generalId=${candidate.id}`);
if (killturn <= 5) {
npcCivilGenerals[candidate.id] = candidate;
continue;
@@ -1,5 +1,5 @@
import type { GeneralAI } from '../core.js';
import { asRecord, readMetaNumber } from '../../aiUtils.js';
import { asRecord, readRequiredMetaNumber } from '../../aiUtils.js';
import { t통솔장 } from './helpers.js';
export const doNPC헌납 = (ai: GeneralAI) => {
@@ -64,7 +64,7 @@ export const doNPC헌납 = (ai: GeneralAI) => {
};
export const doNPC사망대비 = (ai: GeneralAI) => {
const killturn = readMetaNumber(asRecord(ai.general.meta), 'killturn', 999);
const killturn = readRequiredMetaNumber(asRecord(ai.general.meta), 'killturn', `generalId=${ai.general.id}`);
if (killturn > 5) {
return null;
}
@@ -1,5 +1,5 @@
import type { GeneralAI } from '../core.js';
import { asRecord, readMetaNumber } from '../../aiUtils.js';
import { asRecord, readRequiredMetaNumber } from '../../aiUtils.js';
import { buildAwardCandidate, buildSeizureCandidate, pickWeightedCandidate, resolveAwardAmount } from './helpers.js';
export const do유저장긴급포상 = (ai: GeneralAI) => {
@@ -81,7 +81,7 @@ export const doNPC긴급포상 = (ai: GeneralAI) => {
continue;
}
for (const general of Object.values(ai.npcWarGenerals)) {
const killturn = readMetaNumber(asRecord(general.meta), 'killturn', 999);
const killturn = readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`);
if (killturn <= 5) {
continue;
}
@@ -115,7 +115,7 @@ export const doNPC포상 = (ai: GeneralAI) => {
continue;
}
for (const general of Object.values(ai.npcWarGenerals)) {
const killturn = readMetaNumber(asRecord(general.meta), 'killturn', 999);
const killturn = readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`);
if (killturn <= 5) {
continue;
}
@@ -126,7 +126,7 @@ export const doNPC포상 = (ai: GeneralAI) => {
candidates.push([buildAwardCandidate(ai, general.id, amount, resKey === 'gold', 'NPC포상'), amount]);
}
for (const general of Object.values(ai.npcCivilGenerals)) {
const killturn = readMetaNumber(asRecord(general.meta), 'killturn', 999);
const killturn = readRequiredMetaNumber(asRecord(general.meta), 'killturn', `generalId=${general.id}`);
if (killturn <= 5) {
continue;
}
+48 -5
View File
@@ -162,6 +162,46 @@ const normalizeGeneralTurnTime = (general: TurnGeneral, fallback: Date): TurnGen
};
};
const readMetaNumber = (meta: Record<string, unknown>, key: string): number | null => {
const value = meta[key];
if (typeof value === 'number' && Number.isFinite(value)) {
return value;
}
if (typeof value === 'string') {
const parsed = Number(value);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
};
const resolveWorldKillturn = (meta: Record<string, unknown>): number | null => {
const killturn = readMetaNumber(meta, 'killturn');
if (killturn !== null) {
return Math.floor(killturn);
}
return null;
};
const ensureGeneralKillturn = (general: TurnGeneral, worldKillturn: number | null): TurnGeneral => {
const meta = { ...general.meta } as Record<string, unknown>;
const existing = readMetaNumber(meta, 'killturn');
if (existing !== null) {
return general;
}
if (worldKillturn === null) {
throw new Error(`meta.killturn is required (generalId=${general.id}).`);
}
return {
...general,
meta: {
...meta,
killturn: worldKillturn,
},
};
};
export class InMemoryTurnWorld {
// DB에서 읽어온 월드 상태를 메모리에 고정해 턴 처리를 담당한다.
private readonly schedule: TurnSchedule;
@@ -200,8 +240,11 @@ export class InMemoryTurnWorld {
} satisfies GeneralTurnHandler);
this.calendarHandler = options.calendarHandler;
const worldKillturn = resolveWorldKillturn(this.state.meta);
for (const general of snapshot.generals) {
this.generals.set(general.id, normalizeGeneralTurnTime({ ...general }, this.state.lastTurnTime));
const normalized = normalizeGeneralTurnTime({ ...general }, this.state.lastTurnTime);
const ensured = ensureGeneralKillturn(normalized, worldKillturn);
this.generals.set(general.id, ensured);
}
for (const city of snapshot.cities) {
this.cities.set(city.id, { ...city });
@@ -571,10 +614,10 @@ export class InMemoryTurnWorld {
if (this.generals.has(createdGeneral.id)) {
continue;
}
this.generals.set(
createdGeneral.id,
normalizeGeneralTurnTime({ ...createdGeneral }, this.state.lastTurnTime)
);
const worldKillturn = resolveWorldKillturn(this.state.meta);
const normalized = normalizeGeneralTurnTime({ ...createdGeneral }, this.state.lastTurnTime);
const ensured = ensureGeneralKillturn(normalized, worldKillturn);
this.generals.set(createdGeneral.id, ensured);
this.dirtyGeneralIds.add(createdGeneral.id);
this.createdGeneralIds.add(createdGeneral.id);
}
@@ -82,6 +82,14 @@ const defaultRunOptions = {
} satisfies Required<Omit<TurnHarnessRunOptions, 'minutes'>>;
export const createTurnTestHarness = async (options: TurnTestHarnessOptions) => {
const stateMeta = { ...options.state.meta } as Record<string, unknown>;
if (typeof stateMeta.killturn !== 'number' || !Number.isFinite(stateMeta.killturn)) {
stateMeta.killturn = 24;
}
const state: TurnWorldState = {
...options.state,
meta: stateMeta,
};
const mockPrisma = createMockPrisma();
const reservedTurnStore = new InMemoryReservedTurnStore(mockPrisma as any, {
maxGeneralTurns: 10,
@@ -126,7 +134,7 @@ export const createTurnTestHarness = async (options: TurnTestHarnessOptions) =>
...(options.extraCalendarHandlers ?? [])
);
const world = new InMemoryTurnWorldClass(options.state, options.snapshot, {
const world = new InMemoryTurnWorldClass(state, options.snapshot, {
schedule: options.schedule,
generalTurnHandler,
calendarHandler,