Files
Dune/tests/tleilaxu-ambassador-income.test.ts
Quintine 6d30e7c5e1 Connect Tleilaxu income to committed Ambassador revivals
Positive free force/leader returns share the existing beneficiary/turn free-income ledger under the explicit provisional source composition. Actual Ghola remains separately rewarded. Decline earns nothing. Bind a distinct revival-income response to the completed physical return and original Ambassador; allow/cancel finishes once and cancellation preserves the return and spent entitlement.

Add persistent preselection and income guidance. Admit the bare Basic Ecaz+Ix native Tleilaxu composition through existing coalition/Face Dance mechanics, retaining unsupported faction/module guards and existing start/battle assertions.

Verify focused tests, types/lint/build, four original public Basic native/copy allow/cancel programmes, actual control rendering and eight existing legal policies. Exact frozen evidence and boundaries are in docs/TLEILAXU_AMBASSADOR_RULES.md. Full rules/modes, wider combinations, assurance and deployed acceptance remain unfinished. No strategic calibration, saved-room operation, reset, restart or deployment.
2026-10-24 13:07:08 +11:00

124 lines
7.3 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
createAmbassadorRevivalReceipt,
validateAmbassadorRevivalReceipt,
AmbassadorRevivalIncomeError,
type AmbassadorRevivalSource,
type AmbassadorRevivalReturn,
type AmbassadorRevivalReceipt,
} from '../game/tleilaxu-ambassador-income';
const source = (): AmbassadorRevivalSource => ({
event: 'original-ambassador-event', owner: 'ecaz', entrant: 'guild', token: 'ambassador-tleilaxu',
territory: 'arrakeen', sector: 10, turn: 3, phase: 5, beneficiary: 'atreides', incomeOwner: 'tleilaxu',
});
const forces = (): AmbassadorRevivalReturn => ({ kind: 'forces', amount: 3, elite: 1 });
// Pure committed-return witnesses, not earned game histories or live custody
// checks. Native Ambassador and the original free-income ledger stay engine-owned.
void test('positive force, leader and KH returns produce detached one-spice Ambassador receipts', () => {
for (const returned of [forces(), { kind: 'leader', leader: 'duke-vidal', deaths: 2 },
{ kind: 'kwisatz', cycle: 1 }] as AmbassadorRevivalReturn[]) {
const originalSource = source(), before = structuredClone({ source: originalSource, returned });
Object.freeze(originalSource); Object.freeze(returned);
const receipt = createAmbassadorRevivalReceipt(originalSource, returned);
assert.equal(receipt.version, 1);
assert.equal(receipt.amount, 1);
assert.deepEqual(receipt.source, originalSource);
assert.deepEqual(receipt.returned, returned);
assert.notEqual(receipt.source, originalSource);
assert.notEqual(receipt.returned, returned);
validateAmbassadorRevivalReceipt(originalSource, JSON.parse(JSON.stringify(receipt)));
assert.deepEqual({ source: originalSource, returned }, before);
receipt.source.territory = 'carthag';
assert.equal(originalSource.territory, 'arrakeen');
if (receipt.returned.kind === 'forces') receipt.returned.amount = 4;
else if (receipt.returned.kind === 'leader') receipt.returned.deaths = 3;
else receipt.returned.cycle = 2;
assert.deepEqual(returned, before.returned);
}
});
void test('receipt binding is deterministic across equivalent public source property insertion orders', () => {
const original = source(), reordered = Object.fromEntries(Object.entries(original).reverse()) as AmbassadorRevivalSource;
const first = createAmbassadorRevivalReceipt(original, forces());
const second = createAmbassadorRevivalReceipt(reordered, forces());
assert.equal(first.signature, second.signature);
validateAmbassadorRevivalReceipt(original, second);
validateAmbassadorRevivalReceipt(reordered, first);
const firstPhase = { ...original, phase: 0, sector: 1, turn: 1 };
validateAmbassadorRevivalReceipt(firstPhase, createAmbassadorRevivalReceipt(firstPhase, { kind: 'forces', amount: 1, elite: 0 }));
const finalSector = { ...original, sector: 18, phase: Number.MAX_SAFE_INTEGER };
validateAmbassadorRevivalReceipt(finalSector, createAmbassadorRevivalReceipt(finalSector, { kind: 'forces', amount: 4, elite: 4 }));
});
void test('a separately valid receipt from another source cannot settle this original Ambassador income', () => {
const original = source();
const changes: Partial<AmbassadorRevivalSource>[] = [
{ event: 'another-event' }, { owner: 'another-owner' }, { entrant: 'another-entrant' },
{ token: 'copied-ambassador' }, { territory: 'carthag' }, { sector: 11 },
{ turn: 4 }, { phase: 1 }, { beneficiary: 'another-beneficiary' }, { incomeOwner: 'another-income-owner' },
];
for (const change of changes) {
const changed = { ...original, ...change }, receipt = createAmbassadorRevivalReceipt(changed, forces());
validateAmbassadorRevivalReceipt(changed, receipt);
assert.throws(() => validateAmbassadorRevivalReceipt(original, receipt), AmbassadorRevivalIncomeError);
}
});
void test('force types, leader identity and preserved death or KH cycle values remain bound after commitment', () => {
const original = source();
const changedForces = createAmbassadorRevivalReceipt(original, forces());
assert.ok(changedForces.returned.kind === 'forces');
changedForces.returned.elite = 0;
assert.throws(() => validateAmbassadorRevivalReceipt(original, changedForces), /committed return/);
for (const returned of [
{ kind: 'forces', amount: 4, elite: 1 },
{ kind: 'leader', leader: 'ecaz-0', deaths: 1 },
{ kind: 'leader', leader: 'duke-vidal', deaths: 3 },
{ kind: 'kwisatz', cycle: 2 },
] as AmbassadorRevivalReturn[]) {
const initial: AmbassadorRevivalReturn = returned.kind === 'forces' ? forces() :
returned.kind === 'leader' ? { kind: 'leader', leader: 'duke-vidal', deaths: 2 } : { kind: 'kwisatz', cycle: 1 };
const receipt = createAmbassadorRevivalReceipt(original, initial);
receipt.returned = returned;
assert.throws(() => validateAmbassadorRevivalReceipt(original, receipt), /committed return/);
}
});
void test('malformed public sources and zero, excessive or untyped returns cannot create income receipts', () => {
const original = source();
for (const invalid of [
{ ...original, event: '' }, { ...original, owner: ' ' }, { ...original, entrant: 1 },
{ ...original, token: undefined }, { ...original, territory: '' }, { ...original, beneficiary: '' },
{ ...original, incomeOwner: '' }, { ...original, sector: 0 }, { ...original, sector: 19 },
{ ...original, sector: 1.5 }, { ...original, turn: 0 }, { ...original, turn: NaN },
{ ...original, phase: -1 }, { ...original, phase: 0.5 }, { ...original, phase: Infinity },
{ ...original, phase: Number.MAX_SAFE_INTEGER + 1 }, { ...original, wallet: 1 },
]) assert.throws(() => createAmbassadorRevivalReceipt(invalid as AmbassadorRevivalSource, forces()), AmbassadorRevivalIncomeError);
for (const invalid of [
{ kind: 'forces', amount: 0, elite: 0 }, { kind: 'forces', amount: 5, elite: 0 },
{ kind: 'forces', amount: 1.5, elite: 0 }, { kind: 'forces', amount: 3, elite: -1 },
{ kind: 'forces', amount: 3, elite: 4 }, { kind: 'forces', amount: 3, elite: 0.5 },
{ kind: 'forces', amount: 3 }, { kind: 'forces', amount: 3, elite: 0, cost: 0 },
{ kind: 'leader', leader: '', deaths: 1 }, { kind: 'leader', leader: 'ecaz-0', deaths: 0 },
{ kind: 'leader', leader: 'ecaz-0', deaths: Number.MAX_SAFE_INTEGER + 1 },
{ kind: 'kwisatz', cycle: 0 }, { kind: 'kwisatz', cycle: NaN }, { kind: 'kwisatz', cycle: 1.5 },
{ kind: 'unknown', amount: 1 },
]) assert.throws(() => createAmbassadorRevivalReceipt(original, invalid as AmbassadorRevivalReturn), AmbassadorRevivalIncomeError);
});
void test('receipt reward, source shape, format version and signature cannot be changed into another bank award', () => {
const original = source(), receipt = createAmbassadorRevivalReceipt(original, forces());
for (const changed of [
{ ...receipt, version: 2 }, { ...receipt, amount: 0 }, { ...receipt, amount: 2 },
{ ...receipt, signature: '' }, { ...receipt, extra: true },
{ ...receipt, source: { ...receipt.source, hiddenBalance: 5 } },
{ ...receipt, returned: { kind: 'forces', amount: 0, elite: 0 } },
]) assert.throws(() => validateAmbassadorRevivalReceipt(original, changed as AmbassadorRevivalReceipt), AmbassadorRevivalIncomeError);
const before = structuredClone(receipt);
validateAmbassadorRevivalReceipt(original, receipt);
assert.deepEqual(receipt, before);
});