93 lines
5.2 KiB
TypeScript
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/);
|
|
});
|