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

194 lines
6.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, type Card } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import {
prepareRicheseGift,
transferRicheseGift,
type RicheseGiftParticipant,
} from '../game/richese-gift';
function fixture() {
const owner: RicheseGiftParticipant = {
id: 'r',
faction: 'richese',
ally: 'a',
hand: richeseCards(),
};
const ally: RicheseGiftParticipant = {
id: 'a',
faction: 'atreides',
ally: 'r',
hand: baseDeck().slice(0, 2),
};
return { owner, ally };
}
void test('each of the ten physical Richese cards can transfer once without altering either input hand', () => {
for (const card of richeseCards()) {
const { owner, ally } = fixture();
const before = structuredClone({ owner, ally });
const intent = prepareRicheseGift(owner, ally, card.id, 4);
assert.deepEqual(intent, { owner: 'r', recipient: 'a', cardId: card.id });
const transferred = transferRicheseGift(owner, ally, intent, 4);
assert.equal(transferred.ownerHand.length, 9);
assert.equal(transferred.recipientHand.length, 3);
assert.deepEqual(transferred.card, card);
assert.equal(transferred.recipientHand.at(-1)!.id, card.id);
assert.deepEqual(
[...transferred.ownerHand, ...transferred.recipientHand]
.map((c) => c.id)
.sort(),
[...owner.hand, ...ally.hand].map((c) => c.id).sort(),
);
transferred.recipientHand.at(-1)!.name = 'Mutated output';
transferred.card.name = 'Independent receipt';
assert.deepEqual({ owner, ally }, before);
}
});
void test('standard cards, effect/name impostors, cache-only and duplicate physical IDs are rejected immutably', () => {
const { owner, ally } = fixture();
const canonical = richeseCards()[0];
const candidates: Card[][] = [
baseDeck()
.filter((c) => c.effect === 'karama')
.slice(0, 1),
[{ ...canonical, id: 'impostor' }],
[{ ...canonical, name: 'impostor' }],
[{ ...canonical, kind: 'worthless' }],
[{ ...canonical, effect: 'karama' }],
[],
[canonical, { ...canonical }],
];
for (const hand of candidates) {
const supplied = { ...owner, hand };
const before = structuredClone(supplied);
assert.throws(
() => prepareRicheseGift(supplied, ally, hand[0]?.id ?? canonical.id, 4),
/canonical/,
);
assert.deepEqual(supplied, before);
}
assert.throws(
() =>
prepareRicheseGift(
owner,
{ ...ally, hand: [canonical] },
canonical.id,
4,
),
/already/,
);
});
void test('gift requires the Richese actor and a distinct mutual ally, not faction identity or one-sided membership', () => {
const { owner, ally } = fixture();
const card = owner.hand[0].id;
assert.throws(
() => prepareRicheseGift({ ...owner, faction: 'harkonnen' }, ally, card, 4),
/Only Richese/,
);
assert.throws(
() => prepareRicheseGift(owner, { ...ally, id: 'r' }, card, 4),
/different/,
);
for (const changed of [
{ ...owner, ally: null },
{ ...owner, ally: 'someone-else' },
])
assert.throws(
() => prepareRicheseGift(changed, ally, card, 4),
/mutual ally/,
);
assert.throws(
() => prepareRicheseGift(owner, { ...ally, ally: null }, card, 4),
/mutual ally/,
);
});
void test('current capacity is supplied explicitly and supports ordinary, CHOAM and Harkonnen limits', () => {
for (const [faction, limit] of [
['atreides', 4],
['choam', 5],
['harkonnen', 8],
] as const) {
const { owner, ally } = fixture();
const recipient = {
...ally,
faction,
hand: baseDeck().slice(0, limit - 1),
};
const intent = prepareRicheseGift(
owner,
recipient,
owner.hand[0].id,
limit,
);
assert.equal(
transferRicheseGift(owner, recipient, intent, limit).recipientHand.length,
limit,
);
assert.throws(
() =>
prepareRicheseGift(
owner,
{ ...recipient, hand: baseDeck().slice(0, limit) },
owner.hand[0].id,
limit,
),
/full/,
);
}
const { owner, ally } = fixture();
for (const limit of [NaN, Infinity, -1, 4.5, Number.MAX_SAFE_INTEGER + 1])
assert.throws(
() => prepareRicheseGift(owner, ally, owner.hand[0].id, limit),
/hand limit/,
);
});
void test('a JSON-restored intent revalidates identities, alliance, capacity and exact physical custody before execution', () => {
const { owner, ally } = fixture();
const intent = JSON.parse(
JSON.stringify(prepareRicheseGift(owner, ally, owner.hand[0].id, 4)),
);
assert.equal(
transferRicheseGift(owner, ally, intent, 4).recipientHand.length,
3,
);
const staleCases: [RicheseGiftParticipant, RicheseGiftParticipant][] = [
[{ ...owner, id: 'new-owner' }, ally],
[owner, { ...ally, id: 'new-recipient' }],
[{ ...owner, ally: null }, ally],
[owner, { ...ally, hand: baseDeck().slice(0, 4) }],
[{ ...owner, hand: owner.hand.slice(1) }, ally],
[
{
...owner,
hand: [{ ...owner.hand[0], effect: 'karama' }, ...owner.hand.slice(1)],
},
ally,
],
];
for (const [donor, recipient] of staleCases) {
const before = structuredClone({ donor, recipient, intent });
assert.throws(() => transferRicheseGift(donor, recipient, intent, 4));
assert.deepEqual({ donor, recipient, intent }, before);
}
});
void test('replayed transfer fails after custody changes and separate gift declarations have no artificial once-per-turn limit', () => {
const { owner, ally } = fixture();
const intent = prepareRicheseGift(owner, ally, owner.hand[0].id, 4);
const first = transferRicheseGift(owner, ally, intent, 4);
const donor = { ...owner, hand: first.ownerHand },
recipient = { ...ally, hand: first.recipientHand };
assert.throws(
() => transferRicheseGift(donor, recipient, intent, 4),
/canonical/,
);
const second = transferRicheseGift(
donor,
recipient,
prepareRicheseGift(donor, recipient, donor.hand[0].id, 4),
4,
);
assert.equal(second.recipientHand.length, 4);
assert.equal(second.ownerHand.length, 8);
});