Files
Dune/tests/richese-cards.test.ts

149 lines
5.7 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, ixDeck, treacheryDeck } from '../game/cards';
import {
RICHESE_CARD_DEFINITIONS,
RICHESE_COLLECTION,
richeseCards,
richeseCardDefinition,
} from '../game/richese-cards';
void test('Richese physical collection contains exactly the ten photographed identities, one each', () => {
assert.deepEqual(
richeseCards().map(({ id, name, effect }) => [id, name, effect]),
[
['richese-ornithopter', 'Ornithopter', 'ornithopter'],
['richese-residual-poison', 'Residual Poison', 'residualPoison'],
['richese-semuta-drug', 'Semuta Drug', 'semutaDrug'],
['richese-stone-burner', 'Stone Burner', 'stoneBurner'],
['richese-mirror-weapon', 'Mirror Weapon', 'mirrorWeapon'],
['richese-portable-snooper', 'Portable Snooper', 'portableSnooper'],
['richese-distrans', 'Distrans', 'distrans'],
['richese-juice-of-sapho', 'Juice of Sapho', 'juiceOfSapho'],
['richese-karama', 'Karama', 'karama'],
['richese-nullentropy-box', 'Nullentropy Box', 'nullentropyBox'],
],
);
assert.equal(
RICHESE_CARD_DEFINITIONS.reduce((sum, card) => sum + card.quantity, 0),
10,
);
const combined = [...baseDeck(), ...ixDeck(), ...richeseCards()];
assert.equal(new Set(combined.map((card) => card.id)).size, 57);
});
void test('inventory is fresh and deeply frozen metadata cannot be changed through returned cards', () => {
const first = richeseCards();
first[0].name = 'Changed';
first.pop();
assert.equal(richeseCards().length, 10);
assert.equal(richeseCards()[0].name, 'Ornithopter');
assert.ok(Object.isFrozen(RICHESE_CARD_DEFINITIONS));
for (const definition of RICHESE_CARD_DEFINITIONS) {
for (const value of [
definition,
definition.card,
definition.gameplay,
definition.behavior,
definition.behavior.contract,
definition.verification,
definition.verification.unresolved,
])
assert.ok(Object.isFrozen(value));
}
});
void test('identity lookup requires matching physical identity rather than a familiar name or effect', () => {
for (const card of richeseCards()) {
assert.equal(richeseCardDefinition(card)?.card.name, card.name);
for (const invalid of [
{ ...card, id: '' },
{ ...card, name: 'Unrelated' },
{ ...card, effect: undefined },
{ ...card, kind: 'worthless' as const },
{ ...card, id: card.id.toUpperCase() },
])
assert.equal(richeseCardDefinition(invalid), undefined);
}
assert.equal(
richeseCardDefinition(baseDeck().find((card) => card.effect === 'karama')!),
undefined,
);
});
void test('printed battle categories remain explicit without activating unsupported battle kinds or decks', () => {
const weapons = RICHESE_CARD_DEFINITIONS.filter(
(d) => d.behavior.battleCategory === 'weapon',
);
assert.deepEqual(
weapons.map((d) => d.card.name),
['Stone Burner', 'Mirror Weapon'],
);
assert.deepEqual(
RICHESE_CARD_DEFINITIONS.filter(
(d) => d.behavior.battleCategory === 'poison-defense',
).map((d) => d.card.name),
['Portable Snooper'],
);
assert.equal(treacheryDeck().length, 33);
assert.equal(treacheryDeck(['ix']).length, 47);
assert.throws(() => treacheryDeck(['richese']), /not implemented/);
assert.equal(RICHESE_COLLECTION.activation, 'not-implemented');
assert.equal(RICHESE_COLLECTION.cacheCountsTowardHand, false);
});
void test('normal discard routing and exceptional Box ordering do not replenish the faction cache', () => {
assert.equal(RICHESE_COLLECTION.discardedDestination, 'normal-discard');
for (const definition of RICHESE_CARD_DEFINITIONS) {
assert.equal(
definition.behavior.spiceCost,
definition.card.effect === 'nullentropyBox' ? 2 : 0,
);
assert.equal(
definition.behavior.disposal,
definition.card.effect === 'nullentropyBox'
? 'normal-discard-top-after-shuffle'
: 'normal-discard',
);
}
const box = RICHESE_CARD_DEFINITIONS.find(
(d) => d.card.effect === 'nullentropyBox',
)!;
assert.match(
box.gameplay.join(' '),
/Secretly search the Treachery discard pile/,
);
assert.doesNotMatch(box.gameplay.join(' '), /next.*deck card/i);
assert.match(box.gameplay.join(' '), /other than a Nullentropy Box/);
assert.match(box.gameplay.join(' '), /then discard Nullentropy Box on top/);
});
void test('original guides preserve critical face restrictions and separate verification from runtime', () => {
const guide = (effect: string) =>
RICHESE_CARD_DEFINITIONS.find(
(d) => d.card.effect === effect,
)!.gameplay.join(' ');
assert.match(
guide('stoneBurner'),
/ignore the strength of leaders who otherwise survive/,
);
assert.match(guide('stoneBurner'), /more undialed force tokens/);
assert.match(guide('mirrorWeapon'), /copied weapon must be used first/);
assert.match(guide('portableSnooper'), /both a weapon and a Worthless Card/);
assert.match(guide('portableSnooper'), /Voice prevents/);
assert.match(
guide('juiceOfSapho'),
/Once-Around auction permits using it to bid last/,
);
assert.match(guide('distrans'), /other than during a bid/);
assert.match(guide('semutaDrug'), /another player discards/);
assert.match(guide('residualPoison'), /No player collects spice/);
for (const definition of RICHESE_CARD_DEFINITIONS) {
assert.equal(definition.verification.inventory, 'physical-faces-verified');
assert.equal(definition.verification.combinedInteractions, 'incomplete');
assert.ok(definition.verification.unresolved.length > 0);
assert.ok(definition.gameplay.length >= 2);
assert.doesNotMatch(definition.gameplay.join(' '), /https?:\/\//);
}
});