109 lines
6.4 KiB
TypeScript
109 lines
6.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { viewGame, type Game } from '../game/engine';
|
|
import { territory } from '../game/board';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { selectedDiscoverySkillsStart } from './selected-discovery-skills-fixture';
|
|
import { actBasicSemutaAmbassador as act, nextBasicSemutaAmbassadorStep,
|
|
basicSemutaAmbassadorPlayer as player, basicSemutaAmbassadorInventory as inventory } from './fixture-basic-semuta-ambassador';
|
|
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
|
|
|
const owner = 'ecaz', holder = 'guild', rider = 'fremen';
|
|
type Effect = 'choam' | 'ixians';
|
|
|
|
/** The original lobby and Skills setup, real paid Ambassador placement, Spice
|
|
* Blow, Nexus and Fremen ride. Conserved staging chooses the supplied token,
|
|
* transfers Semuta and one Fremen force, and orders the unopened Spice deck. */
|
|
function source(effect: Effect) {
|
|
let g = selectedDiscoverySkillsStart({advanced:false,
|
|
factions:[owner,'richese',rider,holder], expansions:['ecaz','choam']}).game;
|
|
assert.equal(g.semutaPreview,true);
|
|
assert.ok(g.discoveries && g.leaderSkills);
|
|
const packet = g.ecazAmbassadors!;
|
|
const selected = packet.tokens.find(token => token.effect === effect)!;
|
|
if (!packet.cohort.includes(selected.id)) {
|
|
const displaced = packet.tokens.find(token => token.id === packet.cohort[0])!;
|
|
assert.equal(displaced.zone,'supply');
|
|
packet.cohort[0] = selected.id;
|
|
displaced.zone = 'pool'; selected.zone = 'supply';
|
|
}
|
|
const guild = player(g,holder), semutaIndex = g.richeseCache!.findIndex(card => card.id === SEMUTA_DRUG_ID);
|
|
assert.ok(semutaIndex >= 0 && guild.hand.length);
|
|
const semuta = g.richeseCache!.splice(semutaIndex,1)[0];
|
|
g.deck.push(guild.hand[0]); guild.hand[0] = semuta;
|
|
const fremen = player(g,rider), sourceForce = Object.keys(fremen.forces).find(key => fremen.forces[key] > 0)!;
|
|
assert.ok(sourceForce);
|
|
fremen.forces[sourceForce]--;
|
|
if (!fremen.forces[sourceForce]) delete fremen.forces[sourceForce];
|
|
fremen.forces['the_great_flat:15'] = (fremen.forces['the_great_flat:15'] ?? 0) + 1;
|
|
for (const [position, predicate] of [
|
|
[0, (card: Game['spiceDeck'][number]) => 'territory' in card && card.territory === 'the_great_flat'],
|
|
[1, (card: Game['spiceDeck'][number]) => 'worm' in card && !card.greatMaker],
|
|
] as const) {
|
|
const index = g.spiceDeck.findIndex((card, i) => i >= position && predicate(card));
|
|
assert.ok(index >= position);
|
|
[g.spiceDeck[position],g.spiceDeck[index]] = [g.spiceDeck[index],g.spiceDeck[position]];
|
|
}
|
|
const reach = (condition: (game: Game) => boolean) => {
|
|
for (let n = 0; n < 2000; n++) {
|
|
if (condition(g)) return;
|
|
assert.notEqual(g.status,'finished');
|
|
const next = g.decision?.kind === 'richeseCache' ? nextRicheseStrongholdsNativeStep(g) : nextBasicSemutaAmbassadorStep(g);
|
|
assert.ok(next, `Missing action at ${g.status}/${g.turn}/${g.phase}/${g.decision?.kind}`);
|
|
g = act(g,next.actor,next.action);
|
|
}
|
|
throw Error('The mixed game did not reach the worm ride');
|
|
};
|
|
reach(state => state.phase === 4 && !state.response && state.decision?.kind === 'ecazPlacement');
|
|
const destination = ['carthag','arrakeen','habbanya_ridge_sietch','tueks_sietch']
|
|
.find(id => territory(id).sectors.some(sector => sector !== g.storm))!;
|
|
const sector = territory(destination).sectors.find(value => value !== g.storm)!;
|
|
g = act(g,owner,{type:'decision',token:selected.id,territory:destination});
|
|
reach(state => !state.response && state.decision?.kind === 'ecazPlacement');
|
|
g = act(g,owner,{type:'decision',decline:true});
|
|
reach(state => state.turn === 2 && state.decision?.kind === 'wormRide' && state.decision.player === rider);
|
|
assert.equal(g.decision?.kind === 'wormRide' && g.decision.territory,'the_great_flat');
|
|
assert.notEqual(g.storm,15);
|
|
g = act(g,rider,{type:'decision',accept:true,territory:destination,sector,forces:{'the_great_flat:15':1}});
|
|
assert.equal(g.pendingAmbassador?.arrivalCause,'wormRide');
|
|
assert.equal(g.pendingAmbassador?.resume,'wormRide');
|
|
const event = g.pendingAmbassador!.event;
|
|
g = act(g,owner,{type:'decision',event,trigger:true,beneficiary:owner,choice:'effect'});
|
|
assert.equal(g.pendingAmbassador?.stage,'cards');
|
|
const cards = player(g,owner).hand.slice(0,effect === 'choam' ? 2 : 1).map(card => card.id);
|
|
assert.ok(cards.length >= 1);
|
|
return {g,event,cards,destination,sector,original:inventory(g)};
|
|
}
|
|
|
|
for (const effect of ['choam','ixians'] as const) for (const claim of [false,true])
|
|
void test(`Basic Discovery and Skills: worm ride ${effect} Semuta ${claim ? 'claim' : 'pass'}`, () => {
|
|
const f = source(effect), before = structuredClone(f.g);
|
|
const spice = player(before,owner).spice, top = before.deck[0]?.id;
|
|
let g = act(before,owner,{type:'decision',event:f.event,cards:f.cards});
|
|
const pending = g.pendingTreacheryDiscard!;
|
|
assert.equal(pending?.continuation.kind,'ambassador');
|
|
assert.equal(pending?.reaction?.stage,'offer');
|
|
assert.equal(pending.batch.cause,`ambassador:${effect}`);
|
|
assert.deepEqual(pending.batch.entries.map(entry => entry.card.id),f.cards);
|
|
assert.ok(pending.batch.entries.every(entry => entry.discardedBy === owner && !entry.publicFace));
|
|
assert.deepEqual(g.deck,before.deck);
|
|
assert.equal(player(g,owner).spice,spice + (effect === 'choam' ? 3*f.cards.length : 0));
|
|
assert.equal(player(g,rider).forces[`${f.destination}:${f.sector}`],1);
|
|
for (const p of g.players) assert.deepEqual(viewGame(g,p.id).semutaReaction!.candidates,[]);
|
|
const event = pending.batch.event;
|
|
if (claim) {
|
|
g = act(g,holder,{type:'semutaCommit',event});
|
|
if (g.pendingTreacheryDiscard?.reaction?.stage === 'select')
|
|
g = act(g,holder,{type:'semutaSelect',event,card:f.cards[0]});
|
|
} else for (const p of g.players) g = act(g,p.id,{type:'semutaPass',event});
|
|
assert.equal(g.pendingTreacheryDiscard,null);
|
|
assert.equal(g.pendingAmbassador,null);
|
|
assert.equal(player(g,rider).forces[`${f.destination}:${f.sector}`],1);
|
|
assert.equal(player(g,owner).spice,spice + (effect === 'choam' ? 3*f.cards.length : 0));
|
|
assert.equal(player(g,holder).hand.some(card => card.id === f.cards[0]),claim);
|
|
if (effect === 'ixians') assert.ok(player(g,owner).hand.some(card => card.id === top));
|
|
assert.deepEqual(inventory(g),f.original);
|
|
assert.throws(() => act(g,holder,{type:'semutaCommit',event}));
|
|
assert.notEqual(g.decision?.kind,'ecazAmbassador');
|
|
});
|