Files
Dune/tests/distrans.test.ts
T
2026-09-08 07:37:52 +10:00

204 lines
7.0 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, type Card } from '../game/cards';
import { FACTIONS } from '../game/catalog';
import { richeseCards } from '../game/richese-cards';
import { transferDistrans, type DistransParticipant } from '../game/distrans';
function fixture() {
const distrans = richeseCards().find((card) => card.effect === 'distrans')!;
const [given, retained, held] = baseDeck();
const owner: DistransParticipant = {
id: 'owner',
hand: [distrans, given, retained],
};
const recipient: DistransParticipant = { id: 'recipient', hand: [held] };
return { owner, recipient, distrans, given, retained, held };
}
void test('all twelve donor factions may transfer to an unrelated recipient without an alliance or payment contract', () => {
assert.equal(FACTIONS.length, 12);
for (const faction of FACTIONS) {
const { owner, recipient, distrans, given } = fixture();
const donor = { ...owner, faction: faction.id, ally: null, spice: 0 };
const target = { ...recipient, ally: 'someone-else', spice: 0 };
const before = structuredClone({ donor, target });
const result = transferDistrans(donor, target, distrans.id, given.id, 4);
assert.equal(result.ownerHand.length, 1);
assert.deepEqual(result.recipientHand.at(-1), given);
assert.deepEqual(result.discarded, distrans);
assert.deepEqual({ donor, target }, before);
}
});
void test('ordinary and other Richese cards transfer while only the activating Distrans enters discard', () => {
const cards = [
...baseDeck(),
...richeseCards().filter((card) => card.effect !== 'distrans'),
];
for (const given of cards) {
const { distrans } = fixture();
const owner = { id: 'owner', hand: [distrans, given] };
const recipient = { id: 'recipient', hand: [] };
const result = transferDistrans(owner, recipient, distrans.id, given.id, 1);
assert.deepEqual(result.ownerHand, []);
assert.deepEqual(result.recipientHand, [given]);
assert.deepEqual(result.transferred, given);
assert.deepEqual(result.discarded, distrans);
assert.deepEqual(
[...result.ownerHand, ...result.recipientHand, result.discarded]
.map((c) => c.id)
.sort(),
owner.hand.map((c) => c.id).sort(),
);
}
});
void test('current recipient capacity permits the final slot and rejects full, zero and malformed limits immutably', () => {
for (const limit of [1, 4, 5, 8]) {
const { owner, distrans, given } = fixture();
const remaining = baseDeck().filter(
(card) => !owner.hand.some((held) => held.id === card.id),
);
const recipient = { id: 'recipient', hand: remaining.slice(0, limit - 1) };
assert.equal(
transferDistrans(owner, recipient, distrans.id, given.id, limit)
.recipientHand.length,
limit,
);
const full = { ...recipient, hand: remaining.slice(0, limit) };
const before = structuredClone({ owner, full });
assert.throws(
() => transferDistrans(owner, full, distrans.id, given.id, limit),
/full/,
);
assert.deepEqual({ owner, full }, before);
}
const { owner, recipient, distrans, given } = fixture();
assert.throws(
() =>
transferDistrans(
owner,
{ ...recipient, hand: [] },
distrans.id,
given.id,
0,
),
/full/,
);
for (const limit of [
NaN,
Infinity,
-Infinity,
-1,
1.5,
Number.MAX_SAFE_INTEGER + 1,
])
assert.throws(
() => transferDistrans(owner, recipient, distrans.id, given.id, limit),
/hand limit/,
);
});
void test('activation requires the exact canonical held identity and a distinct separately owned target', () => {
const { owner, recipient, distrans, given } = fixture();
for (const activation of [
{ ...distrans, id: 'fake' },
{ ...distrans, name: 'Fake' },
{ ...distrans, kind: 'worthless' as const },
{ ...distrans, effect: 'karama' },
richeseCards().find((card) => card.effect === 'karama')!,
]) {
const donor = { ...owner, hand: [activation, given] };
assert.throws(
() => transferDistrans(donor, recipient, activation.id, given.id, 4),
/canonical/,
);
}
assert.throws(
() =>
transferDistrans(
{ ...owner, hand: [given] },
recipient,
distrans.id,
given.id,
4,
),
/canonical/,
);
assert.throws(
() => transferDistrans(owner, recipient, distrans.id, distrans.id, 4),
/itself is unresolved/,
);
assert.throws(
() =>
transferDistrans(owner, recipient, distrans.id, recipient.hand[0].id, 4),
/own hand/,
);
for (const id of [owner.id, ''])
assert.throws(
() =>
transferDistrans(owner, { ...recipient, id }, distrans.id, given.id, 4),
/different player/,
);
});
void test('duplicate, shared and sparse physical custody is rejected before either hand changes', () => {
const { owner, recipient, distrans, given, retained, held } = fixture();
const hole: Card[] = [];
hole.length = 2;
const emptySlot: Card[] = [];
emptySlot.length = 1;
hole[0] = distrans;
const cases: [DistransParticipant, DistransParticipant][] = [
[{ ...owner, hand: [...owner.hand, { ...distrans }] }, recipient],
[{ ...owner, hand: [...owner.hand, { ...retained }] }, recipient],
[owner, { ...recipient, hand: [held, { ...held }] }],
[owner, { ...recipient, hand: [given] }],
[owner, { ...recipient, hand: [distrans] }],
[owner, { ...recipient, hand: [retained] }],
[{ ...owner, hand: hole }, recipient],
[owner, { ...recipient, hand: emptySlot }],
[{ ...owner, hand: [...owner.hand, { ...held, id: '' }] }, recipient],
];
for (const [donor, target] of cases) {
const before = structuredClone({ donor, target });
assert.throws(
() => transferDistrans(donor, target, distrans.id, given.id, 8),
/unique physical/,
);
assert.deepEqual({ donor, target }, before);
}
});
void test('JSON restoration conserves exact identities, cloned outputs are independent and replay fails', () => {
const original = fixture();
const { owner, recipient, distrans, given } = JSON.parse(
JSON.stringify(original),
) as ReturnType<typeof fixture>;
const before = structuredClone({ owner, recipient });
const result = transferDistrans(owner, recipient, distrans.id, given.id, 4);
const settled = {
owner: { ...owner, hand: result.ownerHand },
recipient: { ...recipient, hand: result.recipientHand },
};
assert.throws(
() =>
transferDistrans(
settled.owner,
settled.recipient,
distrans.id,
given.id,
4,
),
/canonical/,
);
result.ownerHand[0].name = 'Changed retained output';
result.recipientHand[0].name = 'Changed recipient output';
result.transferred.name = 'Changed receipt';
assert.equal(result.recipientHand.at(-1)!.name, given.name);
result.discarded.name = 'Changed discard output';
assert.deepEqual({ owner, recipient }, before);
assert.deepEqual(original.owner, before.owner);
});