import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, viewGame, type Game } from '../game/engine'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { TERRITORIES } from '../game/board'; import { act, advance, clean, createNoFieldAlliedTransitFixture as fixture, finishAlliedMarkerShipment, inventory, player } from './fixture-no-field-allied-transit'; function ship(f: ReturnType): Game { const before = structuredClone(f.game); let game = act(f.game, f.actor, f.ship); assert.deepEqual(f.game, before, 'Declaring the real shipment does not mutate the input.'); // The native action can resolve the no-response case immediately when no // player can intervene, or retain its real Richese response for passes. if (game.response) assert.equal(game.response.kind, 'richeseNoField'); game = finishAlliedMarkerShipment(game); assert.equal(player(before, f.actor).spice - player(game, f.actor).spice, 2); assert.equal(player(game, f.actor).reserves, player(before, f.actor).reserves); assert.equal(player(game, f.actor).forces['imperial_basin:9'], f.baseline); assert.equal(player(game, f.actor).noField!.deployed!.tokenId, f.token); assert.deepEqual(inventory(game), f.original); const warning = viewGame(game, f.actor).basicAlliedShipment; assert.ok(warning); assert.equal(warning.territory, 'Imperial Basin'); assert.equal(warning.normal, 0); assert.equal(warning.elite, 0); assert.equal(warning.noField, true); assert.equal(viewGame(game, f.ally).basicAlliedShipment, null); assert.equal(viewGame(game, f.ally).richeseNoField?.private, null); return game; } for (const value of [0, 3, 5] as const) { void test(`Basic allied marker ${value} may depart on its own original movement`, () => { const f = fixture(value); let game = ship(f); const event = player(game, f.actor).noFieldEvent; assert.ok(event); game = act(game, f.actor, {type: 'move', noField: f.token, event, forces: {}, territory: 'polar_sink', sector: 0}); game = advance(game, clean); assert.deepEqual(player(game, f.actor).noField!.deployed!.location, {territory: 'polar_sink', sector: 0}); assert.equal(viewGame(game, f.actor).basicAlliedShipment, null); game = act(game, f.actor, {type: 'endMovement'}); assert.ok(player(game, f.actor).noField!.deployed); assert.equal(player(game, f.actor).forces['imperial_basin:9'], f.baseline); assert.equal(player(game, f.ally).forces['imperial_basin:11'], 2); assert.deepEqual(inventory(game), f.original); }); void test(`Basic allied marker ${value} left at turn end reveals and loses only its new forces`, () => { const f = fixture(value), game = ship(f), before = player(game, f.actor); const reserves = before.reserves, tanks = before.tanks, event = before.noFieldEvent; const after = act(game, f.actor, {type: 'endMovement'}); const own = player(after, f.actor), materialized = Math.min(value, reserves); assert.equal(own.noField!.deployed, null); assert.notEqual(own.noFieldEvent, event); assert.equal(own.noField!.lastShipped, f.token); assert.equal(own.reserves, reserves - materialized); assert.equal(own.tanks, tanks + materialized); assert.equal(own.forces['imperial_basin:9'], f.baseline); assert.equal(player(after, f.ally).forces['imperial_basin:11'], 2); assert.equal(viewGame(after, f.actor).basicAlliedShipment, null); assert.deepEqual(inventory(after), f.original); }); } void test('voluntary revelation preserves the same visitor obligation without revealing twice', () => { const f = fixture(3); let game = ship(f); const event = player(game, f.actor).noFieldEvent; assert.ok(event); game = act(game, f.actor, {type: 'revealNoField', event, token: f.token}); const revealedEvent = player(game, f.actor).noFieldEvent; assert.equal(player(game, f.actor).forces['imperial_basin:9'], f.baseline + 3); assert.equal(viewGame(game, f.actor).basicAlliedShipment?.normal, 3); game = act(game, f.actor, {type: 'endMovement'}); assert.equal(player(game, f.actor).noFieldEvent, revealedEvent); assert.equal(player(game, f.actor).forces['imperial_basin:9'], f.baseline); assert.equal(player(game, f.actor).tanks, 3); assert.equal(player(game, f.ally).forces['imperial_basin:11'], 2); assert.deepEqual(inventory(game), f.original); }); void test('each minimal AI profile can complete an allied marker visit legally', () => { const f = fixture(3), source = ship(f); for (const difficulty of DIFFICULTIES) { let game = structuredClone(source); for (let count = 0; game.phase === 5 && game.active === f.actor && count < 60; count++) { if (!clean(game)) { game = advance(game, clean); continue; } const view = viewGame(game, f.actor); view.players.find(seat => seat.id === f.actor)!.bot = difficulty; const next = botActions(view)[0]; assert.ok(next); game = act(game, f.actor, next); } assert.notEqual(game.active, f.actor, `${difficulty} must finish the visiting turn.`); assert.equal(player(game, f.ally).forces['imperial_basin:11'], 2); assert.deepEqual(inventory(game), f.original); } }); void test('Karama cancels the marker visit before payment, custody or visitor receipt', () => { const f = fixture(3), game = structuredClone(f.game), opponent = player(game, f.other); // Controlled conserved card transfer: select one original undealt Karama // into an opponent's free hand slot to exercise the native response. const from = game.deck.findIndex(card => card.effect === 'karama'); assert.ok(from >= 0); assert.ok(opponent.hand.length < 4); const [karama] = game.deck.splice(from, 1); opponent.hand.push(karama); assert.deepEqual(inventory(game), f.original); const before = structuredClone(game); let attempted = act(game, f.actor, f.ship); assert.equal(attempted.response?.kind, 'richeseNoField'); attempted = act(attempted, f.other, {type: 'card', mode: 'cancel', card: karama.id}); attempted = advance(attempted, state => clean(state) && !state.pendingShipment); assert.equal(player(attempted, f.actor).spice, player(before, f.actor).spice); assert.equal(player(attempted, f.actor).reserves, player(before, f.actor).reserves); assert.deepEqual(player(attempted, f.actor).noField, player(before, f.actor).noField); assert.equal(player(attempted, f.actor).shipped, false); assert.equal(player(attempted, f.actor).forces['imperial_basin:9'], f.baseline); assert.equal(player(attempted, f.ally).forces['imperial_basin:11'], 2); assert.ok(attempted.basicAlliedShipment == null); assert.equal(attempted.discard.filter(card => card.id === karama.id).length, 1); assert.deepEqual(inventory(attempted), f.original); }); void test('allied transit does not bypass storm or the stronghold occupancy limit', () => { const f = fixture(3), storm = TERRITORIES.find(place => place.type !== 'polar' && place.sectors.includes(f.game.storm)); assert.ok(storm); const original = structuredClone(f.game); assert.throws(() => applyAction(f.game, f.actor, {...f.ship, territory: storm.id, sector: f.game.storm}), /storm/i); assert.deepEqual(f.game, original); const blocked = structuredClone(f.game), ally = player(blocked, f.ally); // Controlled force relocation retains the two physical ally counters; the // native Atreides garrison supplies the second existing stronghold occupant. assert.ok(player(blocked, f.other).forces['arrakeen:10'] > 0); assert.equal(ally.forces['imperial_basin:11'], 2); delete ally.forces['imperial_basin:11']; ally.forces['arrakeen:10'] = 2; assert.notEqual(blocked.storm, 10); const before = structuredClone(blocked); assert.throws(() => applyAction(blocked, f.actor, {...f.ship, territory: 'arrakeen', sector: 10}), /stronghold.*three/i); assert.deepEqual(blocked, before); assert.deepEqual(inventory(blocked), f.original); });