fix: Ref 게임 로직과 시나리오 풀 호환을 보정

월 경계, 전투 기술 상한, 연감과 베팅·설문·경매 정산 순서를 Ref 계약에 맞춘다.\n\n시나리오 일반 풀을 ENGINE mutation과 logical tick 기반으로 직렬화하고 914·915 catalog 및 조건부 100기 pool 실행 경계를 추가한다.\n\n경매 worker는 세대별 durable event만 만들고 ENGINE이 row lock 후 상태 전이와 정산을 단일 transaction으로 소유한다.
This commit is contained in:
2026-08-23 16:26:14 +00:00
parent bf6b7be7b0
commit 85591c68ad
114 changed files with 13327 additions and 901 deletions
@@ -1,11 +1,12 @@
import { existsSync } from 'node:fs';
import { readdir } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, it } from 'vitest';
import {
MONTHLY_EVENT_ACTION_CATALOG,
type MonthlyEventActionName,
} from '../src/turn/monthlyEventHandler.js';
import { resolveScenarioDefaultsPath } from '../src/scenario/scenarioLoader.js';
import { MONTHLY_EVENT_ACTION_CATALOG, type MonthlyEventActionName } from '../src/turn/monthlyEventHandler.js';
import { hasRefSourceRoot, resolveRefSourceRoot } from './refSourceRoot.js';
interface CatalogSegment {
name: string;
@@ -34,13 +35,7 @@ const segments = [
{
name: 'city-economy-boundaries',
kind: 'single-boundary',
actions: [
'RaiseDisaster',
'UpdateCitySupply',
'UpdateNationLevel',
'ProcessSemiAnnual',
'ProcessWarIncome',
],
actions: ['RaiseDisaster', 'UpdateCitySupply', 'UpdateNationLevel', 'ProcessSemiAnnual', 'ProcessWarIncome'],
coreEvidence: [
'monthlyDisasterPersistence.integration.test.ts',
'monthlyCitySupplyPersistence.integration.test.ts',
@@ -84,11 +79,7 @@ const segments = [
kind: 'multi-month',
actions: ['RaiseInvader', 'AutoDeleteInvader', 'InvaderEnding'],
coreEvidence: ['monthlyInvaderPersistence.integration.test.ts'],
refEvidence: [
'monthly_raise_invader.json',
'monthly_auto_delete_invader.json',
'monthly_invader_ending.json',
],
refEvidence: ['monthly_raise_invader.json', 'monthly_auto_delete_invader.json', 'monthly_invader_ending.json'],
},
{
name: 'npc-troop-support',
@@ -136,17 +127,62 @@ const segments = [
coreEvidence: ['monthlyUniqueInheritPersistence.integration.test.ts'],
refEvidence: ['monthly_lost_unique_item.json', 'monthly_merge_inherit_point_rank.json'],
},
{
name: 'centennial-all-star-growth',
kind: 'multi-month',
actions: ['AdvanceCentennialAllStar'],
coreEvidence: ['monthlyCentennialAllStarAction.test.ts'],
refEvidence: ['CentennialAllStarGrowthTest.php', 'AdvanceCentennialAllStar.php'],
},
] as const satisfies readonly CatalogSegment[];
const KNOWN_MISSING_SCENARIO_RESOURCES = [] as const;
const KNOWN_MISSING_MONTHLY_ACTIONS = [] as const;
const listBasenames = async (directory: string, pattern: RegExp): Promise<string[]> =>
(await readdir(directory, { withFileTypes: true }))
.filter((entry) => entry.isFile() && pattern.test(entry.name))
.map((entry) => entry.name)
.sort();
const difference = (left: readonly string[], right: readonly string[]): string[] => {
const rightSet = new Set(right);
return left.filter((value) => !rightSet.has(value)).sort();
};
const refSourceIt = hasRefSourceRoot() ? it : it.skip;
describe('monthly event catalog coverage', () => {
it('assigns every legacy action to exactly one dependency-safe segment', () => {
it('assigns every Core monthly action to exactly one dependency-safe segment', () => {
const covered = segments.flatMap((segment) => segment.actions);
expect(covered).toHaveLength(29);
expect(new Set(covered).size).toBe(29);
expect(new Set(covered).size).toBe(covered.length);
expect(new Set(MONTHLY_EVENT_ACTION_CATALOG).size).toBe(MONTHLY_EVENT_ACTION_CATALOG.length);
expect([...covered].sort()).toEqual([...MONTHLY_EVENT_ACTION_CATALOG].sort());
});
refSourceIt('keeps the Core and Ref scenario resource catalogs complete', async () => {
const refScenarioDirectory = path.join(resolveRefSourceRoot(), 'hwe', 'scenario');
const coreScenarioDirectory = path.dirname(resolveScenarioDefaultsPath());
const [refScenarios, coreScenarios] = await Promise.all([
listBasenames(refScenarioDirectory, /^scenario_\d+\.json$/),
listBasenames(coreScenarioDirectory, /^scenario_\d+\.json$/),
]);
expect(difference(refScenarios, coreScenarios)).toEqual([...KNOWN_MISSING_SCENARIO_RESOURCES]);
expect(difference(coreScenarios, refScenarios)).toEqual([]);
});
refSourceIt('keeps the Core and Ref monthly action catalogs complete', async () => {
const refActionDirectory = path.join(resolveRefSourceRoot(), 'hwe', 'sammo', 'Event', 'Action');
const refActions = (await listBasenames(refActionDirectory, /\.php$/)).map((fileName) =>
fileName.replace(/\.php$/, '')
);
expect(difference(refActions, MONTHLY_EVENT_ACTION_CATALOG)).toEqual([...KNOWN_MISSING_MONTHLY_ACTIONS]);
expect(difference(MONTHLY_EVENT_ACTION_CATALOG, refActions)).toEqual([]);
});
it('keeps every core evidence file executable in this suite', () => {
const testDirectory = fileURLToPath(new URL('.', import.meta.url));
const evidenceFiles = new Set(segments.flatMap((segment) => segment.coreEvidence));
@@ -170,6 +206,7 @@ describe('monthly event catalog coverage', () => {
'InvaderEnding',
'OpenNationBetting',
'FinishNationBetting',
'AdvanceCentennialAllStar',
]);
expect(specialDispositions).toEqual(['CreateAdminNPC', 'UnblockScoutAction']);
expect(segments.every((segment) => segment.refEvidence.length > 0)).toBe(true);