Files
Dune/tests/movement-discard-knowledge.test.ts
T
Quintine 49992d322f Include Ghola force returns in Basic movement promises
Use exact physical Ghola preparation and known/paid Box recovery without
crediting conditional income or normal revival allowances. Guide real
force return, paid shipment and qualifying ground movement; retain the
existing incomplete-mode and final acceptance boundaries.
2026-10-24 16:22:32 +11:00

110 lines
7.0 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import type { Card } from '../game/cards';
import type { FreshDiscardBatch } from '../game/semuta-drug';
import {
recordMovementDiscardKnowledge, knownMovementDiscards, recordMovementBoxKnowledge,
recordMovementClaimKnowledge, clearMovementDiscardKnowledge,
type MovementDiscardKnowledge,
} from '../game/movement-discard-knowledge';
const seats = ['owner', 'rival', 'observer'];
const hajr: Card = { id: 'physical-hajr', kind: 'special', name: 'Hajr', effect: 'hajr' };
const ghola: Card = { id: 'physical-ghola', kind: 'special', name: 'Tleilaxu Ghola', effect: 'ghola' };
const ornithopter: Card = { id: 'richese-ornithopter', kind: 'special', name: 'Ornithopter', effect: 'ornithopter' };
const kulon: Card = { id: 'physical-kulon', kind: 'worthless', name: 'Kulon' };
const shield: Card = { id: 'physical-shield', kind: 'shield', name: 'Shield' };
const entry = (card: Card, discardedBy = 'owner', publicFace = false): FreshDiscardBatch['entries'][number] =>
({ card, discardedBy, publicFace });
void test('Ghola force preparation uses only actually observed public, private or paid Box faces', () => {
const privateKnowledge = recordMovementDiscardKnowledge(undefined, seats, [entry(ghola)]);
assert.deepEqual(knownMovementDiscards(privateKnowledge, 'owner', [ghola]), [ghola]);
assert.deepEqual(knownMovementDiscards(privateKnowledge, 'rival', [ghola]), []);
const publicKnowledge = recordMovementDiscardKnowledge(privateKnowledge, seats, [entry(ghola, 'owner', true)]);
for (const player of seats) assert.deepEqual(knownMovementDiscards(publicKnowledge, player, [ghola]), [ghola]);
const paidKnowledge = recordMovementBoxKnowledge(publicKnowledge, seats, 'rival', [ghola, shield]);
assert.deepEqual(knownMovementDiscards(paidKnowledge, 'rival', [ghola, shield]), [ghola]);
assert.deepEqual(knownMovementDiscards(paidKnowledge, 'owner', [ghola]), []);
assert.deepEqual(knownMovementDiscards(paidKnowledge, 'rival', []), []);
});
void test('a public relevant disposal is remembered by all seats without creating unrelated face knowledge', () => {
const entries = [entry(hajr, 'rival', true), entry(shield, 'rival', true)];
const before = structuredClone(entries);
const result = recordMovementDiscardKnowledge(undefined, seats, entries);
assert.deepEqual(result, { observer: [hajr.id], owner: [hajr.id], rival: [hajr.id] });
for (const player of seats) assert.deepEqual(knownMovementDiscards(result, player, [shield, hajr]), [hajr]);
assert.deepEqual(entries, before);
});
void test('a private disposal teaches only its holder and an unknown relevant face cannot leak through lookup', () => {
const result = recordMovementDiscardKnowledge(undefined, seats, [entry(hajr), entry(kulon, 'rival')]);
assert.deepEqual(result, { owner: [hajr.id], rival: [kulon.id] });
assert.deepEqual(knownMovementDiscards(result, 'owner', [hajr, kulon, ornithopter]), [hajr]);
assert.deepEqual(knownMovementDiscards(result, 'observer', [hajr, kulon, ornithopter]), []);
});
void test('canonical relevance excludes an Ornithopter lookalike, unrelated Worthless and absent state', () => {
const fake = { ...ornithopter, id: 'different-ornithopter' };
const otherWorthless: Card = { id: 'other-worthless', name: 'Baliset', kind: 'worthless' };
assert.equal(recordMovementDiscardKnowledge(undefined, seats, [entry(fake, 'owner', true), entry(otherWorthless), entry(shield)]), undefined);
assert.equal(recordMovementDiscardKnowledge(undefined, seats, []), undefined);
assert.deepEqual(knownMovementDiscards(undefined, 'owner', [hajr, ornithopter, kulon]), []);
assert.deepEqual(recordMovementDiscardKnowledge(undefined, seats, [entry(ornithopter), entry(kulon)]),
{ owner: [kulon.id, ornithopter.id].sort() });
});
void test('a paid private Box invalidates other memories and grants only its owner the actual remaining faces', () => {
const state: MovementDiscardKnowledge = { owner: [hajr.id], rival: [hajr.id, ornithopter.id], observer: [kulon.id] };
const before = structuredClone(state), pile = [shield, ornithopter, kulon], originalPile = structuredClone(pile);
const result = recordMovementBoxKnowledge(state, seats, 'owner', pile);
assert.deepEqual(result, { owner: [kulon.id, ornithopter.id].sort() });
assert.deepEqual(knownMovementDiscards(result, 'owner', pile), [ornithopter, kulon]);
assert.deepEqual(knownMovementDiscards(result, 'rival', pile), []);
assert.equal(recordMovementBoxKnowledge(state, seats, 'owner', [shield]), undefined);
assert.deepEqual(state, before); assert.deepEqual(pile, originalPile);
});
void test('a private mixed claim removes ambiguous fresh memories while preserving identities outside that batch', () => {
const state: MovementDiscardKnowledge = {
owner: [hajr.id, ornithopter.id], rival: [hajr.id, ornithopter.id], observer: [kulon.id, ornithopter.id],
};
const before = structuredClone(state), entries = [entry(hajr, 'rival'), entry(kulon, 'observer')];
const originalEntries = structuredClone(entries);
const result = recordMovementClaimKnowledge(state, seats, 'owner', entries, hajr.id);
assert.deepEqual(result, {
observer: [ornithopter.id], owner: [kulon.id, ornithopter.id].sort(), rival: [ornithopter.id],
});
assert.deepEqual(knownMovementDiscards(result, 'owner', [ornithopter, kulon]), [ornithopter, kulon]);
assert.deepEqual(knownMovementDiscards(result, 'observer', [ornithopter, kulon]), [ornithopter]);
assert.deepEqual(state, before); assert.deepEqual(entries, originalEntries);
});
void test('a full discard reshuffle clears preparation knowledge without mutating the former record', () => {
const state = { owner: [hajr.id], rival: [ornithopter.id] }, before = structuredClone(state);
assert.equal(clearMovementDiscardKnowledge(state), undefined);
assert.equal(clearMovementDiscardKnowledge(undefined), undefined);
assert.deepEqual(state, before);
});
void test('lookup filters stale identities, preserves physical pile order and returns independent card faces', () => {
const state = { owner: [hajr.id, 'removed-card', kulon.id, shield.id] };
const pile = [kulon, shield, hajr], before = structuredClone(pile);
const faces = knownMovementDiscards(state, 'owner', pile);
assert.deepEqual(faces, [kulon, hajr]);
faces[0].name = 'changed'; faces.push(ornithopter);
assert.deepEqual(pile, before);
assert.deepEqual(state.owner, [hajr.id, 'removed-card', kulon.id, shield.id]);
});
void test('repeated observations use deterministic unique identities and do not alias original state arrays', () => {
const state = { owner: [hajr.id, hajr.id] }, before = structuredClone(state);
const events = [entry(ornithopter, 'owner', true), entry(kulon), entry(hajr)];
const first = recordMovementDiscardKnowledge(state, seats, events)!;
const second = recordMovementDiscardKnowledge(first, [...seats].reverse(), [...events].reverse())!;
assert.deepEqual(first, second); assert.deepEqual(state, before);
second.owner.push('new-id');
assert.deepEqual(first.owner, [hajr.id, kulon.id, ornithopter.id].sort());
});