109 lines
6.3 KiB
TypeScript
109 lines
6.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
||
import test from 'node:test';
|
||
import { applyAction, createGame, newPlayer, viewGame, type Game } from '../game/engine';
|
||
import { answerCoalitionAdmission, beginCoalitionAdmission, coalitionOf, emptyCoalitionState } from '../game/coalition-alliance';
|
||
import { quoteBattleBoard, quoteCoalitionAlliedSeparation } from '../game/board-resolution-quote';
|
||
import { isAlliedShipmentVisit } from '../game/allied-separation';
|
||
import { strongholdProgress } from '../game/victory-progress';
|
||
import { quoteVictory } from '../game/victory-quote';
|
||
|
||
/** Controlled staged public board and coalition. The engine cannot yet start
|
||
* this preview or negotiate Advanced groups; no native game is claimed. */
|
||
function staged(): Game {
|
||
const game = createGame('ADVANCEDGROUPBOARD', newPlayer('a', 'Atreides', 'atreides'), true);
|
||
game.players.push(newPlayer('b', 'Emperor', 'emperor'), newPlayer('c', 'Bene Gesserit', 'beneGesserit'),
|
||
newPlayer('d', 'Harkonnen', 'harkonnen'));
|
||
Object.assign(game, { status: 'playing', phase: 5, turn: 2, storm: 18,
|
||
active: 'a', order: ['a', 'b', 'c', 'd'], movementRemaining: ['a', 'b', 'c', 'd'] });
|
||
for (const player of game.players) { player.forces = {}; player.ally = null; player.shipped = false; player.moved = 0; }
|
||
game.players[0].ally = 'b'; game.players[1].ally = 'a';
|
||
game.players[0].allySinceTurn = 2; game.players[1].allySinceTurn = 2;
|
||
const state = emptyCoalitionState('three-short');
|
||
const group = coalitionOf(game.players, state, 'a'); assert.ok(group);
|
||
const offer = beginCoalitionAdmission(game.players, state, { event: 'invite', nexus: 'nexus', turn: 2,
|
||
groupId: group.id, entrant: 'c', proposer: 'a', expectedRevision: 0 });
|
||
const approval = answerCoalitionAdmission(game.players, offer.state, { event: 'invite', nexus: 'nexus',
|
||
turn: 2, actor: 'b', accept: true, expectedRevision: offer.state.revision });
|
||
const joined = answerCoalitionAdmission(game.players, approval.state, { event: 'invite', nexus: 'nexus',
|
||
turn: 2, actor: 'c', accept: true, expectedRevision: approval.state.revision });
|
||
for (const player of game.players) if (Object.hasOwn(joined.allyUpdates, player.id)) player.ally = joined.allyUpdates[player.id];
|
||
game.coalitionState = joined.state;
|
||
return game;
|
||
}
|
||
const p = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
||
|
||
void test('Advanced group separation unions all allies immediately while exempting advisors and Polar Sink', () => {
|
||
const game = staged();
|
||
p(game, 'a').forces = { 'arrakeen:10': 1, 'carthag:11': 1, 'sietch_tabr:14': 1, 'polar_sink:0': 1 };
|
||
p(game, 'b').forces = { 'arrakeen:10': 1, 'carthag:11': 1, 'polar_sink:0': 1 };
|
||
p(game, 'c').forces = { 'arrakeen:10': 1, 'sietch_tabr:14': 1 };
|
||
p(game, 'c').advisors = { sietch_tabr: {} };
|
||
const before = structuredClone(game);
|
||
const losses = quoteCoalitionAlliedSeparation({ ...game, remaining: ['a', 'b', 'c', 'd'] }, 'a');
|
||
assert.deepEqual(new Set(losses), new Set(['arrakeen', 'carthag']));
|
||
assert.deepEqual(quoteCoalitionAlliedSeparation({ ...game, remaining: ['a', 'b', 'c', 'd'] }, 'c'), ['arrakeen']);
|
||
assert.deepEqual(game, before, 'A quote does not remove any counters.');
|
||
assert.equal(isAlliedShipmentVisit(game, p(game, 'a'), 'arrakeen'), true);
|
||
});
|
||
|
||
void test('Advanced endMovement actually removes only the ending group member’s shared fighters', () => {
|
||
const game = staged();
|
||
const owner = p(game, 'a'), emperor = p(game, 'b'), bg = p(game, 'c');
|
||
owner.forces = { 'arrakeen:10': 2, 'carthag:11': 3, 'sietch_tabr:14': 1, 'polar_sink:0': 1 };
|
||
owner.reserves = 13;
|
||
emperor.forces = { 'arrakeen:10': 1, 'polar_sink:0': 1 }; emperor.reserves = 18;
|
||
bg.forces = { 'carthag:11': 1, 'sietch_tabr:14': 1 }; bg.reserves = 18;
|
||
bg.advisors = { sietch_tabr: {} };
|
||
const otherForces = structuredClone(game.players.filter(seat => seat.id !== 'a').map(seat => ({
|
||
id: seat.id, forces: seat.forces, tanks: seat.tanks,
|
||
})));
|
||
const warning = viewGame(game, 'a').advancedAllySeparation;
|
||
assert.ok(warning);
|
||
assert.deepEqual(new Set(warning.territories), new Set(['Arrakeen', 'Carthag']));
|
||
const after = applyAction(game, 'a', { type: 'endMovement' });
|
||
assert.equal(p(after, 'a').tanks, 5);
|
||
assert.equal(p(after, 'a').reserves, 13);
|
||
assert.equal(p(after, 'a').forces['arrakeen:10'] ?? 0, 0);
|
||
assert.equal(p(after, 'a').forces['carthag:11'] ?? 0, 0);
|
||
assert.equal(p(after, 'a').forces['sietch_tabr:14'], 1);
|
||
assert.equal(p(after, 'a').forces['polar_sink:0'], 1);
|
||
assert.deepEqual(after.players.filter(seat => seat.id !== 'a').map(seat => ({
|
||
id: seat.id, forces: seat.forces, tanks: seat.tanks,
|
||
})), otherForces);
|
||
assert.equal(after.active, 'b');
|
||
assert.deepEqual(after.movementRemaining, ['b', 'c', 'd']);
|
||
});
|
||
|
||
void test('Advanced group board generates enemy battles without ever pairing allies', () => {
|
||
const game = staged();
|
||
p(game, 'a').forces = { 'imperial_basin:9': 1 };
|
||
p(game, 'b').forces = { 'imperial_basin:9': 1 };
|
||
p(game, 'd').forces = { 'imperial_basin:9': 1 };
|
||
const battles = quoteBattleBoard(game).battles.filter(battle => battle.territory === 'imperial_basin');
|
||
assert.equal(battles.length, 2);
|
||
assert.deepEqual(new Set(battles.map(battle => [battle.attacker, battle.defender].sort().join('/'))),
|
||
new Set(['a/d', 'b/d']));
|
||
});
|
||
|
||
void test('Advanced three-short group wins on three distinct controlled strongholds', () => {
|
||
const game = staged(); game.phase = 8;
|
||
p(game, 'a').forces = { 'arrakeen:10': 1 };
|
||
p(game, 'b').forces = { 'carthag:11': 1 };
|
||
p(game, 'c').forces = { 'sietch_tabr:14': 1 };
|
||
const row = strongholdProgress(game).progress.find(item => item.player === 'a')!;
|
||
assert.deepEqual(row.members, ['a', 'b', 'c']);
|
||
assert.equal(row.target, 3);
|
||
assert.equal(row.qualifies, true);
|
||
assert.deepEqual(quoteVictory(game).winner, ['a', 'b', 'c']);
|
||
});
|
||
|
||
void test('Advanced groups with Ecaz or another expansion faction stay outside these board quotes', () => {
|
||
for (const faction of ['ecaz', 'ixians'] as const) {
|
||
const game = staged(); p(game, 'd').faction = faction;
|
||
assert.throws(() => quoteBattleBoard(game));
|
||
assert.throws(() => quoteCoalitionAlliedSeparation({ ...game, remaining: game.order }, 'a'));
|
||
assert.throws(() => strongholdProgress(game));
|
||
assert.throws(() => isAlliedShipmentVisit(game, p(game, 'a'), 'arrakeen'));
|
||
}
|
||
});
|