Support mixed Discovery decks with Leader Skills
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
7c1f4b4e09
commit
77eb2392eb
15 files changed
+420
-11
No files matched your search
@@ -94,9 +94,11 @@ void test('Discovery cannot redeal started components or silently remove another
|
||||
for (const game of overlays) assert.throws(() => initializeDiscoveryGameForAudit(game), RuleError);
|
||||
});
|
||||
|
||||
void test('Discovery Skills cannot silently drop unsupported native families or unrelated modules and previews', () => {
|
||||
const unsupported = lobby(['ixians', 'ecaz', 'guild'], ['ix', 'ecaz']);
|
||||
assert.throws(() => initializeLeaderSkillsGameForAudit(unsupported), RuleError);
|
||||
void test('Discovery Skills retain selected native families and reject invalid phase modules', () => {
|
||||
const selected = lobby(['ixians', 'ecaz', 'guild'], ['ix', 'ecaz']);
|
||||
const started = initializeLeaderSkillsGameForAudit(selected);
|
||||
assert.ok(started.leaderSkills && started.discoveries);
|
||||
assert.deepEqual(started.players.map(player => player.faction), ['ixians', 'ecaz', 'guild']);
|
||||
const basicStrongholds = lobby(['guild', 'emperor', 'harkonnen'], []);
|
||||
basicStrongholds.advanced = false; basicStrongholds.strongholdCards = createStrongholdCards();
|
||||
assert.throws(() => initializeLeaderSkillsGameForAudit(basicStrongholds), RuleError);
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
||||
import type { FactionId } from '../game/catalog';
|
||||
import { withClassicDiscoveryNexusLottery } from './fixture-discovery-classic-nexus';
|
||||
import { nextPairedChoamSkillsStep } from './fixture-paired-choam-nexus-skills';
|
||||
import { actSelectedIndependentNexus, advanceSelectedIndependentNexus,
|
||||
type SelectedIndependentNexusStep } from './fixture-selected-independent-nexus-cards';
|
||||
|
||||
export type DiscoverySkillsOptions = {
|
||||
advanced: boolean;
|
||||
factions: readonly FactionId[];
|
||||
expansions?: Game['expansions'];
|
||||
};
|
||||
|
||||
/** Fresh public lobby, real original private training and native setup. Only
|
||||
* the original random lottery is fixed; no cards, leaders or forces are added. */
|
||||
export function selectedDiscoverySkillsLobby(options: DiscoverySkillsOptions) {
|
||||
const [host, ...others] = options.factions;
|
||||
const expansions = options.expansions ?? ['ecaz', 'ix', 'choam'];
|
||||
let game = withClassicDiscoveryNexusLottery(() => createGame('SELECTEDDISCOVERYSKILLS',
|
||||
newPlayer(host, host, host), options.advanced, expansions));
|
||||
for (const faction of others) joinGame(game, newPlayer(faction, faction, faction));
|
||||
const actions: SelectedIndependentNexusStep[] = [];
|
||||
const step = (actor: string, action: Action) => {
|
||||
game = actSelectedIndependentNexus(game, actor, action, actions);
|
||||
};
|
||||
if (expansions.includes('ix') && expansions.includes('choam'))
|
||||
step(host, {type:'moduleOption', module:'kullPreview', enabled:false});
|
||||
step(host, {type:'moduleOption', module:'leaderSkills', enabled:true});
|
||||
step(host, {type:'moduleOption', module:'discoveries', enabled:true});
|
||||
for (const player of game.players) step(player.id, {type:'ready'});
|
||||
return {game, host, step, actions};
|
||||
}
|
||||
|
||||
export function selectedDiscoverySkillsStart(options: DiscoverySkillsOptions) {
|
||||
const fixture = selectedDiscoverySkillsLobby(options);
|
||||
let game = fixture.game;
|
||||
const lobby = structuredClone(game);
|
||||
const start: Action = options.advanced ? {type:'start', advancedPreview:true} : {type:'start'};
|
||||
game = actSelectedIndependentNexus(game, fixture.host, start, fixture.actions);
|
||||
const setup = structuredClone(game);
|
||||
for (let count = 0; game.status === 'setup' && count < 300; count++) {
|
||||
const next = nextPairedChoamSkillsStep(game);
|
||||
assert.ok(next, `Original setup must expose a choice at ${game.setupStage ?? game.decision?.kind}`);
|
||||
game = actSelectedIndependentNexus(game, next.actor, next.action, fixture.actions);
|
||||
}
|
||||
assert.equal(game.status, 'playing');
|
||||
return {...fixture, game, lobby, setup};
|
||||
}
|
||||
|
||||
/** Reorder only the unopened original Spice Cards. The engine resolves the
|
||||
* Discovery and the following bidding and movement through ordinary actions. */
|
||||
export function advanceDiscoverySkillsBlow(game: Game, actions: SelectedIndependentNexusStep[]) {
|
||||
for (const position of game.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]];
|
||||
}
|
||||
return advanceSelectedIndependentNexus(game, state => state.turn === 1 && state.phase === 5 &&
|
||||
!state.phaseOpening && !state.response && !state.decision && !state.truthtrance &&
|
||||
!state.pendingTreacheryDiscard, actions);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
||||
import { placeAmbassador } from '../game/ecaz-ambassadors';
|
||||
import { placeTerror } from '../game/moritani-terror';
|
||||
import { actBasicSemutaAmbassador as act, basicSemutaAmbassadorClean as clean,
|
||||
basicSemutaAmbassadorPlayer as player, nextBasicSemutaAmbassadorStep } from './fixture-basic-semuta-ambassador';
|
||||
import { nextPairedChoamSkillsStep } from './fixture-paired-choam-nexus-skills';
|
||||
|
||||
export { act, clean, player };
|
||||
|
||||
/** Real public mixed-family setup and skills training. Only unopened Spice
|
||||
* order and original Ambassador/Terror placements are controlled. */
|
||||
export function selectedDiscoverySkillsOverlapFixture(advanced: boolean) {
|
||||
let game = createGame('DISCOVERYSKILLSOVERLAP', newPlayer('ecaz', 'Ecaz', 'ecaz'), advanced,
|
||||
['ecaz', 'ix', 'choam']);
|
||||
for (const faction of ['moritani', 'ixians', 'choam', 'guild'] as const)
|
||||
joinGame(game, newPlayer(faction, faction, faction));
|
||||
game = act(game, 'ecaz', {type:'moduleOption', module:'kullPreview', enabled:false});
|
||||
game = act(game, 'ecaz', {type:'moduleOption', module:'discoveries', enabled:true});
|
||||
game = act(game, 'ecaz', {type:'moduleOption', module:'leaderSkills', enabled:true});
|
||||
for (const seat of game.players) game = act(game, seat.id, {type:'ready'});
|
||||
const lobby = structuredClone(game);
|
||||
game = act(game, 'ecaz', {type:'start', ...(advanced ? {advancedPreview:true} : {})});
|
||||
const setup = structuredClone(game);
|
||||
const advance = (initial: Game, until: (state: Game) => boolean) => {
|
||||
let state = initial;
|
||||
for (let count = 0; count < 1800; count++) {
|
||||
if (until(state)) return state;
|
||||
assert.notEqual(state.status, 'finished');
|
||||
const step = state.status === 'setup' && state.setupStage === 'leaderSkills'
|
||||
? nextPairedChoamSkillsStep(state) : nextBasicSemutaAmbassadorStep(state);
|
||||
assert.ok(step, `No original continuation at ${state.status}/${state.phase}/${state.decision?.kind}`);
|
||||
state = act(state, step.actor, step.action);
|
||||
}
|
||||
throw Error('The mixed Discovery/Skills game did not reach its requested boundary');
|
||||
};
|
||||
game = advance(game, state => state.status === 'playing');
|
||||
assert.equal(game.leaderSkills?.assignments.length, game.players.length);
|
||||
assert.ok(game.discoveries && game.ecazAmbassadors && game.moritaniTerror);
|
||||
assert.ok(!game.nexusCards && !game.ecazTreachery);
|
||||
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 && state.active === 'guild' && clean(state));
|
||||
const beforeMarkers = structuredClone(game);
|
||||
const token = game.ecazAmbassadors!.tokens.find(row => row.effect === 'ecaz' && row.zone === 'supply');
|
||||
const terror = game.moritaniTerror!.tokens.find(row => row.kind === 'robbery' && row.status === 'available');
|
||||
assert.ok(token && terror);
|
||||
const destination = 'habbanya_ridge_sietch', sector = 17;
|
||||
game.ecazAmbassadors = placeAmbassador(game.ecazAmbassadors!, token.id, {
|
||||
turn:game.turn, availableSpice:player(game,'ecaz').spice,
|
||||
destination:{id:destination,stronghold:true,inStorm:false,allowed:true},
|
||||
}).state;
|
||||
game.moritaniTerror = placeTerror(game.moritaniTerror!, terror.id, destination, game.turn);
|
||||
const shipment: Action = {type:'ship', territory:destination, sector, amount:1, elite:0, allyPayment:0};
|
||||
return {game, lobby, setup, beforeMarkers, destination, sector, token:token.id,
|
||||
terror:terror.id, shipment, advance};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { act, player, selectedDiscoverySkillsOverlapFixture } from './selected-discovery-skills-overlap-fixture';
|
||||
|
||||
for (const advanced of [false, true]) void test(
|
||||
`${advanced ? 'Advanced' : 'Basic'} mixed Discovery and Skills retain the original Ecaz/Moritani arrival`, () => {
|
||||
const f = selectedDiscoverySkillsOverlapFixture(advanced);
|
||||
assert.equal(f.lobby.discoveryEnabled, true);
|
||||
assert.ok(f.setup.discoveries && f.setup.leaderSkills);
|
||||
assert.equal(f.game.leaderSkills!.assignments.length, f.game.players.length);
|
||||
assert.ok(f.beforeMarkers.spiceDiscard.flat().some(card =>
|
||||
'territory' in card && card.discovery === 'discovery-hagga-basin'));
|
||||
assert.equal(f.beforeMarkers.discoveries!.tokens.filter(token => token.status === 'placed').length, 1);
|
||||
assert.ok(!f.game.nexusCards && !f.game.ecazTreachery);
|
||||
const moritaniSpice = player(f.game, 'moritani').spice;
|
||||
const duke = structuredClone(f.game.dukeVidal!.leader);
|
||||
let game = act(f.game, 'guild', f.shipment);
|
||||
game = f.advance(game, state => !!state.pendingArrivalOverlap);
|
||||
const afterShipment = player(game, 'guild').spice;
|
||||
const stolen = Math.ceil(afterShipment / 2);
|
||||
assert.equal(player(game, 'guild').forces[`${f.destination}:${f.sector}`], 1);
|
||||
const first = game.pendingArrivalOverlap!.first;
|
||||
const responder = first === 'ambassador' ? 'ecaz' : 'moritani';
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, responder);
|
||||
view.players.find(row => row.id === responder)!.bot = difficulty;
|
||||
const choice = botActions(view)[0]; assert.ok(choice, difficulty);
|
||||
assert.doesNotThrow(() => act(game, responder, choice), difficulty);
|
||||
}
|
||||
const ambassador = () => {
|
||||
const entry = game.pendingAmbassador; assert.ok(entry);
|
||||
assert.equal(entry.token, f.token);
|
||||
game = act(game, 'ecaz', {type:'decision', event:entry.event, trigger:true, beneficiary:'ecaz', choice:'duke'});
|
||||
};
|
||||
const robbery = () => {
|
||||
assert.equal(game.pendingTerrorEntry?.token, f.terror);
|
||||
game = act(game, 'moritani', {type:'decision', reveal:true});
|
||||
game = act(game, 'moritani', {type:'decision', choice:'spice'});
|
||||
};
|
||||
if (first === 'ambassador') { ambassador(); robbery(); }
|
||||
else { robbery(); ambassador(); }
|
||||
assert.equal(game.pendingArrivalOverlap ?? null, null);
|
||||
assert.equal(game.pendingAmbassador ?? null, null);
|
||||
assert.equal(game.pendingTerrorEntry ?? null, null);
|
||||
assert.equal(player(game, 'guild').spice, afterShipment - stolen);
|
||||
assert.equal(player(game, 'moritani').spice, moritaniSpice + stolen);
|
||||
assert.equal(player(game, 'guild').forces[`${f.destination}:${f.sector}`], 1);
|
||||
assert.equal(game.moritaniTerror!.tokens.find(row => row.id === f.terror)?.status, 'removed');
|
||||
assert.equal(game.ecazAmbassadors!.tokens.find(row => row.id === f.token)?.zone, 'supply');
|
||||
assert.deepEqual([game.dukeVidal?.controller, game.dukeVidal?.source], ['ecaz', 'ecaz']);
|
||||
assert.deepEqual(game.dukeVidal?.leader, duke);
|
||||
assert.equal(game.leaderSkills!.assignments.length, game.players.length);
|
||||
},
|
||||
);
|
||||
@@ -0,0 +1,92 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { bureaucratPaymentModeSupported } from '../game/bureaucrat-payment';
|
||||
import { discoveryModeSupported, selectedDiscoveryLeaderSkillsProfile } from '../game/discovery-module-profile';
|
||||
import { selectedDiscoveryWithoutCardsProfile, type FactionModuleProfile } from '../game/faction-module-profile';
|
||||
import { advancedMoritaniLeaderSkillsProfile, basicMoritaniLeaderSkillsProfile,
|
||||
nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile,
|
||||
ordinaryLeaderSkillModeSupported, strongholdLeaderSkillsProfile } from '../game/leader-skill-profile';
|
||||
|
||||
const base: FactionModuleProfile = {
|
||||
advanced: false,
|
||||
expansions: ['ix', 'choam', 'ecaz'],
|
||||
players: [{faction:'atreides'}, {faction:'guild'}, {faction:'emperor'}],
|
||||
discoveryEnabled: true,
|
||||
leaderSkills: {},
|
||||
};
|
||||
|
||||
void test('selected Discovery and Skills use the shared registered roster without requiring native seats', () => {
|
||||
const rows: FactionModuleProfile[] = [
|
||||
base,
|
||||
{...base, advanced:true},
|
||||
{...base, expansions:['ecaz'], players:[{faction:'atreides'}, {faction:'guild'}]},
|
||||
{...base, expansions:['ix', 'ecaz'], players:[{faction:'ixians'}, {faction:'guild'}]},
|
||||
{...base, players:[{faction:'ixians'}, {faction:'ecaz'}, {faction:'moritani'}]},
|
||||
{...base, players:[{faction:'ixians'}, {faction:'ecaz'}, {faction:'guild'}],
|
||||
discoveries:{}, discoveryStash:{}, greatMaker:{}, ecazOccupyPreview:true},
|
||||
{...base, advanced:true, players:[{faction:'moritani'}, {faction:'atreides'}, {faction:'guild'}],
|
||||
moritaniAssassinatePreview:true},
|
||||
{...base, advanced:true, homeworlds:{}, techTokens:{}, strongholdCards:{}},
|
||||
{...base, advanced:true, players:[{faction:'harkonnen'}, {faction:'ixians'}]},
|
||||
];
|
||||
for (const row of rows) {
|
||||
const label = JSON.stringify(row);
|
||||
assert.equal(selectedDiscoveryWithoutCardsProfile(row), true, label);
|
||||
assert.equal(selectedDiscoveryLeaderSkillsProfile(row), true, label);
|
||||
assert.equal(discoveryModeSupported(row), true, label);
|
||||
assert.equal(nativeDiscoveryLeaderSkillsProfile(row), true, label);
|
||||
assert.equal(ordinaryLeaderSkillModeSupported(row), true, label);
|
||||
assert.equal(bureaucratPaymentModeSupported(row), true, label);
|
||||
}
|
||||
// The pre-deal lobby has not allocated the physical Skills deck yet.
|
||||
const lobby: FactionModuleProfile = {...base, leaderSkills:undefined};
|
||||
assert.equal(selectedDiscoveryLeaderSkillsProfile(lobby), true);
|
||||
assert.equal(strongholdLeaderSkillsProfile({...base, advanced:true, strongholdCards:{}}), true);
|
||||
});
|
||||
|
||||
void test('selected Discovery Skills preserve native Ecaz and Moritani participation', () => {
|
||||
const pair = {...base, players:[{faction:'ecaz'}, {faction:'moritani'}, {faction:'guild'}]};
|
||||
assert.equal(nativeEcazLeaderSkillsProfile(pair), true);
|
||||
assert.equal(basicMoritaniLeaderSkillsProfile(pair), true);
|
||||
assert.equal(advancedMoritaniLeaderSkillsProfile(pair), false);
|
||||
const advanced = {...pair, advanced:true, moritaniAssassinatePreview:true};
|
||||
assert.equal(nativeEcazLeaderSkillsProfile(advanced), true);
|
||||
assert.equal(advancedMoritaniLeaderSkillsProfile(advanced), true);
|
||||
});
|
||||
|
||||
void test('selected Discovery Skills reject illegal seats, decks, overlays and phase-module prerequisites', () => {
|
||||
const rejected: FactionModuleProfile[] = [
|
||||
{...base, discoveryEnabled:false},
|
||||
{...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:'choam'}, {faction:'atreides'}]},
|
||||
{...base, players:[{faction:'atreides'}, {faction:'guild'}], techTokens:{}},
|
||||
{...base, strongholdCards:{}},
|
||||
{...base, nexusCards:{}},
|
||||
{...base, ecazTreachery:true},
|
||||
{...base, semutaPreview:true},
|
||||
{...base, kullPreview:true},
|
||||
{...base, nexusKullPreview:true},
|
||||
{...base, guildBetrayalPreview:true},
|
||||
{...base, nexusIxianBetrayalPreview:true},
|
||||
{...base, moritaniAssassinatePreview:true},
|
||||
];
|
||||
for (const row of rejected) {
|
||||
const label = JSON.stringify(row);
|
||||
assert.equal(selectedDiscoveryWithoutCardsProfile(row), false, label);
|
||||
assert.equal(selectedDiscoveryLeaderSkillsProfile(row), false, label);
|
||||
}
|
||||
});
|
||||
|
||||
void test('Advanced trained Harkonnen with actual E3 native remains excluded', () => {
|
||||
for (const faction of ['ecaz', 'moritani']) {
|
||||
const players = [{faction:'harkonnen'}, {faction}];
|
||||
assert.equal(selectedDiscoveryLeaderSkillsProfile({...base, players}), true, faction);
|
||||
assert.equal(selectedDiscoveryLeaderSkillsProfile({...base, players, advanced:true}), false, faction);
|
||||
assert.equal(discoveryModeSupported({...base, players, advanced:true}), false, faction);
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,72 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { treacheryDeck } from '../game/cards';
|
||||
import { DISCOVERY_CARD_PLACEMENTS, validateDiscoveryState } from '../game/discoveries';
|
||||
import { validateLeaderSkills } from '../game/leader-skills';
|
||||
import { selectedDiscoverySkillsLobby, selectedDiscoverySkillsStart,
|
||||
advanceDiscoverySkillsBlow } from './selected-discovery-skills-fixture';
|
||||
|
||||
for (const advanced of [false, true]) for (const factions of [
|
||||
['atreides', 'guild', 'emperor'],
|
||||
['ixians', 'ecaz', 'guild'],
|
||||
['moritani', 'choam', 'guild'],
|
||||
['ecaz', 'moritani', 'guild'],
|
||||
] as const) void test(
|
||||
`public ${advanced ? 'Advanced' : 'Basic'} mixed Discovery/Skills: ${factions.join('/')}`, () => {
|
||||
const fixture = selectedDiscoverySkillsStart({advanced, factions});
|
||||
const game = fixture.game;
|
||||
assert.deepEqual(game.expansions, ['ecaz', 'ix', 'choam']);
|
||||
assert.equal(game.discoveryEnabled, true);
|
||||
assert.ok(game.discoveries);
|
||||
assert.equal(game.discoveries.tokens.length, 8);
|
||||
assert.ok(!game.nexusCards && !game.ecazTreachery);
|
||||
assert.equal(game.leaderSkills!.assignments.length, factions.length);
|
||||
assert.equal(fixture.actions.filter(step => step.action.type === 'leaderSkill').length, factions.length);
|
||||
assert.ok(fixture.actions.some(step => step.action.type === 'moduleOption' && step.action.module === 'discoveries'));
|
||||
validateLeaderSkills(game.leaderSkills!, game.players);
|
||||
validateDiscoveryState(game.discoveries!);
|
||||
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 (game.players.some(player => player.faction === 'ecaz')) {
|
||||
assert.equal(game.ecazOccupyPreview, true);
|
||||
assert.ok(game.ecazAmbassadors);
|
||||
}
|
||||
if (game.players.some(player => player.faction === 'moritani')) {
|
||||
assert.ok(game.moritaniTerror);
|
||||
if (advanced) assert.ok(game.moritaniAssassinate);
|
||||
}
|
||||
if (game.players.some(player => player.faction === 'ixians')) assert.ok(game.mobileStronghold);
|
||||
});
|
||||
|
||||
for (const advanced of [false, true]) void test(
|
||||
`${advanced ? 'Advanced' : 'Basic'} mixed Discovery/Skills reaches a real Discovery Spice Blow`, () => {
|
||||
const fixture = selectedDiscoverySkillsStart({advanced, factions:['ixians', 'ecaz', 'guild']});
|
||||
const game = advanceDiscoverySkillsBlow(structuredClone(fixture.game), fixture.actions);
|
||||
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!);
|
||||
validateLeaderSkills(game.leaderSkills!, game.players);
|
||||
assert.ok(!game.nexusCards && !game.ecazTreachery);
|
||||
assert.ok(game.active);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, game.active);
|
||||
view.players.find(player => player.id === game.active)!.bot = difficulty;
|
||||
const action = botActions(view)[0]; assert.ok(action, difficulty);
|
||||
assert.doesNotThrow(() => applyAction(game, game.active!, action), difficulty);
|
||||
}
|
||||
});
|
||||
|
||||
for (const faction of ['ecaz', 'moritani'] as const) void test(
|
||||
`Advanced mixed Discovery/Skills with Harkonnen and ${faction} stays guarded`, () => {
|
||||
const fixture = selectedDiscoverySkillsLobby({advanced:true, factions:[faction, 'harkonnen', 'ixians']});
|
||||
const before = structuredClone(fixture.game);
|
||||
assert.throws(() => applyAction(fixture.game, fixture.host, {type:'start', advancedPreview:true}));
|
||||
assert.deepEqual(fixture.game, before);
|
||||
});
|
||||
Reference in new issue
Block a user