From c454d1b289c1e270dc8d25f9c317bea1df4ad166 Mon Sep 17 00:00:00 2001 From: hided62 Date: Sun, 23 Aug 2026 11:58:16 +0000 Subject: [PATCH] =?UTF-8?q?fix(game-engine):=20=EC=A4=91=EB=A6=BD=EA=B5=AD?= =?UTF-8?q?=EC=9D=84=20=EC=9D=B4=EB=AF=BC=EC=A1=B1=20=EC=A2=85=EB=A3=8C=20?= =?UTF-8?q?=EC=A7=91=EA=B3=84=EC=97=90=EC=84=9C=20=EC=A0=9C=EC=99=B8?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/turn/monthlyInvaderAction.ts | 20 +++++--- .../test/monthlyInvaderAction.test.ts | 18 ++++++- ...thlyInvaderPersistence.integration.test.ts | 51 +++++++++++++++++-- 3 files changed, 77 insertions(+), 12 deletions(-) diff --git a/app/game-engine/src/turn/monthlyInvaderAction.ts b/app/game-engine/src/turn/monthlyInvaderAction.ts index 011c352f..3be311b1 100644 --- a/app/game-engine/src/turn/monthlyInvaderAction.ts +++ b/app/game-engine/src/turn/monthlyInvaderAction.ts @@ -282,7 +282,12 @@ export const createRaiseInvaderHandler = (options: { world.updateCity(city.id, { nationId: 0, frontState: 0, supplyState: 1 }); } - const existingNationIds = world.listNations().map((nation) => nation.id); + // Ref nation table에는 Core의 `id=0 재야` row가 없으므로, 이민족과 + // 전쟁 관계를 만들 실제 국가만 ID 증가와 외교 생성의 기준으로 삼는다. + const existingNationIds = world + .listNations() + .filter((nation) => nation.id > 0) + .map((nation) => nation.id); const currentLastNationId = readNumber(world.getState().meta.lastNationId); const liveNationMaxId = existingNationIds.reduce((maxId, id) => Math.max(maxId, id), 0); let lastNationId = Math.max(currentLastNationId, liveNationMaxId); @@ -499,12 +504,13 @@ export const createInvaderEndingHandler = (options: { if (isUnited === 0 || isUnited === 2) { return; } - const nations = world.listNations(); - // Ref는 국가명 `ⓞ` prefix로 이벤트 승자를 구분한다. 전체 국가 수로 - // 막으면 서로 불가침 중인 이민족이 여럿 남았을 때 일반국 전멸 뒤에도 - // 종료할 수 없으므로, 일반국의 부재를 직접 판정한다. - const ordinaryNations = nations.filter((nation) => !nation.name.startsWith(INVADER_PREFIX)); - const invaderNations = nations.filter((nation) => nation.name.startsWith(INVADER_PREFIX)); + // Ref의 nation table에는 Core가 domain/DB에 유지하는 `id=0 재야` row가 + // 없다. 실제 승패 국가만 남긴 뒤 `ⓞ` prefix로 이벤트 승자를 구분한다. + // 전체 국가 수로 막으면 서로 불가침 중인 이민족이 여럿 남았을 때 + // 일반국 전멸 뒤에도 종료할 수 없으므로, 일반국의 부재를 직접 판정한다. + const activeNations = world.listNations().filter((nation) => nation.id > 0); + const ordinaryNations = activeNations.filter((nation) => !nation.name.startsWith(INVADER_PREFIX)); + const invaderNations = activeNations.filter((nation) => nation.name.startsWith(INVADER_PREFIX)); const cities = world.listCities(); const neutralCityCount = cities.filter((city) => city.nationId === 0).length; const userWin = ordinaryNations.length === 1 && invaderNations.length === 0 && neutralCityCount === 0; diff --git a/app/game-engine/test/monthlyInvaderAction.test.ts b/app/game-engine/test/monthlyInvaderAction.test.ts index 96b396ea..3d0c29c8 100644 --- a/app/game-engine/test/monthlyInvaderAction.test.ts +++ b/app/game-engine/test/monthlyInvaderAction.test.ts @@ -49,6 +49,14 @@ const buildNation = (id: number, capitalCityId = 1, name = `국가${id}`): Natio meta: { tech: 100, war: 1, scout: 1 }, }); +const buildNeutralNation = (): Nation => ({ + ...buildNation(0, 0, '재야'), + chiefGeneralId: null, + level: 0, + typeCode: 'che_중립', + meta: {}, +}); + const buildGeneral = (overrides: Partial = {}): TurnGeneral => ({ id: 1, userId: null, @@ -347,6 +355,7 @@ describe('invader monthly actions', () => { }; const harness = buildHarness({ cities: [buildCity(1, 1, 3)], + nations: [buildNeutralNation(), buildNation(1)], events: [endingEvent], meta: { isunited: 1, refreshLimit: 3 }, }); @@ -387,7 +396,7 @@ describe('invader monthly actions', () => { ]; const harness = buildHarness({ cities: [buildCity(1, 2, 4), buildCity(2, 3, 4)], - nations: invaderNations, + nations: [buildNeutralNation(), ...invaderNations], generals: [], events: [endingEvent], meta: { isunited: 1, refreshLimit: 3 }, @@ -427,7 +436,12 @@ describe('invader monthly actions', () => { }; const harness = buildHarness({ cities: [buildCity(1, 1, 3), buildCity(2, 2, 4), buildCity(3, 3, 4)], - nations: [buildNation(1), buildNation(2, 2, 'ⓞ남만족'), buildNation(3, 3, 'ⓞ산월족')], + nations: [ + buildNeutralNation(), + buildNation(1), + buildNation(2, 2, 'ⓞ남만족'), + buildNation(3, 3, 'ⓞ산월족'), + ], events: [endingEvent], meta: { isunited: 1, refreshLimit: 3 }, }); diff --git a/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts b/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts index 8957b202..e5125874 100644 --- a/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts +++ b/app/game-engine/test/monthlyInvaderPersistence.integration.test.ts @@ -109,6 +109,19 @@ const buildCity = (id: number, nationId: number, level: number, name: string): C const ordinaryCity = buildCity(ordinaryCityId, existingNationId, 3, '기준도시'); const invaderCity = buildCity(invaderCityId, 0, 4, '남만'); +const neutralNation: Nation = { + id: 0, + name: '재야', + color: '#000000', + capitalCityId: null, + chiefGeneralId: null, + gold: 0, + rice: 0, + power: 0, + level: 0, + typeCode: 'che_중립', + meta: {}, +}; const existingNation: Nation = { id: existingNationId, name: '기준국', @@ -199,7 +212,9 @@ integration('RaiseInvader database persistence', () => { ], }, }); - await db.nation.deleteMany({ where: { id: { in: [existingNationId, createdNationId] } } }); + await db.nation.deleteMany({ + where: { id: { in: [neutralNation.id, existingNationId, createdNationId] } }, + }); await db.city.deleteMany({ where: { id: { in: [ordinaryCityId, invaderCityId] } } }); await db.event.deleteMany({ where: { id: { in: [sourceEventId, ...createdEventIds] } } }); await db.worldState.deleteMany({ where: { scenarioCode: 'monthly-invader-persistence' } }); @@ -251,6 +266,21 @@ integration('RaiseInvader database persistence', () => { }); await createCity(ordinaryCity); await createCity(invaderCity); + await db.nation.create({ + data: { + id: neutralNation.id, + name: neutralNation.name, + color: neutralNation.color, + capitalCityId: neutralNation.capitalCityId, + chiefGeneralId: neutralNation.chiefGeneralId, + gold: neutralNation.gold, + rice: neutralNation.rice, + tech: 0, + level: neutralNation.level, + typeCode: neutralNation.typeCode, + meta: neutralNation.meta, + }, + }); await db.nation.create({ data: { id: existingNationId, @@ -343,7 +373,7 @@ integration('RaiseInvader database persistence', () => { map: { id: 'test', name: 'test', cities: [] }, generals: [existingGeneral], cities: [ordinaryCity, invaderCity], - nations: [existingNation], + nations: [neutralNation, existingNation], troops: [], diplomacy: [], events: [sourceEvent], @@ -406,7 +436,22 @@ integration('RaiseInvader database persistence', () => { OR: [{ srcNationId: createdNationId }, { destNationId: createdNationId }], }, }) - ).toBe(2); + ).toBe(4); + expect( + await db.diplomacy.findMany({ + where: { + OR: [ + { srcNationId: neutralNation.id, destNationId: createdNationId }, + { srcNationId: createdNationId, destNationId: neutralNation.id }, + ], + }, + orderBy: [{ srcNationId: 'asc' }, { destNationId: 'asc' }], + select: { srcNationId: true, destNationId: true, stateCode: true, term: true }, + }) + ).toEqual([ + { srcNationId: neutralNation.id, destNationId: createdNationId, stateCode: 2, term: 0 }, + { srcNationId: createdNationId, destNationId: neutralNation.id, stateCode: 2, term: 0 }, + ]); expect( await db.event.findMany({ where: { id: { in: createdEventIds } },