feat: implement diplomacy system with state management and processing logic

This commit is contained in:
2026-01-02 17:48:44 +00:00
parent f137c5be65
commit cb86a47f94
13 changed files with 606 additions and 45 deletions
+82
View File
@@ -0,0 +1,82 @@
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);
}
});
});
+16 -2
View File
@@ -21,7 +21,7 @@ type ScenarioSeederPrismaClient = {
count(): Promise<number>;
findFirst(args: {
where: { srcNationId: number; destNationId: number };
}): Promise<{ stateCode: string } | null>;
}): Promise<{ stateCode: number; term: number } | null>;
};
};
@@ -68,7 +68,9 @@ describeDb('scenario database seed', () => {
expect(nationCount).toBe(seed.nations.length);
expect(cityCount).toBe(seed.cities.length);
expect(generalCount).toBe(seed.generals.length);
expect(diplomacyCount).toBe(seed.diplomacy.length);
expect(diplomacyCount).toBe(
seed.nations.length * Math.max(0, seed.nations.length - 1)
);
expect(generalCount).toBeGreaterThan(0);
if (seed.diplomacy.length > 0) {
@@ -82,6 +84,18 @@ describeDb('scenario database seed', () => {
expect(row).not.toBeNull();
if (row) {
expect(row.stateCode).toBe(sample.state);
expect(row.term).toBe(sample.durationMonths);
}
const reverse = await prisma.diplomacy.findFirst({
where: {
srcNationId: sample.toNationId,
destNationId: sample.fromNationId,
},
});
expect(reverse).not.toBeNull();
if (reverse) {
expect(reverse.stateCode).toBe(sample.state);
expect(reverse.term).toBe(sample.durationMonths);
}
}
} finally {