Files
Dune/tests/coalition-board-rules.test.ts
Quintine cea310b030
Verify and publish checkpoint container / container (push) Successful in 10m14s
Prototype charity Truthtrance and coalition rule foundations
2026-10-28 21:58:30 +11:00

93 lines
5.2 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { createGame, newPlayer } from '../game/engine';
import { quoteBattleBoard, quoteCoalitionAlliedSeparation } from '../game/board-resolution-quote';
import { territoryEntryBlock } from '../game/occupancy';
import type { CoalitionState } from '../game/coalition-alliance';
function fixture() {
const g = createGame('COALBOARD', newPlayer('a', 'Atreides', 'atreides'), false);
g.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Fremen', 'fremen'),
newPlayer('d', 'Harkonnen', 'harkonnen'));
Object.assign(g, { advanced: false, turn: 4, storm: 18, order: ['a', 'b', 'c', 'd'] });
for (const p of g.players) { p.forces = {}; p.ally = null; }
const coalitionState: CoalitionState = { version: 1, revision: 1, proposal: null, usedEvents: [],
groups: [{ id: 'abc', members: ['a', 'b', 'c'], formedTurn: 2, edges: [
{ members: ['a', 'b'], formedTurn: 2 },
{ members: ['a', 'c'], formedTurn: 4 },
{ members: ['b', 'c'], formedTurn: 4 },
] }] };
return { g, coalitionState };
}
void test('coalition battles exclude every ally while preserving separate battles against outsiders', () => {
const { g, coalitionState } = fixture();
for (const p of g.players) p.forces = { 'red_chasm:7': 1 };
const before = structuredClone({ g, coalitionState });
assert.deepEqual(quoteBattleBoard({ ...g, coalitionState }).battles, [
{ territory: 'red_chasm', attacker: 'a', defender: 'd' },
{ territory: 'red_chasm', attacker: 'b', defender: 'd' },
{ territory: 'red_chasm', attacker: 'c', defender: 'd' },
]);
assert.equal(quoteBattleBoard(g).battles.length, 6);
assert.deepEqual({ g, coalitionState }, before);
});
void test('entry checks each coalition ally and retains polar, temporary shipment and capacity rules', () => {
const { g, coalitionState } = fixture();
g.players[1].forces = { 'red_chasm:7': 1 };
g.players[2].forces = { 'arrakeen:10': 1 };
const block = 'You cannot enter a territory occupied by your ally.';
assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm', false, false, coalitionState), block);
assert.equal(territoryEntryBlock(g.players, 'a', 'arrakeen', false, false, coalitionState), block);
assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm'), null);
assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm', false, true, coalitionState), null);
g.players[1].forces['polar_sink:0'] = 1;
assert.equal(territoryEntryBlock(g.players, 'a', 'polar_sink', false, false, coalitionState), null);
g.players[3].forces = { 'arrakeen:10': 1 };
assert.equal(territoryEntryBlock(g.players, 'a', 'arrakeen', false, true, coalitionState),
'A stronghold cannot contain three occupying factions.');
});
void test('Basic separation follows each alliance edge and preserves old formation age', () => {
const { g, coalitionState } = fixture();
g.players[0].forces = { 'red_chasm:7': 1, 'arrakeen:10': 1 };
g.players[1].forces = { 'red_chasm:7': 1 };
g.players[2].forces = { 'arrakeen:10': 1 };
const context = { ...g, coalitionState, remaining: ['a'] };
const before = structuredClone(context);
assert.deepEqual(quoteCoalitionAlliedSeparation(context, 'a'), ['red_chasm']);
assert.deepEqual(quoteCoalitionAlliedSeparation({ ...context, remaining: ['a', 'b'] }, 'a'), []);
assert.deepEqual(new Set(quoteCoalitionAlliedSeparation({ ...context, turn: 5 }, 'a')),
new Set(['red_chasm', 'arrakeen']));
assert.deepEqual(context, before);
coalitionState.groups[0].edges[0].formedTurn = null;
assert.deepEqual(quoteCoalitionAlliedSeparation(context, 'a'), ['red_chasm']);
});
void test('legacy pair timing remains unchanged and unsupported coalition combinations fail closed', () => {
const { g, coalitionState } = fixture();
g.players[0].forces = { 'red_chasm:7': 1 };
g.players[1].forces = { 'red_chasm:7': 1 };
g.players[0].ally = 'b'; g.players[1].ally = 'a';
g.players[0].allySinceTurn = 4; g.players[1].allySinceTurn = 4;
assert.deepEqual(quoteCoalitionAlliedSeparation({ ...g, remaining: ['a'] }, 'a'), []);
g.players[1].allySinceTurn = 2;
assert.deepEqual(quoteCoalitionAlliedSeparation({ ...g, remaining: ['a'] }, 'a'), ['red_chasm']);
assert.equal(territoryEntryBlock(g.players, 'a', 'red_chasm'),
'You cannot enter a territory occupied by your ally.');
g.players[0].ally = null; g.players[1].ally = null;
assert.throws(() => quoteBattleBoard({ ...g, advanced: true, coalitionState }), /Basic play without Ecaz/);
g.players[2] = newPlayer('c', 'Ecaz', 'ecaz');
assert.throws(() => territoryEntryBlock(g.players, 'a', 'red_chasm', false, false, coalitionState), /do not yet support Ecaz/);
});
void test('a malformed null coalition ledger cannot turn a group into enemies', () => {
const { g } = fixture();
for (const p of g.players) p.forces = { 'red_chasm:7': 1 };
const invalid = null as unknown as CoalitionState;
assert.throws(() => quoteBattleBoard({ ...g, coalitionState: invalid }), /ledger cannot be null/);
assert.throws(() => territoryEntryBlock(g.players, 'a', 'red_chasm', false, false, invalid), /ledger cannot be null/);
assert.throws(() => territoryEntryBlock(g.players, 'a', 'red_chasm', true, false, invalid), /ledger cannot be null/);
});