import assert from 'node:assert/strict'; import test from 'node:test'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { coalitionOf } from '../game/coalition-alliance'; import { applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { advanceShortAlliance, shortAllianceLobby, type ShortAllianceStep } from './fixture-short-alliance-victory'; const apply = (game: Game, actor: string, action: Action) => applyAction(game, actor, action); function lobby(): Game { const game = shortAllianceLobby(); joinGame(game, newPlayer('ha', 'Harkonnen', 'harkonnen')); return game; } function ready(game: Game): Game { for (const player of game.players) if (!player.ready) game = apply(game, player.id, {type:'ready'}); return game; } function answer(game: Game, actor: string): Game { const proposal = game.coalitionState?.proposal; assert.ok(proposal); return apply(game, actor, {type:'coalitionAnswer', event:proposal.event, nexus:proposal.nexus, turn:proposal.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } void test('host selects a group preview, clearing readiness, and can restore ordinary rules', () => { let game = ready(lobby()); assert.equal(viewGame(game, 'at').coalitionPreview, 'off'); const original = structuredClone(game); assert.throws(() => apply(game,'em',{type:'coalitionPreview',variant:'three'})); assert.deepEqual(game, original); assert.throws(() => apply(game,'at',{type:'coalitionPreview',variant:'bad'})); assert.throws(() => apply(game,'at',{type:'coalitionPreview',variant:'three', extra:true})); game = apply(game,'at',{type:'coalitionPreview',variant:'three'}); assert.equal(viewGame(game,'em').coalitionPreview,'three'); assert.ok(game.players.every(player => !player.ready)); game = apply(ready(game),'at',{type:'coalitionPreview',variant:'off'}); assert.equal(viewGame(game,'em').coalitionPreview,'off'); assert.ok(game.players.every(player => !player.ready)); game = apply(ready(game),'at',{type:'start'}); assert.equal(game.coalitionState, undefined); }); void test('each selected Basic group variant starts a genuine game with the intended fresh ledger', () => { for (const variant of ['three','three-short','unlimited'] as const) { let game = apply(lobby(),'at',{type:'coalitionPreview',variant}); game = apply(ready(game),'at',{type:'start'}); assert.ok(game.coalitionState); assert.equal(game.coalitionState.variant, variant === 'three' ? undefined : variant); assert.deepEqual(game.coalitionState.groups, []); assert.equal(game.coalitionState.proposal, null); assert.equal(viewGame(game,'em').coalitionPreview,variant); } }); void test('group preview refuses incompatible rules and optional alliance settings at start', () => { let advanced = apply(lobby(),'at',{type:'coalitionPreview',variant:'three'}); advanced = apply(advanced,'at',{type:'rules',advanced:true}); advanced = ready(advanced); assert.throws(() => apply(advanced,'at',{type:'start'}), /Advanced.*preview/i); let shortened = apply(lobby(),'at',{type:'coalitionPreview',variant:'three'}); shortened = apply(shortened,'at',{type:'shortAllianceVictory',enabled:true}); shortened = ready(shortened); assert.throws(() => apply(shortened,'at',{type:'start'}), /group|alliance/i); for (const action of [{type:'homeworlds',enabled:true}, {type:'techTokens',enabled:true}, {type:'moduleOption',module:'leaderSkills',enabled:true}] as const) { let configured = apply(lobby(),'at',{type:'coalitionPreview',variant:'three'}); configured = apply(configured,'at',action); configured = ready(configured); assert.throws(() => apply(configured,'at',{type:'start'}), /group|alliance/i); } let expansion = createGame('GROUPEXP',newPlayer('at','Atreides','atreides'),false,['ix']); joinGame(expansion,newPlayer('em','Emperor','emperor')); joinGame(expansion,newPlayer('gu','Guild','guild')); expansion = apply(expansion,'at',{type:'coalitionPreview',variant:'three'}); expansion = ready(expansion); assert.throws(() => apply(expansion,'at',{type:'start'}), /group|alliance/i); let twoSeats = createGame('GROUP2',newPlayer('at','Atreides','atreides')); joinGame(twoSeats,newPlayer('em','Emperor','emperor')); twoSeats = apply(twoSeats,'at',{type:'coalitionPreview',variant:'three'}); twoSeats = ready(twoSeats); assert.throws(() => apply(twoSeats,'at',{type:'start'}), /three|3|group/i); }); /** Real deal and setup, native first-turn actions, and actual Nexus negotiation. * Only an unopened original worm card is moved to the top of the Spice deck. */ function groupNexus(): Game { let game = apply(lobby(),'at',{type:'coalitionPreview',variant:'three-short'}); game = apply(ready(game),'at',{type:'start'}); const actions: ShortAllianceStep[] = []; game = advanceShortAlliance(game, state => state.status === 'playing' && state.turn === 2 && state.phase === 0, actions); const worm = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper); assert.ok(worm >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(worm, 1)[0]); game = advanceShortAlliance(game, state => state.nexus && !state.spiceWindow && !state.spiceResolution && !state.response && !state.decision && !state.truthtrance && !state.phaseOpening, actions); game = apply(game,'at',{type:'alliance',target:'em'}); return apply(game,'em',{type:'alliance',target:'at'}); } void test('an actual started preview reaches group formation, unanimous consent and legal bot proposals', () => { const original = groupNexus(); const pair = coalitionOf(original.players,original.coalitionState,'at'); assert.ok(pair); assert.deepEqual(new Set(pair.members),new Set(['at','em'])); let formed: Game | undefined; for (const difficulty of DIFFICULTIES) { const view = viewGame(original,'gu'); view.players.find(player => player.id === 'gu')!.bot = difficulty; const proposal = botActions(view).find(action => action.type === 'coalitionPropose'); assert.ok(proposal,difficulty); let game = apply(original,'gu',proposal); game = answer(game,'at'); game = answer(game,'em'); assert.deepEqual(new Set(coalitionOf(game.players,game.coalitionState,'at')!.members),new Set(['at','em','gu'])); assert.equal(game.coalitionState!.proposal,null); const progress = viewGame(game,'at').victoryProgress.find(row => row.player === 'at'); assert.ok(progress); assert.equal(progress.target,3); formed = game; } assert.ok(formed); // Once all original seats finish Nexus, use native controls for the rest of // the turn. The next Storm opens before another Nexus can change alliances. for (const player of formed.players) if (formed.nexus && !formed.ready.includes(player.id)) formed = apply(formed,player.id,{type:'ready'}); const continued = advanceShortAlliance(formed, state => state.status === 'playing' && state.turn === 3 && state.phase === 0, []); assert.equal(continued.turn,3); assert.deepEqual(new Set(coalitionOf(continued.players,continued.coalitionState,'at')!.members),new Set(['at','em','gu'])); });