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