100 lines
5.1 KiB
TypeScript
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);
|
|
});
|