feat: 메타 데이터에서 필수 killturn 값을 읽어오는 로직 추가 및 관련 함수 수정
This commit is contained in:
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user