261 lines
22 KiB
TypeScript
261 lines
22 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { botActions } from '../game/bots';
|
|
import { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { gameDistance, location, splitLocation } from '../game/board';
|
|
import { treacheryDeck } from '../game/cards';
|
|
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
|
import { printedCardPlayCount } from '../game/printed-card-play';
|
|
import { richeseCards } from '../game/richese-cards';
|
|
import { truthFactAnswer, type TruthQuestion } from '../game/truthtrance';
|
|
import {
|
|
actTruthFlightPoison as act, advanceTruthFlightPoison as advance, answerTruthFlightPoison as answer,
|
|
askTruthFlightPoison as ask, bindTruthFlightPoison as bind, closeTruthFlightPoisonPreparation as closePreparation,
|
|
createTruthFlightPoisonFixture as fixture, declareTruthFlightPoison as declare,
|
|
finishTruthFlightPoison as finish, finishTruthFlightPoisonBattle as finishBattle,
|
|
nextTruthFlightPoisonNative as nativeStep, settleTruthFlightPoison as settle,
|
|
truthFlightPoisonAction as poison, truthFlightPoisonClean as clean,
|
|
truthFlightPoisonFact as fact, truthFlightPoisonGroup as group, truthFlightPoisonInventory as inventory,
|
|
truthFlightPoisonMove as move, truthFlightPoisonPayment as payment, truthFlightPoisonPlayer as player,
|
|
truthFlightPoisonPromise as promise, truthFlightPoisonSpice as spice,
|
|
truthFlightPoisonTraitors as traitors, truthFlightPoisonUse as use,
|
|
type TruthFlightPoisonExpression, type TruthFlightPoisonFixture, type TruthFlightPoisonStep,
|
|
} from './fixture-truth-flight-poison';
|
|
|
|
function rejected(game: Game, actor: string, action: Action) {
|
|
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
|
|
}
|
|
function physical(game: Game, f: TruthFlightPoisonFixture) {
|
|
assert.deepEqual(inventory(game), f.originalCards); assert.deepEqual(spice(game), f.originalSpice);
|
|
assert.deepEqual(traitors(game), f.originalTraitors);
|
|
}
|
|
const raw = (f: TruthFlightPoisonFixture, mixed: TruthFlightPoisonExpression): TruthQuestion =>
|
|
({ kind: 'turnActions', target: f.target, turn: f.game.turn, mixed });
|
|
const stage = (game: Game, f: TruthFlightPoisonFixture, index = 0) => game.turnActionPromises![index].cardPlays?.[f.effect];
|
|
const count = (game: Game, f: TruthFlightPoisonFixture) => printedCardPlayCount(game.printedCardPlays, f.target, game.turn, f.effect);
|
|
const pay = (game: Game, f: TruthFlightPoisonFixture, amount: number, actions: TruthFlightPoisonStep[] = []) =>
|
|
act(game, f.target, { type: 'bribe', target: f.recipient, amount }, actions);
|
|
const resources = (game: Game) => game.players.map(candidate => ({ id: candidate.id, spice: candidate.spice,
|
|
forces: structuredClone(candidate.forces), reserves: candidate.reserves, tanks: candidate.tanks,
|
|
elites: structuredClone(candidate.elites), moved: candidate.moved, shipped: candidate.shipped }));
|
|
|
|
void test('flight and Poison sources earn native cache1 cards, real original forces and unopened Truths in both modes', () => {
|
|
for (const effect of ['ornithopter', 'residualPoison'] as const) for (const advanced of [false, true]) {
|
|
const f = fixture({ effect, advanced }); assert.equal(f.initial.status, 'lobby'); assert.equal(f.setup.status, 'setup');
|
|
assert.ok(f.beforeStartingDeal.players.every(candidate => candidate.hand.length === 0));
|
|
assert.deepEqual(f.originalCards, [...treacheryDeck(['choam']), ...richeseCards()].map(card => card.id).sort());
|
|
assert.equal(f.sourceActionCount, f.actions.length); assert.equal(f.sourceCounts.selected, f.actions.length);
|
|
assert.equal(f.sourceCounts.nativeCacheBids, 1); assert.equal(f.sourceCounts.acquisition, f.acquisition.steps.length);
|
|
assert.equal(f.sourceCounts.shipment, f.shipment.steps.length); assert.equal(!!f.game.semutaPreview, !advanced);
|
|
assert.ok(!f.game.nexusCards && !f.game.homeworlds && !f.game.leaderSkills && !f.game.techTokens && !f.game.strongholdCards);
|
|
assert.equal(player(f.acquisition.before, f.target).spice - player(f.acquisition.after, f.target).spice, 1);
|
|
for (const [index, truth] of f.truths.entries()) assert.ok(player(f.game, f.truthHolders[index]).hand.some(card => card.id === truth));
|
|
assert.equal(count(f.game, f), 0); assert.equal(f.game.turnActionPromises?.length ?? 0, 0);
|
|
if (effect === 'ornithopter') {
|
|
assert.equal(player(f.game, f.target).spice, 5); assert.equal(player(f.game, f.target).forces['imperial_basin:9'], 2);
|
|
assert.equal(player(f.game, f.target).elites?.forces['imperial_basin:9'] ?? 0, 0); assert.equal(player(f.game, f.target).moved, 0);
|
|
} else {
|
|
assert.equal(player(f.game, f.target).spice, 9); assert.equal(player(f.game, f.asker).forces['arrakeen:10'], 1);
|
|
assert.equal(player(f.game, f.target).forces['arrakeen:10'], 10); assert.equal(f.game.battle!.preLeader!.closed, false);
|
|
assert.deepEqual(f.game.battle!.plans, {}); assert.ok(viewGame(f.game, f.target).residualPoison && !viewGame(f.game, f.target).residualPoison!.blocked);
|
|
}
|
|
physical(f.game, f);
|
|
}
|
|
});
|
|
|
|
void test('Basic flight AND frozen fact and all five paid counts the first declaration before disposal with no required second group', () => {
|
|
const f = fixture(), mixed = group('and', fact(f.spiceFact), use(f.effect), payment(f.recipient, 5));
|
|
let game = settle(bind(f, 'yes', raw(f, mixed)).game); game = pay(game, f, 5);
|
|
assert.equal(player(game, f.target).spice, 0); assert.equal(truthFactAnswer(player(game, f.target), f.spiceFact), 'no');
|
|
game = settle(act(game, f.target, move(game, f, 'twoGroups')));
|
|
assert.equal(count(game, f), 1); assert.equal(stage(game, f)?.played, true); assert.equal(promise(game).fulfilled, true);
|
|
assert.equal(game.ornithopter?.completed, 1); assert.equal(game.ornithopter?.card.id, f.card);
|
|
assert.equal(game.discard.filter(card => card.id === f.card).length, 0); physical(game, f);
|
|
game = settle(act(game, f.target, f.end));
|
|
assert.equal(game.ornithopter, null); assert.equal(player(game, f.target).moved, 1);
|
|
assert.equal(game.discard.filter(card => card.id === f.card).length, 1); assert.equal(count(game, f), 1);
|
|
assert.equal(stage(game, f)?.played, true); assert.deepEqual(promise(game).mixed, mixed); physical(game, f);
|
|
});
|
|
|
|
void test('range-three and both distinct two-group routes are actual native flights and count only one printed use', () => {
|
|
for (const advanced of [false, true]) for (const mode of ['range3', 'twoGroups'] as const) {
|
|
const f = fixture({ advanced }); let game = settle(bind(f, 'yes', raw(f, use(f.effect))).game);
|
|
const first = move(game, f, mode), destination = location(String(first.territory), Number(first.sector));
|
|
assert.equal(gameDistance(game, f.origin!, destination, key => splitLocation(key).sector === game.storm), mode === 'range3' ? 3 : 1);
|
|
game = settle(act(game, f.target, first)); assert.equal(count(game, f), 1); assert.equal(promise(game).fulfilled, true);
|
|
if (mode === 'twoGroups') {
|
|
assert.equal(game.ornithopter?.completed, 1);
|
|
const second = move(game, f); assert.equal(second.ornithopterEvent, game.ornithopter!.event); assert.equal(second.movementCard, undefined);
|
|
assert.equal(second.from, f.origin); game = settle(act(game, f.target, second));
|
|
assert.equal(player(game, f.target).moved, 2);
|
|
} else assert.equal(player(game, f.target).moved, 1);
|
|
assert.equal(game.ornithopter, null); assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
|
|
assert.equal(count(game, f), 1); physical(game, f);
|
|
}
|
|
});
|
|
|
|
void test('a positive flight answer prevents own ordinary-move or early-end evasion while OR can end and pay later', () => {
|
|
const f = fixture({ advanced: true }); let game = settle(bind(f, 'yes', raw(f, use(f.effect))).game);
|
|
rejected(game, f.target, f.end); rejected(game, f.target, move(game, f)); assert.equal(count(game, f), 0);
|
|
const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide); assert.equal(guide.action.type, 'move');
|
|
assert.equal(guide.action.movementCard, f.card); game = settle(act(game, f.target, guide.action)); assert.equal(promise(game).fulfilled, true);
|
|
let alternate = settle(bind(f, 'yes', raw(f, group('or', use(f.effect), payment(f.recipient)))).game);
|
|
alternate = settle(act(alternate, f.target, f.end)); assert.equal(stage(alternate, f)?.played, false); assert.equal(stage(alternate, f)?.closed, true);
|
|
alternate = advance(alternate, state => state.phase === 8 && clean(state)); alternate = pay(alternate, f, 2);
|
|
assert.equal(promise(alternate).fulfilled, true); assert.ok(player(alternate, f.target).hand.some(card => card.id === f.card));
|
|
physical(game, f); physical(alternate, f);
|
|
});
|
|
|
|
void test('an active flight gives no new-interval credit and its second group or later disposal does not count again', () => {
|
|
const f = fixture({ advanced: true }); let game = settle(act(f.game, f.target, move(f.game, f, 'twoGroups')));
|
|
assert.equal(count(game, f), 1); assert.equal(game.ornithopter?.completed, 1);
|
|
const event = game.ornithopter!.event, declared = declare(f, game), asked = ask(declared.game, declared.asker, raw(f, use(f.effect)));
|
|
assert.deepEqual(viewGame(asked, f.target).truthTurnActionAnswers, ['no']); rejected(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
game = settle(answer(asked, f.target, 'no')); assert.equal(stage(game, f)?.played, false); assert.equal(game.ornithopter!.event, event);
|
|
game = settle(act(game, f.target, move(game, f))); assert.equal(count(game, f), 1); assert.equal(stage(game, f)?.played, false);
|
|
assert.equal(game.discard.filter(card => card.id === f.card).length, 1); game = settle(act(game, f.target, f.end));
|
|
assert.equal(promise(game).fulfilled, true); assert.equal(count(game, f), 1); physical(game, f);
|
|
});
|
|
|
|
void test('flight whole No AND permits one branch and No OR keeps its payment cap after own movement closes', () => {
|
|
const f = fixture({ advanced: true }), packet = move(f.game, f, 'range3');
|
|
let played = settle(bind(f, 'no').game); played = settle(act(played, f.target, packet)); played = pay(played, f, 1);
|
|
rejected(played, f.target, { type: 'bribe', target: f.recipient, amount: 1 }); assert.equal(count(played, f), 1);
|
|
let paid = settle(bind(f, 'no').game); paid = pay(paid, f, 2); rejected(paid, f.target, packet); assert.equal(count(paid, f), 0);
|
|
let neither = settle(bind(f, 'no', raw(f, group('or', use(f.effect), payment(f.recipient)))).game);
|
|
rejected(neither, f.target, packet); neither = pay(neither, f, 1); neither = settle(act(neither, f.target, f.end));
|
|
assert.equal(stage(neither, f)?.closed, true); neither = advance(neither, state => state.phase === 8 && clean(state));
|
|
rejected(neither, f.target, { type: 'bribe', target: f.recipient, amount: 1 }); assert.ok(!promise(neither).released);
|
|
neither = finish(neither); assert.equal(promise(neither).fulfilled, true);
|
|
physical(played, f); physical(paid, f); physical(neither, f);
|
|
});
|
|
|
|
void test('Residual AND frozen fact and all nine paid kills one actual native leader for no fee or bounty before disposal continuation', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = fixture({ effect: 'residualPoison', advanced }), mixed = group('and', fact(f.spiceFact), use(f.effect), payment(f.recipient, 9));
|
|
let game = settle(bind(f, 'yes', raw(f, mixed)).game); const before = resources(game), leaders = structuredClone(player(game, f.asker).leaders);
|
|
const packet = poison(game, f); assert.equal('leader' in packet, false); game = act(game, f.target, packet);
|
|
const victims = player(game, f.asker).leaders.filter(leader => leader.dead && !leaders.find(original => original.id === leader.id)!.dead);
|
|
assert.equal(victims.length, 1); assert.equal(victims[0].deaths, leaders.find(original => original.id === victims[0].id)!.deaths + 1);
|
|
assert.deepEqual(resources(game), before); assert.equal(count(game, f), 1); assert.equal(stage(game, f)?.played, true);
|
|
assert.equal(game.discard.filter(card => card.id === f.card).length, 1); assert.deepEqual(game.battle!.plans, {});
|
|
if (!advanced) assert.ok(game.pendingTreacheryDiscard);
|
|
game = settle(game); game = pay(game, f, 9); assert.equal(player(game, f.target).spice, 0);
|
|
assert.equal(truthFactAnswer(player(game, f.target), f.spiceFact), 'no'); assert.equal(promise(game).fulfilled, true);
|
|
const completed = finishBattle(f, game); game = completed.game;
|
|
assert.equal(stage(game, f)?.played, true); assert.equal(count(game, f), 1); assert.equal(promise(game).fulfilled, true);
|
|
assert.equal(player(game, f.asker).leaders.find(leader => leader.id === victims[0].id)!.dead, true); physical(game, f);
|
|
}
|
|
});
|
|
|
|
void test('Residual completion quotes neither sample a random victim nor create hypothetical Tanks or bounty funding', t => {
|
|
const f = fixture({ effect: 'residualPoison' }), declared = declare(f), before = structuredClone(declared.game);
|
|
let rng = 0; const mock = t.mock.method(crypto, 'getRandomValues', () => { rng++; throw Error('A future-role quote must not choose a victim'); });
|
|
const funded = applyAction(declared.game, declared.asker, { type: 'truthAsk', question: raw(f, group('and', use(f.effect), payment(f.recipient, 9))) });
|
|
assert.deepEqual(viewGame(funded, f.target).truthTurnActionAnswers, ['yes', 'no']);
|
|
const unfunded = applyAction(declared.game, declared.asker, { type: 'truthAsk', question: raw(f, group('and', use(f.effect), payment(f.recipient, 10))) });
|
|
assert.deepEqual(viewGame(unfunded, f.target).truthTurnActionAnswers, ['no']); assert.equal(rng, 0); mock.mock.restore();
|
|
assert.deepEqual(declared.game, before); assert.deepEqual(funded.players, before.players); assert.deepEqual(unfunded.players, before.players);
|
|
assert.equal(player(funded, f.asker).leaders.filter(leader => leader.dead).length, 0); assert.equal(count(funded, f), 0); physical(funded, f);
|
|
});
|
|
|
|
void test('Residual Yes rejects own first leader commitment while a real foreign first plan releases the unfulfilled known-window condition', () => {
|
|
const f = fixture({ effect: 'residualPoison', advanced: true });
|
|
let bound = settle(bind(f, 'yes', raw(f, group('and', use(f.effect), payment(f.recipient)))).game); bound = pay(bound, f, 2);
|
|
bound = closePreparation(bound); const packet = poison(bound, f);
|
|
const ownLeader = player(bound, f.target).leaders.find(leader => !leader.dead && !leader.usedAt)!;
|
|
rejected(bound, f.target, { type: 'battlePlan', dial: 0, support: 0, leader: ownLeader.id });
|
|
assert.equal(stage(bound, f)?.played, false); assert.ok(!promise(bound).released);
|
|
const enemyLeader = player(bound, f.asker).leaders.find(leader => !leader.dead && !leader.usedAt)!;
|
|
const foreign = act(bound, f.asker, { type: 'battlePlan', dial: 0, support: 0, leader: enemyLeader.id });
|
|
assert.equal(promise(foreign).released, true); assert.equal(stage(foreign, f)?.played, false);
|
|
assert.equal(foreign.turnActionPromises![0].payments[0].amount, 2); assert.ok(player(foreign, f.target).hand.some(card => card.id === f.card));
|
|
rejected(foreign, f.target, packet); assert.equal(player(foreign, f.asker).leaders.filter(leader => leader.dead).length, 0); physical(foreign, f);
|
|
});
|
|
|
|
void test('Residual whole OR supports actual manual printed use or payment followed by native battle cleanup with the card unused', () => {
|
|
const f = fixture({ effect: 'residualPoison', advanced: true }), question = raw(f, group('or', use(f.effect), payment(f.recipient)));
|
|
const bound = settle(bind(f, 'yes', question).game);
|
|
const played = settle(act(bound, f.target, poison(bound, f))); assert.equal(promise(played).fulfilled, true);
|
|
assert.equal(player(played, f.target).spice, 9); assert.equal(player(played, f.recipient).bribes, 0);
|
|
let paid = pay(bound, f, 2); paid = finishBattle(f, paid).game;
|
|
assert.equal(promise(paid).fulfilled, true); assert.equal(stage(paid, f)?.played, false);
|
|
assert.ok(player(paid, f.target).hand.some(card => card.id === f.card)); assert.equal(player(paid, f.asker).leaders.filter(leader => leader.dead).length, 0);
|
|
physical(played, f); physical(paid, f);
|
|
});
|
|
|
|
void test('Residual whole No caps payments and blocks the forbidden random effect before any victim is sampled', t => {
|
|
const f = fixture({ effect: 'residualPoison', advanced: true }), originalPacket = poison(f.game, f);
|
|
let played = settle(bind(f, 'no').game); played = settle(act(played, f.target, originalPacket)); played = pay(played, f, 1);
|
|
rejected(played, f.target, { type: 'bribe', target: f.recipient, amount: 1 }); assert.equal(count(played, f), 1);
|
|
let paid = settle(bind(f, 'no').game); paid = pay(paid, f, 2);
|
|
let neither = settle(bind(f, 'no', raw(f, group('or', use(f.effect), payment(f.recipient)))).game);
|
|
for (const game of [paid, neither]) {
|
|
const before = structuredClone(game); let rng = 0;
|
|
const mock = t.mock.method(crypto, 'getRandomValues', () => { rng++; throw Error('No must reject before random victim selection'); });
|
|
assert.throws(() => applyAction(game, f.target, originalPacket)); assert.equal(rng, 0); mock.mock.restore(); assert.deepEqual(game, before);
|
|
assert.equal(count(game, f), 0); assert.equal(player(game, f.asker).leaders.filter(leader => leader.dead).length, 0);
|
|
}
|
|
neither = pay(neither, f, 1); neither = finishBattle(f, neither).game;
|
|
neither = advance(neither, state => state.phase === 8 && clean(state)); rejected(neither, f.target, { type: 'bribe', target: f.recipient, amount: 1 });
|
|
neither = finish(neither); assert.equal(promise(neither).fulfilled, true); assert.equal(stage(neither, f)?.played, false);
|
|
physical(played, f); physical(paid, f); physical(neither, f);
|
|
});
|
|
|
|
void test('earlier Residual use is not a second original Truth future-play receipt and cannot create a new victim on disposal', () => {
|
|
const f = fixture({ effect: 'residualPoison', advanced: true }); let game = settle(act(f.game, f.target, poison(f.game, f)));
|
|
assert.equal(count(game, f), 1); const dead = player(game, f.asker).leaders.filter(leader => leader.dead).map(leader => leader.id);
|
|
const declared = declare(f, game), asked = ask(declared.game, declared.asker, raw(f, use(f.effect)));
|
|
assert.deepEqual(viewGame(asked, f.target).truthTurnActionAnswers, ['no']); rejected(asked, f.target, { type: 'truthAnswer', answer: 'yes' });
|
|
game = settle(answer(asked, f.target, 'no')); assert.equal(stage(game, f)?.played, false); assert.equal(count(game, f), 1);
|
|
assert.deepEqual(player(game, f.asker).leaders.filter(leader => leader.dead).map(leader => leader.id), dead); physical(game, f);
|
|
});
|
|
|
|
void test('exact flight and Poison guides bind native packets through JSON and reject stale promise, source or forged victim fields', () => {
|
|
for (const effect of ['ornithopter', 'residualPoison'] as const) {
|
|
const f = fixture({ effect, advanced: true }); let game = settle(bind(f, 'yes', raw(f, use(f.effect))).game);
|
|
const guide = viewGame(game, f.target).turnActionGuide; assert.ok(guide); assert.equal(guide.event, promise(game).event);
|
|
assert.equal(guide.action.turnActionPromiseEvent, promise(game).event); assert.equal(guide.action.turnActionPromisePhase, game.phase);
|
|
rejected(game, f.asker, guide.action); rejected(game, f.target, { ...guide.action, turnActionPromiseEvent: 'stale-flight-poison-answer' });
|
|
rejected(game, f.target, { ...guide.action, turnActionPromisePhase: 8 });
|
|
if (effect === 'ornithopter') {
|
|
assert.equal(guide.action.type, 'move'); assert.equal(guide.action.movementCard, f.card);
|
|
assert.ok(guide.action.ornithopter === 'range3' || guide.action.ornithopter === 'twoGroups');
|
|
rejected(game, f.target, { ...guide.action, ornithopterEvent: 'already-started-stale-flight' });
|
|
} else {
|
|
assert.equal(guide.action.type, 'card'); assert.equal(guide.action.card, f.card); assert.equal(guide.action.target, f.asker);
|
|
assert.equal(guide.action.event, f.event); assert.equal('leader' in guide.action, false);
|
|
rejected(game, f.target, { ...guide.action, event: 'stale-poison-battle' });
|
|
rejected(game, f.target, { ...guide.action, leader: player(game, f.asker).leaders[0].id });
|
|
}
|
|
const packet: Action = JSON.parse(JSON.stringify(guide.action)); game = settle(act(game, f.target, packet));
|
|
assert.equal(promise(game).fulfilled, true); assert.equal(stage(game, f)?.played, true); assert.equal(count(game, f), 1);
|
|
rejected(game, f.target, packet); physical(game, f);
|
|
}
|
|
});
|
|
|
|
void test('four minimal profiles consume exact guides for a human-bound Yes in both roles and modes without requiring strategic answer policy', () => {
|
|
for (const effect of ['ornithopter', 'residualPoison'] as const) for (const advanced of [false, true]) {
|
|
const f = fixture({ effect, advanced }), declared = declare(f), asked = ask(declared.game, declared.asker, f.question);
|
|
for (const difficulty of DIFFICULTIES) {
|
|
const answering = viewGame(asked, f.target); answering.players.find(candidate => candidate.id === f.target)!.bot = difficulty;
|
|
const proposed = botActions(answering).find(candidate => candidate.type === 'truthAnswer'); assert.ok(proposed);
|
|
assert.ok(answering.truthTurnActionAnswers?.includes(proposed.answer as 'yes' | 'no' | 'unknown'));
|
|
let game = answer(structuredClone(asked), f.target, 'yes'); const actions: TruthFlightPoisonStep[] = [];
|
|
for (let step = 0; step < 100 && (!promise(game).fulfilled || !clean(game)); step++) {
|
|
const view = viewGame(game, f.target); view.players.find(candidate => candidate.id === f.target)!.bot = difficulty;
|
|
if (view.turnActionGuide) {
|
|
const guide = view.turnActionGuide, packet = botActions(view).find(candidate => candidate.turnActionPromiseEvent === guide.action.turnActionPromiseEvent);
|
|
assert.ok(packet); assert.deepEqual(packet, guide.action); game = act(game, f.target, packet, actions);
|
|
} else {
|
|
const next = nativeStep(game); assert.ok(next); game = act(game, next.actor, next.action, actions);
|
|
}
|
|
}
|
|
assert.equal(promise(game).fulfilled, true); assert.equal(count(game, f), 1); assert.equal(player(game, f.recipient).bribes, 2);
|
|
if (effect === 'ornithopter' && game.ornithopter) game = settle(act(game, f.target, f.end, actions), actions);
|
|
assert.equal(game.discard.filter(card => card.id === f.card).length, 1); assert.equal(count(game, f), 1);
|
|
assert.equal(player(game, f.target).spice, player(f.game, f.target).spice - 2); physical(game, f);
|
|
}
|
|
}
|
|
});
|