125 lines
7.2 KiB
TypeScript
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']]}));
|
|
});
|