Files
Dune/tests/richese-card-presentation.test.ts
T
Quintine 788453e07e Integrate Smuggler companion with owned Richese No-Field
Connect the GF9 FAQ combination: one free physical reserve force accompanies an owned concealed No-Field at its unchanged one-marker price. Preserve zero/three/five token values, actual supply at later reveal, optional human choice and all four legal AI paths. Genuine CHOAM & Richese Leader Skills development setup adds the two sourced battle cards without duplicating their Ix identities. Public mode and publication gates stay closed; allied and remaining combined routes are unfinished.

Bind the private declaration across initial No-Field cancellation and later Advanced Guild interception, including saved interruptions. Validate before projection/actions/normalization, hide the internal proof, reject altered declarations immutably, and withdraw the companion once only on successful commitment. Independent rules/privacy/persistence review found no blocker. Preserve other audit setup gates despite expanded deck assembly. Repair the existing randomized recovery test with a genuine seeded Rihani offer and explicit human choice, replacing an inappropriate AI-policy call on a human seat. Initial Karama and later Guild stop retain their distinct existing opportunity outcomes.

Validation: 72 focused regressions plus 10 deck tests; typecheck, lint, 4520 offline tests, production build and 43 HTTP tests passed. SQLite restart, concurrent allowance/cancellation and exact physical custody passed. Browser genuine assignment, private token selection, opt-in, one-spice shipment, refresh, five-force reveal and final refresh preserved six physical Arrakeen forces, 14 reserves, zero Tanks, four spice, Crysknife and Soo-Soo Sook. Genuine four-profile Basic/Advanced games completed in 77/116 actions with 3/4 JSON restores and no rejected candidates; Advanced exercised one companion shipment and later reveal.

After the outage the existing server was healthy. Preserved all 529 opening rooms and original seat/recovery records (563 after QA/HTTP). SQLite integrity and foreign keys passed. No restart, database reset or automation.

Verified source tree: c2aee4138bcb46a05814a340d62eadace3470247ca620844eb0375e78ea4d6c4
2026-09-13 21:37:50 +10:00

223 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import test from 'node:test';
import assert from 'node:assert/strict';
import {
cardPresentation,
richeseCardActionBlock,
} from '../game/card-presentation';
import { baseDeck, ixDeck, treacheryDeck, type Card } from '../game/cards';
import { ecazTreacheryCards } from '../game/ecaz-cards';
import { RICHESE_CARD_DEFINITIONS, richeseCards } from '../game/richese-cards';
import { RULE_TOPICS, RULE_CHECKLIST_AREAS } from '../game/reference';
void test('every Richese physical card presents its complete guide and actual printed category without changing its runtime kind', () => {
for (const definition of RICHESE_CARD_DEFINITIONS) {
const supplied = { ...definition.card };
const original = structuredClone(supplied);
const presentation = cardPresentation(supplied);
assert.deepEqual(supplied, original);
assert.equal(presentation.category, definition.printedType);
assert.equal(presentation.guidance, definition.summary);
assert.deepEqual(presentation.gameplay, definition.gameplay);
assert.equal(
presentation.role,
definition.behavior.battleCategory === 'weapon'
? 'weapon'
: definition.behavior.battleCategory === 'poison-defense'
? 'defense'
: 'utility',
);
assert.match(
presentation.availability!,
definition.card.effect === 'stoneBurner'
? /Combined allocation timing and Ix timing remain guarded/
: /effect integration and verification are incomplete/,
);
assert.deepEqual(
presentation.topics.map((topic) => topic.id),
[
definition.card.effect === 'juiceOfSapho'
? 'juice-of-sapho'
: definition.card.effect === 'stoneBurner'
? 'stone-burner'
: definition.card.effect === 'portableSnooper'
? 'portable-snooper'
: 'richese-cards',
],
);
assert.equal(supplied.kind, 'special');
}
});
void test('presentation does not export source-verification metadata or unverified interaction guesses', () => {
for (const definition of RICHESE_CARD_DEFINITIONS) {
const presentation = cardPresentation(definition.card);
assert.deepEqual(Object.keys(presentation).sort(), [
'availability',
'category',
'gameplay',
'guidance',
'role',
'topics',
]);
const serialized = JSON.stringify(presentation);
for (const note of definition.verification.unresolved)
assert.equal(serialized.includes(note), false);
for (const metadata of [
'physical-faces-verified',
'original-paraphrase-of-physical-faces',
'combinedInteractions',
'https://cdn.anyfinder',
])
assert.equal(serialized.includes(metadata), false);
}
});
void test('missing, altered and merely similar Richese identities receive no canonical full guide', () => {
for (const card of richeseCards()) {
const impostors: Card[] = [
{ ...card, id: 'other-card' },
{ ...card, name: 'Other name' },
{ ...card, kind: 'poison' },
{ ...card, effect: 'unknown-effect' },
{ ...card, effect: undefined },
{ ...card, id: card.id.toUpperCase() },
];
for (const impostor of impostors) {
const result = cardPresentation(impostor);
assert.equal(result.gameplay, undefined);
assert.equal(result.availability, undefined);
assert.equal(
result.topics.some((topic) => topic.id === 'richese-cards'),
false,
);
}
}
});
void test('the public collection and ten full card topics are searchable, linked and identify implemented handlers', () => {
const ids = new Set(RULE_TOPICS.map((topic) => topic.id));
assert.equal(ids.size, RULE_TOPICS.length);
const hub = RULE_TOPICS.find((topic) => topic.id === 'richese-cards')!;
assert.deepEqual(
hub.checklist!.map((item) => item.area),
[...RULE_CHECKLIST_AREAS],
);
assert.equal(hub.coverage, 'Partial');
assert.equal(
hub.checklist!.find((item) => item.area === 'AI')!.status,
'Partial',
);
assert.match(
hub.steps.join(' '),
/not the contents of any player’s current cache or hand/,
);
assert.match(
hub.steps.join(' '),
/Mirror Weapon and Semuta Drug remain unfinished/,
);
assert.match(hub.steps.join(' '), /Discard contents remain private/);
for (const definition of RICHESE_CARD_DEFINITIONS) {
const id = `card-${definition.card.id}`;
const topic = RULE_TOPICS.find((candidate) => candidate.id === id)!;
assert.ok(hub.related!.includes(id));
assert.equal(topic.coverage, 'Partial');
assert.ok(topic.summary.includes(definition.printedType));
assert.deepEqual(topic.steps.slice(0, -1), definition.gameplay);
assert.match(
topic.steps.at(-1)!,
definition.card.effect === 'juiceOfSapho'
? /Juice of Sapho panel/
: definition.card.effect === 'karama'
? /existing generic handler/
: definition.card.effect === 'distrans'
? /Distrans transfer panel/
: definition.card.effect === 'nullentropyBox'
? /Nullentropy Box panel/
: definition.card.effect === 'ornithopter'
? /Ornithopter movement controls/
: definition.card.effect === 'residualPoison'
? /Residual Poison panel/
: definition.card.effect === 'portableSnooper'
? /Portable Snooper/
: definition.card.effect === 'stoneBurner'
? /Stone Burner/
: /actions are not enabled/,
);
for (const related of topic.related!) assert.ok(ids.has(related));
assert.doesNotMatch(JSON.stringify(topic), /https?:\/\//);
}
for (const id of ['implementation-checklist', 'choam-modules'])
assert.ok(
RULE_TOPICS.find((topic) => topic.id === id)!.related!.includes(
'richese-cards',
),
);
});
void test('base, Ix and Ecaz presentations stay unchanged as the two CHOAM deck replacements become available', () => {
for (const card of [...baseDeck(), ...ixDeck()]) {
const presentation = cardPresentation(card);
assert.equal(presentation.availability, undefined);
assert.equal(presentation.gameplay, undefined);
assert.equal(
presentation.topics.some((topic) => topic.id === 'richese-cards'),
false,
);
}
for (const card of ecazTreacheryCards()) {
const presentation = cardPresentation(card);
assert.equal(presentation.category, 'Special treachery');
assert.ok(presentation.gameplay!.length >= 4);
assert.equal(presentation.availability, undefined);
assert.deepEqual(presentation.topics, []);
}
assert.equal(treacheryDeck().length, 33);
assert.equal(treacheryDeck(['ix']).length, 47);
assert.equal(treacheryDeck(['choam']).length, 35);
assert.throws(() => treacheryDeck(['richese']), /not implemented/);
});
void test('unsupported canonical Richese effects have an explicit action block while Karama and unrelated cards retain their validators', () => {
for (const card of richeseCards()) {
if (card.effect === 'karama')
assert.equal(richeseCardActionBlock(card), null);
else if (card.effect === 'portableSnooper')
assert.match(richeseCardActionBlock(card)!, /late-defense panel/);
else if (card.effect === 'stoneBurner')
assert.match(richeseCardActionBlock(card)!, /battle weapon slot/);
else if (card.effect === 'residualPoison')
assert.match(richeseCardActionBlock(card)!, /Residual Poison panel/);
else if (card.effect === 'ornithopter')
assert.match(
richeseCardActionBlock(card)!,
/Ornithopter movement controls/,
);
else if (card.effect === 'nullentropyBox')
assert.match(richeseCardActionBlock(card)!, /Nullentropy Box panel/);
else if (card.effect === 'distrans')
assert.match(richeseCardActionBlock(card)!, /Distrans transfer panel/);
else if (card.effect === 'juiceOfSapho')
assert.match(richeseCardActionBlock(card)!, /Juice of Sapho panel/);
else
assert.match(
richeseCardActionBlock(card)!,
/effect is not implemented yet/,
);
assert.equal(
richeseCardActionBlock({ ...card, id: 'another-physical-card' }),
null,
);
assert.equal(
richeseCardActionBlock({ ...card, name: 'Another name' }),
null,
);
assert.equal(richeseCardActionBlock({ ...card, kind: 'worthless' }), null);
assert.equal(
richeseCardActionBlock({ ...card, effect: 'another-effect' }),
null,
);
}
for (const card of [...baseDeck(), ...ixDeck(), ...ecazTreacheryCards()])
assert.equal(richeseCardActionBlock(card), null);
});