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); } });