Files
Dune/tests/truth-opaque-card-plan.test.ts

100 lines
5.1 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { baseDeck } from '../game/cards';
import { viewGame, type Game } from '../game/engine';
import { createDistransPrebidFixture } from './fixture-distrans-prebid';
import {
askTruthTurnCombat as ask,
createTruthTurnCombatFixture as fixture,
declareTruthTurnCombat as declare,
truthTurnCombatGroup as group,
truthTurnCombatInventory as inventory,
truthTurnCombatPlan as plan,
truthTurnCombatPlayer as player,
truthTurnCombatTraitors as traitors,
type TruthTurnCombatFixture,
} from './fixture-truth-turn-combat';
type OpaqueQuote = Game & { _turnActionUnknownCards: string[] };
/** Detached-forecast-only metadata supplement. The fixture's card was really
* acquired and is physically held; this marker models an opaque future card
* only for read-only quotation. It is never installed by a gameplay action. */
function opaqueQuote(source: Game, card: string): OpaqueQuote {
return Object.assign(structuredClone(source), { _turnActionUnknownCards: [card] });
}
function originalWeapon(f: TruthTurnCombatFixture) {
assert.ok(f.ordinary);
assert.equal(f.game.battle!.plans[f.target], undefined);
const held = player(f.game, f.target).hand.find(card => card.id === f.ordinary);
assert.ok(held);
const canonical = baseDeck().find(card => card.id === held.id);
assert.ok(canonical);
assert.equal(canonical.kind, 'projectile');
assert.deepEqual(held, canonical);
assert.equal(f.sourceActions.length, f.sourceActionCount);
assert.equal(Object.hasOwn(f.game, '_turnActionUnknownCards'), false);
return held.id;
}
function physical(source: Game, f: TruthTurnCombatFixture) {
assert.deepEqual(inventory(source), f.originalCards);
assert.deepEqual(traitors(source), f.originalTraitors);
assert.equal(inventory(source).length, 33);
}
void test('detached opaque forecast cannot fund a whole positive weapon-plan quote with the same physically held canonical card', () => {
const f = fixture(), card = originalWeapon(f), original = structuredClone(f.game);
const mixed = plan(f.event, f.territory, f.weaponClaim);
const asked = ask(declare(f).game, f.asker, f.target, mixed), before = structuredClone(asked);
assert.deepEqual(viewGame(asked, f.target).truthTurnActionAnswers, ['yes', 'no']);
const opaque = opaqueQuote(asked, card), quoteBefore = structuredClone(opaque);
assert.ok(player(opaque, f.target).hand.some(held => held.id === card));
const opaqueView = viewGame(opaque, f.target);
assert.deepEqual(opaqueView.truthTurnActionAnswers, ['no']);
assert.equal(Object.hasOwn(opaqueView, '_turnActionUnknownCards'), false);
assert.deepEqual(opaque, quoteBefore);
assert.deepEqual(asked, before);
assert.deepEqual(f.game, original);
assert.equal(Object.hasOwn(asked, '_turnActionUnknownCards'), false);
physical(asked, f); physical(opaque, f); physical(f.game, f);
});
void test('detached opaque weapon forecast retains an independent positive dial-plan alternative and whole No', () => {
const f = fixture(), card = originalWeapon(f), original = structuredClone(f.game);
const mixed = group('or', plan(f.event, f.territory, f.weaponClaim),
plan(f.event, f.territory, { kind: 'dial', compare: 'gte', value: 1 }));
const asked = ask(declare(f).game, f.asker, f.target, mixed), before = structuredClone(asked);
const known = viewGame(asked, f.target).truthTurnActionAnswers;
assert.ok(known?.includes('yes'));
assert.ok(known?.includes('no'), 'An unarmed zero-dial plan satisfies the whole No');
const opaque = opaqueQuote(asked, card), quoteBefore = structuredClone(opaque);
const answers = viewGame(opaque, f.target).truthTurnActionAnswers;
assert.ok(answers?.includes('yes'), 'The positive-dial branch does not borrow the opaque weapon');
assert.ok(answers?.includes('no'), 'An unarmed zero-dial plan remains legal with the opaque card held');
assert.deepEqual(opaque, quoteBefore);
assert.deepEqual(asked, before);
assert.deepEqual(f.game, original);
assert.equal(Object.hasOwn(asked, '_turnActionUnknownCards'), false);
physical(asked, f); physical(opaque, f); physical(f.game, f);
});
void test('detached opaque acquisition supplies generic Distrans transfer, but cannot activate Distrans itself', () => {
const f = createDistransPrebidFixture(), original = structuredClone(f.game);
const recipient = f.action.target as string, given = f.action.give as string;
assert.ok(viewGame(f.game, f.holder).distrans?.choices.some(choice =>
choice.recipient === recipient && choice.cards.some(card => card.id === given)));
const opaque = opaqueQuote(f.game, given), before = structuredClone(opaque);
assert.ok(viewGame(opaque, f.holder).distrans?.choices.some(choice =>
choice.recipient === recipient && choice.cards.some(card => card.id === given)),
'A generic other-card transfer does not require an unseen specific effect');
assert.deepEqual(opaque, before);
const unknownRole = opaqueQuote(f.game, f.card);
assert.match(viewGame(unknownRole, f.holder).distrans?.blocked ?? '', /unseen future acquired card/i);
assert.deepEqual(f.game, original);
assert.equal(Object.hasOwn(f.game, '_turnActionUnknownCards'), false);
});