Files
Dune/tests/coalition-victory.test.ts
Quintine cea310b030
Verify and publish checkpoint container / container (push) Successful in 10m14s
Prototype charity Truthtrance and coalition rule foundations
2026-10-28 21:58:30 +11:00

125 lines
7.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { beginCoalitionAdmission, answerCoalitionAdmission, coalitionOf, type CoalitionState } from '../game/coalition-alliance';
import { createGame, newPlayer, type Game } from '../game/engine';
import type { FactionId } from '../game/catalog';
import { strongholdProgress, VictoryProgressError } from '../game/victory-progress';
import { quoteVictory, VictoryQuoteError } from '../game/victory-quote';
import { fremenSpecialVictory } from '../game/fremen-victory';
import { createVictoryContinuation, voteVictoryContinuation } from '../game/victory-continuation';
import { createTechTokens } from '../game/tech-tokens';
type CoalitionGame = Game & {coalitionState: CoalitionState};
function game(factions: FactionId[] = ['atreides', 'emperor', 'harkonnen', 'beneGesserit']): CoalitionGame {
const g = createGame('COALITIONVICTORY', newPlayer('p0', factions[0], factions[0]), false);
factions.slice(1).forEach((faction, i) => g.players.push(newPlayer(`p${i + 1}`, faction, faction)));
Object.assign(g, { status: 'playing', phase: 8, turn: 2, storm: 18,
order: g.players.map(player => player.id) });
for (const p of g.players) Object.assign(p, {forces: {}, ally: null});
g.players[0].ally = 'p1'; g.players[1].ally = 'p0';
const group = coalitionOf(g.players, undefined, 'p0')!;
const start = beginCoalitionAdmission(g.players, undefined, {event: 'invite', nexus: 'nexus', turn: 2,
groupId: group.id, entrant: 'p2', proposer: 'p0', expectedRevision: 0});
const second = answerCoalitionAdmission(g.players, start.state, {event: 'invite', nexus: 'nexus',
turn: 2, actor: 'p1', accept: true, expectedRevision: start.state.revision});
const joined = answerCoalitionAdmission(g.players, second.state, {event: 'invite', nexus: 'nexus',
turn: 2, actor: 'p2', accept: true, expectedRevision: second.state.revision});
for (const p of g.players) if (Object.hasOwn(joined.allyUpdates, p.id)) p.ally = joined.allyUpdates[p.id];
return Object.assign(g, {coalitionState: joined.state});
}
function hold(g: CoalitionGame, player: number, ...sites: string[]) {
g.players[player].forces = Object.fromEntries(sites.map(site => [site, 1]));
}
const a = 'arrakeen:10', b = 'carthag:11', c = 'sietch_tabr:14', d = 'habbanya_ridge_sietch:17';
void test('three members share a distinct uncontested stronghold union with ordered members', () => {
const g = game();
g.order = ['p2', 'p0', 'p3', 'p1'];
hold(g, 0, a, b); hold(g, 1, b, c); hold(g, 2, d);
const before = structuredClone(g);
const rows = strongholdProgress(g).progress.filter(row => row.members.length === 3);
assert.equal(rows.length, 3);
for (const row of rows) {
assert.deepEqual(row.members, ['p2', 'p0', 'p1']);
assert.equal(row.strongholds.length, 4); assert.equal(row.target, 4); assert.equal(row.qualifies, true);
}
assert.deepEqual(quoteVictory(g).winner, ['p2', 'p0', 'p1']);
assert.deepEqual(g, before);
hold(g, 3, d);
assert.equal(strongholdProgress(g).progress.find(row => row.player === 'p0')!.qualifies, false);
});
void test('two-member shortened target does not apply to three-member alliances', () => {
const g = game(); hold(g, 0, a); hold(g, 1, b); hold(g, 2, c);
let row = strongholdProgress(g).progress[0];
assert.equal(row.target, 4); assert.equal(row.qualifies, false);
g.shortAllianceVictory = true;
row = strongholdProgress(g).progress[0]; assert.equal(row.target, 4); assert.equal(row.qualifies, false);
g.victoryExtraStrongholds = 1;
row = strongholdProgress(g).progress[0]; assert.equal(row.target, 5); assert.equal(row.qualifies, false);
g.victoryExtraStrongholds = 0;
g.techTokens = createTechTokens();
for (const token of Object.values(g.techTokens)) token.owner = 'p2';
row = strongholdProgress(g).progress[0]; assert.equal(row.techStronghold, true); assert.equal(row.qualifies, true);
g.techTokens.production.owner = 'p1';
assert.equal(strongholdProgress(g).progress[0].techStronghold, false);
g.twoPlayerControllers = {version: 1, controllers: [
{id: 'p0', seats: ['p0', 'p1']}, {id: 'p2', seats: ['p2', 'p3']},
]};
assert.equal(strongholdProgress(g).progress[0].target, 4);
});
void test('ordinary prediction checks every coalition winner while final Fremen special still wins', () => {
const g = game(); hold(g, 0, a, b); hold(g, 1, c); hold(g, 2, d);
g.players[3].prediction = {faction: 'harkonnen', turn: 2};
assert.deepEqual(quoteVictory(g).winner, ['p3']);
g.players[3].prediction.turn = 3;
assert.deepEqual(quoteVictory(g).winner, ['p0', 'p1', 'p2']);
const f = game(['fremen', 'atreides', 'emperor', 'beneGesserit']);
f.turn = 10; f.players[3].prediction = {faction: 'atreides', turn: 10};
assert.deepEqual(quoteVictory(f).winner, ['p0', 'p1', 'p2']);
});
void test('Fremen, Guild and no-Guild Fremen final branches award the full alliance', () => {
const fremen = game(['fremen', 'atreides', 'emperor', 'guild']); fremen.turn = 10;
assert.deepEqual(fremenSpecialVictory(fremen)!.members, ['p0', 'p1', 'p2']);
assert.deepEqual(quoteVictory(fremen).winner, ['p0', 'p1', 'p2']);
hold(fremen, 1, c);
assert.equal(fremenSpecialVictory(fremen)!.qualifies, false);
assert.deepEqual(quoteVictory(fremen).winner, ['p3']);
const guild = game(['guild', 'atreides', 'emperor', 'beneGesserit']); guild.turn = 10;
assert.deepEqual(quoteVictory(guild).winner, ['p0', 'p1', 'p2']);
const noGuild = game(['fremen', 'atreides', 'emperor', 'harkonnen']); noGuild.turn = 10;
hold(noGuild, 1, c);
assert.equal(fremenSpecialVictory(noGuild)!.qualifies, false);
assert.deepEqual(quoteVictory(noGuild).winner, ['p0', 'p1', 'p2']);
});
void test('unsupported triples and malformed coalition records fail closed across quotes', () => {
const g = game(); g.advanced = true;
assert.throws(() => strongholdProgress(g), VictoryProgressError);
assert.throws(() => fremenSpecialVictory(g), VictoryProgressError);
assert.throws(() => quoteVictory(g), VictoryQuoteError);
g.advanced = false; g.players[3].faction = 'ecaz';
assert.throws(() => strongholdProgress(g), VictoryProgressError);
assert.throws(() => quoteVictory(g), VictoryQuoteError);
g.players[3].faction = 'beneGesserit'; g.coalitionState.groups[0].edges.pop();
assert.throws(() => strongholdProgress(g), VictoryProgressError);
assert.throws(() => quoteVictory(g), VictoryQuoteError);
});
void test('a continuation deduplicates a triple side and requires every participant to consent', () => {
let frame = createVictoryContinuation({event: 'continue', turn: 2, resume: 'mentat',
order: ['p2', 'p0', 'p3', 'p1'], sides: [['p0', 'p1', 'p2'], ['p2', 'p0', 'p1'], ['p3']]});
assert.deepEqual(frame.sides, [['p2', 'p0', 'p1'], ['p3']]);
assert.deepEqual(frame.voters, ['p2', 'p0', 'p3', 'p1']);
for (const [index, player] of frame.voters.entries()) {
const vote = voteVictoryContinuation(frame, {event: 'continue', player, choice: 'continue',
...(index === 0 ? {extraStrongholds: 1} : {})});
if (vote.outcome === 'pending') frame = vote.frame;
else { assert.equal(index, 3); assert.deepEqual(vote, {outcome: 'continue', extraStrongholds: 1}); }
}
assert.throws(() => createVictoryContinuation({event: 'x', turn: 2, resume: 'mentat',
order: ['p0', 'p1', 'p2', 'p3'], sides: [['p0', 'p1', 'p2'], ['p2', 'p3']]}));
});