110 lines
5.5 KiB
TypeScript
110 lines
5.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { baseDeck } from '../game/cards';
|
|
import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import { createRicheseNoField } from '../game/richese-no-field';
|
|
|
|
// Controlled Basic turn: both allies already have conserved forces in the
|
|
// destination, as can happen when they formed their alliance this turn.
|
|
function fixture() {
|
|
const game = createGame('ALLYTRANSIT', newPlayer('r', 'Richese', 'richese'), false, ['choam']);
|
|
game.players.push(newPlayer('a', 'Emperor', 'emperor'), newPlayer('e', 'Atreides', 'atreides'));
|
|
game.status = 'playing'; game.phase = 5; game.turn = 2; game.active = 'a';
|
|
game.order = ['r', 'a', 'e']; game.movementRemaining = ['a', 'r', 'e']; game.storm = 18;
|
|
game.deck = baseDeck();
|
|
for (const p of game.players) {
|
|
p.forces = {}; p.reserves = 20; p.tanks = 0; p.spice = 10; p.hand = [];
|
|
p.traitorChoices = []; p.traitors = [];
|
|
}
|
|
const [richese, recipient] = game.players;
|
|
richese.ally = 'a'; recipient.ally = 'r';
|
|
richese.allySinceTurn = 2; recipient.allySinceTurn = 2;
|
|
richese.forces['imperial_basin:11'] = 2; richese.reserves = 18;
|
|
recipient.forces['imperial_basin:11'] = 2; recipient.reserves = 18;
|
|
recipient.elites = {reserves: 4, tanks: 0, forces: {'imperial_basin:11': 1}, revived: 0};
|
|
richese.noField = createRicheseNoField(['zero', 'three', 'five']);
|
|
richese.noFieldEvent = 'original-marker-event';
|
|
return game;
|
|
}
|
|
const send = (game: Game, id: string, action: Action) => applyAction(game, id, action);
|
|
function offer(game: Game, token = 'three') {
|
|
return send(game, 'r', {type: 'offerRicheseNoField', token,
|
|
event: game.players[0].noFieldEvent, payer: 'r', territory: 'imperial_basin', sector: 9});
|
|
}
|
|
function accept(game: Game, elite = 2) {
|
|
return send(game, 'a', {type: 'decision', event: game.richeseAllyOffer!.event, accept: true, elite});
|
|
}
|
|
function finishResponse(game: Game) {
|
|
for (let count = 0; game.response && count < 10; count++) {
|
|
const pending = game.response;
|
|
const next = game.players.find(p => !pending.passed.includes(p.id));
|
|
assert.ok(next, 'Every response must resolve after all eligible players pass.');
|
|
game = send(game, next.id, {type: 'passResponse'});
|
|
}
|
|
assert.equal(game.pendingShipment, null);
|
|
return game;
|
|
}
|
|
function physicalAt(game: Game, id: string, territory: string) {
|
|
const p = game.players.find(seat => seat.id === id)!;
|
|
return Object.entries(p.forces).reduce((sum, [key, n]) => sum + (key.startsWith(`${territory}:`) ? n : 0), 0);
|
|
}
|
|
|
|
void test('Basic allied No-Field physical visitors may leave after the real offer and acceptance', () => {
|
|
const initial = fixture();
|
|
let game = offer(initial);
|
|
assert.equal(game.decision?.kind, 'richeseAllyShipment');
|
|
assert.equal(game.players[0].spice, 10);
|
|
game = finishResponse(accept(game));
|
|
assert.equal(game.players[0].spice, 8);
|
|
assert.equal(game.players[0].noField!.lastShipped, 'three');
|
|
assert.equal(game.players[0].noField!.deployed, null);
|
|
assert.equal(game.players[1].shipped, true);
|
|
assert.equal(game.players[1].reserves, 15);
|
|
assert.equal(game.players[1].forces['imperial_basin:9'], 3);
|
|
assert.deepEqual(viewGame(game, 'a').basicAlliedShipment, {territory: 'Imperial Basin', normal: 1, elite: 2});
|
|
game = send(game, 'a', {type: 'move', forces: {'imperial_basin:9': 3},
|
|
eliteForces: {'imperial_basin:9': 2}, territory: 'polar_sink', sector: 0});
|
|
assert.equal(viewGame(game, 'a').basicAlliedShipment, null);
|
|
game = send(game, 'a', {type: 'endMovement'});
|
|
assert.equal(physicalAt(game, 'a', 'imperial_basin'), 2);
|
|
assert.equal(game.players[1].forces['polar_sink:0'], 3);
|
|
assert.equal(game.players[1].tanks, 0);
|
|
assert.equal(game.players[0].forces['imperial_basin:11'], 2);
|
|
});
|
|
|
|
void test('Basic allied No-Field visitors left in the territory lose only their physical and elite excess', () => {
|
|
let game = finishResponse(accept(offer(fixture())));
|
|
game = send(game, 'a', {type: 'endMovement'});
|
|
const recipient = game.players[1];
|
|
assert.equal(physicalAt(game, 'a', 'imperial_basin'), 2);
|
|
assert.equal(Object.values(recipient.elites!.forces).reduce((sum, n) => sum + n, 0), 1);
|
|
assert.equal(recipient.tanks, 3);
|
|
assert.equal(recipient.elites!.tanks, 2);
|
|
assert.equal(recipient.reserves, 15);
|
|
assert.equal(game.players[0].forces['imperial_basin:11'], 2);
|
|
assert.equal(viewGame(game, 'a').basicAlliedShipment, null);
|
|
});
|
|
|
|
void test('Basic allied No-Field zero does not create visitor casualties or disturb the elite baseline', () => {
|
|
let game = finishResponse(accept(offer(fixture(), 'zero'), 0));
|
|
assert.equal(game.players[0].noField!.lastShipped, 'zero');
|
|
assert.equal(game.players[1].shipped, true);
|
|
assert.equal(game.players[1].reserves, 18);
|
|
assert.equal(viewGame(game, 'a').basicAlliedShipment, null);
|
|
game = send(game, 'a', {type: 'endMovement'});
|
|
assert.equal(physicalAt(game, 'a', 'imperial_basin'), 2);
|
|
assert.equal(game.players[1].elites!.forces['imperial_basin:11'], 1);
|
|
assert.equal(game.players[1].tanks, 0);
|
|
assert.equal(game.players[0].forces['imperial_basin:11'], 2);
|
|
});
|
|
|
|
void test('declining the allied No-Field visit keeps payment, reserves and visitor receipt untouched', () => {
|
|
let game = offer(fixture());
|
|
game = send(game, 'a', {type: 'decision', event: game.richeseAllyOffer!.event, decline: true});
|
|
assert.equal(game.players[0].spice, 10);
|
|
assert.equal(game.players[0].noField!.lastShipped, null);
|
|
assert.equal(game.players[1].shipped, false);
|
|
assert.equal(game.players[1].reserves, 18);
|
|
assert.ok(game.basicAlliedShipment == null);
|
|
});
|