Support Basic allied No-Field transit
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
40d567a053
commit
9c6b2d884b
8 files changed
+171
-7
No files matched your search
@@ -0,0 +1,109 @@
|
||||
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);
|
||||
});
|
||||
@@ -272,7 +272,7 @@ void test('stale acceptance and changed pending reserve allocation cannot cause
|
||||
assert.ok(k);
|
||||
conserve(g);
|
||||
});
|
||||
void test('invalid payer, occupancy, repeated token, wrong turn and on-planet Fremen reserves reject atomically', () => {
|
||||
void test('invalid payer, Advanced allied occupancy, wrong turn and on-planet Fremen reserves reject atomically', () => {
|
||||
const g = fixture(),
|
||||
before = structuredClone(g);
|
||||
assert.throws(() => offer(g, 'five', 'e'), /ally payer/);
|
||||
@@ -285,6 +285,7 @@ void test('invalid payer, occupancy, repeated token, wrong turn and on-planet Fr
|
||||
g.active = 'a';
|
||||
g.players[0].forces = { 'arrakeen:10': 1 };
|
||||
g.players[0].reserves = 19;
|
||||
g.advanced = true;
|
||||
assert.throws(() => offer(g), /occupied by your ally/);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user