import assert from 'node:assert/strict'; import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { actSalusaGhola, advanceSalusaGhola, salusaGholaClean } from './fixture-salusa-ghola'; export function leaderlessSeat(game: Game, id: string) { const seat = game.players.find(player => player.id === id); assert.ok(seat); return seat; } /** Public Basic start with Harkonnen and Emperor's actual paid shipments to * Imperial Basin. Only unopened Spice order is selected. Later tests openly * stage leader history/capture and conserved physical hand custody. */ export function createBasicLeaderlessHarkonnenFixture() { let game = createGame('BASICHARKLEADER', newPlayer('h','Harkonnen','harkonnen'), false); for (const [id,faction] of [['e','emperor'],['g','guild'],['a','atreides']] as const) joinGame(game, newPlayer(id, faction, faction)); const act = (id:string, action:Action) => {game = actSalusaGhola(game,id,action);}; const reach = (until:(g:Game)=>boolean) => {game = advanceSalusaGhola(game,until);}; for (const seat of game.players) act(seat.id,{type:'ready'}); act('h',{type:'start'}); assert.equal(game.status,'setup'); reach(g => g.status === 'playing'); for (let position = 0; position < 2; position++) { const index = game.spiceDeck.findIndex((card,i) => i >= position && 'territory' in card && !card.discovery && card.territory !== 'imperial_basin'); assert.ok(index >= position); game.spiceDeck.splice(position,0,game.spiceDeck.splice(index,1)[0]); } reach(g => g.phase === 5 && salusaGholaClean(g)); for (let n = 0; game.phase === 5 && n < 100; n++) { if (!salusaGholaClean(game)) {reach(salusaGholaClean); continue;} const owner = game.active!; if (owner === 'h' || owner === 'e') { act(owner,{type:'ship',territory:'imperial_basin',sector:9,amount:1,elite:0,allyPayment:0}); reach(salusaGholaClean); } act(owner,{type:'endMovement'}); } assert.equal(game.phase,6); reach(g => salusaGholaClean(g) && !g.battle && !!g.active); const chooser = game.active!; const choice = viewGame(game,chooser).battleChoices.find(c => c.territory === 'imperial_basin' && [c.attacker,c.defender].includes('h') && [c.attacker,c.defender].includes('e')); assert.ok(choice); return {game,chooser,choice,action:{type:'chooseBattle',territory:choice.territory, target:choice.attacker === chooser ? choice.defender : choice.attacker} as Action}; } /** Replace a held card with one physical matching card from a real zone. */ export function stageLeaderlessCard(game: Game, owner: string, effect: 'hero'|'ghola'|'none') { const seat = leaderlessSeat(game,owner); const selected = (card: {kind:string;effect?:string}) => card.kind === 'hero' || card.effect === 'ghola'; for (let i=0; i !selected(card)); assert.ok(j >= 0); [seat.hand[i],game.deck[j]] = [game.deck[j],seat.hand[i]]; } if (effect === 'none') return; const wanted = (card:{kind:string;effect?:string}) => effect === 'hero' ? card.kind === 'hero' : card.effect === 'ghola'; const zones = [game.deck,...game.players.filter(p => p.id !== owner).map(p => p.hand),game.discard]; for (const zone of zones) { const i = zone.findIndex(wanted); if (i < 0) continue; if (seat.hand.length) [seat.hand[0],zone[i]] = [zone[i],seat.hand[0]]; else seat.hand.push(zone.splice(i,1)[0]); return; } assert.fail(`Physical ${effect} must be in a known zone`); }