Connect CHOAM Nexus Betrayal random discard
This commit is contained in:
1 parent
90c5c7af25
commit
0bf61c8067
12 files changed
+709
No files matched your search
@@ -0,0 +1,121 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import type { GameView } from '../game/engine';
|
||||
import {
|
||||
nexusChoamBetrayalAction,
|
||||
nexusChoamBetrayalBotActions,
|
||||
} from '../game/nexus-choam-betrayal-options';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
|
||||
},
|
||||
});
|
||||
// Register the Next alias hook before loading the component under test.
|
||||
const { NexusChoamBetrayal } = await import('../components/nexus-choam-betrayal');
|
||||
|
||||
function ownerView(overrides: Record<string, unknown> = {}): GameView {
|
||||
return {
|
||||
me: 'owner',
|
||||
players: [
|
||||
{ id: 'owner', name: 'House Rival', faction: 'atreides', ally: null, bot: 'Easy' },
|
||||
{ id: 'choam', name: 'CHOAM', faction: 'choam', ally: null },
|
||||
{ id: 'observer', name: 'Observer', faction: 'guild', ally: null },
|
||||
],
|
||||
status: 'playing',
|
||||
phase: 3,
|
||||
automaticContinuationPending: false,
|
||||
nexusCards: { card: 'choam' },
|
||||
nexusChoamBetrayal: {
|
||||
event: '["nexusChoamBetrayal",1,3,"owner","choam"]',
|
||||
blocked: null,
|
||||
target: { id: 'choam', name: 'CHOAM', handSize: 3 },
|
||||
},
|
||||
...overrides,
|
||||
} as unknown as GameView;
|
||||
}
|
||||
|
||||
function markup(game: GameView, busy = false): string {
|
||||
return renderToStaticMarkup(createElement(NexusChoamBetrayal, { game, act() {}, busy }));
|
||||
}
|
||||
|
||||
void test('unallied CHOAM card owner offers one server-random discard action at every bot profile', () => {
|
||||
for (const profile of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
||||
const view = ownerView();
|
||||
view.players[0].bot = profile;
|
||||
const action = { type: 'nexusChoamBetrayal', event: view.nexusChoamBetrayal!.event };
|
||||
assert.deepEqual(nexusChoamBetrayalAction(view), action);
|
||||
assert.deepEqual(nexusChoamBetrayalBotActions(view), [action]);
|
||||
}
|
||||
const projected = ownerView();
|
||||
Object.defineProperty(projected.players[1], 'hand', {
|
||||
get() { throw new Error('Rival hand identity read'); },
|
||||
});
|
||||
const html = markup(projected);
|
||||
assert.match(html, /CHOAM Nexus Betrayal/);
|
||||
assert.match(html, /CHOAM discard one/);
|
||||
assert.match(html, /CHOAM holds 3 cards during Bidding/);
|
||||
assert.match(html, /uniformly random/);
|
||||
assert.match(html, /CHOAM receives no spice/);
|
||||
assert.match(html, /Spend CHOAM Nexus for random discard/);
|
||||
assert.doesNotMatch(html, /disabled=""/);
|
||||
});
|
||||
|
||||
void test('blocked and busy private offers explain the restriction and disable the action', () => {
|
||||
const blocked = ownerView({ nexusChoamBetrayal: {
|
||||
event: 'offer', blocked: 'Finish the current response first.',
|
||||
target: { id: 'choam', name: 'CHOAM', handSize: 1 },
|
||||
} });
|
||||
assert.equal(nexusChoamBetrayalAction(blocked), null);
|
||||
assert.deepEqual(nexusChoamBetrayalBotActions(blocked), []);
|
||||
assert.match(markup(blocked), /Finish the current response first/);
|
||||
assert.match(markup(blocked), /CHOAM holds 1 card during Bidding/);
|
||||
assert.match(markup(blocked), /disabled=""[^>]*>Spend CHOAM Nexus/);
|
||||
assert.match(markup(ownerView(), true), /disabled=""[^>]*>Spend CHOAM Nexus/);
|
||||
for (const changes of [
|
||||
{ status: 'finished' },
|
||||
{ automaticContinuationPending: true },
|
||||
{ nexusCards: { card: null } },
|
||||
{ players: [
|
||||
{ id: 'owner', faction: 'atreides', ally: 'observer' },
|
||||
{ id: 'choam', faction: 'choam', ally: null },
|
||||
] },
|
||||
]) {
|
||||
const view = ownerView(changes);
|
||||
assert.equal(nexusChoamBetrayalAction(view), null);
|
||||
assert.deepEqual(nexusChoamBetrayalBotActions(view), []);
|
||||
}
|
||||
assert.match(markup(ownerView({ automaticContinuationPending: true })), /disabled=""[^>]*>Spend CHOAM Nexus/);
|
||||
const concealed = ownerView({
|
||||
phase: 2,
|
||||
nexusChoamBetrayal: {
|
||||
event: '["nexusChoamBetrayal",1,2,"owner","choam"]',
|
||||
blocked: null,
|
||||
target: { id: 'choam', name: 'CHOAM', handSize: null },
|
||||
},
|
||||
});
|
||||
assert.ok(nexusChoamBetrayalAction(concealed));
|
||||
assert.deepEqual(nexusChoamBetrayalBotActions(concealed), []);
|
||||
assert.doesNotMatch(markup(concealed), /holds [0-9]+ cards/);
|
||||
});
|
||||
|
||||
void test('nonowner projection exposes neither the offer nor the rival hand or target control', () => {
|
||||
const observer = ownerView({
|
||||
me: 'observer',
|
||||
nexusCards: { card: null },
|
||||
nexusChoamBetrayal: null,
|
||||
});
|
||||
Object.defineProperty(observer.players[1], 'hand', {
|
||||
get() { throw new Error('Opponent hand read'); },
|
||||
});
|
||||
assert.equal(nexusChoamBetrayalAction(observer), null);
|
||||
assert.deepEqual(nexusChoamBetrayalBotActions(observer), []);
|
||||
assert.equal(markup(observer), '');
|
||||
assert.equal(markup(ownerView({ nexusChoamBetrayal: null })), '');
|
||||
assert.equal(markup(ownerView({ nexusCards: { card: null } })), '');
|
||||
});
|
||||
|
||||
aliases.deregister();
|
||||
@@ -0,0 +1,147 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, normalizeAutomaticGame, viewGame, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { drawNexusCard } from '../game/nexus-cards';
|
||||
import { nexusChoamBetrayalAction } from '../game/nexus-choam-betrayal-options';
|
||||
import { nexusChoamBetrayalSignature } from '../game/nexus-choam-betrayal';
|
||||
import { nexusTraitorFixture, nexusTraitorInventory } from './fixture-nexus-traitors';
|
||||
|
||||
function position(advanced = false, phase = 2) {
|
||||
const g = nexusTraitorFixture({ ownerFaction: 'atreides', opponentFaction: 'choam', advanced, phase });
|
||||
const owner = 'p', target = 'q', observer = 'r';
|
||||
const cards = g.nexusCards!.cards!;
|
||||
const index = cards.deck.indexOf('choam');
|
||||
assert.ok(index >= 0);
|
||||
cards.deck[index] = cards.hands[owner]!;
|
||||
cards.hands[owner] = 'choam';
|
||||
const victim = g.players.find(player => player.id === target)!;
|
||||
victim.hand.push(...g.deck.splice(0, 3));
|
||||
nexusTraitorInventory(g);
|
||||
const action = nexusChoamBetrayalAction(viewGame(g, owner));
|
||||
assert.ok(action);
|
||||
return { g, owner, target, observer, action };
|
||||
}
|
||||
|
||||
function reload(g: Game): Game {
|
||||
return JSON.parse(JSON.stringify(g)) as Game;
|
||||
}
|
||||
|
||||
function reject(g: Game, owner: string, action: { type: string; [key: string]: unknown }) {
|
||||
const before = reload(g);
|
||||
assert.throws(() => applyAction(g, owner, action));
|
||||
assert.deepEqual(g, before);
|
||||
}
|
||||
|
||||
void test('CHOAM Betrayal spends one Nexus card and randomly discards one actual held card without spice in Basic and Advanced', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const { g, owner, target, observer, action } = position(advanced);
|
||||
const before = reload(g);
|
||||
const beforeLog = g.log.length;
|
||||
const done = applyAction(g, owner, action);
|
||||
assert.deepEqual(g, before);
|
||||
const victim = done.players.find(player => player.id === target)!;
|
||||
const original = before.players.find(player => player.id === target)!;
|
||||
const removed = original.hand.filter(card => !victim.hand.some(held => held.id === card.id));
|
||||
assert.equal(removed.length, 1);
|
||||
assert.deepEqual(victim.hand, original.hand.filter(card => card.id !== removed[0].id));
|
||||
assert.equal(done.discard.filter(card => card.id === removed[0].id).length, 1);
|
||||
assert.equal(victim.spice, original.spice);
|
||||
assert.equal(done.nexusCards!.cards!.hands[owner], null);
|
||||
assert.equal(done.nexusCards!.cards!.discard.filter(card => card === 'choam').length, 1);
|
||||
assert.equal(done.nexusChoamBetrayals?.at(-1)?.stage, 'complete');
|
||||
assert.equal(done.nexusChoamBetrayals?.at(-1)?.card, removed[0].id);
|
||||
assert.equal(done.pendingTreacheryDiscard, null);
|
||||
assert.equal(done.log.slice(beforeLog).some(row => row.text.includes(removed[0].name)), false);
|
||||
assert.equal(viewGame(done, observer).nexusChoamBetrayal, null);
|
||||
assert.equal(Object.hasOwn(viewGame(done, observer), 'nexusChoamBetrayals'), false);
|
||||
nexusTraitorInventory(done);
|
||||
reject(done, owner, action);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(done)), done);
|
||||
}
|
||||
});
|
||||
|
||||
void test('CHOAM Betrayal hides non-Bidding hand count; all bot levels take a legal action during public-count Bidding', () => {
|
||||
const { g, owner, target, observer } = position();
|
||||
const offer = viewGame(g, owner).nexusChoamBetrayal!;
|
||||
assert.equal(offer.blocked, null);
|
||||
assert.equal(offer.target.id, target);
|
||||
assert.equal(offer.target.handSize, null);
|
||||
assert.equal(viewGame(g, target).nexusChoamBetrayal, null);
|
||||
assert.equal(viewGame(g, observer).nexusChoamBetrayal, null);
|
||||
for (const card of g.players.find(player => player.id === target)!.hand)
|
||||
assert.equal(JSON.stringify(offer).includes(card.id), false);
|
||||
const publicCount = position(false, 3);
|
||||
const biddingOffer = viewGame(publicCount.g, owner).nexusChoamBetrayal!;
|
||||
assert.equal(biddingOffer.target.handSize, publicCount.g.players.find(player => player.id === target)!.hand.length);
|
||||
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
||||
const view = viewGame(publicCount.g, owner);
|
||||
view.players.find(player => player.id === owner)!.bot = difficulty;
|
||||
assert.deepEqual(botActions(view)[0], publicCount.action);
|
||||
}
|
||||
const botPlayed = applyAction(publicCount.g, owner, publicCount.action);
|
||||
assert.equal(botPlayed.nexusChoamBetrayals?.at(-1)?.stage, 'complete');
|
||||
nexusTraitorInventory(botPlayed);
|
||||
});
|
||||
|
||||
void test('CHOAM Betrayal rejects forged, stale, foreign and interrupted plays without spending the card', () => {
|
||||
const { g, owner, target, observer, action } = position();
|
||||
reject(g, target, action);
|
||||
reject(g, observer, action);
|
||||
reject(g, owner, { ...action, event: 'stale' });
|
||||
reject(g, owner, { ...action, card: 'forged' });
|
||||
const pending = reload(g);
|
||||
pending.decision = { kind: 'choamMarket', player: target };
|
||||
assert.ok(viewGame(pending, owner).nexusChoamBetrayal?.blocked);
|
||||
reject(pending, owner, action);
|
||||
const empty = reload(g);
|
||||
empty.deck.push(...empty.players.find(player => player.id === target)!.hand.splice(0));
|
||||
assert.equal(viewGame(empty, owner).nexusChoamBetrayal?.blocked, null);
|
||||
reject(empty, owner, action);
|
||||
assert.equal(g.nexusCards!.cards!.hands[owner], 'choam');
|
||||
});
|
||||
|
||||
void test('a committed random discard resumes exactly once after JSON restore and rejects damaged saved proof', () => {
|
||||
const { g, owner, target, action } = position();
|
||||
const done = applyAction(g, owner, action);
|
||||
const paused = reload(done), record = paused.nexusChoamBetrayals!.at(-1)!;
|
||||
record.stage = 'discard'; record.signature = nexusChoamBetrayalSignature(record);
|
||||
paused.nexusChoamBetrayalLast = { event: record.event, stage: record.stage };
|
||||
const sequence = paused.treacheryDiscardSequence!;
|
||||
paused.resolvedTreacheryDiscardSequence = sequence - 1;
|
||||
paused.pendingTreacheryDiscard = {
|
||||
sequence,
|
||||
batch: {
|
||||
event: `discard:${paused.turn}:${paused.phase}:${sequence}`,
|
||||
turn: paused.turn,
|
||||
phase: paused.phase,
|
||||
cause: 'nexus:choamBetrayal',
|
||||
entries: [{ card: structuredClone(paused.discard.find(card => card.id === record.card)!), discardedBy: target, publicFace: false }],
|
||||
},
|
||||
continuation: { kind: 'nexusChoamBetrayal', event: record.event, owner, target, card: record.card, handBefore: record.handBefore },
|
||||
};
|
||||
assert.equal(viewGame(paused, owner).automaticContinuationPending, true);
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(paused)), done);
|
||||
reject(paused, owner, action);
|
||||
for (const damage of [
|
||||
(state: Game) => { state.nexusChoamBetrayals![0].card = 'forged'; },
|
||||
(state: Game) => { state.pendingTreacheryDiscard!.continuation = { kind: 'nexusChoamBetrayal', event: record.event, owner, target, card: 'forged', handBefore: record.handBefore }; },
|
||||
(state: Game) => { state.pendingTreacheryDiscard = null; state.resolvedTreacheryDiscardSequence = sequence; },
|
||||
]) {
|
||||
const bad = reload(paused); damage(bad); const before = reload(bad);
|
||||
assert.throws(() => viewGame(bad, owner));
|
||||
assert.deepEqual(bad, before);
|
||||
}
|
||||
});
|
||||
|
||||
void test('a completed Betrayal remains valid when its Nexus discard is recycled by a later draw in the same turn', () => {
|
||||
const { g, owner, action } = position(false, 0);
|
||||
const done = applyAction(g, owner, action);
|
||||
const recycled = reload(done), cards = recycled.nexusCards!.cards!;
|
||||
cards.discard.push(...cards.deck.splice(0));
|
||||
recycled.nexusCards!.cards = drawNexusCard(cards, owner, recycled.players, () => 0);
|
||||
assert.equal(recycled.nexusCards!.cards!.discard.includes('choam'), false);
|
||||
nexusTraitorInventory(recycled);
|
||||
assert.doesNotThrow(() => viewGame(recycled, owner));
|
||||
assert.deepEqual(normalizeAutomaticGame(reload(recycled)), recycled);
|
||||
});
|
||||
@@ -0,0 +1,144 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import type { Card } from '../game/cards';
|
||||
import type { Game } from '../game/engine';
|
||||
import {
|
||||
nexusChoamBetrayalSignature, quoteNexusChoamBetrayal,
|
||||
validateNexusChoamBetrayal, type NexusChoamBetrayal,
|
||||
} from '../game/nexus-choam-betrayal';
|
||||
import { nexusTraitorFixture } from './fixture-nexus-traitors';
|
||||
import { nexusReload } from './fixture-nexus-cards';
|
||||
|
||||
function position(phase = 5) {
|
||||
const g = nexusTraitorFixture({ ownerFaction: 'atreides', opponentFaction: 'choam', phase });
|
||||
const owner = g.players[0].id, target = g.players[1].id;
|
||||
const cards = g.nexusCards!.cards!;
|
||||
const index = cards.deck.indexOf('choam');
|
||||
assert.ok(index >= 0);
|
||||
cards.deck[index] = cards.hands[owner]!;
|
||||
cards.hands[owner] = 'choam';
|
||||
const selected = g.deck.splice(0, 2);
|
||||
assert.equal(selected.length, 2);
|
||||
g.players[1].hand.push(...selected);
|
||||
return { g, owner, target, selected };
|
||||
}
|
||||
|
||||
void test('outside Bidding, a CHOAM Betrayal offer reads neither hidden hand count nor identities', () => {
|
||||
const { g, owner, target, selected } = position();
|
||||
const offer = quoteNexusChoamBetrayal(g, owner)!;
|
||||
assert.deepEqual(offer.target, { id: target, name: g.players[1].name, handSize: null });
|
||||
assert.equal(offer.blocked, null);
|
||||
assert.deepEqual(JSON.parse(offer.event), ['nexusChoamBetrayal', g.turn, g.phase, owner, target]);
|
||||
for (const card of selected) assert.ok(!JSON.stringify(offer).includes(card.id));
|
||||
for (const count of [0, 1, 3]) {
|
||||
const alternate = nexusReload(g);
|
||||
alternate.deck.push(...alternate.players[1].hand.splice(0));
|
||||
alternate.players[1].hand.push(...alternate.deck.splice(0, count));
|
||||
assert.deepEqual(quoteNexusChoamBetrayal(alternate, owner), offer);
|
||||
}
|
||||
g.players[1].hand = new Proxy(g.players[1].hand, {
|
||||
get() { throw new Error('Private CHOAM hand was read'); },
|
||||
});
|
||||
assert.deepEqual(quoteNexusChoamBetrayal(g, owner), offer);
|
||||
const bidding = position(3);
|
||||
assert.equal(quoteNexusChoamBetrayal(bidding.g, bidding.owner)!.target.handSize, 2);
|
||||
bidding.g.deck.push(...bidding.g.players[1].hand.splice(0));
|
||||
assert.match(quoteNexusChoamBetrayal(bidding.g, bidding.owner)!.blocked!, /no Treachery/);
|
||||
});
|
||||
|
||||
void test('the same clean boundary is available across printed phases, but not through live interactions', () => {
|
||||
for (const phase of [0, 2, 5, 7, 8]) {
|
||||
const { g, owner } = position(phase);
|
||||
assert.equal(quoteNexusChoamBetrayal(g, owner)!.blocked, null);
|
||||
assert.match(quoteNexusChoamBetrayal(g, owner, true)!.blocked!, /current interaction/);
|
||||
}
|
||||
const { g, owner, target } = position();
|
||||
for (const change of [
|
||||
(copy: Game) => { copy.response = { kind: 'card' } as never; },
|
||||
(copy: Game) => { copy.decision = { kind: 'guildTiming' } as never; },
|
||||
(copy: Game) => { copy.pendingTreacheryDiscard = { sequence: 1 } as never; },
|
||||
(copy: Game) => { copy.nexusCards!.phase = { stage: 'drawing' } as never; },
|
||||
(copy: Game) => { copy.battle = { event: 'ongoing' } as never; },
|
||||
(copy: Game) => { copy.pendingExchange = { response: null, decision: null }; },
|
||||
]) {
|
||||
const copy = nexusReload(g); change(copy);
|
||||
assert.match(quoteNexusChoamBetrayal(copy, owner)!.blocked!, /current interaction/);
|
||||
}
|
||||
const empty = nexusReload(g);
|
||||
empty.deck.push(...empty.players[1].hand.splice(0));
|
||||
assert.equal(quoteNexusChoamBetrayal(empty, owner)!.blocked, null);
|
||||
const allied = nexusReload(g);
|
||||
allied.players[0].ally = allied.players[2].id;
|
||||
allied.players[2].ally = owner;
|
||||
assert.match(quoteNexusChoamBetrayal(allied, owner)!.blocked!, /unallied/);
|
||||
assert.equal(quoteNexusChoamBetrayal(g, target), null);
|
||||
assert.equal(quoteNexusChoamBetrayal(g, 'foreign'), null);
|
||||
const noCard = nexusReload(g);
|
||||
noCard.nexusCards!.cards!.hands[owner] = null;
|
||||
assert.equal(quoteNexusChoamBetrayal(noCard, owner), null);
|
||||
const absent = nexusReload(g);
|
||||
absent.players[1].faction = 'guild';
|
||||
assert.equal(quoteNexusChoamBetrayal(absent, owner), null);
|
||||
});
|
||||
|
||||
void test('signed sampled-card receipt validates after discard and JSON reload without future hand custody', () => {
|
||||
const { g, owner, target, selected } = position();
|
||||
const offer = quoteNexusChoamBetrayal(g, owner)!;
|
||||
const record: NexusChoamBetrayal = {
|
||||
event: offer.event, turn: g.turn, phase: g.phase, owner, target,
|
||||
card: selected[0].id, handBefore: 2, stage: 'discard', signature: '',
|
||||
};
|
||||
record.signature = nexusChoamBetrayalSignature(record);
|
||||
const physical = [...g.deck, ...g.players.flatMap(player => player.hand), ...g.discard];
|
||||
assert.doesNotThrow(() => validateNexusChoamBetrayal(g, record, physical));
|
||||
const spent = nexusReload(g);
|
||||
spent.nexusCards!.cards!.hands[owner] = null;
|
||||
spent.nexusCards!.cards!.discard.push('choam');
|
||||
const discarded = spent.players[1].hand.shift()!;
|
||||
spent.discard.push(discarded);
|
||||
spent.players[1].hand.push(spent.deck.shift()!);
|
||||
const restored = nexusReload(spent), receipt = JSON.parse(JSON.stringify(record)) as NexusChoamBetrayal;
|
||||
assert.doesNotThrow(() => validateNexusChoamBetrayal(restored, receipt,
|
||||
[...restored.deck, ...restored.discard, ...restored.players.flatMap(player => player.hand)]));
|
||||
receipt.stage = 'complete'; receipt.signature = nexusChoamBetrayalSignature(receipt);
|
||||
restored.turn++;
|
||||
assert.doesNotThrow(() => validateNexusChoamBetrayal(restored, receipt,
|
||||
[...restored.deck, ...restored.discard, ...restored.players.flatMap(player => player.hand)]));
|
||||
assert.equal(restored.players[1].hand.length, record.handBefore);
|
||||
});
|
||||
|
||||
void test('malformed, stale or forged source/selection receipts cannot become historical evidence', () => {
|
||||
const { g, owner, target, selected } = position();
|
||||
const event = quoteNexusChoamBetrayal(g, owner)!.event;
|
||||
const receipt: NexusChoamBetrayal = {
|
||||
event, turn: g.turn, phase: g.phase, owner, target, card: selected[0].id,
|
||||
handBefore: 2, stage: 'discard', signature: '',
|
||||
};
|
||||
receipt.signature = nexusChoamBetrayalSignature(receipt);
|
||||
const physical = [...g.deck, ...g.players.flatMap(player => player.hand), ...g.discard];
|
||||
const invalid = (patch: Partial<NexusChoamBetrayal>, cards: readonly Card[] = physical) =>
|
||||
assert.throws(() => validateNexusChoamBetrayal(g, { ...receipt, ...patch }, cards));
|
||||
invalid({ event: 'forged' });
|
||||
invalid({ card: selected[1].id });
|
||||
invalid({ owner: target });
|
||||
invalid({ target: g.players[2].id });
|
||||
invalid({ phase: receipt.phase + 1 });
|
||||
invalid({ turn: receipt.turn + 1 });
|
||||
invalid({ handBefore: 1 });
|
||||
invalid({ handBefore: 0 });
|
||||
invalid({ handBefore: Number.MAX_SAFE_INTEGER + 1 });
|
||||
invalid({ stage: 'pending' as never });
|
||||
invalid({ card: 'invented', signature: nexusChoamBetrayalSignature({ ...receipt, card: 'invented' }) });
|
||||
invalid({}, physical.filter(card => card.id !== receipt.card));
|
||||
invalid({}, [...physical, selected[0]]);
|
||||
invalid({ extra: 'hidden' } as never);
|
||||
const changedRoster = nexusReload(g);
|
||||
changedRoster.players[1].faction = 'guild';
|
||||
assert.throws(() => validateNexusChoamBetrayal(changedRoster, receipt, physical));
|
||||
const foreignOwner = nexusReload(g);
|
||||
foreignOwner.players[0].faction = 'choam';
|
||||
assert.throws(() => validateNexusChoamBetrayal(foreignOwner, receipt, physical));
|
||||
const future = nexusReload(g);
|
||||
future.phase = 2;
|
||||
assert.throws(() => validateNexusChoamBetrayal(future, receipt, physical));
|
||||
});
|
||||
Reference in new issue
Block a user