93 lines
5.8 KiB
TypeScript
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' };
|
|
}
|