merge: 전쟁 기간 양방향 동기화를 반영한다
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { LogCategory, LogFormat, LogScope, type Nation } from '@sammo-ts/logic';
|
||||
import { DIPLOMACY_STATE, LogCategory, LogFormat, LogScope, type Nation } from '@sammo-ts/logic';
|
||||
|
||||
import { InMemoryTurnWorld } from '../src/turn/inMemoryWorld.js';
|
||||
import { createMonthlyDiplomacyHandler } from '../src/turn/monthlyNationStatsHandler.js';
|
||||
@@ -27,10 +27,12 @@ const diplomacy: TurnDiplomacy[] = [
|
||||
{ fromNationId: 3, toNationId: 4, state: 0, term: 1, dead: 0, meta: {} },
|
||||
{ fromNationId: 4, toNationId: 3, state: 0, term: 1, dead: 0, meta: {} },
|
||||
{ fromNationId: 2, toNationId: 4, state: 7, term: 1, dead: 999, meta: {} },
|
||||
{ fromNationId: 5, toNationId: 6, state: 0, term: 1, dead: 0, meta: {} },
|
||||
{ fromNationId: 6, toNationId: 5, state: 0, term: 3, dead: 0, meta: {} },
|
||||
];
|
||||
|
||||
describe('monthly diplomacy post-update', () => {
|
||||
it('matches legacy casualty terms, state transitions, and global log order', async () => {
|
||||
it('shares war terms across both directions and emits each transition log once', async () => {
|
||||
const state: TurnWorldState = {
|
||||
id: 1,
|
||||
currentYear: 193,
|
||||
@@ -58,6 +60,8 @@ describe('monthly diplomacy post-update', () => {
|
||||
buildNation(2, '을국', 1),
|
||||
buildNation(3, '병국', 1),
|
||||
buildNation(4, '정국', 1),
|
||||
buildNation(5, '무국', 1),
|
||||
buildNation(6, '기국', 1),
|
||||
],
|
||||
troops: [],
|
||||
};
|
||||
@@ -77,10 +81,12 @@ describe('monthly diplomacy post-update', () => {
|
||||
{ fromNationId: 1, toNationId: 2, state: 0, term: 6, dead: 0, meta: {} },
|
||||
{ fromNationId: 2, toNationId: 1, state: 0, term: 6, dead: 0, meta: {} },
|
||||
{ fromNationId: 1, toNationId: 3, state: 0, term: 5, dead: 50, meta: {} },
|
||||
{ fromNationId: 3, toNationId: 1, state: 0, term: 4, dead: 50, meta: {} },
|
||||
{ fromNationId: 3, toNationId: 1, state: 0, term: 5, dead: 50, meta: {} },
|
||||
{ fromNationId: 3, toNationId: 4, state: 2, term: 0, dead: 0, meta: {} },
|
||||
{ fromNationId: 4, toNationId: 3, state: 2, term: 0, dead: 0, meta: {} },
|
||||
{ fromNationId: 2, toNationId: 4, state: 2, term: 0, dead: 0, meta: {} },
|
||||
{ fromNationId: 5, toNationId: 6, state: 0, term: 2, dead: 0, meta: {} },
|
||||
{ fromNationId: 6, toNationId: 5, state: 0, term: 2, dead: 0, meta: {} },
|
||||
]);
|
||||
expect(world.consumeDirtyState().logs).toEqual([
|
||||
{
|
||||
@@ -96,5 +102,44 @@ describe('monthly diplomacy post-update', () => {
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
},
|
||||
]);
|
||||
|
||||
await world.advanceMonth(new Date('0193-03-01T00:00:00.000Z'));
|
||||
expect(
|
||||
world
|
||||
.listDiplomacy()
|
||||
.filter(
|
||||
(entry) =>
|
||||
(entry.fromNationId === 5 && entry.toNationId === 6) ||
|
||||
(entry.fromNationId === 6 && entry.toNationId === 5)
|
||||
)
|
||||
.map(({ state, term }) => ({ state, term }))
|
||||
).toEqual([
|
||||
{ state: DIPLOMACY_STATE.WAR, term: 1 },
|
||||
{ state: DIPLOMACY_STATE.WAR, term: 1 },
|
||||
]);
|
||||
expect(world.consumeDirtyState().logs).toEqual([]);
|
||||
|
||||
await world.advanceMonth(new Date('0193-04-01T00:00:00.000Z'));
|
||||
expect(
|
||||
world
|
||||
.listDiplomacy()
|
||||
.filter(
|
||||
(entry) =>
|
||||
(entry.fromNationId === 5 && entry.toNationId === 6) ||
|
||||
(entry.fromNationId === 6 && entry.toNationId === 5)
|
||||
)
|
||||
.map(({ state, term }) => ({ state, term }))
|
||||
).toEqual([
|
||||
{ state: DIPLOMACY_STATE.TRADE, term: 0 },
|
||||
{ state: DIPLOMACY_STATE.TRADE, term: 0 },
|
||||
]);
|
||||
expect(world.consumeDirtyState().logs).toEqual([
|
||||
{
|
||||
scope: LogScope.SYSTEM,
|
||||
category: LogCategory.HISTORY,
|
||||
text: '<R><b>【종전】</b></><D><b>무국</b></>과 <D><b>기국</b></>이 <S>종전</>합니다.',
|
||||
format: LogFormat.YEAR_MONTH,
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -13,7 +13,7 @@ const nationIds = [992_101, 992_102, 992_103, 992_104];
|
||||
const scenarioCode = 'monthly-diplomacy-persistence';
|
||||
const startLog =
|
||||
'<C>●</>193년 2월:<R><b>【개전】</b></><D><b>갑국</b></>과 <D><b>을국</b></>이 <R>전쟁</>을 시작합니다.';
|
||||
const stopLog = '<C>●</>193년 2월:<R><b>【종전】</b></><D><b>병국</b></>과 <D><b>정국</b></>이 <S>종전</>합니다.';
|
||||
const stopLog = '<C>●</>193년 4월:<R><b>【종전】</b></><D><b>병국</b></>과 <D><b>정국</b></>이 <S>종전</>합니다.';
|
||||
|
||||
const buildNation = (id: number, name: string, generalCount: number): Nation => ({
|
||||
id,
|
||||
@@ -35,7 +35,7 @@ const diplomacy: TurnDiplomacy[] = [
|
||||
{ fromNationId: nationIds[0]!, toNationId: nationIds[2]!, state: 0, term: 5, dead: 250, meta: {} },
|
||||
{ fromNationId: nationIds[2]!, toNationId: nationIds[0]!, state: 0, term: 5, dead: 50, meta: {} },
|
||||
{ fromNationId: nationIds[2]!, toNationId: nationIds[3]!, state: 0, term: 1, dead: 0, meta: {} },
|
||||
{ fromNationId: nationIds[3]!, toNationId: nationIds[2]!, state: 0, term: 1, dead: 0, meta: {} },
|
||||
{ fromNationId: nationIds[3]!, toNationId: nationIds[2]!, state: 0, term: 3, dead: 0, meta: {} },
|
||||
{ fromNationId: nationIds[1]!, toNationId: nationIds[3]!, state: 7, term: 1, dead: 999, meta: {} },
|
||||
];
|
||||
|
||||
@@ -70,7 +70,7 @@ integration('monthly diplomacy persistence', () => {
|
||||
await closeDb?.();
|
||||
});
|
||||
|
||||
it('commits diplomacy transitions and exact global history text together', async () => {
|
||||
it('commits a shared war countdown and the exact transition history together', async () => {
|
||||
const nations = [
|
||||
buildNation(nationIds[0]!, '갑국', 2),
|
||||
buildNation(nationIds[1]!, '을국', 1),
|
||||
@@ -156,6 +156,22 @@ integration('monthly diplomacy persistence', () => {
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
});
|
||||
await world.advanceMonth(new Date('0193-03-01T00:00:00.000Z'));
|
||||
await hooks.hooks.flushChanges?.({
|
||||
lastTurnTime: '0193-03-01T00:00:00.000Z',
|
||||
processedGenerals: 0,
|
||||
processedTurns: 0,
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
});
|
||||
await world.advanceMonth(new Date('0193-04-01T00:00:00.000Z'));
|
||||
await hooks.hooks.flushChanges?.({
|
||||
lastTurnTime: '0193-04-01T00:00:00.000Z',
|
||||
processedGenerals: 0,
|
||||
processedTurns: 0,
|
||||
durationMs: 0,
|
||||
partial: false,
|
||||
});
|
||||
|
||||
const rows = await db.diplomacy.findMany({
|
||||
where: {
|
||||
@@ -178,11 +194,11 @@ integration('monthly diplomacy persistence', () => {
|
||||
dead: (row.meta as { dead?: number }).dead ?? 0,
|
||||
}))
|
||||
).toEqual([
|
||||
{ fromNationId: nationIds[0], toNationId: nationIds[1], state: 0, term: 6, dead: 0 },
|
||||
{ fromNationId: nationIds[0], toNationId: nationIds[2], state: 0, term: 5, dead: 50 },
|
||||
{ fromNationId: nationIds[1], toNationId: nationIds[0], state: 0, term: 6, dead: 0 },
|
||||
{ fromNationId: nationIds[0], toNationId: nationIds[1], state: 0, term: 4, dead: 0 },
|
||||
{ fromNationId: nationIds[0], toNationId: nationIds[2], state: 0, term: 3, dead: 50 },
|
||||
{ fromNationId: nationIds[1], toNationId: nationIds[0], state: 0, term: 4, dead: 0 },
|
||||
{ fromNationId: nationIds[1], toNationId: nationIds[3], state: 2, term: 0, dead: 0 },
|
||||
{ fromNationId: nationIds[2], toNationId: nationIds[0], state: 0, term: 4, dead: 50 },
|
||||
{ fromNationId: nationIds[2], toNationId: nationIds[0], state: 0, term: 3, dead: 50 },
|
||||
{ fromNationId: nationIds[2], toNationId: nationIds[3], state: 2, term: 0, dead: 0 },
|
||||
{ fromNationId: nationIds[3], toNationId: nationIds[2], state: 2, term: 0, dead: 0 },
|
||||
]);
|
||||
@@ -194,8 +210,12 @@ integration('monthly diplomacy persistence', () => {
|
||||
})
|
||||
).toEqual([
|
||||
{ scope: 'SYSTEM', category: 'HISTORY', year: 193, month: 2, text: startLog },
|
||||
{ scope: 'SYSTEM', category: 'HISTORY', year: 193, month: 2, text: stopLog },
|
||||
{ scope: 'SYSTEM', category: 'HISTORY', year: 193, month: 4, text: stopLog },
|
||||
]);
|
||||
expect(await db.worldState.findUniqueOrThrow({ where: { id: worldRow.id } })).toMatchObject({
|
||||
currentYear: 193,
|
||||
currentMonth: 4,
|
||||
});
|
||||
} finally {
|
||||
await hooks.close();
|
||||
}
|
||||
|
||||
@@ -92,10 +92,12 @@ export const processDiplomacyMonth = (
|
||||
entry.term = clamp(entry.term + termIncrease, 0, MAX_WAR_TERM);
|
||||
}
|
||||
|
||||
// 전쟁 종료 판정: 양방 term이 1 이하이면 통상으로 전환.
|
||||
// 전쟁은 양방향 diplomacy row로 저장되지만 기간은 하나의 관계가 소유한다.
|
||||
// Ref처럼 방향별 사상자 연장을 먼저 계산하되, 더 긴 기간을 양쪽에 적용하여
|
||||
// 한쪽만 0개월 전쟁으로 남는 비대칭 상태를 다음 월까지 노출하지 않는다.
|
||||
const processedPairs = new Set<string>();
|
||||
for (const entry of next) {
|
||||
if (entry.state !== DIPLOMACY_STATE.WAR || entry.term > 1) {
|
||||
if (entry.state !== DIPLOMACY_STATE.WAR) {
|
||||
continue;
|
||||
}
|
||||
const pairKey = buildDiplomacyKey(
|
||||
@@ -106,12 +108,20 @@ export const processDiplomacyMonth = (
|
||||
continue;
|
||||
}
|
||||
const opposite = byKey.get(buildDiplomacyKey(entry.toNationId, entry.fromNationId));
|
||||
if (opposite && opposite.state === DIPLOMACY_STATE.WAR && opposite.term <= 1) {
|
||||
if (!opposite || opposite.state !== DIPLOMACY_STATE.WAR) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const sharedTerm = Math.max(entry.term, opposite.term);
|
||||
entry.term = sharedTerm;
|
||||
opposite.term = sharedTerm;
|
||||
processedPairs.add(pairKey);
|
||||
|
||||
if (sharedTerm <= 1) {
|
||||
entry.state = DIPLOMACY_STATE.TRADE;
|
||||
entry.term = 0;
|
||||
opposite.state = DIPLOMACY_STATE.TRADE;
|
||||
opposite.term = 0;
|
||||
processedPairs.add(pairKey);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -61,6 +61,32 @@ describe('diplomacy month processing', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('keeps an asymmetric war on one shared countdown and ends it once', () => {
|
||||
let entries = [buildEntry(1, 2, DIPLOMACY_STATE.WAR, 1), buildEntry(2, 1, DIPLOMACY_STATE.WAR, 3)];
|
||||
const generalCounts = new Map([
|
||||
[1, 1],
|
||||
[2, 1],
|
||||
]);
|
||||
|
||||
entries = processDiplomacyMonth(entries, generalCounts);
|
||||
expect(entries.map(({ state, term }) => ({ state, term }))).toEqual([
|
||||
{ state: DIPLOMACY_STATE.WAR, term: 2 },
|
||||
{ state: DIPLOMACY_STATE.WAR, term: 2 },
|
||||
]);
|
||||
|
||||
entries = processDiplomacyMonth(entries, generalCounts);
|
||||
expect(entries.map(({ state, term }) => ({ state, term }))).toEqual([
|
||||
{ state: DIPLOMACY_STATE.WAR, term: 1 },
|
||||
{ state: DIPLOMACY_STATE.WAR, term: 1 },
|
||||
]);
|
||||
|
||||
entries = processDiplomacyMonth(entries, generalCounts);
|
||||
expect(entries.map(({ state, term }) => ({ state, term }))).toEqual([
|
||||
{ state: DIPLOMACY_STATE.TRADE, term: 0 },
|
||||
{ state: DIPLOMACY_STATE.TRADE, term: 0 },
|
||||
]);
|
||||
});
|
||||
|
||||
it('extends war term based on accumulated casualties', () => {
|
||||
const entries = [buildEntry(1, 2, DIPLOMACY_STATE.WAR, 3, 400), buildEntry(2, 1, DIPLOMACY_STATE.WAR, 3, 0)];
|
||||
const result = processDiplomacyMonth(
|
||||
@@ -75,7 +101,7 @@ describe('diplomacy month processing', () => {
|
||||
const reverse = result.find((entry) => entry.fromNationId === 2 && entry.toNationId === 1);
|
||||
expect(forward?.term).toBe(4);
|
||||
expect(forward?.dead).toBe(0);
|
||||
expect(reverse?.term).toBe(2);
|
||||
expect(reverse?.term).toBe(4);
|
||||
});
|
||||
|
||||
it('expires non-aggression pact into trade', () => {
|
||||
|
||||
Reference in New Issue
Block a user