Files
Dune/tests/choam-kull.test.ts
T

247 lines
12 KiB
TypeScript

import test from 'node:test';
import assert from 'node:assert/strict';
import { baseDeck, ixStandardCards, type Card } from '../game/cards';
import { canUseAsKarama } from '../game/karama';
import { canUseAsKaramaRole } from '../game/shrine';
import {
activeKullRestrictions,
distinctKullCounter,
kullBlocksKarama,
kullCounterCards,
kullNativeCostCards,
kullNexusCostCards,
validateKullAttemptStamp,
validateKullPhaseRestrictions,
type KullAttemptStamp,
type KullContext,
type KullPhaseRestriction,
} from '../game/choam-kull';
const context: KullContext = {
turn: 2,
phase: 5,
players: [
{ id: 'choam', faction: 'choam' },
{ id: 'actor', faction: 'beneGesserit' },
{ id: 'third', faction: 'atreides' },
],
};
void test('a Kull restriction expires by both turn and phase and follows actor, not beneficiary', () => {
const restriction = Object.freeze({ player: 'actor', turn: 2, phase: 5 });
const history = Object.freeze([
restriction,
Object.freeze({ player: 'third', turn: 1, phase: 5 }),
]);
validateKullPhaseRestrictions(context, history);
assert.equal(kullBlocksKarama(history, 2, 5, 'actor'), true);
assert.equal(kullBlocksKarama(history, 2, 5, 'third'), false);
assert.equal(kullBlocksKarama(history, 2, 5, 'choam'), false);
for (const [turn, phase] of [[2, 6], [3, 5], [3, 0]]) {
assert.equal(kullBlocksKarama(history, turn, phase, 'actor'), false);
assert.deepEqual(activeKullRestrictions(history, turn, phase), []);
}
assert.deepEqual(activeKullRestrictions(history, 2, 5), [restriction]);
assert.notEqual(activeKullRestrictions(history, 2, 5)[0], restriction);
assert.equal(kullBlocksKarama(undefined, 2, 5, 'actor'), false);
});
void test('malformed and future restriction history is rejected without repair or input mutation', () => {
const valid = { player: 'actor', turn: 2, phase: 5 };
const malformed: unknown[] = [
null,
{ ...valid, turn: 0 },
{ ...valid, turn: 1.5 },
{ ...valid, turn: Number.MAX_SAFE_INTEGER + 1 },
{ ...valid, turn: 3 },
{ ...valid, phase: -1 },
{ ...valid, phase: 9 },
{ ...valid, phase: 5.5 },
{ ...valid, phase: 6 },
{ ...valid, player: 'foreign' },
{ ...valid, player: '' },
{ ...valid, extra: true },
];
for (const record of malformed) {
const history = [record];
const before = structuredClone(history);
assert.throws(() => validateKullPhaseRestrictions(context, history));
assert.deepEqual(history, before);
}
const duplicate = [valid, { ...valid }];
const before = structuredClone(duplicate);
assert.throws(() => validateKullPhaseRestrictions(context, duplicate));
assert.deepEqual(duplicate, before);
assert.throws(() => validateKullPhaseRestrictions(context, valid));
assert.throws(() => validateKullPhaseRestrictions({ ...context, turn: 0 }, []));
assert.equal(kullBlocksKarama([{ ...valid, phase: 9 }], 2, 9, 'actor'), false);
});
void test('valid expired restrictions stay inert without erasing another player current restriction', () => {
const history: KullPhaseRestriction[] = [
{ player: 'actor', turn: 1, phase: 8 },
{ player: 'actor', turn: 2, phase: 4 },
{ player: 'third', turn: 2, phase: 5 },
];
const before = structuredClone(history);
validateKullPhaseRestrictions(context, history);
assert.equal(kullBlocksKarama(history, 2, 5, 'actor'), false);
assert.equal(kullBlocksKarama(history, 2, 5, 'third'), true);
assert.deepEqual(activeKullRestrictions(history, 2, 5), [history[2]]);
assert.deepEqual(history, before);
});
void test('saved attempts require the current stamp, original seated actors and unique physical identity', () => {
const physical = baseDeck();
const card = physical.find(candidate => candidate.effect === 'karama')!;
const attempt: KullAttemptStamp = {
event: 'attempt-1', player: 'choam', owner: 'actor', card: card.id,
turn: 2, phase: 5, stage: 'offer', form: 'printed',
};
const patches: Partial<KullAttemptStamp>[] = [
{ event: '' }, { player: 'third' }, { player: 'foreign' },
{ owner: 'choam' }, { owner: 'foreign' }, { card: 'invented' },
{ turn: 1 }, { turn: 3 }, { turn: 0 }, { phase: 4 }, { phase: 6 },
{ phase: 9 }, { stage: 'complete' as never }, { form: 'converted' as never },
];
for (const patch of patches) {
const invalid = { ...attempt, ...patch };
const before = structuredClone({ invalid, physical, context });
assert.throws(() => validateKullAttemptStamp(context, invalid, physical));
assert.deepEqual({ invalid, physical, context }, before);
}
for (const inventory of [physical.filter(candidate => candidate.id !== card.id), [...physical, card]]) {
const before = structuredClone(inventory);
assert.throws(() => validateKullAttemptStamp(context, attempt, inventory));
assert.deepEqual(inventory, before);
}
// Validate both live stages after serialization; the exact original stays reserved.
for (const stage of ['offer', 'counter'] as const) {
const saved = JSON.parse(JSON.stringify({ ...attempt, stage })) as KullAttemptStamp;
validateKullAttemptStamp(context, saved, physical);
assert.equal(distinctKullCounter(saved.card, attempt.card), false);
assert.equal(distinctKullCounter(physical.find(candidate =>
candidate.effect === 'karama' && candidate.id !== card.id)!.id, saved.card), true);
}
const substitution = physical.find(candidate => candidate.kind === 'worthless')!;
const bgSaved = { ...attempt, card: substitution.id, form: 'substitution' as const };
validateKullAttemptStamp(context, bgSaved, physical);
assert.equal(distinctKullCounter(substitution.id, bgSaved.card), false);
});
void test('native Kull costs require canonical identity and unique actual custody, not a matching label', () => {
const physical = [...baseDeck(), ...ixStandardCards()];
const kull = physical.find(card => card.id === 'ix-kull-wahad')!;
const another = physical.find(card => card.kind === 'worthless' && card.id !== kull.id)!;
const held = Object.freeze([Object.freeze(kull), Object.freeze(another)]);
const before = structuredClone({ held, physical });
assert.deepEqual(kullNativeCostCards(held, physical).map(card => card.id), [kull.id]);
assert.deepEqual(kullNativeCostCards([another], physical), []);
assert.deepEqual(kullNativeCostCards(held, physical, [kull.id]), []);
assert.deepEqual(kullNativeCostCards(held, physical.filter(card => card.id !== kull.id)), []);
assert.deepEqual(kullNativeCostCards(held, [...physical, { ...kull }]), []);
assert.deepEqual(kullNativeCostCards([kull, { ...kull }], physical), []);
for (const fake of [
{ ...another, name: kull.name },
{ ...kull, kind: 'special' as const },
{ ...kull, name: 'different' },
{ ...kull, effect: 'karama' },
]) {
assert.deepEqual(kullNativeCostCards([fake], physical), []);
}
assert.deepEqual({ held, physical }, before);
});
void test('Nexus Kull accepts canonical non-Worthless Treachery fuel without narrowing to printed Kull', () => {
const physical = [...baseDeck(), ...ixStandardCards()];
const weapon = physical.find(card => card.kind === 'projectile')!;
const karama = physical.find(card => card.effect === 'karama')!;
const worthless = physical.find(card => card.kind === 'worthless' && card.id !== 'ix-kull-wahad')!;
const kull = physical.find(card => card.id === 'ix-kull-wahad')!;
const held: Card[] = JSON.parse(JSON.stringify([weapon, karama, worthless, kull]));
const before = structuredClone({ held, physical });
assert.deepEqual(kullNexusCostCards(held, physical).map(card => card.id),
[weapon.id, karama.id, worthless.id, kull.id]);
assert.deepEqual(kullNativeCostCards(held, physical).map(card => card.id), [kull.id]);
assert.deepEqual(kullNexusCostCards(held, physical, [karama.id, kull.id, 'absent']).map(card => card.id),
[weapon.id, worthless.id]);
assert.deepEqual(kullNexusCostCards([], physical), []);
assert.deepEqual({ held, physical }, before);
});
void test('Nexus Kull excludes counterfeit, duplicated and reserved custody without losing unrelated real fuel', () => {
const physical = [...baseDeck(), ...ixStandardCards()];
const weapon = physical.find(card => card.kind === 'projectile')!;
const karama = physical.find(card => card.effect === 'karama')!;
const held = Object.freeze([Object.freeze({ ...weapon }), Object.freeze({ ...karama })]);
const before = structuredClone({ held, physical });
for (const fake of [
{ ...weapon, id: 'invented' },
{ ...weapon, id: '' },
{ ...weapon, name: 'counterfeit' },
{ ...weapon, kind: 'worthless' as const },
{ ...weapon, effect: 'karama' },
]) {
assert.deepEqual(kullNexusCostCards([fake, karama], physical).map(card => card.id), [karama.id]);
}
assert.deepEqual(kullNexusCostCards([weapon, { ...weapon }, karama], physical).map(card => card.id),
[karama.id]);
assert.deepEqual(kullNexusCostCards(held, [...physical, { ...weapon }]).map(card => card.id),
[karama.id]);
assert.deepEqual(kullNexusCostCards(held, physical.filter(card => card.id !== weapon.id)).map(card => card.id),
[karama.id]);
assert.deepEqual(kullNexusCostCards(held, physical, [weapon.id, karama.id]), []);
assert.deepEqual({ held, physical }, before);
});
void test('distinct counters honor effective BG and revealed Shrine eligibility without reusing the original', () => {
const physical = baseDeck();
const karamas = physical.filter(card => card.effect === 'karama');
const worthless = physical.find(card => card.kind === 'worthless')!;
const truthtrance = physical.find(card => card.effect === 'truthtrance')!;
const held = [karamas[0], karamas[1], worthless, truthtrance];
const before = structuredClone({ held, physical });
assert.deepEqual(kullCounterCards(held, physical, karamas[0].id,
card => canUseAsKarama(false, 'atreides', card)).map(card => card.id), [karamas[1].id]);
assert.deepEqual(kullCounterCards(held, physical, worthless.id,
card => canUseAsKarama(true, 'beneGesserit', card)).map(card => card.id),
[karamas[0].id, karamas[1].id]);
assert.deepEqual(kullCounterCards(held, physical, karamas[0].id,
card => canUseAsKarama(true, 'beneGesserit', card)).map(card => card.id),
[karamas[1].id, worthless.id]);
const shrine = {
advanced: true,
discoveries: { tokens: [{ face: 'shrine', status: 'placed', revealedTurn: 2 }] },
};
const actor = { id: 'third', faction: 'atreides' as const, forces: { 'shrine:0': 1 } };
assert.deepEqual(kullCounterCards(held, physical, karamas[0].id,
card => canUseAsKaramaRole(shrine, actor, card)).map(card => card.id),
[karamas[1].id, truthtrance.id]);
assert.deepEqual(kullCounterCards(held, physical, karamas[0].id,
card => canUseAsKaramaRole(shrine, { ...actor, forces: {} }, card)).map(card => card.id),
[karamas[1].id]);
const block = [{ player: actor.id, turn: 2, phase: 5 }];
assert.deepEqual(kullCounterCards(held, physical, karamas[0].id,
card => !kullBlocksKarama(block, 2, 5, actor.id) && canUseAsKaramaRole(shrine, actor, card)), []);
assert.deepEqual({ held, physical }, before);
});
void test('counter rejection checks exact physical custody before invoking effective eligibility', () => {
const physical = baseDeck();
const original = physical.find(card => card.effect === 'karama')!;
const distinct = physical.find(card => card.effect === 'karama' && card.id !== original.id)!;
const forged: Card = { ...distinct, kind: 'worthless', effect: undefined };
let eligibilityCalls = 0;
const eligible = (card: Card) => { eligibilityCalls++; return card.effect === 'karama'; };
assert.deepEqual(kullCounterCards([original], physical, original.id, eligible), []);
assert.deepEqual(kullCounterCards([forged], physical, original.id, eligible), []);
assert.deepEqual(kullCounterCards([distinct, distinct], physical, original.id, eligible), []);
assert.deepEqual(kullCounterCards([distinct], [...physical, distinct], original.id, eligible), []);
assert.deepEqual(kullCounterCards([distinct], physical, original.id, eligible, [distinct.id]), []);
assert.equal(eligibilityCalls, 0);
assert.deepEqual(kullCounterCards([distinct], physical, original.id, eligible).map(card => card.id), [distinct.id]);
assert.equal(eligibilityCalls, 1);
assert.equal(distinctKullCounter('', original.id), false);
assert.equal(distinctKullCounter(distinct.id, ''), false);
});