feat: 프론트 상태 핸들러 추가 및 예약 턴 핸들러와 통합
This commit is contained in:
@@ -0,0 +1,164 @@
|
|||||||
|
import type { City, MapDefinition, Nation } from '@sammo-ts/logic';
|
||||||
|
import type { InMemoryTurnWorld, TurnCalendarHandler } from './inMemoryWorld.js';
|
||||||
|
import type { TurnDiplomacy } from './types.js';
|
||||||
|
|
||||||
|
type ConnectionMap = Map<number, number[]>;
|
||||||
|
|
||||||
|
const buildConnectionMap = (map: MapDefinition): ConnectionMap => {
|
||||||
|
const connectionMap = new Map<number, number[]>();
|
||||||
|
for (const city of map.cities) {
|
||||||
|
connectionMap.set(city.id, city.connections ?? []);
|
||||||
|
}
|
||||||
|
return connectionMap;
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectAdjacentCities = (cityIds: number[], connectionMap: ConnectionMap, bucket: Set<number>): void => {
|
||||||
|
for (const cityId of cityIds) {
|
||||||
|
const neighbors = connectionMap.get(cityId) ?? [];
|
||||||
|
for (const neighborId of neighbors) {
|
||||||
|
bucket.add(neighborId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectNationCityIds = (cities: City[]): Map<number, number[]> => {
|
||||||
|
const map = new Map<number, number[]>();
|
||||||
|
for (const city of cities) {
|
||||||
|
const list = map.get(city.nationId) ?? [];
|
||||||
|
list.push(city.id);
|
||||||
|
map.set(city.nationId, list);
|
||||||
|
}
|
||||||
|
return map;
|
||||||
|
};
|
||||||
|
|
||||||
|
const collectDiplomacyTargets = (entries: TurnDiplomacy[], nationId: number): {
|
||||||
|
warTargets: number[];
|
||||||
|
declareTargets: number[];
|
||||||
|
} => {
|
||||||
|
const warTargets: number[] = [];
|
||||||
|
const declareTargets: number[] = [];
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.fromNationId !== nationId) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (entry.state === 0) {
|
||||||
|
warTargets.push(entry.toNationId);
|
||||||
|
} else if (entry.state === 1 && entry.term <= 5) {
|
||||||
|
declareTargets.push(entry.toNationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { warTargets, declareTargets };
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveNationFrontStates = (
|
||||||
|
nationId: number,
|
||||||
|
connectionMap: ConnectionMap,
|
||||||
|
cityIdsByNation: Map<number, number[]>,
|
||||||
|
diplomacy: TurnDiplomacy[]
|
||||||
|
): Map<number, number> => {
|
||||||
|
const frontStates = new Map<number, number>();
|
||||||
|
if (nationId <= 0) {
|
||||||
|
return frontStates;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { warTargets, declareTargets } = collectDiplomacyTargets(diplomacy, nationId);
|
||||||
|
const adj3 = new Set<number>();
|
||||||
|
const adj2 = new Set<number>();
|
||||||
|
const adj1 = new Set<number>();
|
||||||
|
|
||||||
|
for (const targetNationId of warTargets) {
|
||||||
|
const targetCities = cityIdsByNation.get(targetNationId) ?? [];
|
||||||
|
collectAdjacentCities(targetCities, connectionMap, adj3);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const targetNationId of declareTargets) {
|
||||||
|
const targetCities = cityIdsByNation.get(targetNationId) ?? [];
|
||||||
|
collectAdjacentCities(targetCities, connectionMap, adj1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (adj3.size === 0 && adj1.size === 0) {
|
||||||
|
const neutralCities = cityIdsByNation.get(0) ?? [];
|
||||||
|
collectAdjacentCities(neutralCities, connectionMap, adj2);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationCityIds = cityIdsByNation.get(nationId) ?? [];
|
||||||
|
for (const cityId of nationCityIds) {
|
||||||
|
let nextFrontState = 0;
|
||||||
|
if (adj1.has(cityId)) {
|
||||||
|
nextFrontState = 1;
|
||||||
|
}
|
||||||
|
if (adj2.has(cityId)) {
|
||||||
|
nextFrontState = 2;
|
||||||
|
}
|
||||||
|
if (adj3.has(cityId)) {
|
||||||
|
nextFrontState = 3;
|
||||||
|
}
|
||||||
|
frontStates.set(cityId, nextFrontState);
|
||||||
|
}
|
||||||
|
return frontStates;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const buildFrontStatePatches = (options: {
|
||||||
|
worldView: {
|
||||||
|
listCities(): City[];
|
||||||
|
listNations(): Nation[];
|
||||||
|
listDiplomacy(): TurnDiplomacy[];
|
||||||
|
};
|
||||||
|
map: MapDefinition | null | undefined;
|
||||||
|
nationIds?: number[];
|
||||||
|
}): Array<{ id: number; patch: Partial<City> }> => {
|
||||||
|
if (!options.map) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const connectionMap = buildConnectionMap(options.map);
|
||||||
|
if (connectionMap.size === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const cities = options.worldView.listCities();
|
||||||
|
const cityIdsByNation = collectNationCityIds(cities);
|
||||||
|
const diplomacy = options.worldView.listDiplomacy();
|
||||||
|
const cityMap = new Map<number, City>();
|
||||||
|
for (const city of cities) {
|
||||||
|
cityMap.set(city.id, city);
|
||||||
|
}
|
||||||
|
|
||||||
|
const nationList = options.nationIds
|
||||||
|
? options.worldView
|
||||||
|
.listNations()
|
||||||
|
.filter((nation) => options.nationIds?.includes(nation.id))
|
||||||
|
: options.worldView.listNations();
|
||||||
|
const nations = nationList.filter((nation) => nation.level > 0 && nation.capitalCityId);
|
||||||
|
const patches: Array<{ id: number; patch: Partial<City> }> = [];
|
||||||
|
for (const nation of nations) {
|
||||||
|
const frontStates = resolveNationFrontStates(nation.id, connectionMap, cityIdsByNation, diplomacy);
|
||||||
|
for (const [cityId, nextFrontState] of frontStates) {
|
||||||
|
const city = cityMap.get(cityId);
|
||||||
|
if (!city || city.frontState === nextFrontState) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
patches.push({ id: cityId, patch: { frontState: nextFrontState } });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patches;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createFrontStateHandler = (options: {
|
||||||
|
getWorld: () => InMemoryTurnWorld | null;
|
||||||
|
map: MapDefinition | null | undefined;
|
||||||
|
}): TurnCalendarHandler => {
|
||||||
|
return {
|
||||||
|
onMonthChanged: () => {
|
||||||
|
const world = options.getWorld();
|
||||||
|
if (!world) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const patches = buildFrontStatePatches({
|
||||||
|
worldView: world,
|
||||||
|
map: options.map,
|
||||||
|
});
|
||||||
|
for (const patch of patches) {
|
||||||
|
world.updateCity(patch.id, patch.patch);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -37,6 +37,7 @@ import {
|
|||||||
type DiplomacyPatch,
|
type DiplomacyPatch,
|
||||||
} from '@sammo-ts/logic';
|
} from '@sammo-ts/logic';
|
||||||
import { buildCommandEnv, buildReservedTurnDefinitions } from './reservedTurnCommands.js';
|
import { buildCommandEnv, buildReservedTurnDefinitions } from './reservedTurnCommands.js';
|
||||||
|
import { buildFrontStatePatches } from './frontStateHandler.js';
|
||||||
import { buildActionContext } from './reservedTurnActionContext.js';
|
import { buildActionContext } from './reservedTurnActionContext.js';
|
||||||
import { GeneralAI, shouldUseAi } from './ai/generalAi.js';
|
import { GeneralAI, shouldUseAi } from './ai/generalAi.js';
|
||||||
import type { AiReservedTurnProvider } from './ai/types.js';
|
import type { AiReservedTurnProvider } from './ai/types.js';
|
||||||
@@ -701,6 +702,31 @@ export const createReservedTurnHandler = async (options: {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasDiplomacyChange = diplomacyPatches.length > 0;
|
||||||
|
const hasNationChange = (resolution.patches?.cities ?? []).some((patch) =>
|
||||||
|
Object.prototype.hasOwnProperty.call(patch.patch ?? {}, 'nationId')
|
||||||
|
);
|
||||||
|
if (hasDiplomacyChange || hasNationChange) {
|
||||||
|
const worldView = worldOverlay?.view ?? worldRef;
|
||||||
|
if (worldView && options.map) {
|
||||||
|
const frontPatches = buildFrontStatePatches({
|
||||||
|
worldView,
|
||||||
|
map: options.map,
|
||||||
|
});
|
||||||
|
if (frontPatches.length > 0) {
|
||||||
|
for (const patch of frontPatches) {
|
||||||
|
const existing = patches.cities.find((entry) => entry.id === patch.id);
|
||||||
|
if (existing) {
|
||||||
|
existing.patch = { ...existing.patch, ...patch.patch };
|
||||||
|
} else {
|
||||||
|
patches.cities.push({ id: patch.id, patch: patch.patch });
|
||||||
|
}
|
||||||
|
worldOverlay?.applyCityPatch(patch.id, patch.patch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { nextTurnAt: applyNextTurnAt ? resolution.nextTurnAt : undefined, actionKey, usedFallback, blockedReason };
|
return { nextTurnAt: applyNextTurnAt ? resolution.nextTurnAt : undefined, actionKey, usedFallback, blockedReason };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { createGatewayAdminActionConsumer } from './gatewayAdminActions.js';
|
|||||||
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
||||||
import { composeCalendarHandlers } from './calendarHandlers.js';
|
import { composeCalendarHandlers } from './calendarHandlers.js';
|
||||||
import { createIncomeHandler } from './incomeHandler.js';
|
import { createIncomeHandler } from './incomeHandler.js';
|
||||||
|
import { createFrontStateHandler } from './frontStateHandler.js';
|
||||||
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
||||||
import { createReservedTurnStore } from './reservedTurnStore.js';
|
import { createReservedTurnStore } from './reservedTurnStore.js';
|
||||||
import { createTurnDaemonCommandHandler } from './worldCommandHandler.js';
|
import { createTurnDaemonCommandHandler } from './worldCommandHandler.js';
|
||||||
@@ -122,6 +123,10 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
scenarioConfig: snapshot.scenarioConfig,
|
scenarioConfig: snapshot.scenarioConfig,
|
||||||
nationTraits: nationTraitMap,
|
nationTraits: nationTraitMap,
|
||||||
});
|
});
|
||||||
|
const frontStateHandler = createFrontStateHandler({
|
||||||
|
getWorld: () => worldRef,
|
||||||
|
map: snapshot.map ?? null,
|
||||||
|
});
|
||||||
const tournamentAutoStartHandler = createTournamentAutoStartHandler({
|
const tournamentAutoStartHandler = createTournamentAutoStartHandler({
|
||||||
profileName: options.profileName ?? options.profile,
|
profileName: options.profileName ?? options.profile,
|
||||||
getRedisClient: () => redisConnector?.client,
|
getRedisClient: () => redisConnector?.client,
|
||||||
@@ -131,6 +136,7 @@ export const createTurnDaemonRuntime = async (options: TurnDaemonRuntimeOptions)
|
|||||||
const calendarHandler = composeCalendarHandlers(
|
const calendarHandler = composeCalendarHandlers(
|
||||||
options.calendarHandler ?? unification?.handler,
|
options.calendarHandler ?? unification?.handler,
|
||||||
incomeHandler,
|
incomeHandler,
|
||||||
|
frontStateHandler,
|
||||||
tournamentAutoStartHandler
|
tournamentAutoStartHandler
|
||||||
);
|
);
|
||||||
const worldOptions: InMemoryTurnWorldOptions = {
|
const worldOptions: InMemoryTurnWorldOptions = {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { createReservedTurnHandler } from '../src/turn/reservedTurnHandler.js';
|
|||||||
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
|
import { InMemoryTurnProcessor } from '../src/turn/inMemoryTurnProcessor.js';
|
||||||
import { createIncomeHandler } from '../src/turn/incomeHandler.js';
|
import { createIncomeHandler } from '../src/turn/incomeHandler.js';
|
||||||
import { createNpcTaxHandler } from '../src/turn/npcTaxHandler.js';
|
import { createNpcTaxHandler } from '../src/turn/npcTaxHandler.js';
|
||||||
|
import { createFrontStateHandler } from '../src/turn/frontStateHandler.js';
|
||||||
import { composeCalendarHandlers } from '../src/turn/calendarHandlers.js';
|
import { composeCalendarHandlers } from '../src/turn/calendarHandlers.js';
|
||||||
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
import { LARGE_TEST_MAP, buildLargeTestCities } from './fixtures/largeTestMap.js';
|
||||||
|
|
||||||
@@ -272,7 +273,12 @@ describe('NPC 대형 시뮬레이션', () => {
|
|||||||
getWorld: () => wrapper.world,
|
getWorld: () => wrapper.world,
|
||||||
});
|
});
|
||||||
|
|
||||||
const calendarHandler = composeCalendarHandlers(incomeHandler, npcTaxHandler);
|
const frontStateHandler = createFrontStateHandler({
|
||||||
|
getWorld: () => wrapper.world,
|
||||||
|
map: LARGE_TEST_MAP,
|
||||||
|
});
|
||||||
|
|
||||||
|
const calendarHandler = composeCalendarHandlers(incomeHandler, npcTaxHandler, frontStateHandler);
|
||||||
|
|
||||||
const world = new InMemoryTurnWorld(state, snapshot, {
|
const world = new InMemoryTurnWorld(state, snapshot, {
|
||||||
schedule,
|
schedule,
|
||||||
|
|||||||
Reference in New Issue
Block a user