Files
Dune/tests/fixture-advanced-overlap-child.ts
T
Quintine 8a39f5a172
Verify and publish checkpoint container / container (push) Successful in 10m16s
Enable advanced ambassador child arrivals
2026-10-29 19:42:27 +11:00

93 lines
5.8 KiB
TypeScript

import assert from 'node:assert/strict';
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
import { placeAmbassador, validateAmbassadors } from '../game/ecaz-ambassadors';
import { placeTerror } from '../game/moritani-terror';
import { fremenAmbassadorMovement } from '../game/fremen-ambassador';
import { guildAmbassadorShipments } from '../game/guild-ambassador';
import { advanceBasicSemutaAmbassador as advance, actBasicSemutaAmbassador as act,
basicSemutaAmbassadorClean as clean, basicSemutaAmbassadorPlayer as player } from './fixture-basic-semuta-ambassador';
export { act, player };
export type ChildEffect = 'fremen' | 'guild';
export type First = 'ambassador' | 'terror';
/** Public Advanced native setup and an actual paid faction shipment. The chosen
* original Ambassador cohort member and the single original Terror marker are
* controlled, physical placements at the clean turn boundary. No player force,
* purse, order, pending decision or receipt is staged. */
export function advancedOverlapChildFixture(effect: ChildEffect, first: First, withBG = false) {
let game = createGame('ADVOVERLAPCHILD', newPlayer('ecaz','Ecaz','ecaz'), true, ['ecaz']);
for (const faction of ['atreides','emperor',...(withBG ? ['beneGesserit'] as const : []),
...(withBG ? [] : ['guild'] as const),'moritani'] as const)
joinGame(game, newPlayer(faction, faction, faction));
const send = (actor: string, action: Action) => { game = act(game, actor, action); };
const reach = (until: (state: Game) => boolean) => { game = advance(game, until); };
send(withBG ? 'ecaz' : 'guild', { type:'seatPosition', position:6 });
if (withBG) {
send('atreides', { type:'seatPosition', position:1 });
send('emperor', { type:'seatPosition', position:2 });
send('beneGesserit', { type:'seatPosition', position:3 });
}
if (first === 'terror') send('moritani', { type:'seatPosition', position:4 });
send('ecaz', { type:'seatPosition', position:first === 'ambassador' ? 4 : 5 });
if (!withBG) {
send('atreides', { type:'seatPosition', position:1 });
send('emperor', { type:'seatPosition', position:2 });
}
if (withBG) joinGame(game, newPlayer('guild','guild','guild'));
for (const seat of game.players) send(seat.id, { type:'ready' });
send('ecaz', { type:'start', advancedPreview:true });
reach(state => state.status === 'playing');
assert.ok(game.advanced && !game.homeworlds && !game.nexusCards && !game.leaderSkills &&
!game.techTokens && !game.strongholdCards && !game.discoveryEnabled && !game.ecazTreachery);
// Select existing unopened ordinary Spice cards, never an invented draw.
for (const position of [0,1]) {
const index = game.spiceDeck.findIndex((card, i) => i >= position && 'territory' in card && !card.discovery);
assert.ok(index >= position);
[game.spiceDeck[position], game.spiceDeck[index]] = [game.spiceDeck[index], game.spiceDeck[position]];
}
const entrant = effect === 'guild' ? 'emperor' : 'guild';
reach(state => state.turn === 1 && state.phase === 5 && state.active === entrant && clean(state));
assert.equal(game.order.indexOf('ecaz') < game.order.indexOf('moritani'), first === 'ambassador');
const candidates = fremenAmbassadorMovement(game, 'ecaz').sources.flatMap(source =>
source.sectors.filter(sector => sector.forces > 0).flatMap(sector =>
source.destinations.filter(destination => destination.territory !== source.territory &&
['arrakeen','carthag','sietch_tabr','habbanya_ridge_sietch','tueks_sietch'].includes(destination.territory))
.map(destination => ({ source, sector, destination }))))
.filter(option => guildAmbassadorShipments(game, 'ecaz').destinations.some(destination =>
destination.territory === option.destination.territory && destination.sector === option.destination.sector));
const candidate = candidates[0];
assert.ok(candidate, 'Ecaz needs a reachable physical stronghold and legal reserve destination');
const destination = candidate.destination.territory, sector = candidate.destination.sector;
const ambassadors = game.ecazAmbassadors!;
const token = ambassadors.tokens.find(item => item.effect === effect); assert.ok(token);
if (token.zone === 'pool') {
const replacement = ambassadors.tokens.find(item => item.zone === 'supply' && item.effect !== 'ecaz');
assert.ok(replacement);
const index = ambassadors.cohort.indexOf(replacement.id); assert.ok(index >= 0);
ambassadors.cohort[index] = token.id; replacement.zone = 'pool'; token.zone = 'supply';
}
assert.equal(token.zone, 'supply');
validateAmbassadors(ambassadors);
game.ecazAmbassadors = placeAmbassador(ambassadors, token.id, { turn:game.turn,
availableSpice:player(game,'ecaz').spice,
destination:{id:destination,stronghold:true,inStorm:false,allowed:true} }).state;
const terror = game.moritaniTerror!.tokens.find(item => item.kind === 'robbery' && item.status === 'available');
assert.ok(terror);
game.moritaniTerror = placeTerror(game.moritaniTerror!, terror.id, destination, game.turn);
const alternate = candidates.find(item => item.destination.territory !== destination);
let sneak: string | undefined;
if (withBG) {
assert.ok(alternate, 'The Sneak fixture requires a second reachable stronghold');
const child = game.moritaniTerror.tokens.find(item => item.kind === 'sneakAttack' && item.status === 'available');
assert.ok(child);
// A second existing marker is staged without adding a force or resource.
child.status = 'placed'; child.location = alternate.destination.territory; sneak = child.id;
}
const shipment: Action = { type:'ship', territory:destination, sector, amount:1, elite:0, allyPayment:0 };
return { game, effect, first, destination, sector, token:token.id, terror:terror.id, shipment,
moveFrom:candidate.sector.key, alternate, sneak,
entrant, owner:'ecaz', moritani:'moritani' };
}