import assert from 'node:assert/strict'; import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine'; import type { NoFieldValue } from '../game/richese-no-field'; import { actNoFieldAlliedSeparation as act, noFieldAlliedSeparationClean as clean, noFieldAlliedSeparationPlayer as player, nextNoFieldAlliedSeparation } from './fixture-no-field-allied-separation'; import { nextRicheseHomeworldShipmentNativeStep, richeseHomeworldShipmentInventory as inventory, richeseHomeworldShipmentPopulation } from './fixture-richese-homeworld-shipment'; export { act, clean, inventory, player }; /** Original Basic + Homeworlds setup: one real paid counter per side in the * storm-separated Basin, then an actual worm, consensual alliance and an aid * pledge. Only unopened Treachery and Spice order is selected. */ export function createNoFieldAlliedCompanionsFixture(value: NoFieldValue) { const actor = 'richese', ally = 'emperor', other = 'atreides'; let game = createGame('NOFIELDALLIEDCOMPANIONS', newPlayer(actor, 'Richese', 'richese'), false, ['choam']); joinGame(game, newPlayer(ally, 'Emperor', 'emperor')); joinGame(game, newPlayer(other, 'Atreides', 'atreides')); const actions: {actor: string; action: Action}[] = []; const perform = (id: string, action: Action) => { game = act(game, id, action, actions); }; const reach = (predicate: (state: Game) => boolean) => { for (let i = 0; i < 1800 && !predicate(game); i++) { if (game.turn === 2 && game.phase === 2 && clean(game) && player(game, actor).spice < 2 && player(game, actor).charityTurn !== game.turn) { perform(actor, {type: 'charity'}); continue; } const step = nextNoFieldAlliedSeparation(game) ?? nextRicheseHomeworldShipmentNativeStep(game); assert.ok(step, `Missing native control at ${game.turn}/${game.phase}/${game.decision?.kind}`); perform(step.actor, step.action); } assert.ok(predicate(game), 'Reach the requested native boundary'); }; for (const [index, seat] of game.players.entries()) perform(seat.id, {type: 'seatPosition', position: index + 4}); perform(ally, {type: 'seatPosition', position: 1}); perform(actor, {type: 'seatPosition', position: 2}); perform(other, {type: 'seatPosition', position: 3}); perform(game.host, {type: 'homeworlds', enabled: true}); for (const seat of game.players) perform(seat.id, {type: 'ready'}); perform(game.host, {type: 'start'}); const ordinary = game.deck.find(card => card.kind === 'projectile'); const shield = game.deck.find(card => card.kind === 'shield'); const karama = game.deck.find(card => card.effect === 'karama'); assert.ok(ordinary && shield && karama); for (const [index, id] of [ordinary.id, shield.id, karama.id].entries()) { const from = game.deck.findIndex(card => card.id === id); assert.ok(from >= index); [game.deck[index], game.deck[from]] = [game.deck[from], game.deck[index]]; } reach(state => state.status === 'playing'); assert.ok(player(game, other).hand.some(card => card.id === karama.id)); const original = inventory(game); const safe = (position: number, worm: boolean) => { const from = game.spiceDeck.findIndex((card, i) => i >= position && (worm ? 'worm' in card && !card.greatMaker && !card.suppressed : 'territory' in card && !card.discovery && card.territory !== 'imperial_basin' && game.players.every(seat => !Object.entries(seat.forces).some(([key, n]) => n > 0 && key.startsWith(`${card.territory}:`))))); assert.ok(from >= position); game.spiceDeck.splice(position, 0, game.spiceDeck.splice(from, 1)[0]); }; safe(0, false); for (const [index, dialer] of game.stormDialers.entries()) perform(dialer, {type: 'stormDial', amount: index ? 5 : 4}); reach(state => state.turn === 1 && state.phase === 5 && clean(state)); assert.equal(game.storm, 10); while (game.phase === 5) { const current = game.active; if (clean(game) && current === ally && !player(game, ally).shipped) { perform(ally, {type: 'ship', territory: 'imperial_basin', sector: 11, amount: 1, elite: 0, allyPayment: 0, homeworldSources: {'homeworld:emperor': {normal: 1, elite: 0}}}); reach(clean); continue; } if (clean(game) && current === actor && !player(game, actor).shipped) { perform(actor, {type: 'ship', territory: 'imperial_basin', sector: 9, amount: 1, elite: 0, allyPayment: 0, homeworldSources: {'homeworld:richese': {normal: 1, elite: 0}}}); reach(clean); perform(actor, {type: 'endMovement'}); break; } const step = nextNoFieldAlliedSeparation(game) ?? nextRicheseHomeworldShipmentNativeStep(game); assert.ok(step); perform(step.actor, step.action); } reach(state => state.turn === 2 && state.phase === 0 && clean(state)); safe(0, true); safe(1, false); reach(state => state.turn === 2 && state.phase === 1 && !!state.nexus && !state.spiceWindow && !state.spiceResolution && clean(state)); perform(actor, {type: 'alliance', target: ally}); perform(ally, {type: 'alliance', target: actor}); reach(state => state.turn === 2 && state.phase === 5 && state.active === actor && clean(state)); assert.equal(player(game, actor).forces['imperial_basin:9'], 1); assert.equal(player(game, ally).forces['imperial_basin:11'], 1); assert.equal(richeseHomeworldShipmentPopulation(game).side, 'high'); assert.equal(player(game, actor).ally, ally); assert.equal(player(game, ally).ally, actor); const own = player(game, actor), token = own.noField!.tokens.find(token => token.value === value); assert.ok(token && own.noFieldEvent); const needed = Math.max(0, 4 - own.spice); assert.ok(player(game, ally).spice >= needed); if (needed) perform(ally, {type: 'pledgeAid', amount: needed}); const ship: Action = {type: 'ship', noField: token.id, event: player(game, actor).noFieldEvent!, noFieldReserves: 1, territory: 'imperial_basin', sector: 9, allyPayment: needed}; return {game, actor, ally, other, token: token.id, karama: karama.id, value, ship, allyPayment: needed, baseline: 1, companions: 1, original, actions}; } export function advanceAlliedCompanions(game: Game, until: (state: Game) => boolean): Game { for (let i = 0; i < 1800; i++) { if (until(game)) return game; const step = nextNoFieldAlliedSeparation(game) ?? nextRicheseHomeworldShipmentNativeStep(game); assert.ok(step); game = act(game, step.actor, step.action); } throw Error('Native allied companion controls did not reach the requested boundary'); }