Files
Dune/tests/fixture-basic-leaderless-harkonnen.ts
T
Quintine 31c6e79c80
Verify and publish checkpoint container / container (push) Successful in 10m56s
Support public leader preparation with Harkonnen
2026-10-29 08:51:11 +11:00

68 lines
3.5 KiB
TypeScript

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<seat.hand.length; i++) if (selected(seat.hand[i])) {
const j=game.deck.findIndex(card => !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`);
}