diff --git a/app/game-engine/test/monthlyNationBettingPersistence.integration.test.ts b/app/game-engine/test/monthlyNationBettingPersistence.integration.test.ts index 73e0434..5bade09 100644 --- a/app/game-engine/test/monthlyNationBettingPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyNationBettingPersistence.integration.test.ts @@ -8,6 +8,7 @@ import { createFinishNationBettingHandler, createOpenNationBettingHandler, } from '../src/turn/monthlyNationBettingAction.js'; +import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js'; import type { TurnEvent, TurnGeneral, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js'; const databaseUrl = process.env.INPUT_EVENT_DATABASE_URL; @@ -96,7 +97,7 @@ const event: TurnEvent = { targetCode: 'month', priority: 1_000, condition: true, - action: [], + action: [['OpenNationBetting', 1, 100]], meta: {}, }; @@ -109,7 +110,13 @@ integration('monthly nation betting persistence', () => { await connector.connect(); db = connector.prisma; closeDb = () => connector.disconnect(); + await db.event.deleteMany({ where: { id: { in: [2, 3] } } }); await db.nationBetting.deleteMany({ where: { id: bettingId } }); + await db.diplomacy.deleteMany({ + where: { + OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }], + }, + }); await db.rankData.deleteMany({ where: { generalId: { in: [...generalIds] } } }); await db.inheritanceLog.deleteMany({ where: { userId: { in: [...userIds] } } }); await db.inheritancePoint.deleteMany({ where: { userId: { in: [...userIds] } } }); @@ -120,7 +127,13 @@ integration('monthly nation betting persistence', () => { }); afterAll(async () => { + await db.event.deleteMany({ where: { id: { in: [2, 3] } } }); await db.nationBetting.deleteMany({ where: { id: bettingId } }); + await db.diplomacy.deleteMany({ + where: { + OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }], + }, + }); await db.rankData.deleteMany({ where: { generalId: { in: [...generalIds] } } }); await db.inheritanceLog.deleteMany({ where: { userId: { in: [...userIds] } } }); await db.inheritancePoint.deleteMany({ where: { userId: { in: [...userIds] } } }); @@ -197,8 +210,8 @@ integration('monthly nation betting persistence', () => { const row = await db.worldState.create({ data: { scenarioCode: 'monthly-nation-betting-persistence', - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, config: {}, meta: { lastBettingId: bettingId - 1 }, @@ -206,8 +219,8 @@ integration('monthly nation betting persistence', () => { }); const state: TurnWorldState = { id: row.id, - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, lastTurnTime: new Date('2026-07-25T00:00:00.000Z'), meta: { lastBettingId: bettingId - 1 }, @@ -219,7 +232,10 @@ integration('monthly nation betting persistence', () => { const: {}, environment: { mapName: 'test', unitSet: 'default' }, }; - const world = new InMemoryTurnWorld( + let world: InMemoryTurnWorld | null = null; + const open = createOpenNationBettingHandler({ getWorld: () => world }); + const finish = createFinishNationBettingHandler({ getWorld: () => world }); + world = new InMemoryTurnWorld( state, { scenarioConfig, @@ -232,19 +248,22 @@ integration('monthly nation betting persistence', () => { events: [event], initialEvents: [], }, - { schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] } } + { + schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + calendarHandler: createMonthlyEventHandler({ + getWorld: () => world, + startYear: 190, + actions: new Map([ + ['OpenNationBetting', open], + ['FinishNationBetting', finish], + ]), + }), + } ); const hooks = await createDatabaseTurnHooks(databaseUrl!, world); - const environment = { - year: 200, - month: 1, - startyear: 190, - currentEventID: 1, - turnTime: state.lastTurnTime, - }; try { - await createOpenNationBettingHandler({ getWorld: () => world })([1, 100], environment, event); + await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z')); await hooks.hooks.flushChanges?.({ lastTurnTime: state.lastTurnTime.toISOString(), processedGenerals: 0, @@ -289,7 +308,18 @@ integration('monthly nation betting persistence', () => { data: userIds.map((userId) => ({ userId, key: 'previous', value: 900 })), }); world.updateNation(nationIds[0], { level: 0 }); - await createFinishNationBettingHandler({ getWorld: () => world })([bettingId], environment, event); + expect(world.removeEvent(event.id)).toBe(true); + expect( + world.addEvent({ + id: 3, + targetCode: 'month', + priority: 1_000, + condition: true, + action: [['FinishNationBetting', bettingId]], + meta: {}, + }) + ).toBe(true); + await world.advanceMonth(new Date('0200-02-01T00:00:00.000Z')); await hooks.hooks.flushChanges?.({ lastTurnTime: state.lastTurnTime.toISOString(), processedGenerals: 0, @@ -324,8 +354,8 @@ integration('monthly nation betting persistence', () => { }) ).toMatchObject({ year: 200, - month: 1, - text: '●200년 1월:【내기】 200년 1월에 열렸던 천통국 예상 내기의 결과가 나왔습니다!', + month: 2, + text: '●200년 2월:【내기】 200년 1월에 열렸던 천통국 예상 내기의 결과가 나왔습니다!', }); } finally { await hooks.close(); diff --git a/app/game-engine/test/monthlyScoutBlockAction.test.ts b/app/game-engine/test/monthlyScoutBlockAction.test.ts index 84ba83d..2c97d59 100644 --- a/app/game-engine/test/monthlyScoutBlockAction.test.ts +++ b/app/game-engine/test/monthlyScoutBlockAction.test.ts @@ -2,6 +2,7 @@ import { describe, expect, it } from 'vitest'; import type { Nation } from '@sammo-ts/logic'; import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js'; +import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js'; import { createScoutBlockHandler } from '../src/turn/monthlyScoutBlockAction.js'; import type { TurnEvent, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js'; @@ -28,11 +29,15 @@ const event: TurnEvent = { meta: {}, }; -const buildWorld = (scout: number, blockChangeScout?: boolean) => { +const buildWorld = ( + scout: number, + blockChangeScout?: boolean, + actionName?: 'BlockScoutAction' | 'UnblockScoutAction' +) => { const state: TurnWorldState = { id: 1, - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, lastTurnTime: new Date('0200-01-01T00:00:00.000Z'), meta: blockChangeScout === undefined ? {} : { block_change_scout: blockChangeScout }, @@ -44,7 +49,16 @@ const buildWorld = (scout: number, blockChangeScout?: boolean) => { const: {}, environment: { mapName: 'test', unitSet: 'default' }, }; - return new InMemoryTurnWorld( + const activeEvent: TurnEvent = { + ...event, + action: actionName ? [[actionName, false]] : [], + }; + let world: InMemoryTurnWorld | null = null; + const handler = createScoutBlockHandler({ + actionName: actionName ?? 'BlockScoutAction', + getWorld: () => world, + }); + world = new InMemoryTurnWorld( state, { scenarioConfig, @@ -54,11 +68,19 @@ const buildWorld = (scout: number, blockChangeScout?: boolean) => { nations: [buildNation(1, scout), buildNation(2, scout)], troops: [], diplomacy: [], - events: [event], + events: [activeEvent], initialEvents: [], }, - { schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] } } + { + schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + calendarHandler: createMonthlyEventHandler({ + getWorld: () => world, + startYear: 190, + actions: new Map([[actionName ?? 'BlockScoutAction', handler]]), + }), + } ); + return world; }; describe('monthly scout block actions', () => { @@ -78,14 +100,10 @@ describe('monthly scout block actions', () => { }); it('preserves the legacy UnblockScoutAction missing-WHERE failure without mutations', async () => { - const world = buildWorld(1, true); - expect(() => - createScoutBlockHandler({ actionName: 'UnblockScoutAction', getWorld: () => world })( - [false], - { year: 200, month: 1, startyear: 190, currentEventID: 1, turnTime: new Date() }, - event - ) - ).toThrow('update(): at least 3 arguments expected'); + const world = buildWorld(1, true, 'UnblockScoutAction'); + await expect(world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'))).rejects.toThrow( + 'update(): at least 3 arguments expected' + ); expect(world.listNations().map((nation) => nation.meta.scout)).toEqual([1, 1]); expect(world.getState().meta.block_change_scout).toBe(true); diff --git a/app/game-engine/test/monthlyScoutBlockPersistence.integration.test.ts b/app/game-engine/test/monthlyScoutBlockPersistence.integration.test.ts index dcdcd71..a34b13b 100644 --- a/app/game-engine/test/monthlyScoutBlockPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyScoutBlockPersistence.integration.test.ts @@ -4,6 +4,7 @@ import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/in import { createDatabaseTurnHooks } from '../src/turn/databaseHooks.js'; import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js'; +import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js'; import { createScoutBlockHandler } from '../src/turn/monthlyScoutBlockAction.js'; import type { TurnEvent, TurnWorldSnapshot, TurnWorldState } from '../src/turn/types.js'; @@ -30,7 +31,7 @@ const event: TurnEvent = { targetCode: 'month', priority: 1_000, condition: true, - action: [], + action: [['BlockScoutAction', true]], meta: {}, }; @@ -43,10 +44,20 @@ integration('monthly scout block persistence', () => { await connector.connect(); db = connector.prisma; closeDb = () => connector.disconnect(); + await db.diplomacy.deleteMany({ + where: { + OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }], + }, + }); await db.nation.deleteMany({ where: { id: { in: [...nationIds] } } }); }); afterAll(async () => { + await db.diplomacy.deleteMany({ + where: { + OR: [{ srcNationId: { in: [...nationIds] } }, { destNationId: { in: [...nationIds] } }], + }, + }); await db.nation.deleteMany({ where: { id: { in: [...nationIds] } } }); await closeDb?.(); }); @@ -66,8 +77,8 @@ integration('monthly scout block persistence', () => { const row = await db.worldState.create({ data: { scenarioCode: 'monthly-scout-block-persistence', - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, config: {}, meta: { block_change_scout: false }, @@ -75,8 +86,8 @@ integration('monthly scout block persistence', () => { }); const state: TurnWorldState = { id: row.id, - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, lastTurnTime: new Date('2026-07-25T00:00:00.000Z'), meta: { block_change_scout: false }, @@ -88,7 +99,12 @@ integration('monthly scout block persistence', () => { const: {}, environment: { mapName: 'test', unitSet: 'default' }, }; - const world = new InMemoryTurnWorld( + let world: InMemoryTurnWorld | null = null; + const handler = createScoutBlockHandler({ + actionName: 'BlockScoutAction', + getWorld: () => world, + }); + world = new InMemoryTurnWorld( state, { scenarioConfig, @@ -101,22 +117,19 @@ integration('monthly scout block persistence', () => { events: [event], initialEvents: [], }, - { schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] } } + { + schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + calendarHandler: createMonthlyEventHandler({ + getWorld: () => world, + startYear: 190, + actions: new Map([['BlockScoutAction', handler]]), + }), + } ); const hooks = await createDatabaseTurnHooks(databaseUrl!, world); try { - await createScoutBlockHandler({ actionName: 'BlockScoutAction', getWorld: () => world })( - [true], - { - year: 200, - month: 1, - startyear: 190, - currentEventID: 1, - turnTime: state.lastTurnTime, - }, - event - ); + await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z')); await hooks.hooks.flushChanges?.({ lastTurnTime: state.lastTurnTime.toISOString(), processedGenerals: 0, @@ -129,7 +142,10 @@ integration('monthly scout block persistence', () => { (await db.nation.findMany({ where: { id: { in: [...nationIds] } }, orderBy: { id: 'asc' } })).map( (nation) => nation.meta ) - ).toEqual([{ scout: 1 }, { scout: 1 }]); + ).toEqual([ + { power: 0, scout: 1 }, + { power: 0, scout: 1 }, + ]); expect(await db.worldState.findUniqueOrThrow({ where: { id: row.id } })).toMatchObject({ meta: { block_change_scout: true }, }); diff --git a/app/game-engine/test/monthlySpecialityBetrayPersistence.integration.test.ts b/app/game-engine/test/monthlySpecialityBetrayPersistence.integration.test.ts index 12c3913..b16990a 100644 --- a/app/game-engine/test/monthlySpecialityBetrayPersistence.integration.test.ts +++ b/app/game-engine/test/monthlySpecialityBetrayPersistence.integration.test.ts @@ -4,6 +4,7 @@ import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/in import { createDatabaseTurnHooks } from '../src/turn/databaseHooks.js'; import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js'; +import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js'; import { createAddGlobalBetrayHandler, createAssignGeneralSpecialityHandler, @@ -20,7 +21,7 @@ const event: TurnEvent = { targetCode: 'month', priority: 9_000, condition: true, - action: [], + action: [['AssignGeneralSpeciality'], ['AddGlobalBetray', 2, 1]], meta: {}, }; @@ -144,8 +145,8 @@ integration('monthly speciality and betrayal persistence', () => { const row = await db.worldState.create({ data: { scenarioCode: 'monthly-speciality-betray-persistence', - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, config: {}, meta: { hiddenSeed: 'monthly-speciality-persistence' }, @@ -153,8 +154,8 @@ integration('monthly speciality and betrayal persistence', () => { }); const state: TurnWorldState = { id: row.id, - currentYear: 200, - currentMonth: 1, + currentYear: 199, + currentMonth: 12, tickSeconds: 600, lastTurnTime: new Date('2026-07-25T00:00:00.000Z'), meta: { hiddenSeed: 'monthly-speciality-persistence' }, @@ -180,21 +181,24 @@ integration('monthly speciality and betrayal persistence', () => { events: [event], initialEvents: [], }; - const world = new InMemoryTurnWorld(state, snapshot, { + let world: InMemoryTurnWorld | null = null; + const assign = createAssignGeneralSpecialityHandler({ getWorld: () => world }); + const betray = createAddGlobalBetrayHandler({ getWorld: () => world }); + world = new InMemoryTurnWorld(state, snapshot, { schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + calendarHandler: createMonthlyEventHandler({ + getWorld: () => world, + startYear: 190, + actions: new Map([ + ['AssignGeneralSpeciality', assign], + ['AddGlobalBetray', betray], + ]), + }), }); const hooks = await createDatabaseTurnHooks(databaseUrl!, world); - const environment = { - year: 200, - month: 1, - startyear: 190, - currentEventID: 1, - turnTime: state.lastTurnTime, - }; try { - await createAssignGeneralSpecialityHandler({ getWorld: () => world })([], environment, event); - await createAddGlobalBetrayHandler({ getWorld: () => world })([2, 1], environment, event); + await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z')); await hooks.hooks.flushChanges?.({ lastTurnTime: state.lastTurnTime.toISOString(), processedGenerals: 0, diff --git a/app/game-engine/test/monthlyUniqueInheritPersistence.integration.test.ts b/app/game-engine/test/monthlyUniqueInheritPersistence.integration.test.ts index a7f67c6..b66a07b 100644 --- a/app/game-engine/test/monthlyUniqueInheritPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyUniqueInheritPersistence.integration.test.ts @@ -5,6 +5,7 @@ import { createGamePostgresConnector, type GamePrismaClient } from '@sammo-ts/in import { createDatabaseTurnHooks } from '../src/turn/databaseHooks.js'; import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js'; import { loadTurnWorldFromDatabase } from '../src/turn/worldLoader.js'; +import { createMonthlyEventHandler } from '../src/turn/monthlyEventHandler.js'; import { createLostUniqueItemHandler, createMergeInheritPointRankHandler, @@ -21,7 +22,7 @@ const event: TurnEvent = { targetCode: 'month', priority: 9_000, condition: true, - action: [], + action: [['LostUniqueItem', 1], ['MergeInheritPointRank']], meta: {}, }; @@ -180,8 +181,8 @@ integration('monthly unique-item and inheritance-rank persistence', () => { const worldRow = await db.worldState.create({ data: { scenarioCode: 'monthly-unique-inherit-persistence', - currentYear: 210, - currentMonth: 1, + currentYear: 209, + currentMonth: 12, tickSeconds: 600, config: { stat: { total: 300, min: 10, max: 100, npcTotal: 150, npcMax: 75, npcMin: 10, chiefMin: 70 }, @@ -195,8 +196,8 @@ integration('monthly unique-item and inheritance-rank persistence', () => { }); const state: TurnWorldState = { id: worldRow.id, - currentYear: 210, - currentMonth: 1, + currentYear: 209, + currentMonth: 12, tickSeconds: 600, lastTurnTime: general.turnTime, meta: { hiddenSeed: 'monthly-unique-inherit-persistence' }, @@ -232,22 +233,25 @@ integration('monthly unique-item and inheritance-rank persistence', () => { events: [event], initialEvents: [], }; - const world = new InMemoryTurnWorld(state, snapshot, { + const modules = (await loadActionModuleBundle()).itemModules; + let world: InMemoryTurnWorld | null = null; + const lost = createLostUniqueItemHandler({ getWorld: () => world, itemModules: modules }); + const merge = createMergeInheritPointRankHandler({ getWorld: () => world }); + world = new InMemoryTurnWorld(state, snapshot, { schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] }, + calendarHandler: createMonthlyEventHandler({ + getWorld: () => world, + startYear: 180, + actions: new Map([ + ['LostUniqueItem', lost], + ['MergeInheritPointRank', merge], + ]), + }), }); const hooks = await createDatabaseTurnHooks(databaseUrl!, world); - const modules = (await loadActionModuleBundle()).itemModules; - const environment = { - year: 210, - month: 1, - startyear: 180, - currentEventID: 1, - turnTime: state.lastTurnTime, - }; try { - await createLostUniqueItemHandler({ getWorld: () => world, itemModules: modules })([1], environment, event); - await createMergeInheritPointRankHandler({ getWorld: () => world })([], environment, event); + await world.advanceMonth(new Date('0210-01-01T00:00:00.000Z')); await hooks.hooks.flushChanges?.({ lastTurnTime: state.lastTurnTime.toISOString(), processedGenerals: 0,