Files
Dune/tests/nexus-fremen-revival.test.ts
T

63 lines
3.7 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, normalizeAutomaticGame, viewGame, type Game, type Player } from '../game/engine';
import { createNexusFremenFreeQuotaFixture } from './fixture-nexus-fremen-free-quota';
import { awaitSpiceBankerIncomeRevivalActor } from './fixture-spice-banker-income';
function pools(player: Player) {
return { reserves: player.reserves, tanks: player.tanks,
eliteReserves: player.elites?.reserves ?? 0, eliteTanks: player.elites?.tanks ?? 0,
eliteRevived: player.elites?.revived ?? 0, revived: player.revived,
freeForcesRevived: player.freeForcesRevived ?? 0, spice: player.spice };
}
for (const advanced of [false, true]) {
void test(`${advanced ? 'Advanced' : 'Basic'} saved exact-three physical receipt adopts its exhausted quota without replaying the actual return`, () => {
const fixture = createNexusFremenFreeQuotaFixture({ advanced });
const source = awaitSpiceBankerIncomeRevivalActor(fixture.game, fixture.owner);
const before = source.players.find(player => player.id === fixture.owner)!;
const offer = viewGame(source, fixture.owner).nexusFremenRevival!;
const granted = applyAction(source, fixture.owner,
{ type: 'nexusFremenRevival', event: offer.event });
const returned = applyAction(granted, fixture.owner, { type: 'revive', amount: 3, elite: 0 });
const after = returned.players.find(player => player.id === fixture.owner)!;
assert.equal(after.tanks, before.tanks - 3);
assert.equal(after.freeForcesRevived, 3);
// Only historical receipt encoding is changed; both physical snapshots came
// from the genuine original producer and the actual native return above.
const legacyReceipt = { event: offer.event, owner: fixture.owner, turn: returned.turn,
phase: 4, faction: after.faction, advanced, elite: 0,
before: pools(before), after: pools(after), signature: '' };
legacyReceipt.signature = JSON.stringify({ ...legacyReceipt, signature: undefined });
const legacy = JSON.parse(JSON.stringify({ ...returned,
nexusFremenRevivalHistory: [legacyReceipt] })) as Game;
delete legacy.revivalRules!.nexusFremen;
const original = structuredClone(legacy);
const projected = viewGame(legacy, fixture.owner);
assert.equal(projected.revival.freeRemaining, 0);
assert.deepEqual(legacy, original, 'Reading an old encoding cannot rewrite its physical state');
const adopted = normalizeAutomaticGame(legacy);
assert.deepEqual(adopted.players, returned.players);
assert.deepEqual(adopted.nexusCards, returned.nexusCards);
assert.deepEqual(adopted.ready, returned.ready);
assert.equal(adopted.phase, 4);
assert.equal(adopted.nexusFremenRevivalHistory![0].version, 2);
assert.deepEqual(adopted.nexusFremenRevivalHistory![0].legacyReturn,
{ elite: 0, before: pools(before), after: pools(after) });
assert.deepEqual(adopted.revivalRules!.nexusFremen,
{ turn: adopted.turn, owners: [fixture.owner] });
assert.deepEqual(normalizeAutomaticGame(adopted), adopted,
'A second adoption cannot replay the return or reopen the free quota');
assert.throws(() => applyAction(adopted, fixture.owner,
{ type: 'nexusFremenRevival', event: offer.event }));
assert.throws(() => applyAction(adopted, fixture.owner, { type: 'revive', amount: 1, elite: 0 }));
assert.deepEqual(legacy, original);
const broken = structuredClone(legacy);
broken.nexusFremenRevivalHistory![0].signature = 'broken';
const damaged = structuredClone(broken);
assert.throws(() => viewGame(broken, fixture.owner));
assert.throws(() => normalizeAutomaticGame(broken));
assert.deepEqual(broken, damaged, 'Invalid historical facts fail without repairing or returning any force');
});
}