Files
Dune/tests/allied-no-field-transit.test.ts
T
Quintine 9c6b2d884b
Verify and publish checkpoint container / container (push) Canceled after 0s
Support Basic allied No-Field transit
2026-10-28 22:53:10 +11:00

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);
});