feat(engine): migrate monthly pre-update boundary
This commit is contained in:
@@ -887,6 +887,24 @@ export const createDatabaseTurnHooks = async (
|
||||
data: buildGeneralUpdate(general),
|
||||
})
|
||||
),
|
||||
...generals
|
||||
.filter(
|
||||
(general) =>
|
||||
typeof general.refreshScoreTotal === 'number' && Number.isFinite(general.refreshScoreTotal)
|
||||
)
|
||||
.map((general) =>
|
||||
prisma.generalAccessLog.upsert({
|
||||
where: { generalId: general.id },
|
||||
update: {
|
||||
refreshScoreTotal: Math.floor(general.refreshScoreTotal ?? 0),
|
||||
},
|
||||
create: {
|
||||
generalId: general.id,
|
||||
userId: general.userId ?? null,
|
||||
refreshScoreTotal: Math.floor(general.refreshScoreTotal ?? 0),
|
||||
},
|
||||
})
|
||||
),
|
||||
...cities.map((city) =>
|
||||
prisma.city.update({
|
||||
where: { id: city.id },
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { asRecord } from '@sammo-ts/common';
|
||||
import type { TurnCommandEnv } from '@sammo-ts/logic';
|
||||
|
||||
import type { InMemoryTurnWorld, TurnCalendarHandler } from './inMemoryWorld.js';
|
||||
|
||||
const CITY_STATE_TRANSITIONS = new Map<number, number>([
|
||||
[31, 0],
|
||||
[32, 31],
|
||||
[33, 0],
|
||||
[34, 33],
|
||||
[41, 0],
|
||||
[42, 41],
|
||||
[43, 42],
|
||||
]);
|
||||
|
||||
const readFiniteNumber = (value: unknown, fallback = 0): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
};
|
||||
|
||||
/**
|
||||
* ref preUpdateMonthly()에서 국가 외 상태를 날짜 변경 전에 갱신한다.
|
||||
* 연감 저장은 이 handler보다 먼저, MONTH event는 이 handler보다 나중에
|
||||
* 실행되도록 turnDaemon의 calendar handler 순서가 계약을 보장한다.
|
||||
*/
|
||||
export const createMonthlyBoundaryPreHandler = (options: {
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
startYear: number;
|
||||
commandEnv: TurnCommandEnv;
|
||||
}): TurnCalendarHandler => ({
|
||||
beforeMonthChanged: (context) => {
|
||||
const world = options.getWorld();
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
|
||||
const develCost = (context.previousYear - options.startYear + 10) * 2;
|
||||
options.commandEnv.develCost = develCost;
|
||||
world.updateWorldMeta({ develcost: develCost });
|
||||
|
||||
for (const general of world.listGenerals()) {
|
||||
const meta = asRecord(general.meta);
|
||||
world.updateGeneral(general.id, {
|
||||
...(general.refreshScoreTotal === undefined
|
||||
? {}
|
||||
: { refreshScoreTotal: Math.floor(readFiniteNumber(general.refreshScoreTotal) * 0.99) }),
|
||||
meta: {
|
||||
...general.meta,
|
||||
makelimit: Math.max(0, Math.floor(readFiniteNumber(meta.makelimit)) - 1),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
for (const city of world.listCities()) {
|
||||
const meta = asRecord(city.meta);
|
||||
const nextTerm = Math.max(0, Math.floor(readFiniteNumber(meta.term)) - 1);
|
||||
world.updateCity(city.id, {
|
||||
state: CITY_STATE_TRANSITIONS.get(city.state) ?? city.state,
|
||||
conflict: nextTerm === 0 ? {} : city.conflict,
|
||||
meta: {
|
||||
...city.meta,
|
||||
term: nextTerm,
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -15,11 +15,56 @@ const decrementLimit = (value: unknown): number => {
|
||||
return 0;
|
||||
};
|
||||
|
||||
// ref preUpdateMonthly(): 전략 제한과 외교 제한은 매 월턴마다 1씩 감소한다.
|
||||
const readRate = (value: unknown): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return 20;
|
||||
};
|
||||
|
||||
const readSpyRemain = (value: unknown): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) {
|
||||
return value;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const parsed = Number(value);
|
||||
if (Number.isFinite(parsed)) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const decrementSpy = (value: unknown): Record<string, number> => {
|
||||
let raw: unknown = value;
|
||||
if (typeof value === 'string') {
|
||||
try {
|
||||
raw = JSON.parse(value);
|
||||
} catch {
|
||||
raw = {};
|
||||
}
|
||||
}
|
||||
const result: Record<string, number> = {};
|
||||
for (const [cityId, remain] of Object.entries(asRecord(raw))) {
|
||||
const numeric = readSpyRemain(remain);
|
||||
if (numeric > 1) {
|
||||
result[cityId] = numeric - 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
// ref preUpdateMonthly(): 국가 제한·세율·첩보는 MONTH action보다 먼저 갱신한다.
|
||||
export const createNationTurnMonthlyHandler = (options: {
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
}): TurnCalendarHandler => ({
|
||||
onMonthChanged: () => {
|
||||
beforeMonthChanged: () => {
|
||||
const world = options.getWorld();
|
||||
if (!world) {
|
||||
return;
|
||||
@@ -31,6 +76,8 @@ export const createNationTurnMonthlyHandler = (options: {
|
||||
...nation.meta,
|
||||
strategic_cmd_limit: decrementLimit(meta.strategic_cmd_limit),
|
||||
surlimit: decrementLimit(meta.surlimit),
|
||||
rate_tmp: readRate(meta.rate),
|
||||
spy: decrementSpy(meta.spy),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -716,6 +716,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
unitSet?: UnitSetDefinition;
|
||||
getWorld: () => InMemoryTurnWorld | null;
|
||||
commandProfile?: TurnCommandProfile;
|
||||
commandEnv?: TurnCommandEnv;
|
||||
commandRngFactory?: (input: { kind: 'nation' | 'general'; actionKey: string; seed: string }) => RandUtil;
|
||||
onActionResolved?: (payload: {
|
||||
kind: 'nation' | 'general';
|
||||
@@ -728,7 +729,7 @@ export const createReservedTurnHandler = async (options: {
|
||||
aiState?: ReturnType<GeneralAI['getDebugState']>;
|
||||
}) => void;
|
||||
}): Promise<GeneralTurnHandler> => {
|
||||
const env = buildCommandEnv(options.scenarioConfig, options.unitSet);
|
||||
const env = options.commandEnv ?? buildCommandEnv(options.scenarioConfig, options.unitSet);
|
||||
const itemRegistry = createItemModuleRegistry(await loadItemModules([...ITEM_KEYS]));
|
||||
const uniqueConfig = resolveUniqueConfig(asRecord(options.scenarioConfig.const));
|
||||
if (Object.keys(uniqueConfig.allItems).length === 0) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { createGatewayProfileGate } from './gatewayProfileGate.js';
|
||||
import { composeCalendarHandlers } from './calendarHandlers.js';
|
||||
import { createIncomeHandler } from './incomeHandler.js';
|
||||
import { createNationTurnMonthlyHandler } from './nationTurnMonthlyHandler.js';
|
||||
import { createMonthlyBoundaryPreHandler } from './monthlyBoundaryPreHandler.js';
|
||||
import { createFrontStateHandler } from './frontStateHandler.js';
|
||||
import { createReservedTurnHandler } from './reservedTurnHandler.js';
|
||||
import { createReservedTurnStore } from './reservedTurnStore.js';
|
||||
@@ -443,6 +444,11 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
const nationTurnMonthlyHandler = createNationTurnMonthlyHandler({
|
||||
getWorld: () => worldRef,
|
||||
});
|
||||
const monthlyBoundaryPreHandler = createMonthlyBoundaryPreHandler({
|
||||
getWorld: () => worldRef,
|
||||
startYear: snapshot.scenarioMeta?.startYear ?? state.currentYear,
|
||||
commandEnv: monthlyCommandEnv,
|
||||
});
|
||||
const frontStateHandler = createFrontStateHandler({
|
||||
getWorld: () => worldRef,
|
||||
map: snapshot.map ?? null,
|
||||
@@ -467,13 +473,14 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
});
|
||||
const calendarHandler = composeCalendarHandlers(
|
||||
monthlyEventHandler,
|
||||
yearbookHandler.handler,
|
||||
options.calendarHandler ?? unification?.handler,
|
||||
monthlyBoundaryPreHandler,
|
||||
nationTurnMonthlyHandler,
|
||||
hasEventAction('ProcessIncome') ? null : incomeHandler,
|
||||
frontStateHandler,
|
||||
neutralAuctionRegistrar.handler,
|
||||
tournamentAutoStartHandler,
|
||||
yearbookHandler.handler
|
||||
tournamentAutoStartHandler
|
||||
);
|
||||
const worldOptions: InMemoryTurnWorldOptions = {
|
||||
schedule,
|
||||
@@ -487,6 +494,7 @@ const createTurnDaemonRuntimeWithLease = async (
|
||||
unitSet: snapshot.unitSet,
|
||||
getWorld: () => worldRef,
|
||||
commandProfile,
|
||||
commandEnv: monthlyCommandEnv,
|
||||
})),
|
||||
calendarHandler: calendarHandler ?? undefined,
|
||||
};
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface TurnGeneral extends General {
|
||||
lastTurn?: GeneralLastTurn;
|
||||
penalty?: unknown;
|
||||
inheritancePoints?: Record<string, number>;
|
||||
refreshScoreTotal?: number;
|
||||
}
|
||||
|
||||
export interface TurnDiplomacy {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type TurnEngineDatabaseClient,
|
||||
type TurnEngineDiplomacyRow,
|
||||
type TurnEngineGeneralRow,
|
||||
type TurnEngineGeneralAccessLogRow,
|
||||
type TurnEngineInheritancePointRow,
|
||||
type TurnEngineRankDataRow,
|
||||
type TurnEngineNationRow,
|
||||
@@ -170,7 +171,8 @@ const GENERAL_RANK_META_PREFIX_TYPES = new Set([
|
||||
const mapGeneralRow = (
|
||||
row: TurnEngineGeneralRow,
|
||||
rankRows: readonly TurnEngineRankDataRow[],
|
||||
inheritanceRows: readonly TurnEngineInheritancePointRow[]
|
||||
inheritanceRows: readonly TurnEngineInheritancePointRow[],
|
||||
accessRow?: TurnEngineGeneralAccessLogRow
|
||||
): TurnGeneral => {
|
||||
const legacySlots: GeneralItemSlots = {
|
||||
horse: normalizeCode(row.horseCode),
|
||||
@@ -243,6 +245,7 @@ const mapGeneralRow = (
|
||||
turnTime: row.turnTime,
|
||||
recentWarTime: row.recentWarTime ?? null,
|
||||
inheritancePoints,
|
||||
...(accessRow ? { refreshScoreTotal: accessRow.refreshScoreTotal } : {}),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -340,19 +343,29 @@ export const loadTurnWorldFromDatabase = async (options: TurnWorldLoaderOptions)
|
||||
throw new Error('world_state row is required to start turn daemon.');
|
||||
}
|
||||
|
||||
const [generalRows, rankRows, inheritanceRows, cityRows, nationRows, diplomacyRows, troopRows, eventRows] =
|
||||
await Promise.all([
|
||||
prisma.general.findMany(),
|
||||
prisma.rankData.findMany(),
|
||||
prisma.inheritancePoint.findMany(),
|
||||
prisma.city.findMany(),
|
||||
prisma.nation.findMany(),
|
||||
prisma.diplomacy.findMany(),
|
||||
prisma.troop.findMany(),
|
||||
prisma.event.findMany({
|
||||
orderBy: [{ priority: 'desc' }, { id: 'asc' }],
|
||||
}),
|
||||
]);
|
||||
const [
|
||||
generalRows,
|
||||
rankRows,
|
||||
inheritanceRows,
|
||||
accessRows,
|
||||
cityRows,
|
||||
nationRows,
|
||||
diplomacyRows,
|
||||
troopRows,
|
||||
eventRows,
|
||||
] = await Promise.all([
|
||||
prisma.general.findMany(),
|
||||
prisma.rankData.findMany(),
|
||||
prisma.inheritancePoint.findMany(),
|
||||
prisma.generalAccessLog.findMany(),
|
||||
prisma.city.findMany(),
|
||||
prisma.nation.findMany(),
|
||||
prisma.diplomacy.findMany(),
|
||||
prisma.troop.findMany(),
|
||||
prisma.event.findMany({
|
||||
orderBy: [{ priority: 'desc' }, { id: 'asc' }],
|
||||
}),
|
||||
]);
|
||||
|
||||
const ranksByGeneral = new Map<number, TurnEngineRankDataRow[]>();
|
||||
for (const row of rankRows) {
|
||||
@@ -366,11 +379,13 @@ export const loadTurnWorldFromDatabase = async (options: TurnWorldLoaderOptions)
|
||||
bucket.push(row);
|
||||
inheritanceByUser.set(row.userId, bucket);
|
||||
}
|
||||
const accessByGeneral = new Map(accessRows.map((row) => [row.generalId, row]));
|
||||
const generals = generalRows.map((row) =>
|
||||
mapGeneralRow(
|
||||
row,
|
||||
ranksByGeneral.get(row.id) ?? [],
|
||||
row.userId ? (inheritanceByUser.get(row.userId) ?? []) : []
|
||||
row.userId ? (inheritanceByUser.get(row.userId) ?? []) : [],
|
||||
accessByGeneral.get(row.id)
|
||||
)
|
||||
);
|
||||
const cities = cityRows.map(mapCityRow);
|
||||
|
||||
@@ -56,12 +56,9 @@ const buildMapSnapshot = (world: InMemoryTurnWorld, year: number, month: number)
|
||||
return [city.id, city.level, stateValue, city.nationId, region, supplyFlag];
|
||||
});
|
||||
|
||||
const nationList: MapNationCompact[] = world.listNations().map((nation) => [
|
||||
nation.id,
|
||||
nation.name,
|
||||
nation.color,
|
||||
nation.capitalCityId ?? 0,
|
||||
]);
|
||||
const nationList: MapNationCompact[] = world
|
||||
.listNations()
|
||||
.map((nation) => [nation.id, nation.name, nation.color, nation.capitalCityId ?? 0]);
|
||||
|
||||
return {
|
||||
result: true,
|
||||
@@ -84,8 +81,7 @@ const buildNationSnapshot = (world: InMemoryTurnWorld): YearbookNation[] => {
|
||||
|
||||
for (const city of cities) {
|
||||
const entry = cityStatsByNation.get(city.nationId) ?? { popSum: 0, valueSum: 0, maxSum: 0 };
|
||||
const valueSum =
|
||||
city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||
const valueSum = city.population + city.agriculture + city.commerce + city.security + city.wall + city.defence;
|
||||
const maxSum =
|
||||
city.populationMax +
|
||||
city.agricultureMax +
|
||||
@@ -152,40 +148,38 @@ export const createYearbookHandler = (options: {
|
||||
const ready = connector.connect();
|
||||
|
||||
const handler: TurnCalendarHandler = {
|
||||
onMonthChanged: (context) => {
|
||||
beforeMonthChanged: async (context) => {
|
||||
const world = options.getWorld();
|
||||
if (!world) {
|
||||
return;
|
||||
}
|
||||
void (async () => {
|
||||
await ready;
|
||||
const map = buildMapSnapshot(world, context.previousYear, context.previousMonth);
|
||||
const nations = buildNationSnapshot(world);
|
||||
const hash = buildHash(map, nations);
|
||||
await ready;
|
||||
const map = buildMapSnapshot(world, context.previousYear, context.previousMonth);
|
||||
const nations = buildNationSnapshot(world);
|
||||
const hash = buildHash(map, nations);
|
||||
|
||||
await connector.prisma.yearbookHistory.upsert({
|
||||
where: {
|
||||
profileName_year_month: {
|
||||
profileName: options.profileName,
|
||||
year: context.previousYear,
|
||||
month: context.previousMonth,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
map,
|
||||
nations,
|
||||
hash,
|
||||
},
|
||||
create: {
|
||||
await connector.prisma.yearbookHistory.upsert({
|
||||
where: {
|
||||
profileName_year_month: {
|
||||
profileName: options.profileName,
|
||||
year: context.previousYear,
|
||||
month: context.previousMonth,
|
||||
map,
|
||||
nations,
|
||||
hash,
|
||||
},
|
||||
});
|
||||
})();
|
||||
},
|
||||
update: {
|
||||
map,
|
||||
nations,
|
||||
hash,
|
||||
},
|
||||
create: {
|
||||
profileName: options.profileName,
|
||||
year: context.previousYear,
|
||||
month: context.previousMonth,
|
||||
map,
|
||||
nations,
|
||||
hash,
|
||||
},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -194,4 +188,4 @@ export const createYearbookHandler = (options: {
|
||||
};
|
||||
|
||||
return { handler, close };
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user