import assert from 'node:assert/strict'; import test from 'node:test'; import { createElement } from 'react'; import { renderToStaticMarkup } from 'react-dom/server'; import { CoalitionNexus, coalitionNexusOptions } from '../components/coalition-nexus'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { coalitionOf, emptyCoalitionState } from '../game/coalition-alliance'; import { applyAction, joinGame, newPlayer, viewGame, type Game } from '../game/engine'; import { quoteVictory } from '../game/victory-quote'; import { advanceShortAlliance, shortAllianceCollection, shortAllianceLobby, type ShortAllianceStep } from './fixture-short-alliance-victory'; /** Real four-seat Basic deal, first turn, normal worm and Nexus; the empty * preview ledger and unopened worm order are the only staged inputs here. */ function nexus(): Game { let game = shortAllianceLobby(); joinGame(game, newPlayer('ha', 'Harkonnen', 'harkonnen')); for (const player of game.players) game = applyAction(game, player.id, {type:'ready'}); game = applyAction(game, 'at', {type:'start'}); const actions: ShortAllianceStep[] = []; game = advanceShortAlliance(game, state => state.status === 'playing' && state.turn === 2 && state.phase === 0, actions); const index = game.spiceDeck.findIndex(card => 'worm' in card && !card.greatMaker && !card.suppressed && !card.thumper); assert.ok(index >= 0); game.spiceDeck.unshift(game.spiceDeck.splice(index, 1)[0]); game = advanceShortAlliance(game, state => state.nexus && !state.spiceWindow && !state.spiceResolution && !state.response && !state.decision && !state.truthtrance && !state.phaseOpening, actions); assert.ok(game.players.every(player => !player.ally)); game.coalitionState = emptyCoalitionState('three-short'); game = applyAction(game, 'at', {type:'alliance', target:'em'}); return applyAction(game, 'em', {type:'alliance', target:'at'}); } function accept(game: Game, actor: string): Game { const offer = game.coalitionState!.proposal; assert.ok(offer); return applyAction(game, actor, {type:'coalitionAnswer', event:offer.event, nexus:offer.nexus, turn:offer.turn, accept:true, expectedRevision:game.coalitionState!.revision}); } void test('three-short pair invites a third with unanimous native consent and each bot can propose', () => { const game = nexus(); const pair = coalitionOf(game.players, game.coalitionState, 'at'); assert.ok(pair); assert.deepEqual(new Set(pair.members), new Set(['at','em'])); const controls = viewGame(game, 'gu').coalitionNexus; assert.ok(controls); assert.equal(controls.variant, 'three-short'); assert.deepEqual(coalitionNexusOptions(controls).invitations.map(row => row.entrant), ['gu']); const markup = renderToStaticMarkup(createElement(CoalitionNexus, {view:controls, onIntent(){}})); assert.match(markup, /up to three members and need three strongholds/); assert.match(markup, /Ask to join Atreides, Emperor/); for (const difficulty of DIFFICULTIES) { const view = viewGame(game, 'gu'); view.players.find(player => player.id === 'gu')!.bot = difficulty; const action = botActions(view).find(row => row.type === 'coalitionPropose'); assert.ok(action, difficulty); let pending = applyAction(game, 'gu', action); assert.deepEqual(pending.coalitionState!.proposal!.approvals, ['gu']); assert.equal(viewGame(pending, 'em').coalitionNexus!.proposal!.entrant, 'gu'); pending = accept(pending, 'at'); assert.deepEqual(new Set(coalitionOf(pending.players, pending.coalitionState, 'at')!.members), new Set(['at','em'])); pending = accept(pending, 'em'); assert.deepEqual(new Set(coalitionOf(pending.players, pending.coalitionState, 'at')!.members), new Set(['at','em','gu'])); const full = viewGame(pending, 'ha').coalitionNexus; assert.ok(full); assert.deepEqual(coalitionNexusOptions(full).invitations, []); const before = structuredClone(pending); assert.throws(() => applyAction(pending, 'ha', {type:'coalitionPropose', groupId:pair.id, entrant:'ha', expectedRevision:pending.coalitionState!.revision})); assert.deepEqual(pending, before); } }); void test('three-short pair and triple share the three-stronghold target and native win', () => { const pair = nexus(); const group = coalitionOf(pair.players, pair.coalitionState, 'at'); assert.ok(group); let triple = applyAction(pair, 'at', {type:'coalitionPropose', groupId:group.id, entrant:'gu', expectedRevision:pair.coalitionState!.revision}); triple = accept(triple, 'em'); triple = accept(triple, 'gu'); for (const [source, members] of [[pair, ['at','em']], [triple, ['at','em','gu']]] as const) { let closed = source; for (const player of source.players) { if (closed.nexus && !closed.ready.includes(player.id)) closed = applyAction(closed, player.id, {type:'ready'}); } closed = advanceShortAlliance(closed, state => !state.nexus && state.phase >= 2, []); // Preserve the actual alliance; only the Collection boundary and original // physical forces are staged by the common stronghold fixture. const game = shortAllianceCollection(closed); const row = viewGame(game, 'at').victoryProgress.find(progress => progress.player === 'at'); assert.ok(row); assert.equal(row.target, 3); assert.equal(row.qualifies, true); assert.equal(row.strongholds.length, 3); assert.deepEqual(new Set(quoteVictory(game).winner), new Set(members)); let current = game; for (const player of game.players) { if (current.status === 'finished') break; current = applyAction(current, player.id, {type:'ready'}); } assert.equal(current.status, 'finished'); assert.deepEqual(new Set(current.winner), new Set(members)); } });