63 lines
3.7 KiB
TypeScript
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');
|
|
});
|
|
}
|