104 lines
6.6 KiB
TypeScript
104 lines
6.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine';
|
|
import { splitLocation } from '../game/board';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
|
import { actBasicSemutaAmbassador as act, basicSemutaAmbassadorClean as clean,
|
|
basicSemutaAmbassadorInventory as inventory, basicSemutaAmbassadorPlayer as player } from './fixture-basic-semuta-ambassador';
|
|
|
|
export { act, clean, inventory, player };
|
|
|
|
/** Original public setup, Richese cache sale, paid shipments and battle. Only
|
|
* unopened starting-deck and Spice-card order are selected for the witness. */
|
|
export function createIndependentSemutaBattleFixture(kind: 'loser-reinforcements' | 'loser-harass' | 'winner-mandatory' | 'winner-selected') {
|
|
let game = createGame('SEMUTAINDEPENDENTBATTLE', newPlayer('atreides', 'Atreides', 'atreides'), false, ['choam']);
|
|
for (const faction of ['emperor', 'richese', 'guild'] as const) joinGame(game, newPlayer(faction, faction, faction));
|
|
const loser = 'atreides', winner = 'emperor', holder = 'guild', location = 'red_chasm:7';
|
|
const perform = (actor: string, action: Action) => { game = act(game, actor, action); };
|
|
const reach = (until: (state: Game) => boolean) => {
|
|
for (let count = 0; count < 1800 && !until(game); count++) {
|
|
assert.notEqual(game.status, 'finished');
|
|
if (game.pendingTreacheryDiscard?.reaction?.stage === 'offer') {
|
|
const reaction = game.pendingTreacheryDiscard.reaction;
|
|
const actor = game.players.find(p => !reaction.passed.includes(p.id));
|
|
assert.ok(actor);
|
|
perform(actor.id, {type:'semutaPass', event:game.pendingTreacheryDiscard.batch.event});
|
|
} else {
|
|
const step = nextRicheseStrongholdsNativeStep(game); assert.ok(step, `Native step required at ${game.phase}`);
|
|
perform(step.actor, step.action);
|
|
}
|
|
}
|
|
assert.ok(until(game), 'The real game must reach the requested boundary.');
|
|
};
|
|
perform(loser, {type:'moduleOption', module:'ecazTreachery', enabled:true});
|
|
for (const seat of game.players) perform(seat.id, {type:'ready'});
|
|
perform(loser, {type:'start'});
|
|
assert.equal(game.semutaRecruitsPreview, true);
|
|
const originalInventory = inventory(game);
|
|
const cardId = kind === 'loser-harass' ? 'ecaz-harass-withdraw' : 'ecaz-reinforcements';
|
|
const shield = game.deck.find(card => card.kind === 'shield'); assert.ok(shield);
|
|
const reserved = new Set([cardId, shield.id]);
|
|
const filler = game.deck.filter(card => !reserved.has(card.id)).slice(0, 2); assert.equal(filler.length, 2);
|
|
const starting = kind === 'winner-mandatory'
|
|
? [filler[0].id, cardId, filler[1].id, shield.id]
|
|
: [cardId, shield.id, filler[0].id, filler[1].id];
|
|
for (let index = 0; index < starting.length; index++) {
|
|
const from = game.deck.findIndex(card => card.id === starting[index]); 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, kind === 'winner-mandatory' ? winner : loser).hand.some(card => card.id === cardId));
|
|
if (kind !== 'winner-mandatory') assert.ok(player(game, winner).hand.some(card => card.id === shield.id));
|
|
for (let index = 0; index < 3; index++) {
|
|
const from = game.spiceDeck.findIndex((card, pos) => pos >= index && 'territory' in card && !card.discovery);
|
|
assert.ok(from >= index); game.spiceDeck.splice(index, 0, game.spiceDeck.splice(from, 1)[0]);
|
|
}
|
|
reach(state => !state.response && !state.phaseOpening && state.decision?.kind === 'richeseCache');
|
|
perform('richese', {type:'decision', event:game.richeseBidding!.event, card:SEMUTA_DRUG_ID,
|
|
method:'onceAround', direction:'counterclockwise'});
|
|
reach(state => clean(state) && !!state.richeseAuction && !state.richeseAuction.outcome);
|
|
for (let count = 0; !player(game, holder).hand.some(card => card.id === SEMUTA_DRUG_ID) && count < 8; count++) {
|
|
const lot = game.richeseAuction; assert.ok(lot?.active && !lot.outcome);
|
|
perform(lot.active, {type:'richeseBid', event:lot.event, amount:lot.active === holder ? 1 : null, allyPayment:0});
|
|
reach(clean);
|
|
}
|
|
assert.ok(player(game, holder).hand.some(card => card.id === SEMUTA_DRUG_ID));
|
|
const shipped = new Set<string>();
|
|
while (shipped.size < 2) {
|
|
reach(state => state.phase === 5 && !!state.active && clean(state));
|
|
const actor = game.active!;
|
|
if ([loser, winner].includes(actor) && !shipped.has(actor)) {
|
|
perform(actor, {type:'ship', ...splitLocation(location), amount:actor === winner ? 4 : 1, elite:0, allyPayment:0});
|
|
reach(clean); shipped.add(actor);
|
|
}
|
|
perform(actor, {type:'endMovement'});
|
|
}
|
|
reach(state => state.phase === 6 && !state.battle && clean(state));
|
|
const actor = game.active!;
|
|
const choice = viewGame(game, actor).battleChoices.find(row => row.territory === 'red_chasm' &&
|
|
[row.attacker, row.defender].includes(loser) && [row.attacker, row.defender].includes(winner));
|
|
assert.ok(choice);
|
|
perform(choice.chooser, {type:'chooseBattle', territory:choice.territory,
|
|
target:choice.attacker === choice.chooser ? choice.defender : choice.attacker});
|
|
reach(state => !!state.battle && !state.battle.preparation && state.battle.preLeader?.closed !== false && clean(state));
|
|
const strongest = player(game, winner).leaders.filter(leader => !leader.dead && !leader.usedAt).sort((a,b) => b.strength-a.strength)[0];
|
|
const weakest = player(game, loser).leaders.filter(leader => !leader.dead && !leader.usedAt).sort((a,b) => a.strength-b.strength)[0];
|
|
assert.ok(strongest && weakest && strongest.strength + 1 > weakest.strength + 2);
|
|
perform(winner, {type:'battlePlan', dial:1, support:0, leader:strongest.id, weapon:null,
|
|
defense:kind === 'winner-mandatory' ? cardId : shield.id});
|
|
perform(loser, {type:'battlePlan', dial:0, support:0, leader:weakest.id,
|
|
weapon:kind === 'winner-mandatory' ? null : cardId, defense:null});
|
|
let final: {actor:string;action:Action} | undefined;
|
|
for (let count = 0; count < 100; count++) {
|
|
const step = nextRicheseStrongholdsNativeStep(game); assert.ok(step);
|
|
if (step.action.type === 'traitorCall') {
|
|
const voters = viewGame(game, game.host).battle!.traitorVoters;
|
|
if (voters.every(id => id === step.actor || game.battle!.traitorCalls[id] !== undefined)) { final = step; break; }
|
|
}
|
|
perform(step.actor, step.action);
|
|
}
|
|
assert.ok(final);
|
|
return {game, loser, winner, holder, location, card:cardId, shield:shield.id, originalInventory,
|
|
finalActor:final.actor, finalAction:final.action, kind};
|
|
}
|