Files
Dune/tests/homeworld-revival-ambassador-engine.test.ts
T
Quintine 28ba1618ba Implement Homeworld mechanics through Caladan reinforcement checkpoint
Connect typed Homeworld custody, transport, battle, income, revival and Caladan reinforcement flows with private multiplayer recovery and controls. Preserve the optimized test workflow and keep unfinished rules modes gated. Verified with 3682 offline tests, typecheck, lint, production build and 40 HTTP tests; existing saved games remain unchanged.
2026-09-10 08:12:26 +10:00

243 lines
7.3 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import {
applyAction,
normalizeAutomaticGame,
viewGame,
type Game,
} from '../game/engine';
import {
revivalPlayer as p,
revivalInventory as inventory,
revivalReload as reload,
} from './fixture-homeworld-revival';
import { homeworldRevivalAmbassadorFixture as fixture } from './fixture-homeworld-revival-ambassador';
function open() {
let g = fixture();
g = applyAction(g, 'f', { type: 'revive', amount: 1, elite: 1 });
if (g.decision?.kind === 'revivalStop')
g = applyAction(g, 't', { type: 'decision', decline: true });
while (!g.homeworldRevivalReturn && g.response) g = allow(g, g.response.kind);
assert.equal(
g.homeworldRevivalReturn?.stage,
'choice',
JSON.stringify({
decision: g.decision,
response: g.response,
revival: g.pendingRevival,
}),
);
assert.equal(g.homeworldRevivalReturn.resumeResponse?.kind, 'revivalIncome');
const event = g.homeworldRevivalReturn.event;
g = applyAction(reload(g), 'f', {
type: 'decision',
event,
destination: 'arrakeen:10',
});
assert.equal(g.pendingAmbassador?.stage, 'offer');
assert.equal(g.homeworldRevivalReturn?.stage, 'arrival');
inventory(g);
return g;
}
function allow(g: Game, kind: string) {
for (let n = 0; g.response?.kind === kind && n < 40; n++) {
const player = g.players.find(
(player) => !g.response!.passed.includes(player.id),
);
assert.ok(player);
g = applyAction(reload(g), player.id, { type: 'passResponse' });
}
return g;
}
void test('actual phase-four revived arrival holds income through Guild shipment, BG child and Fremen relocation across JSON', () => {
let g = open();
const original = structuredClone(g.homeworldRevivalReturn!);
const beforeIncome = p(g, 't').spice;
const parent = g.pendingAmbassador!.event;
g = applyAction(reload(g), 'ec', {
type: 'decision',
event: parent,
trigger: true,
beneficiary: 'a',
});
assert.equal(g.pendingAmbassador?.stage, 'ship');
g = applyAction(reload(g), 'a', {
type: 'decision',
event: parent,
amount: 2,
territory: 'sietch_tabr',
sector: 14,
});
assert.equal(g.decision?.kind, 'intrusion');
g = applyAction(reload(g), 'bg', { type: 'decision', accept: false });
assert.equal(g.decision?.kind, 'advisor');
g = applyAction(reload(g), 'bg', {
type: 'decision',
accept: true,
accompany: true,
});
assert.equal(g.response?.kind, 'advisor');
g = allow(g, 'advisor');
assert.equal(g.pendingAmbassador?.stage, 'offer');
assert.equal(g.pendingAmbassador?.entrant, 'bg');
assert.equal(g.homeworldRevivalReturn?.ambassadors?.length, 2);
assert.equal(g.homeworldRevivalReturn!.ambassadors![1].parent, parent);
assert.equal(p(g, 't').spice, beforeIncome);
assert.deepEqual(
g.homeworldRevivalReturn!.resumeResponse,
original.resumeResponse,
);
inventory(g);
assert.deepEqual(normalizeAutomaticGame(reload(g)), reload(g));
g = applyAction(reload(g), 'ec', {
type: 'decision',
event: g.pendingAmbassador!.event,
trigger: true,
beneficiary: 'ec',
});
assert.equal(g.pendingAmbassador?.stage, 'move');
g = applyAction(reload(g), 'ec', {
type: 'decision',
event: g.pendingAmbassador!.event,
forces: { 'red_chasm:7': 1 },
territory: 'carthag',
sector: 11,
});
assert.equal(g.pendingAmbassador ?? null, null);
assert.equal(g.homeworldRevivalReturn?.stage, 'complete');
assert.deepEqual(g.response, original.resumeResponse);
assert.equal(p(g, 't').spice, beforeIncome);
g = allow(g, 'revivalIncome');
assert.equal(
p(g, 't').spice,
beforeIncome + original.resumeResponse!.amount!,
);
assert.equal(p(g, 'f').forces['arrakeen:10'], 13);
assert.equal(p(g, 'a').forces['sietch_tabr:14'], 2);
assert.equal(p(g, 'bg').forces['sietch_tabr:14'], 2);
assert.equal(p(g, 'ec').forces['carthag:11'], 1);
inventory(g);
assert.deepEqual(normalizeAutomaticGame(reload(g)), reload(g));
});
void test('an actual revival Ambassador offer rejects edited original arrival identities before view or action', () => {
const original = open();
for (const edit of [
(g: Game) => {
g.pendingAmbassador!.entrant = 'a';
},
(g: Game) => {
g.pendingAmbassador!.territory = 'carthag';
g.pendingAmbassador!.sector = 11;
},
(g: Game) => {
delete g.pendingAmbassador!.revivalEvent;
},
]) {
const g = reload(original);
edit(g);
const saved = reload(g);
assert.throws(() => viewGame(g, 'ec'));
assert.throws(() =>
applyAction(g, 'ec', {
type: 'decision',
event: g.pendingAmbassador!.event,
trigger: false,
}),
);
assert.deepEqual(g, saved);
}
});
void test('deleting a still-open revival Ambassador cannot silently finish its retained income', () => {
const g = open();
g.pendingAmbassador = null;
g.decision = null;
const saved = reload(g);
assert.throws(() => viewGame(g, 'ec'));
assert.throws(() => applyAction(g, 'ec', { type: 'ready' }));
assert.throws(() => normalizeAutomaticGame(g));
assert.deepEqual(g, saved);
g.homeworldRevivalReturn!.stage = 'complete';
const falselyCompleted = reload(g);
assert.throws(() => viewGame(g, 'ec'));
assert.throws(() => applyAction(g, 'ec', { type: 'ready' }));
assert.throws(() => normalizeAutomaticGame(g));
assert.deepEqual(g, falselyCompleted);
});
void test('a real revival arrival cannot rewind and spend an older Fedaykin still in native reserves', () => {
const original = open();
assert.equal(
p(original, 'f').elites!.reserves,
1,
'an older elite remains physically available',
);
assert.equal(original.homeworldRevivalProgress?.stage, 'arrival');
const g = reload(original);
const frame = g.homeworldRevivalReturn!;
frame.stage = 'choice';
delete frame.destination;
delete frame.ambassadors;
delete frame.arrivalSignature;
g.pendingAmbassador = null;
g.decision = {
kind: 'homeworldRevivalDeployment',
player: 'f',
event: frame.event,
};
const before = reload(g);
assert.throws(() => viewGame(g, 'f'));
assert.throws(() => normalizeAutomaticGame(g));
assert.throws(() =>
applyAction(g, 'f', {
type: 'decision',
event: frame.event,
destination: 'arrakeen:10',
}),
);
assert.deepEqual(g, before);
});
void test('new revival progress and its signed version cannot be deleted or downgraded during a live child', () => {
const original = open();
for (const edit of [
(g: Game) => {
delete g.homeworldRevivalProgress;
},
(g: Game) => {
delete g.homeworldRevivalReturn!.progressVersion;
},
(g: Game) => {
delete g.homeworldRevivalProgress;
delete g.homeworldRevivalReturn!.progressVersion;
},
(g: Game) => {
g.homeworldRevivalReturn!.progressVersion = 0 as 1;
},
(g: Game) => {
g.homeworldRevivalProgress!.stage = 'choice';
},
(g: Game) => {
delete g.homeworldRevivalReturn;
},
]) {
const g = reload(original);
edit(g);
const before = reload(g);
assert.throws(() => viewGame(g, 'f'));
assert.throws(() => normalizeAutomaticGame(g));
assert.throws(() =>
applyAction(g, 'ec', {
type: 'decision',
event: g.pendingAmbassador!.event,
decline: true,
}),
);
assert.deepEqual(g, before);
}
});