import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, createGame, newPlayer, type Game } from '../game/engine'; import { territory } from '../game/board'; import { baseDeck } from '../game/cards'; import { createTechTokens } from '../game/tech-tokens'; import { quoteVictory } from '../game/victory-quote'; /** Controlled public board using five printed strongholds, one physically * placed mobile stronghold, and the original three-token stronghold bonus. */ function position(advanced = true): Game { const g = createGame('VICTORYORDER', newPlayer('ix', 'Ixians', 'ixians'), advanced); g.players.push(newPlayer('at', 'Atreides', 'atreides')); g.players.push(newPlayer('em', 'Emperor', 'emperor')); g.players.push(newPlayer('bg', 'Bene Gesserit', 'beneGesserit')); Object.assign(g, { status: 'playing', phase: 8, turn: 2, storm: 18, order: g.players.map(p => p.id), ready: [], phaseOpening: null, deck: baseDeck(), active: null, mobileStronghold: { location: 'red_chasm:7' }, techTokens: createTechTokens() }); for (const p of g.players) Object.assign(p, { hand: [], forces: {}, reserves: 20, spice: 20, traitors: [], traitorChoices: [] }); return g; } function holds(g: Game, id: string, ...names: string[]) { const player = g.players.find(p => p.id === id)!; player.forces = Object.fromEntries(names.map(name => [ `${name}:${territory(name).sectors[0]}`, 1, ])); } function tech(g: Game, id: string) { for (const token of Object.values(g.techTokens!)) token.owner = id; } function actual(g: Game) { let result = structuredClone(g); result.phase = 7; for (const p of result.players) result = applyAction(result, p.id, { type: 'ready' }); return result; } void test('Advanced awards the unallied higher qualified stronghold count, including a mobile and Tech stronghold', () => { const g = position(); holds(g, 'ix', 'arrakeen', 'carthag', 'sietch_tabr'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; holds(g, 'at', 'tueks_sietch', 'habbanya_ridge_sietch'); tech(g, 'at'); const before = structuredClone(g); assert.deepEqual(quoteVictory(g).winner, ['ix']); assert.deepEqual(actual(g).winner, ['ix']); assert.deepEqual(g, before); }); void test('equal qualified Advanced solo stronghold counts still share the win', () => { const g = position(); holds(g, 'ix', 'arrakeen', 'carthag'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; holds(g, 'at', 'sietch_tabr', 'tueks_sietch', 'habbanya_ridge_sietch'); assert.deepEqual(quoteVictory(g).winner, ['ix', 'at']); const offered = actual(g); assert.ok(offered.victoryContinuation); const settled = applyAction(offered, offered.victoryContinuation.voters[0], { type: 'victoryContinuation', event: offered.victoryContinuation.event, mode: 'finish', }); assert.deepEqual(settled.winner, ['ix', 'at']); }); void test('equally ranked Advanced solos may unanimously continue instead of sharing victory', () => { const g = position(); holds(g, 'ix', 'arrakeen', 'carthag'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; holds(g, 'at', 'sietch_tabr', 'tueks_sietch', 'habbanya_ridge_sietch'); let offered = actual(g); assert.deepEqual(offered.victoryContinuation?.voters, ['ix', 'at']); const event = offered.victoryContinuation!.event; offered = applyAction(offered, 'ix', { type: 'victoryContinuation', event, mode: 'continue', extra: 1 }); offered = applyAction(offered, 'at', { type: 'victoryContinuation', event, mode: 'accept' }); assert.equal(offered.status, 'playing'); assert.equal(offered.victoryExtraStrongholds, 1); assert.deepEqual(offered.winner, []); }); void test('the complete Tech set is one extra stronghold for comparing qualified solos', () => { const g = position(); holds(g, 'ix', 'arrakeen', 'carthag'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; holds(g, 'at', 'sietch_tabr', 'tueks_sietch', 'habbanya_ridge_sietch'); tech(g, 'at'); assert.deepEqual(quoteVictory(g).winner, ['at']); assert.deepEqual(actual(g).winner, ['at']); }); void test('Basic retains its existing shared-win policy for unequal qualified solos', () => { const g = position(false); holds(g, 'ix', 'arrakeen', 'carthag', 'sietch_tabr'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; holds(g, 'at', 'tueks_sietch', 'habbanya_ridge_sietch'); tech(g, 'at'); assert.deepEqual(quoteVictory(g).winner, ['ix', 'at']); const offered = actual(g); assert.ok(offered.victoryContinuation); const settled = applyAction(offered, offered.victoryContinuation.voters[0], { type: 'victoryContinuation', event: offered.victoryContinuation.event, mode: 'finish', }); assert.deepEqual(settled.winner, ['ix', 'at']); }); void test('a qualifying alliance and solo still share an Advanced win regardless of their totals', () => { const g = position(); g.players[1].ally = 'em'; g.players[2].ally = 'at'; holds(g, 'at', 'arrakeen', 'carthag'); holds(g, 'em', 'sietch_tabr'); tech(g, 'at'); holds(g, 'ix', 'tueks_sietch', 'habbanya_ridge_sietch'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; assert.deepEqual(quoteVictory(g).winner, ['ix', 'at', 'em']); const offered = actual(g); assert.ok(offered.victoryContinuation); const settled = applyAction(offered, offered.victoryContinuation.voters[0], { type: 'victoryContinuation', event: offered.victoryContinuation.event, mode: 'finish', }); assert.deepEqual(settled.winner, ['ix', 'at', 'em']); }); void test('Bene Gesserit prediction uses the actual higher solo winner, not the lower qualifier', () => { const g = position(); holds(g, 'ix', 'arrakeen', 'carthag', 'sietch_tabr'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; holds(g, 'at', 'tueks_sietch', 'habbanya_ridge_sietch'); tech(g, 'at'); g.players[3].prediction = { faction: 'atreides', turn: 2 }; assert.deepEqual(quoteVictory(g).winner, ['ix']); assert.deepEqual(actual(g).winner, ['ix']); g.players[3].prediction = { faction: 'ixians', turn: 2 }; assert.deepEqual(quoteVictory(g).winner, ['bg']); assert.deepEqual(actual(g).winner, ['bg']); });