fix(game-engine): 이민족 단독 잔존 승리 종료를 보장한다
일반국이 모두 사라지면 남은 이민족 수와 무관하게 이벤트 기수를 종료한다. 통일 archive queue를 만들지 않는 경계를 단위 및 PostgreSQL 통합 테스트로 고정한다.
This commit is contained in:
@@ -500,14 +500,16 @@ export const createInvaderEndingHandler = (options: {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const nations = world.listNations();
|
const nations = world.listNations();
|
||||||
if (nations.length >= 2) {
|
// Ref는 국가명 `ⓞ` prefix로 이벤트 승자를 구분한다. 전체 국가 수로
|
||||||
return;
|
// 막으면 서로 불가침 중인 이민족이 여럿 남았을 때 일반국 전멸 뒤에도
|
||||||
}
|
// 종료할 수 없으므로, 일반국의 부재를 직접 판정한다.
|
||||||
const neutralCityCount = world.listCities().filter((city) => city.nationId === 0).length;
|
const ordinaryNations = nations.filter((nation) => !nation.name.startsWith(INVADER_PREFIX));
|
||||||
let userWin = false;
|
const invaderNations = nations.filter((nation) => nation.name.startsWith(INVADER_PREFIX));
|
||||||
if (neutralCityCount === 0) {
|
const cities = world.listCities();
|
||||||
userWin = nations.length === 1 && !nations[0]!.name.startsWith(INVADER_PREFIX);
|
const neutralCityCount = cities.filter((city) => city.nationId === 0).length;
|
||||||
} else if (neutralCityCount !== world.listCities().length) {
|
const userWin = ordinaryNations.length === 1 && invaderNations.length === 0 && neutralCityCount === 0;
|
||||||
|
const invaderWin = ordinaryNations.length === 0;
|
||||||
|
if (!userWin && !invaderWin) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const texts = userWin
|
const texts = userWin
|
||||||
|
|||||||
@@ -372,6 +372,85 @@ describe('invader monthly actions', () => {
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('finishes as an invader victory when no ordinary nation remains without queuing unification archives', async () => {
|
||||||
|
const endingEvent: TurnEvent = {
|
||||||
|
id: 9,
|
||||||
|
targetCode: 'month',
|
||||||
|
priority: 1_000,
|
||||||
|
condition: true,
|
||||||
|
action: [['InvaderEnding']],
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
const invaderNations = [
|
||||||
|
buildNation(2, 1, 'ⓞ남만족'),
|
||||||
|
buildNation(3, 2, 'ⓞ산월족'),
|
||||||
|
];
|
||||||
|
const harness = buildHarness({
|
||||||
|
cities: [buildCity(1, 2, 4), buildCity(2, 3, 4)],
|
||||||
|
nations: invaderNations,
|
||||||
|
generals: [],
|
||||||
|
events: [endingEvent],
|
||||||
|
meta: { isunited: 1, refreshLimit: 3 },
|
||||||
|
});
|
||||||
|
const state = { ...harness.state, currentYear: 199, currentMonth: 12 };
|
||||||
|
let world: InMemoryTurnWorld | null = null;
|
||||||
|
const handler = createInvaderEndingHandler({ getWorld: () => world });
|
||||||
|
world = new InMemoryTurnWorld(state, harness.snapshot, {
|
||||||
|
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||||
|
calendarHandler: createMonthlyEventHandler({
|
||||||
|
getWorld: () => world,
|
||||||
|
startYear: 190,
|
||||||
|
actions: new Map([['InvaderEnding', handler]]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
||||||
|
|
||||||
|
expect(world.getState().meta).toMatchObject({ isunited: 3, isUnited: 3, refreshLimit: 300 });
|
||||||
|
expect(world.listEvents()).toHaveLength(0);
|
||||||
|
expect(world.peekDirtyState().logs.map((log) => log.text)).toEqual([
|
||||||
|
'<L><b>【이벤트】</b></>중원은 이민족에 의해 혼란에 빠졌습니다.',
|
||||||
|
'<L><b>【이벤트】</b></>백성은 언젠가 영웅이 나타나길 기다립니다.',
|
||||||
|
]);
|
||||||
|
expect(world.peekDirtyState().pendingUnificationFinalizations).toEqual([]);
|
||||||
|
expect(world.peekDirtyState().pendingYearbookSnapshots).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps the invader event running while an ordinary nation remains', async () => {
|
||||||
|
const endingEvent: TurnEvent = {
|
||||||
|
id: 9,
|
||||||
|
targetCode: 'month',
|
||||||
|
priority: 1_000,
|
||||||
|
condition: true,
|
||||||
|
action: [['InvaderEnding']],
|
||||||
|
meta: {},
|
||||||
|
};
|
||||||
|
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, 'ⓞ산월족')],
|
||||||
|
events: [endingEvent],
|
||||||
|
meta: { isunited: 1, refreshLimit: 3 },
|
||||||
|
});
|
||||||
|
const state = { ...harness.state, currentYear: 199, currentMonth: 12 };
|
||||||
|
let world: InMemoryTurnWorld | null = null;
|
||||||
|
const handler = createInvaderEndingHandler({ getWorld: () => world });
|
||||||
|
world = new InMemoryTurnWorld(state, harness.snapshot, {
|
||||||
|
schedule: { entries: [{ startMinute: 0, tickMinutes: 10 }] },
|
||||||
|
calendarHandler: createMonthlyEventHandler({
|
||||||
|
getWorld: () => world,
|
||||||
|
startYear: 190,
|
||||||
|
actions: new Map([['InvaderEnding', handler]]),
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
await world.advanceMonth(new Date('0200-01-01T00:00:00.000Z'));
|
||||||
|
|
||||||
|
expect(world.getState().meta).toMatchObject({ isunited: 1, refreshLimit: 3 });
|
||||||
|
expect(world.listEvents().map((entry) => entry.id)).toEqual([9]);
|
||||||
|
expect(world.peekDirtyState().logs).toEqual([]);
|
||||||
|
expect(world.peekDirtyState().pendingUnificationFinalizations).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
it('does not execute dynamically-added events in the same monthly dispatch', async () => {
|
it('does not execute dynamically-added events in the same monthly dispatch', async () => {
|
||||||
const harness = buildHarness();
|
const harness = buildHarness();
|
||||||
const actions = new Map([
|
const actions = new Map([
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ const ordinaryCityId = 991_100;
|
|||||||
const invaderCityId = 991_101;
|
const invaderCityId = 991_101;
|
||||||
const sourceEventId = 991_100;
|
const sourceEventId = 991_100;
|
||||||
const createdEventIds = [991_101, 991_102];
|
const createdEventIds = [991_101, 991_102];
|
||||||
|
const serverId = 'raise-invader-persistence';
|
||||||
|
|
||||||
type ReferenceInvaderLifecycleTrace = {
|
type ReferenceInvaderLifecycleTrace = {
|
||||||
phases: {
|
phases: {
|
||||||
@@ -172,6 +173,11 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
|
|
||||||
const clean = async () => {
|
const clean = async () => {
|
||||||
const createdGeneralIds = Array.from({ length: 10 }, (_, index) => firstCreatedGeneralId + index);
|
const createdGeneralIds = Array.from({ length: 10 }, (_, index) => firstCreatedGeneralId + index);
|
||||||
|
await db.unificationFinalization.deleteMany({ where: { serverId } });
|
||||||
|
await db.yearbookHistory.deleteMany({ where: { profileName: serverId } });
|
||||||
|
await db.hallOfFame.deleteMany({ where: { serverId } });
|
||||||
|
await db.emperor.deleteMany({ where: { serverId } });
|
||||||
|
await db.gameHistory.deleteMany({ where: { serverId } });
|
||||||
await db.logEntry.deleteMany({
|
await db.logEntry.deleteMany({
|
||||||
where: { year: 200, text: { contains: '【이벤트】' } },
|
where: { year: 200, text: { contains: '【이벤트】' } },
|
||||||
});
|
});
|
||||||
@@ -304,10 +310,10 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
config: {},
|
config: {},
|
||||||
meta: {
|
meta: {
|
||||||
hiddenSeed: 'raise-invader-persistence',
|
hiddenSeed: serverId,
|
||||||
lastGeneralId: firstCreatedGeneralId - 1,
|
lastGeneralId: firstCreatedGeneralId - 1,
|
||||||
lastNationId: createdNationId - 1,
|
lastNationId: createdNationId - 1,
|
||||||
serverId: 'raise-invader-persistence',
|
serverId,
|
||||||
refreshLimit: 3,
|
refreshLimit: 3,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -319,10 +325,10 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
tickSeconds: 600,
|
tickSeconds: 600,
|
||||||
lastTurnTime: new Date('0200-01-01T00:00:00.000Z'),
|
lastTurnTime: new Date('0200-01-01T00:00:00.000Z'),
|
||||||
meta: {
|
meta: {
|
||||||
hiddenSeed: 'raise-invader-persistence',
|
hiddenSeed: serverId,
|
||||||
lastGeneralId: firstCreatedGeneralId - 1,
|
lastGeneralId: firstCreatedGeneralId - 1,
|
||||||
lastNationId: createdNationId - 1,
|
lastNationId: createdNationId - 1,
|
||||||
serverId: 'raise-invader-persistence',
|
serverId,
|
||||||
refreshLimit: 3,
|
refreshLimit: 3,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -572,6 +578,11 @@ integration('RaiseInvader database persistence', () => {
|
|||||||
{ text: '<C>●</>200년 4월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!' },
|
{ text: '<C>●</>200년 4월:<L><b>【이벤트】</b></>이민족을 모두 소탕했습니다!' },
|
||||||
{ text: '<C>●</>200년 4월:<L><b>【이벤트】</b></>중원은 당분간 태평성대를 누릴 것입니다.' },
|
{ text: '<C>●</>200년 4월:<L><b>【이벤트】</b></>중원은 당분간 태평성대를 누릴 것입니다.' },
|
||||||
]);
|
]);
|
||||||
|
await expect(db.unificationFinalization.count({ where: { serverId } })).resolves.toBe(0);
|
||||||
|
await expect(db.yearbookHistory.count({ where: { profileName: serverId } })).resolves.toBe(0);
|
||||||
|
await expect(db.hallOfFame.count({ where: { serverId } })).resolves.toBe(0);
|
||||||
|
await expect(db.emperor.count({ where: { serverId } })).resolves.toBe(0);
|
||||||
|
await expect(db.gameHistory.count({ where: { serverId } })).resolves.toBe(0);
|
||||||
if (referenceTrace) {
|
if (referenceTrace) {
|
||||||
expect(referenceTrace.phases.afterUserWin).toEqual({
|
expect(referenceTrace.phases.afterUserWin).toEqual({
|
||||||
result: 'Deleted',
|
result: 'Deleted',
|
||||||
|
|||||||
Reference in New Issue
Block a user