Files
Dune/tests/tupile-intelligence-answer.test.ts

173 lines
6.1 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, ixDeck, type Card } from '../game/cards';
import { richeseCards } from '../game/richese-cards';
import { ecazTreacheryCards } from '../game/ecaz-cards';
import { quoteTupileIntelligenceAnswer as answer } from '../game/tupile-intelligence-answer';
const inventory = () => [
...baseDeck(),
...ixDeck(),
...richeseCards(),
...ecazTreacheryCards(),
];
const card = (id: string) => inventory().find((c) => c.id === id)!;
void test('every registered physical Treachery card has its printed primary category, independently of activation', () => {
const weapons = new Set([
...Array.from({ length: 9 }, (_, n) => `treachery-${n}`),
'ix-poison-tooth',
'ix-artillery',
'ix-poison-blade',
'ix-weirding-way',
'ix-hunter-seeker',
'ix-basilia-weapon',
'richese-stone-burner',
'richese-mirror-weapon',
]);
const defenses = new Set([
...Array.from({ length: 8 }, (_, n) => `treachery-${n + 9}`),
'ix-shield-snooper',
'ix-chemistry',
'ix-shield',
'ix-snooper',
'richese-portable-snooper',
]);
const cards = inventory();
assert.equal(cards.length, 60);
assert.equal(new Set(cards.map((c) => c.id)).size, 60);
for (const c of cards) {
assert.deepEqual(
answer([c], 7, 'weapons'),
{ spice: 7, count: Number(weapons.has(c.id)) },
c.id,
);
assert.deepEqual(
answer([c], 7, 'defenses'),
{ spice: 7, count: Number(defenses.has(c.id)) },
c.id,
);
}
assert.deepEqual(answer(cards, 0, 'weapons'), { spice: 0, count: 17 });
assert.deepEqual(answer(cards, 0, 'defenses'), { spice: 0, count: 13 });
});
void test('hybrids count once in their default printed category, while slot-compatible noncombat cards count neither', () => {
const hybrids = [
'ix-poison-blade',
'ix-shield-snooper',
'ix-weirding-way',
'ix-chemistry',
].map(card);
assert.deepEqual(answer(hybrids, 2, 'weapons'), { spice: 2, count: 2 });
assert.deepEqual(answer(hybrids, 2, 'defenses'), { spice: 2, count: 2 });
const neither = inventory().filter(
(c) =>
c.kind === 'worthless' || c.kind === 'hero' || c.id.startsWith('ecaz-'),
);
assert.equal(neither.length, 11);
assert.equal(answer(neither, 4, 'weapons').count, 0);
assert.equal(answer(neither, 4, 'defenses').count, 0);
for (const c of hybrids)
Object.defineProperty(c, 'battleRole', {
get() {
throw new Error('Held count must not inspect a committed battle role');
},
});
assert.equal(answer(hybrids, 2, 'weapons').count, 2);
});
void test('Richese transport specials use canonical physical printed categories, not name or effect guesses', () => {
const cards = richeseCards();
assert.ok(cards.every((c) => c.kind === 'special'));
assert.equal(answer(cards, 3, 'weapons').count, 2);
assert.equal(answer(cards, 3, 'defenses').count, 1);
assert.equal(
answer([card('richese-residual-poison')], 3, 'weapons').count,
0,
);
for (const forged of [
{ ...card('richese-residual-poison'), effect: 'stoneBurner' },
{ ...card('richese-stone-burner'), id: 'forged-weapon' },
{ ...card('richese-mirror-weapon'), kind: 'poison' },
{ ...card('richese-portable-snooper'), name: 'Snooper' },
])
assert.throws(() => answer([forged as Card], 3, 'weapons'));
});
void test('same-name physical copies count separately; duplicate IDs and invented leader-skill cards reject', () => {
const shields = inventory().filter((c) => c.name === 'Shield');
const sparse: Card[] = [];
sparse.length = 2;
assert.equal(answer(shields, 0, 'defenses').count, 5);
for (const invalid of [
[shields[0], shields[0]],
[shields[0], { ...shields[0] }],
[
{
id: 'leader-skill-master-of-assassins',
name: 'Master of Assassins',
kind: 'poison',
},
],
[{ ...card('treachery-0'), id: 'treachery-999' }],
[null],
sparse,
])
assert.throws(() => answer(invalid as Card[], 0, 'weapons'));
});
void test('zero and safe balances are truthful, malformed requests reject without partial private answers or mutation', () => {
const hand = [card('treachery-0')];
assert.deepEqual(answer([], 0, 'defenses'), { spice: 0, count: 0 });
assert.deepEqual(answer(hand, Number.MAX_SAFE_INTEGER, 'weapons'), {
spice: Number.MAX_SAFE_INTEGER,
count: 1,
});
for (const spice of [-1, 0.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])
assert.throws(() => answer(hand, spice, 'weapons'));
for (const category of ['both', 'weapon', null, 1])
assert.throws(() => answer(hand, 0, category as 'weapons'));
const corrupt = [
hand[0],
{ ...card('richese-stone-burner'), name: 'Private corrupt face' },
];
const before = JSON.stringify(corrupt);
assert.throws(
() => answer(corrupt, 109, 'weapons'),
(error: Error) => {
assert.doesNotMatch(
error.message,
/treachery|richese|109|Private corrupt face/,
);
return true;
},
);
assert.equal(JSON.stringify(corrupt), before);
});
void test('the answer is a detached two-number snapshot across hand changes, JSON restore and frozen inputs', () => {
const hand = [card('treachery-0'), card('treachery-9')];
const receipt = answer(hand, 12, 'weapons');
assert.deepEqual(Object.keys(receipt).sort(), ['count', 'spice']);
assert.deepEqual(JSON.parse(JSON.stringify(receipt)), {
spice: 12,
count: 1,
});
hand.splice(0, 1);
assert.equal(answer(hand, 1, 'weapons').count, 0);
assert.deepEqual(receipt, { spice: 12, count: 1 });
Object.freeze(hand[0]);
Object.freeze(hand);
assert.deepEqual(answer(hand, 1, 'defenses'), { spice: 1, count: 1 });
});
void test('the documented legacy Ellaca Drug name is recognized without changing its saved identity or the distinct Ix weapon', () => {
const legacy = { ...card('treachery-7'), name: 'Basilia Weapon' };
const hand = [legacy, card('ix-basilia-weapon')];
const before = JSON.stringify(hand);
assert.equal(answer(hand, 6, 'weapons').count, 2);
assert.equal(JSON.stringify(hand), before);
assert.throws(() => answer([{ ...legacy, kind: 'shield' }], 6, 'defenses'));
});