Support unused selected decks with Nexus and Discovery
Verify and publish checkpoint container / container (push) Canceled after 0s
Verify and publish checkpoint container / container (push) Canceled after 0s
This commit is contained in:
1 parent
c8dbd44975
commit
91d45d1dec
14 files changed
+405
-17
No files matched your search
@@ -38,6 +38,14 @@ function originalLoss(game: Game): void {
|
||||
}
|
||||
function reviveOriginalE3Loss(game: Game): Game {
|
||||
game = advanceE3IndependentNexusCards(game, state => state.turn === 3 && state.phase === 4 && clean(state));
|
||||
// Advanced Revival now follows the native player order. Finish preceding
|
||||
// opportunities normally before making Moritani's own revival request.
|
||||
while (game.revivalOrder && game.revivalOrder.remaining[0] !== 'moritani') {
|
||||
assert.ok(game.revivalOrder.remaining.includes('moritani'));
|
||||
const actor = game.revivalOrder.remaining[0]; assert.ok(actor);
|
||||
game = actE3IndependentNexusCard(game, actor, { type: 'ready' });
|
||||
game = advanceE3IndependentNexusCards(game, state => state.phase === 4 && clean(state));
|
||||
}
|
||||
const spice = player(game, 'moritani').spice;
|
||||
assert.equal(player(game, 'moritani').tanks, 9);
|
||||
const play = recruitsPlayAction(viewGame(game, 'moritani').recruitsPreview); assert.ok(play);
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import type { FactionId } from '../game/catalog';
|
||||
import { withClassicDiscoveryNexusLottery } from './fixture-discovery-classic-nexus';
|
||||
import { advanceSelectedIndependentNexus, actSelectedIndependentNexus,
|
||||
selectedIndependentNexusClean } from './fixture-selected-independent-nexus-cards';
|
||||
|
||||
export interface SelectedNexusNoCardsOptions {
|
||||
factions: readonly FactionId[];
|
||||
advanced?: boolean;
|
||||
discovery?: boolean;
|
||||
}
|
||||
|
||||
/** Uses the public lobby controls and real private setup; the three selected
|
||||
* faction decks do not imply the separate Ecaz independent-card option. */
|
||||
export function createSelectedNexusNoCards(options: SelectedNexusNoCardsOptions) {
|
||||
const [host, ...others] = options.factions;
|
||||
let game = withClassicDiscoveryNexusLottery(() => createGame('SELECTEDNEXUSNOCARDS',
|
||||
newPlayer(host, host, host), options.advanced ?? false, ['ix', 'choam', 'ecaz']));
|
||||
for (const faction of others) joinGame(game, newPlayer(faction, faction, faction));
|
||||
const actions: { actor: string; action: Action }[] = [];
|
||||
const act = (actor: string, action: Action) => {
|
||||
actions.push({ actor, action: structuredClone(action) });
|
||||
game = withClassicDiscoveryNexusLottery(() => applyAction(game, actor, action));
|
||||
};
|
||||
act(host, { type: 'moduleOption', module: 'nexusCards', enabled: true });
|
||||
if (options.discovery) act(host, { type: 'moduleOption', module: 'discoveries', enabled: true });
|
||||
for (const player of game.players) act(player.id, { type: 'ready' });
|
||||
const lobby = structuredClone(game);
|
||||
act(host, options.advanced ? { type: 'start', advancedPreview: true } : { type: 'start' });
|
||||
const setup = structuredClone(game);
|
||||
for (let count = 0; game.status === 'setup' && count < 100; count++) {
|
||||
let next: { actor: string; action: Action } | undefined;
|
||||
for (const player of game.players) {
|
||||
const view = viewGame(game, player.id);
|
||||
view.players.find(seat => seat.id === player.id)!.bot = 'Easy';
|
||||
const action = botActions(view)[0];
|
||||
if (action) { next = { actor: player.id, action }; break; }
|
||||
}
|
||||
assert.ok(next, `Original setup must expose an owned choice at ${game.setupStage}`);
|
||||
act(next.actor, next.action);
|
||||
}
|
||||
assert.equal(game.status, 'playing');
|
||||
return { lobby, setup, game, actions };
|
||||
}
|
||||
|
||||
/** Real second-turn worm and closing Nexus. Reorder only undealt singleton
|
||||
* spice and Nexus cards; all hands and alliances result from player actions. */
|
||||
export function selectedNexusNoCardsDraw(advanced = false) {
|
||||
const fixture = createSelectedNexusNoCards({ factions: ['atreides', 'guild', 'harkonnen', 'emperor'], advanced });
|
||||
let game = structuredClone(fixture.game);
|
||||
const take = (worm: boolean) => {
|
||||
const index = game.spiceDeck.findIndex(card => worm
|
||||
? 'worm' in card && !card.greatMaker && !card.thumper && !card.suppressed
|
||||
: 'territory' in card && !card.discovery);
|
||||
assert.ok(index >= 0);
|
||||
return game.spiceDeck.splice(index, 1)[0];
|
||||
};
|
||||
const first = [take(false), ...(advanced ? [take(false)] : [])];
|
||||
const second = [take(true), take(false), ...(advanced ? [take(false)] : [])];
|
||||
game.spiceDeck.unshift(...first, ...second);
|
||||
game = advanceSelectedIndependentNexus(game, state => state.turn === 2 && !!state.nexus &&
|
||||
!state.spiceWindow && !state.spiceResolution && selectedIndependentNexusClean(state), fixture.actions);
|
||||
game = actSelectedIndependentNexus(game, 'guild', { type: 'alliance', target: 'harkonnen' }, fixture.actions);
|
||||
game = actSelectedIndependentNexus(game, 'harkonnen', { type: 'alliance', target: 'guild' }, fixture.actions);
|
||||
game = advanceSelectedIndependentNexus(game, state => state.nexusCards?.phase?.stage === 'drawing' &&
|
||||
selectedIndependentNexusClean(state), fixture.actions);
|
||||
const cards = game.nexusCards!.cards!, index = cards.deck.indexOf('richese');
|
||||
assert.ok(index >= 0);
|
||||
cards.deck.unshift(cards.deck.splice(index, 1)[0]);
|
||||
const beforeDraw = structuredClone(game);
|
||||
const draw: Action = { type: 'nexusCardChoice', turn: game.turn, card: cards.hands.atreides,
|
||||
choice: 'draw', ownRedraws: 0 };
|
||||
game = actSelectedIndependentNexus(game, 'atreides', draw, fixture.actions);
|
||||
const afterDraw = structuredClone(game);
|
||||
game = advanceSelectedIndependentNexus(game, state => state.phase === 5 && state.active === 'atreides' &&
|
||||
selectedIndependentNexusClean(state), fixture.actions);
|
||||
return { ...fixture, beforeDraw, draw, afterDraw, game };
|
||||
}
|
||||
@@ -399,7 +399,15 @@ for (const fuelKind of ['special', 'shield'] as const) void test(
|
||||
assert.equal(g.lastBattleContext!.winner, choam);
|
||||
assert.deepEqual(combatArmy(g, choam, 'homeworld:guild'), { normal: 2, elite: 0 });
|
||||
assert.equal(player(g, choam).tanks, player(before, choam).tanks + 1);
|
||||
assert.equal(player(g, choam).spice, player(before, choam).spice - 2 + 1 + collectionIncome(g, choam));
|
||||
// Finishing the battle can also open Collection: CHOAM's sole survivor
|
||||
// earns Junction's separate occupied-World bank award.
|
||||
const occupied = g.homeworldOccupiedIncomeHistory!.slice(before.homeworldOccupiedIncomeHistory!.length);
|
||||
assert.equal(occupied.length, 1);
|
||||
const junction = occupied[0].receipts.find(receipt => receipt.world === 'homeworld:guild');
|
||||
assert.ok(junction?.status === 'settled');
|
||||
assert.deepEqual(junction.credits, [{ player: choam, amount: 2 }]);
|
||||
assert.equal(player(g, choam).spice, player(before, choam).spice - 2 + 1 +
|
||||
collectionIncome(g, choam) + junction.credits[0].amount);
|
||||
for (const token of tokens) assert.equal(g.techTokens![token].owner, choam);
|
||||
inventory(g);
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { nexusCoreProfile, selectedNexusWithoutCardsProfile,
|
||||
type FactionModuleProfile } from '../game/faction-module-profile';
|
||||
import { discoveryModeSupported } from '../game/discovery-module-profile';
|
||||
import { createSelectedNexusNoCards } from './fixture-selected-nexus-no-cards';
|
||||
|
||||
const base: FactionModuleProfile = {
|
||||
advanced: false,
|
||||
expansions: ['ix', 'choam', 'ecaz'],
|
||||
players: [{ faction: 'atreides' }, { faction: 'guild' }, { faction: 'emperor' }],
|
||||
nexusCards: true,
|
||||
};
|
||||
|
||||
void test('selected Nexus admits independently selected decks with some or all natives absent', () => {
|
||||
const configurations: FactionModuleProfile[] = [
|
||||
base,
|
||||
{ ...base, advanced: true },
|
||||
{ ...base, expansions: ['ix'], players: [{ faction: 'atreides' }, { faction: 'guild' }] },
|
||||
{ ...base, expansions: ['choam', 'ecaz'], players: [{ faction: 'richese' }, { faction: 'guild' }] },
|
||||
{ ...base, players: [{ faction: 'ixians' }, { faction: 'ecaz' }, { faction: 'guild' }] },
|
||||
{ ...base, players: [{ faction: 'ixians' }, { faction: 'ecaz' }, { faction: 'guild' }],
|
||||
nexusIxianBetrayalPreview: true, ecazOccupyPreview: true },
|
||||
{ ...base, advanced: true, players: [{ faction: 'moritani' }, { faction: 'atreides' }],
|
||||
moritaniAssassinatePreview: true },
|
||||
{ ...base, advanced: true, discoveryEnabled: true, homeworlds: {}, techTokens: {}, strongholdCards: {} },
|
||||
];
|
||||
for (const configuration of configurations) {
|
||||
assert.equal(selectedNexusWithoutCardsProfile(configuration), true, JSON.stringify(configuration));
|
||||
assert.equal(nexusCoreProfile(configuration), true, JSON.stringify(configuration));
|
||||
}
|
||||
// No missing native family belongs to the existing selected-native paths.
|
||||
assert.equal(selectedNexusWithoutCardsProfile({ ...base, players: [
|
||||
{ faction: 'ixians' }, { faction: 'choam' }, { faction: 'ecaz' },
|
||||
] }), false);
|
||||
});
|
||||
|
||||
void test('selected Nexus rejects unrelated modules and illegal deck or seat configurations', () => {
|
||||
const rejected = [
|
||||
{ ...base, nexusCards: undefined },
|
||||
{ ...base, leaderSkills: {} },
|
||||
{ ...base, ecazTreachery: true },
|
||||
{ ...base, expansions: [] },
|
||||
{ ...base, expansions: ['ix', 'ix'] },
|
||||
{ ...base, expansions: ['ix', 'unknown'] },
|
||||
{ ...base, players: [{ faction: 'atreides' }] },
|
||||
{ ...base, players: [{ faction: 'atreides' }, { faction: 'atreides' }] },
|
||||
{ ...base, players: [{ faction: 'atreides' }, { faction: 'unknown' }] },
|
||||
{ ...base, expansions: ['ix'], players: [{ faction: 'atreides' }, { faction: 'choam' }] },
|
||||
{ ...base, players: [{ faction: 'atreides' }, { faction: 'guild' }], techTokens: {} },
|
||||
{ ...base, strongholdCards: {} },
|
||||
{ ...base, semutaPreview: true },
|
||||
{ ...base, kullPreview: true },
|
||||
{ ...base, nexusKullPreview: true },
|
||||
{ ...base, nexusIxianBetrayalPreview: true },
|
||||
{ ...base, moritaniAssassinatePreview: true },
|
||||
];
|
||||
for (const configuration of rejected)
|
||||
assert.equal(selectedNexusWithoutCardsProfile(configuration), false, JSON.stringify(configuration));
|
||||
});
|
||||
|
||||
void test('Discovery requires its explicit enabled selection, even with selected-deck Nexus', () => {
|
||||
for (const disabled of [undefined, false]) {
|
||||
const configuration = { ...base, discoveryEnabled: disabled };
|
||||
assert.equal(selectedNexusWithoutCardsProfile(configuration), true);
|
||||
assert.equal(discoveryModeSupported(configuration), false);
|
||||
assert.equal(selectedNexusWithoutCardsProfile({ ...configuration, discoveries: {} }), false);
|
||||
assert.equal(discoveryModeSupported({ ...configuration, discoveries: {} }), false);
|
||||
}
|
||||
const enabled = { ...base, discoveryEnabled: true };
|
||||
assert.equal(selectedNexusWithoutCardsProfile(enabled), true);
|
||||
assert.equal(discoveryModeSupported(enabled), true);
|
||||
assert.equal(discoveryModeSupported({ ...enabled, discoveries: {}, discoveryStash: {}, greatMaker: {} }), true,
|
||||
'Live Discovery objects do not erase an explicitly selected module');
|
||||
assert.equal(discoveryModeSupported({ ...enabled, leaderSkills: {} }), false);
|
||||
});
|
||||
|
||||
void test('Advanced Harkonnen with either E3 native stays on its existing excluded boundary', () => {
|
||||
for (const faction of ['ecaz', 'moritani']) {
|
||||
const players = [{ faction: 'harkonnen' }, { faction }];
|
||||
const basic = { ...base, players, advanced: false };
|
||||
const advanced = { ...base, players, advanced: true };
|
||||
assert.equal(selectedNexusWithoutCardsProfile(basic), true, faction);
|
||||
assert.equal(selectedNexusWithoutCardsProfile(advanced), false, faction);
|
||||
assert.equal(selectedNexusWithoutCardsProfile({ ...advanced, discoveryEnabled: true }), false, faction);
|
||||
assert.equal(discoveryModeSupported({ ...advanced, discoveryEnabled: true }), false, faction);
|
||||
assert.equal(selectedNexusWithoutCardsProfile({ ...basic, leaderSkills: {} }), false, faction);
|
||||
}
|
||||
assert.equal(selectedNexusWithoutCardsProfile({ ...base, advanced: true,
|
||||
players: [{ faction: 'harkonnen' }, { faction: 'ixians' }] }), true,
|
||||
'Selecting the unused E3 deck alone does not create an E3 native capture');
|
||||
});
|
||||
|
||||
void test('public Advanced selected Nexus initializes native Moritani assassination with unused Ix and CHOAM', () => {
|
||||
const { setup, game } = createSelectedNexusNoCards({ advanced: true,
|
||||
factions: ['moritani', 'atreides', 'guild'] });
|
||||
assert.equal(selectedNexusWithoutCardsProfile(setup), true);
|
||||
assert.equal(selectedNexusWithoutCardsProfile(game), true);
|
||||
assert.equal(game.status, 'playing');
|
||||
assert.ok(game.moritaniAssassinate, 'The public start initializes the native Advanced faction state');
|
||||
assert.ok(game.nexusCards?.cards);
|
||||
});
|
||||
@@ -0,0 +1,65 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { TERRITORIES } from '../game/board';
|
||||
import { treacheryDeck } from '../game/cards';
|
||||
import { nexusCoreProfile, selectedNexusWithoutCardsProfile } from '../game/faction-module-profile';
|
||||
import { validateNexusCards } from '../game/nexus-cards';
|
||||
import { validateDiscoveryState } from '../game/discoveries';
|
||||
import { advanceSelectedIndependentNexus } from './fixture-selected-independent-nexus-cards';
|
||||
import { createSelectedNexusNoCards, selectedNexusNoCardsDraw,
|
||||
type SelectedNexusNoCardsOptions } from './fixture-selected-nexus-no-cards';
|
||||
|
||||
const setups: SelectedNexusNoCardsOptions[] = [
|
||||
{ factions: ['atreides', 'guild', 'emperor'] as const },
|
||||
{ factions: ['atreides', 'guild', 'emperor'] as const, advanced: true },
|
||||
{ factions: ['ixians', 'ecaz', 'guild'] as const },
|
||||
{ factions: ['ixians', 'ecaz', 'guild'] as const, advanced: true, discovery: true },
|
||||
];
|
||||
for (const options of setups) void test(`public ${options.advanced ? 'Advanced' : 'Basic'} selected Nexus without independent cards: ${options.factions.join('/')}${options.discovery ? ' Discovery' : ''}`, () => {
|
||||
const { lobby, setup, game, actions } = createSelectedNexusNoCards(options);
|
||||
assert.ok(actions.some(step => step.action.type === 'moduleOption' && step.action.module === 'nexusCards'));
|
||||
assert.ok(!actions.some(step => step.action.type === 'moduleOption' && step.action.module === 'ecazTreachery'));
|
||||
assert.notEqual(lobby.lobbyOptions?.ecazTreachery, true);
|
||||
assert.ok(selectedNexusWithoutCardsProfile(setup));
|
||||
assert.ok(selectedNexusWithoutCardsProfile(game));
|
||||
assert.ok(nexusCoreProfile(game));
|
||||
assert.notEqual(game.ecazTreachery, true);
|
||||
assert.equal(game.leaderSkills, undefined);
|
||||
assert.deepEqual(game.expansions, ['ix', 'choam', 'ecaz']);
|
||||
assert.equal(game.phase, 0);
|
||||
validateNexusCards(game.nexusCards!.cards!, game.players);
|
||||
assert.equal(game.nexusCards!.cards!.deck.length, 12);
|
||||
assert.ok(Object.values(game.nexusCards!.cards!.hands).every(card => card === null));
|
||||
assert.deepEqual([...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand)]
|
||||
.map(card => card.id).sort(), treacheryDeck(game.expansions).map(card => card.id).sort());
|
||||
if (options.discovery) { assert.ok(game.discoveries); validateDiscoveryState(game.discoveries); }
|
||||
if (options.factions.includes('ixians')) assert.equal(game.nexusIxianBetrayalPreview, true);
|
||||
if (options.factions.includes('ecaz')) assert.equal(game.ecazOccupyPreview, true);
|
||||
});
|
||||
|
||||
for (const advanced of [false, true]) void test(`${advanced ? 'Advanced' : 'Basic'} selected decks deal a real Nexus card and spend its shipment power`, () => {
|
||||
const fixture = selectedNexusNoCardsDraw(advanced);
|
||||
const { beforeDraw, afterDraw } = fixture;
|
||||
assert.equal(beforeDraw.nexusCards!.phase!.stage, 'drawing');
|
||||
assert.equal(beforeDraw.nexusCards!.cards!.hands.atreides, null);
|
||||
const botView = viewGame(beforeDraw, 'atreides');
|
||||
botView.players.find(player => player.id === 'atreides')!.bot = 'Easy';
|
||||
const botChoice = botActions(botView).find(action => action.type === 'nexusCardChoice');
|
||||
assert.ok(botChoice, 'The real eligible player has a legal bot choice');
|
||||
assert.doesNotThrow(() => applyAction(beforeDraw, 'atreides', botChoice));
|
||||
assert.equal(afterDraw.nexusCards!.cards!.hands.atreides, 'richese');
|
||||
const offer = viewGame(fixture.game, 'atreides').nexusRichese;
|
||||
assert.ok(offer && !offer.blocked);
|
||||
const place = TERRITORIES.find(territory => territory.type === 'stronghold' &&
|
||||
!territory.sectors.includes(fixture.game.storm) && fixture.game.players.every(player =>
|
||||
Object.keys(player.forces).every(key => !key.startsWith(`${territory.id}:`))));
|
||||
assert.ok(place);
|
||||
let game = applyAction(fixture.game, 'atreides', { type: 'ship', territory: place.id,
|
||||
sector: place.sectors[0], amount: 1, elite: 0, allyPayment: 0, nexus: offer.event });
|
||||
game = advanceSelectedIndependentNexus(game, state => state.nexusRicheseLast?.stage === 'shipped');
|
||||
assert.equal(game.nexusCards!.cards!.hands.atreides, null);
|
||||
assert.equal(game.nexusCards!.cards!.discard.filter(card => card === 'richese').length, 1);
|
||||
assert.equal(game.players.find(player => player.id === 'atreides')!.forces[`${place.id}:${place.sectors[0]}`], 1);
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { spiceDeck, treacheryDeck } from '../game/cards';
|
||||
import { DISCOVERY_CARD_PLACEMENTS, DISCOVERY_SPICE_CARDS, validateDiscoveryState } from '../game/discoveries';
|
||||
import { actBasicSemutaAmbassador as act, basicSemutaAmbassadorClean } from './fixture-basic-semuta-ambassador';
|
||||
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
||||
|
||||
function advance(game: Game, until: (state: Game) => boolean): Game {
|
||||
for (let count = 0; count < 1600; count++) {
|
||||
if (until(game)) return game;
|
||||
assert.notEqual(game.status, 'finished');
|
||||
const step = nextRicheseStrongholdsNativeStep(game);
|
||||
assert.ok(step, `No native continuation at ${game.status}/${game.turn}/${game.phase}`);
|
||||
game = act(game, step.actor, step.action);
|
||||
}
|
||||
throw Error('The original public game did not reach the requested boundary');
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) void test(
|
||||
`${advanced ? 'Advanced' : 'Basic'} classic seats with only E3 selected resolve an original Discovery Spice Blow`, () => {
|
||||
let game = createGame('UNUSEDE3DISCOVERY', newPlayer('guild', 'Guild', 'guild'), advanced, ['ecaz']);
|
||||
for (const faction of ['atreides', 'emperor'] as const)
|
||||
joinGame(game, newPlayer(faction, faction, faction));
|
||||
game = act(game, 'guild', {type:'moduleOption', module:'discoveries', enabled:true});
|
||||
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
|
||||
assert.equal(game.discoveryEnabled, true);
|
||||
game = act(game, 'guild', {type:'start', ...(advanced ? {advancedPreview:true} : {})});
|
||||
assert.equal(game.status, 'setup');
|
||||
assert.equal(game.advanced, advanced);
|
||||
assert.deepEqual(game.expansions, ['ecaz']);
|
||||
assert.deepEqual(game.players.map(p => p.faction).sort(), ['atreides', 'emperor', 'guild']);
|
||||
assert.ok(game.discoveries);
|
||||
assert.equal(game.discoveries.tokens.length, 8);
|
||||
assert.deepEqual(game.deck.map(c => c.id).sort(), treacheryDeck(['ecaz']).map(c => c.id).sort());
|
||||
assert.equal(game.spiceDeck.length, spiceDeck().length + 7);
|
||||
assert.deepEqual(game.spiceDeck.flatMap(c => 'territory' in c && c.discovery ? [c.discovery] : []).sort(),
|
||||
DISCOVERY_SPICE_CARDS.map(c => c.discovery).sort());
|
||||
assert.ok(!game.nexusCards && !game.ecazTreachery && !game.leaderSkills);
|
||||
assert.ok(!game.ecazAmbassadors && !game.moritaniTerror);
|
||||
validateDiscoveryState(game.discoveries);
|
||||
|
||||
game = advance(game, state => state.status === 'playing');
|
||||
// Reorder original unopened cards only; all draws, token selection and phase
|
||||
// transitions still pass through the native engine.
|
||||
for (const position of advanced ? [0, 1] : [0]) {
|
||||
const index = game.spiceDeck.findIndex((card, i) => i >= position && 'territory' in card &&
|
||||
(position === 0 ? card.discovery === 'discovery-hagga-basin' : !card.discovery));
|
||||
assert.ok(index >= position);
|
||||
[game.spiceDeck[position], game.spiceDeck[index]] = [game.spiceDeck[index], game.spiceDeck[position]];
|
||||
}
|
||||
game = advance(game, state => state.turn === 1 && state.phase === 5 && basicSemutaAmbassadorClean(state));
|
||||
assert.ok(game.spiceDiscard.flat().some(card => 'territory' in card && card.discovery === 'discovery-hagga-basin'));
|
||||
const placed = game.discoveries!.tokens.filter(token => token.status === 'placed');
|
||||
assert.equal(placed.length, 1);
|
||||
const source = DISCOVERY_CARD_PLACEMENTS['discovery-hagga-basin'];
|
||||
assert.deepEqual([placed[0].type, placed[0].territory, placed[0].sector],
|
||||
[source.type, source.territory, source.sector]);
|
||||
validateDiscoveryState(game.discoveries!);
|
||||
assert.ok(!game.nexusCards && !game.ecazTreachery);
|
||||
assert.ok(game.active);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, game.active);
|
||||
view.players.find(p => p.id === game.active)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.ok(action, difficulty);
|
||||
assert.doesNotThrow(() => act(game, game.active!, action), difficulty);
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user