100 lines
5.7 KiB
TypeScript
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));
|
|
}
|
|
});
|