Support Semuta on Discovery Skills Ambassador discards
Verify and publish checkpoint container / container (push) Successful in 11m47s
Verify and publish checkpoint container / container (push) Successful in 11m47s
This commit is contained in:
1 parent
528b152f7d
commit
6204efb7e4
8 files changed
+367
-2
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { passArrivalOrder, passCurrentSemuta } from './fixture-semuta-arrival-overlap';
|
||||
import { act, player, semutaDiscoverySkillsOverlapFixture } from './semuta-discovery-skills-overlap-fixture';
|
||||
|
||||
// Select a legal Ambassador cohort and prior card acquisitions at the clean
|
||||
// pre-arrival boundary. The shipment, Ambassador grant, discard and Terror
|
||||
// continuation are all original player actions, never installed as receipts.
|
||||
for (const effect of ['choam', 'ixians'] as const) for (const outcome of ['claim', 'pass'] as const)
|
||||
void test(`Basic Discovery and Skills: ${effect} Ambassador ${outcome} resumes original Terror`, () => {
|
||||
const f = semutaDiscoverySkillsOverlapFixture(false);
|
||||
let game = f.game;
|
||||
const ambassadors = game.ecazAmbassadors!;
|
||||
const previous = ambassadors.tokens.find(t => t.id === f.token)!;
|
||||
const marker = ambassadors.tokens.find(t => t.effect === effect)!;
|
||||
if (!ambassadors.cohort.includes(marker.id)) {
|
||||
const replacedId = ambassadors.cohort[0];
|
||||
const replaced = ambassadors.tokens.find(t => t.id === replacedId)!;
|
||||
assert.equal(replaced.zone, 'supply');
|
||||
replaced.zone = 'pool'; marker.zone = 'supply';
|
||||
ambassadors.cohort[0] = marker.id;
|
||||
}
|
||||
assert.equal(previous.zone, 'placed'); assert.equal(marker.zone, 'supply');
|
||||
previous.zone = 'supply'; previous.location = null;
|
||||
marker.zone = 'placed'; marker.location = f.destination;
|
||||
const cache = game.richeseCache!;
|
||||
const semutaIndex = cache.findIndex(card => card.id === SEMUTA_DRUG_ID);
|
||||
assert.ok(semutaIndex >= 0);
|
||||
player(game, 'richese').hand.push(cache.splice(semutaIndex, 1)[0]);
|
||||
const owner = player(game, 'ecaz');
|
||||
while (owner.hand.length < 2) { const card = game.deck.shift(); assert.ok(card); owner.hand.push(card); }
|
||||
const chosen = owner.hand.slice(0, effect === 'choam' ? 2 : 1).map(card => card.id);
|
||||
game = passArrivalOrder(act(game, 'guild', f.shipment));
|
||||
game = f.advance(game, state => !!state.pendingAmbassador || !!state.pendingTerrorEntry);
|
||||
assert.equal(game.pendingArrivalOverlap?.first, 'ambassador');
|
||||
assert.equal(game.pendingAmbassador?.token, marker.id);
|
||||
game = act(game, 'ecaz', {type:'decision', event:game.pendingAmbassador!.event,
|
||||
trigger:true, beneficiary:'ecaz', choice:'effect'});
|
||||
assert.equal(game.pendingAmbassador?.stage, 'cards');
|
||||
const balance = player(game, 'ecaz').spice;
|
||||
const deckSize = game.deck.length, replacement = game.deck[0]?.id;
|
||||
game = act(game, 'ecaz', {type:'decision', event:game.pendingAmbassador!.event, cards:chosen});
|
||||
assert.equal(game.pendingTreacheryDiscard?.continuation.kind, 'ambassador');
|
||||
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage, 'offer');
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'first');
|
||||
assert.deepEqual(viewGame(game, 'guild').semutaReaction!.candidates, []);
|
||||
assert.deepEqual(viewGame(game, 'richese').semutaReaction!.candidates, []);
|
||||
assert.equal(player(game, 'ecaz').spice, balance + (effect === 'choam' ? 6 : 0));
|
||||
assert.equal(game.deck.length, deckSize, 'The Ixian replacement must wait for the discard reaction');
|
||||
if (outcome === 'claim') {
|
||||
const event = game.pendingTreacheryDiscard!.batch.event;
|
||||
game = act(game, 'richese', {type:'semutaCommit', event});
|
||||
if (game.pendingTreacheryDiscard?.reaction?.stage === 'select') {
|
||||
assert.deepEqual(viewGame(game, 'richese').semutaReaction!.candidates.map(card => card.id), chosen);
|
||||
assert.deepEqual(viewGame(game, 'guild').semutaReaction!.candidates, []);
|
||||
game = act(game, 'richese', {type:'semutaSelect', event, card:chosen[0]});
|
||||
}
|
||||
assert.ok(player(game, 'richese').hand.some(card => card.id === chosen[0]));
|
||||
} else game = passCurrentSemuta(game);
|
||||
assert.equal(player(game, 'ecaz').spice, balance + (effect === 'choam' ? 6 : 0));
|
||||
assert.equal(game.deck.length, deckSize - Number(effect === 'ixians'));
|
||||
if (effect === 'ixians') assert.ok(player(game, 'ecaz').hand.some(card => card.id === replacement));
|
||||
assert.equal(game.pendingArrivalOverlap?.stage, 'second');
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
game = act(game, 'moritani', {type:'decision', reveal:true});
|
||||
game = act(game, 'moritani', {type:'decision', choice:'spice'});
|
||||
assert.equal(game.pendingArrivalOverlap ?? null, null);
|
||||
assert.equal(game.pendingTreacheryDiscard ?? null, null);
|
||||
assert.equal(player(game, 'guild').forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(game.moritaniTerror!.tokens.find(token => token.id === f.terror)?.status, 'removed');
|
||||
});
|
||||
@@ -0,0 +1,108 @@
|
||||
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');
|
||||
});
|
||||
@@ -0,0 +1,148 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { placeAmbassador } from '../game/ecaz-ambassadors';
|
||||
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
||||
import { basicSemutaAmbassadorInventory as inventory } from './fixture-basic-semuta-ambassador';
|
||||
import { semutaDiscoverySkillsOverlapFixture, act, clean, player } from './semuta-discovery-skills-overlap-fixture';
|
||||
|
||||
/** Stage physical cards and the original Ambassador cohort on a fresh public
|
||||
* Discovery + Skills setup. No cards or tokens are created or destroyed. The
|
||||
* arrival, Ambassador decision, discard and reaction are actual game actions. */
|
||||
function fixture(effect: 'choam' | 'ixians') {
|
||||
const source = semutaDiscoverySkillsOverlapFixture(false);
|
||||
let game = structuredClone(source.beforeMarkers);
|
||||
const originalCards = inventory(game);
|
||||
const ambassadors = game.ecazAmbassadors!;
|
||||
const token = ambassadors.tokens.find(row => row.effect === effect)!;
|
||||
if (token.zone === 'pool') {
|
||||
const replacement = ambassadors.tokens.find(row => row.zone === 'supply' && row.effect !== 'ecaz')!;
|
||||
assert.ok(replacement);
|
||||
const position = ambassadors.cohort.indexOf(replacement.id);
|
||||
assert.ok(position >= 0);
|
||||
ambassadors.cohort[position] = token.id;
|
||||
token.zone = 'supply'; replacement.zone = 'pool';
|
||||
}
|
||||
assert.equal(token.zone, 'supply');
|
||||
const originalTokenIds = ambassadors.tokens.map(row => row.id).sort();
|
||||
// Choose Semuta from the actual physical cache, deck, discard or a hand;
|
||||
// exchange it with an existing Richese card if necessary.
|
||||
const holders = [game.deck, game.discard, game.richeseCache!, ...game.players.map(row => row.hand)];
|
||||
const richese = player(game, 'richese');
|
||||
const semutaSource = holders.find(cards => cards.some(card => card.id === SEMUTA_DRUG_ID));
|
||||
assert.ok(semutaSource);
|
||||
if (semutaSource !== richese.hand) {
|
||||
const index = semutaSource.findIndex(card => card.id === SEMUTA_DRUG_ID);
|
||||
const [semuta] = semutaSource.splice(index, 1);
|
||||
if (richese.hand.length >= 4) semutaSource.push(richese.hand.pop()!);
|
||||
richese.hand.push(semuta);
|
||||
}
|
||||
const ecaz = player(game, 'ecaz');
|
||||
while (ecaz.hand.length < 2) { const card = game.deck.shift(); assert.ok(card); ecaz.hand.push(card); }
|
||||
assert.deepEqual(inventory(game), originalCards);
|
||||
const territory = source.destination, sector = source.sector;
|
||||
assert.notEqual(game.storm, sector);
|
||||
game.ecazAmbassadors = placeAmbassador(ambassadors, token.id, {
|
||||
turn: game.turn, availableSpice: ecaz.spice,
|
||||
destination: { id: territory, stronghold: true, inStorm: false, allowed: true },
|
||||
}).state;
|
||||
// A normal shipment followed by actual movement into the stronghold tests
|
||||
// resumption of the original movement instead of merely an entry snapshot.
|
||||
game = act(game, 'guild', {type:'ship', territory:'habbanya_ridge_flat', sector, amount:1, elite:0, allyPayment:0});
|
||||
game = source.advance(game, state => state.phase === 5 && state.active === 'guild' && clean(state));
|
||||
game = act(game, 'guild', {type:'move', from:`habbanya_ridge_flat:${sector}`, territory, sector, amount:1});
|
||||
game = source.advance(game, state => !state.response && state.pendingAmbassador?.stage === 'offer');
|
||||
const event = game.pendingAmbassador!.event;
|
||||
assert.equal(game.pendingAmbassador!.arrivalCause, 'movement');
|
||||
assert.equal(game.pendingAmbassador!.token, token.id);
|
||||
game = act(game, 'ecaz', {type:'decision', event, trigger:true, beneficiary:'ecaz', choice:'effect'});
|
||||
assert.equal(game.pendingAmbassador?.stage, 'cards');
|
||||
assert.equal(game.pendingAmbassador?.effect, effect);
|
||||
const chosen = player(game, 'ecaz').hand.slice(0, effect === 'choam' ? 2 : 1).map(card => card.id);
|
||||
assert.equal(chosen.length, effect === 'choam' ? 2 : 1);
|
||||
return {game, event, chosen, originalCards, originalTokenIds, advance:source.advance, location:`${territory}:${sector}`};
|
||||
}
|
||||
|
||||
for (const effect of ['choam', 'ixians'] as const) void test(
|
||||
`Basic Discovery + Skills native ${effect} Ambassador discard offers Semuta and resumes movement`, () => {
|
||||
const f = fixture(effect), source = structuredClone(f.game);
|
||||
const ecazSpice = player(source, 'ecaz').spice;
|
||||
const pending = act(source, 'ecaz', {type:'decision', event:f.event, cards:f.chosen});
|
||||
assert.deepEqual(source, f.game);
|
||||
const receipt = pending.pendingTreacheryDiscard!;
|
||||
assert.equal(receipt?.reaction?.stage, 'offer');
|
||||
assert.equal(receipt.continuation.kind, 'ambassador');
|
||||
assert.equal(receipt.batch.cause, `ambassador:${effect}`);
|
||||
assert.deepEqual(receipt.batch.entries.map(row => row.card.id), f.chosen);
|
||||
assert.ok(receipt.batch.entries.every(row => !row.publicFace && row.discardedBy === 'ecaz'));
|
||||
assert.equal(player(pending, 'ecaz').spice, ecazSpice + (effect === 'choam' ? 6 : 0));
|
||||
assert.deepEqual(pending.deck, source.deck);
|
||||
const event = receipt.batch.event;
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(pending, 'richese');
|
||||
view.players.find(row => row.id === 'richese')!.bot = difficulty;
|
||||
const choice = botActions(view).find(row => row.type === 'semutaCommit' || row.type === 'semutaPass');
|
||||
assert.ok(choice, difficulty);
|
||||
assert.doesNotThrow(() => applyAction(pending, 'richese', choice), difficulty);
|
||||
}
|
||||
for (const claim of [false, true]) {
|
||||
let resumed: Game = structuredClone(pending);
|
||||
if (claim) {
|
||||
resumed = act(resumed, 'richese', {type:'semutaCommit', event});
|
||||
if (resumed.pendingTreacheryDiscard?.reaction?.stage === 'select')
|
||||
resumed = act(resumed, 'richese', {type:'semutaSelect', event, card:f.chosen[0]});
|
||||
} else for (const seat of resumed.players) resumed = act(resumed, seat.id, {type:'semutaPass', event});
|
||||
assert.equal(resumed.pendingTreacheryDiscard, null);
|
||||
assert.equal(resumed.pendingAmbassador, null);
|
||||
assert.equal(resumed.active, 'guild');
|
||||
assert.equal(player(resumed, 'guild').moved, 1);
|
||||
assert.equal(player(resumed, 'guild').forces[f.location], 1);
|
||||
assert.equal(player(resumed, 'ecaz').spice, ecazSpice + (effect === 'choam' ? 6 : 0));
|
||||
assert.equal(player(resumed, 'richese').hand.some(card => card.id === f.chosen[0]), claim);
|
||||
assert.equal(resumed.discard.some(card => card.id === SEMUTA_DRUG_ID), claim);
|
||||
assert.equal(resumed.discard.some(card => card.id === f.chosen[0]), !claim);
|
||||
if (effect === 'ixians') {
|
||||
assert.ok(player(resumed, 'ecaz').hand.some(card => card.id === source.deck[0].id));
|
||||
assert.deepEqual(resumed.deck.map(card => card.id), source.deck.slice(1).map(card => card.id));
|
||||
} else assert.deepEqual(resumed.deck, source.deck);
|
||||
assert.deepEqual(inventory(resumed), f.originalCards);
|
||||
assert.deepEqual(resumed.ecazAmbassadors!.tokens.map(row => row.id).sort(), f.originalTokenIds);
|
||||
resumed = f.advance(resumed, state => clean(state));
|
||||
resumed = act(resumed, 'guild', {type:'endMovement'});
|
||||
assert.ok(!resumed.movementRemaining?.includes('guild'));
|
||||
assert.deepEqual(inventory(resumed), f.originalCards);
|
||||
}
|
||||
});
|
||||
|
||||
for (const effect of ['choam', 'ixians'] as const) void test(
|
||||
`legacy Basic Discovery + Skills ${effect} Ambassador receipt without a reaction continues automatically`, () => {
|
||||
const f = fixture(effect), originalSpice = player(f.game, 'ecaz').spice;
|
||||
const pending = act(f.game, 'ecaz', {type:'decision', event:f.event, cards:f.chosen});
|
||||
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'ambassador');
|
||||
assert.ok(pending.pendingTreacheryDiscard?.reaction);
|
||||
// Model the serialized shape of an older already-committed receipt; the
|
||||
// actual discard, payout, token and continuation are left untouched.
|
||||
const legacy = structuredClone(pending);
|
||||
delete legacy.pendingTreacheryDiscard!.reaction;
|
||||
assert.equal(viewGame(legacy, 'guild').automaticContinuationPending, true);
|
||||
const resumed = applyAction(legacy, 'guild', {type:'advanceBots'});
|
||||
assert.equal(resumed.pendingTreacheryDiscard, null);
|
||||
assert.equal(resumed.pendingAmbassador, null);
|
||||
assert.equal(resumed.active, 'guild');
|
||||
assert.equal(player(resumed, 'guild').moved, 1);
|
||||
assert.equal(player(resumed, 'guild').forces[f.location], 1);
|
||||
assert.equal(player(resumed, 'ecaz').spice, originalSpice + (effect === 'choam' ? 6 : 0));
|
||||
assert.ok(player(resumed, 'richese').hand.some(card => card.id === SEMUTA_DRUG_ID));
|
||||
assert.ok(f.chosen.every(id => resumed.discard.filter(card => card.id === id).length === 1));
|
||||
if (effect === 'ixians') {
|
||||
assert.ok(player(resumed, 'ecaz').hand.some(card => card.id === f.game.deck[0].id));
|
||||
assert.deepEqual(resumed.deck.map(card => card.id), f.game.deck.slice(1).map(card => card.id));
|
||||
} else assert.deepEqual(resumed.deck, f.game.deck);
|
||||
assert.deepEqual(inventory(resumed), f.originalCards);
|
||||
assert.deepEqual(resumed.ecazAmbassadors!.tokens.map(row => row.id).sort(), f.originalTokenIds);
|
||||
const finished = act(f.advance(resumed, state => clean(state)), 'guild', {type:'endMovement'});
|
||||
assert.ok(!finished.movementRemaining?.includes('guild'));
|
||||
assert.deepEqual(inventory(finished), f.originalCards);
|
||||
});
|
||||
Reference in new issue
Block a user