Files
Dune/tests/coalition-three-short-runtime.test.tsx
T
Quintine ef92ddf2b9
Verify and publish checkpoint container / container (push) Canceled after 0s
Add three-member short alliance prototype
2026-10-29 02:31:50 +11:00

100 lines
5.7 KiB
TypeScript

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