feat: migrate diplomacy logic to new module and update imports
This commit is contained in:
@@ -18,7 +18,7 @@ import { finalizeLogEntry, type LogEntryDraft } from '@sammo-ts/logic';
|
||||
import type { TurnDaemonHooks } from '../lifecycle/types.js';
|
||||
import type { InMemoryTurnWorld } from './inMemoryWorld.js';
|
||||
import type { InMemoryReservedTurnStore } from './reservedTurnStore.js';
|
||||
import { buildDiplomacyMeta } from './diplomacy.js';
|
||||
import { buildDiplomacyMeta } from '@sammo-ts/logic';
|
||||
|
||||
export interface DatabaseTurnHooks {
|
||||
hooks: TurnDaemonHooks;
|
||||
|
||||
@@ -1,160 +0,0 @@
|
||||
import type { TurnDiplomacy } from './types.js';
|
||||
import { clamp } from 'es-toolkit';
|
||||
|
||||
// 외교 상태 코드 (legacy 기준).
|
||||
export const DIPLOMACY_STATE = {
|
||||
WAR: 0,
|
||||
DECLARATION: 1,
|
||||
TRADE: 2,
|
||||
NON_AGGRESSION: 7,
|
||||
} as const;
|
||||
|
||||
export const DEFAULT_DECLARE_WAR_TERM = 24;
|
||||
export const DEFAULT_WAR_TERM = 6;
|
||||
|
||||
// TODO: 불가침 제의/수락 등 외교 커맨드 전환 규칙을 이 모듈에 추가 예정.
|
||||
const MAX_WAR_TERM = 13;
|
||||
|
||||
export const buildDiplomacyKey = (srcNationId: number, destNationId: number): string =>
|
||||
`${srcNationId}:${destNationId}`;
|
||||
|
||||
export const buildDefaultDiplomacy = (
|
||||
srcNationId: number,
|
||||
destNationId: number
|
||||
): TurnDiplomacy => ({
|
||||
fromNationId: srcNationId,
|
||||
toNationId: destNationId,
|
||||
state: DIPLOMACY_STATE.TRADE,
|
||||
term: 0,
|
||||
dead: 0,
|
||||
meta: {},
|
||||
});
|
||||
|
||||
export interface DiplomacyPatch {
|
||||
state?: number;
|
||||
term?: number;
|
||||
dead?: number;
|
||||
deadDelta?: number;
|
||||
meta?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export const applyDiplomacyPatch = (
|
||||
entry: TurnDiplomacy,
|
||||
patch: DiplomacyPatch
|
||||
): TurnDiplomacy => {
|
||||
const nextDead =
|
||||
typeof patch.dead === 'number'
|
||||
? patch.dead
|
||||
: typeof patch.deadDelta === 'number'
|
||||
? entry.dead + patch.deadDelta
|
||||
: entry.dead;
|
||||
return {
|
||||
...entry,
|
||||
state: patch.state ?? entry.state,
|
||||
term: patch.term ?? entry.term,
|
||||
dead: clamp(nextDead, 0, Number.MAX_SAFE_INTEGER),
|
||||
meta: patch.meta ? { ...entry.meta, ...patch.meta } : entry.meta,
|
||||
};
|
||||
};
|
||||
|
||||
export const readDiplomacyMeta = (
|
||||
meta: Record<string, unknown>
|
||||
): { meta: Record<string, unknown>; dead: number } => {
|
||||
const rawDead = meta.dead;
|
||||
const dead = typeof rawDead === 'number' ? rawDead : 0;
|
||||
const cleaned = { ...meta };
|
||||
delete cleaned.dead;
|
||||
return { meta: cleaned, dead };
|
||||
};
|
||||
|
||||
export const buildDiplomacyMeta = (
|
||||
entry: TurnDiplomacy
|
||||
): Record<string, unknown> => ({
|
||||
...entry.meta,
|
||||
dead: entry.dead,
|
||||
});
|
||||
|
||||
export const processDiplomacyMonth = (
|
||||
diplomacy: TurnDiplomacy[],
|
||||
generalCounts: Map<number, number>
|
||||
): TurnDiplomacy[] => {
|
||||
const next = diplomacy.map((entry) => ({
|
||||
...entry,
|
||||
meta: { ...entry.meta },
|
||||
}));
|
||||
const byKey = new Map<string, TurnDiplomacy>(
|
||||
next.map((entry) => [
|
||||
buildDiplomacyKey(entry.fromNationId, entry.toNationId),
|
||||
entry,
|
||||
])
|
||||
);
|
||||
|
||||
// 전쟁 기간 갱신: 사상자에 따라 term 증가, 잔여 사상자 유지.
|
||||
for (const entry of next) {
|
||||
if (entry.state !== DIPLOMACY_STATE.WAR || entry.dead <= 0) {
|
||||
continue;
|
||||
}
|
||||
const genCount = Math.max(1, generalCounts.get(entry.fromNationId) ?? 1);
|
||||
const termIncrease = Math.floor(entry.dead / 100 / genCount);
|
||||
if (termIncrease <= 0) {
|
||||
continue;
|
||||
}
|
||||
entry.dead -= termIncrease * 100 * genCount;
|
||||
entry.term = clamp(entry.term + termIncrease, 0, MAX_WAR_TERM);
|
||||
}
|
||||
|
||||
// 전쟁 종료 판정: 양방 term이 1 이하이면 통상으로 전환.
|
||||
const processedPairs = new Set<string>();
|
||||
for (const entry of next) {
|
||||
if (entry.state !== DIPLOMACY_STATE.WAR || entry.term > 1) {
|
||||
continue;
|
||||
}
|
||||
const pairKey = buildDiplomacyKey(
|
||||
Math.min(entry.fromNationId, entry.toNationId),
|
||||
Math.max(entry.fromNationId, entry.toNationId)
|
||||
);
|
||||
if (processedPairs.has(pairKey)) {
|
||||
continue;
|
||||
}
|
||||
const opposite = byKey.get(
|
||||
buildDiplomacyKey(entry.toNationId, entry.fromNationId)
|
||||
);
|
||||
if (
|
||||
opposite &&
|
||||
opposite.state === DIPLOMACY_STATE.WAR &&
|
||||
opposite.term <= 1
|
||||
) {
|
||||
entry.state = DIPLOMACY_STATE.TRADE;
|
||||
entry.term = 0;
|
||||
opposite.state = DIPLOMACY_STATE.TRADE;
|
||||
opposite.term = 0;
|
||||
processedPairs.add(pairKey);
|
||||
}
|
||||
}
|
||||
|
||||
// 사상자 초기화 및 term 감소.
|
||||
for (const entry of next) {
|
||||
if (entry.state !== DIPLOMACY_STATE.WAR) {
|
||||
entry.dead = 0;
|
||||
}
|
||||
entry.term = Math.max(0, entry.term - 1);
|
||||
}
|
||||
|
||||
// 불가침/선전포고 만료 처리.
|
||||
for (const entry of next) {
|
||||
if (
|
||||
entry.state === DIPLOMACY_STATE.NON_AGGRESSION &&
|
||||
entry.term === 0
|
||||
) {
|
||||
entry.state = DIPLOMACY_STATE.TRADE;
|
||||
} else if (
|
||||
entry.state === DIPLOMACY_STATE.DECLARATION &&
|
||||
entry.term === 0
|
||||
) {
|
||||
entry.state = DIPLOMACY_STATE.WAR;
|
||||
entry.term = DEFAULT_WAR_TERM;
|
||||
}
|
||||
}
|
||||
|
||||
return next;
|
||||
};
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
buildDiplomacyKey,
|
||||
processDiplomacyMonth,
|
||||
type DiplomacyPatch,
|
||||
} from './diplomacy.js';
|
||||
} from '@sammo-ts/logic';
|
||||
|
||||
export interface GeneralTurnContext {
|
||||
general: TurnGeneral;
|
||||
|
||||
@@ -35,7 +35,7 @@ import {
|
||||
buildDefaultDiplomacy,
|
||||
buildDiplomacyKey,
|
||||
type DiplomacyPatch,
|
||||
} from './diplomacy.js';
|
||||
} from '@sammo-ts/logic';
|
||||
import {
|
||||
buildCommandEnv,
|
||||
buildReservedTurnDefinitions,
|
||||
|
||||
@@ -24,7 +24,7 @@ import { loadMapDefinitionByName } from '../scenario/mapLoader.js';
|
||||
import type { UnitSetLoaderOptions } from '../scenario/unitSetLoader.js';
|
||||
import { loadUnitSetDefinitionByName } from '../scenario/unitSetLoader.js';
|
||||
import type { TurnDiplomacy, TurnGeneral, TurnWorldLoadResult } from './types.js';
|
||||
import { readDiplomacyMeta } from './diplomacy.js';
|
||||
import { readDiplomacyMeta } from '@sammo-ts/logic';
|
||||
|
||||
interface TurnWorldLoaderOptions {
|
||||
databaseUrl: string;
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
import {
|
||||
DEFAULT_WAR_TERM,
|
||||
DIPLOMACY_STATE,
|
||||
processDiplomacyMonth,
|
||||
} from '../src/turn/diplomacy.js';
|
||||
import type { TurnDiplomacy } from '../src/turn/types.js';
|
||||
|
||||
const buildEntry = (
|
||||
fromNationId: number,
|
||||
toNationId: number,
|
||||
state: number,
|
||||
term: number,
|
||||
dead = 0
|
||||
): TurnDiplomacy => ({
|
||||
fromNationId,
|
||||
toNationId,
|
||||
state,
|
||||
term,
|
||||
dead,
|
||||
meta: {},
|
||||
});
|
||||
|
||||
describe('diplomacy month processing', () => {
|
||||
it('promotes declaration to war after term expires', () => {
|
||||
const entries = [
|
||||
buildEntry(1, 2, DIPLOMACY_STATE.DECLARATION, 1),
|
||||
buildEntry(2, 1, DIPLOMACY_STATE.DECLARATION, 1),
|
||||
];
|
||||
const result = processDiplomacyMonth(entries, new Map([[1, 1], [2, 1]]));
|
||||
|
||||
for (const entry of result) {
|
||||
expect(entry.state).toBe(DIPLOMACY_STATE.WAR);
|
||||
expect(entry.term).toBe(DEFAULT_WAR_TERM);
|
||||
}
|
||||
});
|
||||
|
||||
it('ends war when both terms reach zero without casualties', () => {
|
||||
const entries = [
|
||||
buildEntry(1, 2, DIPLOMACY_STATE.WAR, 1),
|
||||
buildEntry(2, 1, DIPLOMACY_STATE.WAR, 1),
|
||||
];
|
||||
const result = processDiplomacyMonth(entries, new Map([[1, 1], [2, 1]]));
|
||||
|
||||
for (const entry of result) {
|
||||
expect(entry.state).toBe(DIPLOMACY_STATE.TRADE);
|
||||
expect(entry.term).toBe(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(entries, new Map([[1, 2], [2, 2]]));
|
||||
|
||||
const forward = result.find(
|
||||
(entry) => entry.fromNationId === 1 && entry.toNationId === 2
|
||||
);
|
||||
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);
|
||||
});
|
||||
|
||||
it('expires non-aggression pact into trade', () => {
|
||||
const entries = [
|
||||
buildEntry(1, 2, DIPLOMACY_STATE.NON_AGGRESSION, 1),
|
||||
buildEntry(2, 1, DIPLOMACY_STATE.NON_AGGRESSION, 1),
|
||||
];
|
||||
const result = processDiplomacyMonth(entries, new Map([[1, 1], [2, 1]]));
|
||||
|
||||
for (const entry of result) {
|
||||
expect(entry.state).toBe(DIPLOMACY_STATE.TRADE);
|
||||
expect(entry.term).toBe(0);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user