import assert from 'node:assert/strict'; import test from 'node:test'; import { applyAction, createGame, newPlayer, viewGame, type Action, type Game } from '../game/engine'; import { territory } from '../game/board'; import { baseDeck } from '../game/cards'; import { botActions } from '../game/bots'; import { DIFFICULTIES } from '../game/bot-profiles'; import { createTechTokens } from '../game/tech-tokens'; /** Controlled late-game board: original public strongholds and Tech tokens are * placed directly to reach the rare simultaneous qualified result. Actions * from Collection to Mentat, each vote, and the subsequent phase are native. * This is not a naturally completed game or saved-room/deployment proof. */ function position(options: { advanced?: boolean; turn?: number; choam?: boolean; onlyAlliance?: boolean; prediction?: boolean } = {}): Game { const { advanced = true, turn = 2, choam = false, onlyAlliance = false, prediction = false } = options; const g = createGame('VICTORYVOTE', newPlayer('ix', 'Ixians', 'ixians'), advanced, choam ? ['choam'] : []); g.players.push(newPlayer('at', 'Atreides', 'atreides')); g.players.push(newPlayer('em', 'Emperor', 'emperor')); g.players.push(newPlayer('bg', 'Bene Gesserit', 'beneGesserit')); if (choam) g.players.push(newPlayer('ch', 'CHOAM', 'choam')); Object.assign(g, { status: 'playing', phase: 7, turn, 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: [] }); const holds = (id: string, ...names: string[]) => { const p = g.players.find(row => row.id === id)!; p.forces = Object.fromEntries(names.map(name => [`${name}:${territory(name).sectors[0]}`, 1])); p.reserves -= names.length; }; g.players[1].ally = 'em'; g.players[2].ally = 'at'; holds('at', 'arrakeen', 'carthag'); holds('em', 'sietch_tabr'); for (const token of Object.values(g.techTokens!)) token.owner = 'at'; if (!onlyAlliance) { holds('ix', 'tueks_sietch', 'habbanya_ridge_sietch'); g.players[0].forces['hidden_mobile_stronghold:0'] = 1; g.players[0].reserves -= 1; } if (prediction) g.players[3].prediction = { faction: 'atreides', turn }; return g; } function reachMentat(game: Game): Game { let g = structuredClone(game); for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' }); if (g.phase === 7 && g.decision?.kind === 'choamMarket') g = applyAction(g, g.decision.player, { type: 'decision', done: true }); return g; } function finishMentat(game: Game): Game { let g = game; for (const p of g.players) g = applyAction(g, p.id, { type: 'ready' }); return g; } function offer(game: Game): NonNullable { const pending = game.victoryContinuation; assert.ok(pending, 'The Mentat victory check offers a public agreement'); return pending; } function act(game: Game, player: string, mode: 'finish' | 'continue' | 'accept', extra?: number): Game { const pending = offer(game); return applyAction(game, player, { type: 'victoryContinuation', event: pending.event, mode, ...(extra === undefined ? {} : { extra }) }); } function reject(game: Game, player: string, action: Action) { const before = structuredClone(game); assert.throws(() => applyAction(game, player, action)); assert.deepEqual(game, before); } function completeVotes(game: Game, extra: number): Game { let g = game; const pending = offer(g), voters = [...pending.voters]; g = act(g, voters[0], 'continue', extra); for (const voter of voters.slice(1)) g = act(g, voter, 'accept'); return g; } void test('Advanced public simultaneous victory waits for each qualifying participant and rejects other actions', () => { const opened = reachMentat(position()); assert.equal(opened.phase, 8); assert.ok(!opened.victoryContinuation); const game = finishMentat(opened); assert.equal(game.phase, 8); assert.equal(game.status, 'playing'); assert.deepEqual(game.winner, []); const pending = offer(game); assert.equal(pending.resume, 'advance'); assert.deepEqual(pending.sides, [['ix'], ['at', 'em']]); assert.deepEqual(pending.voters, ['ix', 'at', 'em']); assert.deepEqual(pending.accepted, []); const projected = viewGame(game, 'bg').victoryContinuation; assert.ok(projected); assert.equal(projected.actor, 'ix'); assert.deepEqual(projected.sides, pending.sides); assert.equal(projected.extra, null); reject(game, 'bg', { type: 'victoryContinuation', event: pending.event, mode: 'finish' }); reject(game, 'at', { type: 'victoryContinuation', event: pending.event, mode: 'continue', extra: 1 }); reject(game, 'ix', { type: 'victoryContinuation', event: 'stale-event', mode: 'continue', extra: 1 }); reject(game, 'ix', { type: 'victoryContinuation', event: pending.event, mode: 'continue', extra: -1 }); reject(game, 'ix', { type: 'ready' }); assert.deepEqual(offer(game), pending); }); void test('declining the common proposal settles the original winners and correct private prediction', () => { const game = finishMentat(reachMentat(position({ prediction: true }))); const vote = offer(game); assert.deepEqual(game.winner, []); const publicIx = viewGame(game, 'ix').victoryContinuation; const publicOther = viewGame(game, 'em').victoryContinuation; assert.deepEqual(publicIx, publicOther, 'The public vote does not expose the private prediction'); let next = act(game, 'ix', 'continue', 1); assert.deepEqual(offer(next).accepted, ['ix']); reject(next, 'at', { type: 'victoryContinuation', event: vote.event, mode: 'accept', extra: 2 }); next = act(next, 'at', 'finish'); assert.equal(next.status, 'finished'); assert.deepEqual(next.winner, ['bg']); assert.ok(!next.victoryContinuation); }); void test('unanimous continuation raises targets and advances once after completed Mentat', () => { const original = finishMentat(reachMentat(position())); const beforeForces = original.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves })); const next = completeVotes(original, 2); assert.equal(next.status, 'playing'); assert.equal(next.phase, 0); assert.equal(next.turn, original.turn + 1); assert.deepEqual(next.winner, []); assert.ok(!next.victoryContinuation); assert.equal(next.victoryExtraStrongholds, 2); assert.deepEqual(next.players.map(player => ({ id: player.id, forces: player.forces, reserves: player.reserves })), beforeForces); const progress = viewGame(next, 'ix').victoryProgress; assert.equal(progress.find(row => row.player === 'ix')?.target, 5); assert.equal(progress.find(row => row.player === 'at')?.target, 6); assert.equal(progress.find(row => row.player === 'at')?.qualifies, false); }); void test('zero-increase agreement preserves ordinary targets for the next turn', () => { const next = completeVotes(finishMentat(reachMentat(position())), 0); assert.equal(next.status, 'playing'); assert.equal(next.phase, 0); assert.equal(next.victoryExtraStrongholds ?? 0, 0); const progress = viewGame(next, 'ix').victoryProgress; assert.equal(progress.find(row => row.player === 'ix')?.target, 3); assert.equal(progress.find(row => row.player === 'at')?.target, 4); }); void test('Basic multiple qualifiers can agree while one qualifying side still wins directly', () => { const basic = reachMentat(position({ advanced: false })); assert.equal(basic.status, 'playing'); assert.ok(basic.victoryContinuation); const settled = act(basic, basic.victoryContinuation.voters[0], 'finish'); assert.equal(settled.status, 'finished'); const single = finishMentat(reachMentat(position({ onlyAlliance: true }))); assert.equal(single.status, 'finished'); assert.ok(single.winner.length > 0); assert.ok(!single.victoryContinuation); }); void test('a saved legacy Advanced agreement resumes its original Mentat opportunity', () => { // Stage the old opening-time receipt using the still-supported Basic path. // This controlled saved frame is not a historical-room recovery proof. const game = reachMentat(position({ advanced: false })); assert.equal(offer(game).resume, 'mentat'); game.advanced = true; assert.equal(game.mentatVictoryAfterActions, undefined); const next = completeVotes(game, 1); assert.equal(next.status, 'playing'); assert.equal(next.phase, 8); assert.equal(next.turn, game.turn); assert.ok(!next.victoryContinuation); const advanced = finishMentat(next); assert.equal(advanced.turn, game.turn + 1); assert.equal(advanced.phase, 0); }); void test('all four minimal bot profiles produce a legal response for the public continuation', () => { const game = finishMentat(reachMentat(position())); for (const level of DIFFICULTIES) { const view = viewGame(game, 'ix'); view.players.find(player => player.id === 'ix')!.bot = level; // A pending native agreement takes priority over unrelated action guides. view.turnActionGuide = { event: 'controlled-guide', action: { type: 'ready' } }; const action = botActions(view).find(row => row.type === 'victoryContinuation'); assert.ok(action, `${level} resolves its own public agreement opportunity`); const next = applyAction(game, 'ix', action); assert.ok(next.status === 'finished' || next.victoryContinuation); } }); void test('CHOAM final Mentat completion waits for unanimous votes before advancing', () => { let game = reachMentat(position({ choam: true })); assert.equal(game.phase, 8); assert.ok(!game.victoryContinuation); for (const p of game.players) game = applyAction(game, p.id, { type: 'ready' }); if (game.decision?.kind === 'choamMarket') game = applyAction(game, 'ch', { type: 'decision', done: true }); assert.equal(game.decision?.kind, 'choamMentat'); game = applyAction(game, 'ch', { type: 'decision', done: true }); assert.equal(offer(game).resume, 'advance'); assert.equal(game.phase, 8); assert.equal(game.turn, 2); game = completeVotes(game, 1); assert.equal(game.status, 'playing'); assert.equal(game.turn, 3); assert.equal(game.victoryExtraStrongholds, 1); assert.ok(!game.victoryContinuation); });