491 lines
18 KiB
TypeScript
491 lines
18 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { createGame, newPlayer, type Game, type Player } from '../game/engine';
|
|
import { createAuditorLeader, leaders } from '../game/cards';
|
|
import type { FactionId } from '../game/catalog';
|
|
import { createNexusCards } from '../game/nexus-cards';
|
|
import { forceRevivalRemaining, leaderRevivalOptions, newRevivalRules } from '../game/revival';
|
|
import { createDukeVidal, DUKE_VIDAL_ID } from '../game/duke-vidal';
|
|
import {
|
|
NEXUS_TLEILAXU_FORCE_GROUP_CAP,
|
|
NEXUS_TLEILAXU_POLICY,
|
|
TleilaxuSecretAllyError,
|
|
quoteTleilaxuSecretAlly,
|
|
tleilaxuSecretAllyBlock,
|
|
tleilaxuSecretAllyEvent,
|
|
tleilaxuSecretAllyForceBounds,
|
|
tleilaxuSecretAllyOptions,
|
|
} from '../game/nexus-tleilaxu-revival';
|
|
|
|
/** A Treachery card id, i.e. what a battle plan uses for a Cheap Hero. */
|
|
const CHEAP_HERO = 'treachery-17';
|
|
|
|
function holdNexusCard(g: Game, holder: string) {
|
|
const cards = createNexusCards(
|
|
g.players.map((seat) => ({
|
|
id: seat.id,
|
|
faction: seat.faction,
|
|
ally: seat.ally,
|
|
})),
|
|
() => 0.5,
|
|
);
|
|
cards.deck = cards.deck.filter((card) => card !== 'tleilaxu');
|
|
cards.hands[holder] = 'tleilaxu';
|
|
g.nexusCards = { cards, phase: null };
|
|
}
|
|
|
|
function fixture(advanced = true, faction: FactionId = 'atreides') {
|
|
const g = createGame('NEXUSREVIVAL', newPlayer('a', 'Holder', faction), advanced);
|
|
g.players.push(newPlayer('h', 'Rival', 'harkonnen'));
|
|
Object.assign(g, {
|
|
status: 'playing',
|
|
phase: 4,
|
|
turn: 3,
|
|
order: ['a', 'h'],
|
|
revivalRules: newRevivalRules(),
|
|
});
|
|
for (const p of g.players) {
|
|
p.spice = 20;
|
|
p.tanks = 10;
|
|
p.elites = { reserves: 0, tanks: 0, forces: {}, revived: 0 };
|
|
}
|
|
holdNexusCard(g, 'a');
|
|
return g;
|
|
}
|
|
|
|
function kill(p: Player, indexes: number[], deaths = 1) {
|
|
for (const index of indexes) {
|
|
p.leaders[index].dead = true;
|
|
p.leaders[index].deaths = deaths;
|
|
}
|
|
}
|
|
|
|
void test('actual held card, unallied non-Tleilaxu holder, quiet Revival and an owner-only event bind the projection', () => {
|
|
const g = fixture();
|
|
kill(g.players[0], [0]);
|
|
const before = structuredClone(g);
|
|
const offer = tleilaxuSecretAllyOptions(g, g.players[0]);
|
|
assert.equal(offer.blocked, null);
|
|
assert.equal(tleilaxuSecretAllyBlock(g, g.players[0]), null);
|
|
assert.equal(offer.event, tleilaxuSecretAllyEvent(g, g.players[0]));
|
|
assert.equal(
|
|
offer.event,
|
|
JSON.stringify(['nexusTleilaxuSecretAlly', 3, 4, 'a']),
|
|
);
|
|
assert.equal(offer.policy, NEXUS_TLEILAXU_POLICY);
|
|
assert.deepEqual(offer.leaders, [
|
|
{
|
|
id: g.players[0].leaders[0].id,
|
|
name: g.players[0].leaders[0].name,
|
|
kind: 'leader',
|
|
early: true,
|
|
},
|
|
]);
|
|
assert.deepEqual(g, before);
|
|
|
|
g.nexusCards!.cards!.hands.a = null;
|
|
assert.match(tleilaxuSecretAllyBlock(g, g.players[0])!, /holder of the Tleilaxu Nexus card/);
|
|
g.nexusCards!.cards!.hands.a = 'tleilaxu';
|
|
|
|
g.players[0].ally = 'h';
|
|
g.players[1].ally = 'a';
|
|
assert.match(tleilaxuSecretAllyBlock(g, g.players[0])!, /allied player/);
|
|
g.players[0].ally = null;
|
|
g.players[1].ally = null;
|
|
|
|
g.players.push(newPlayer('t', 'Native', 'tleilaxu'));
|
|
assert.match(tleilaxuSecretAllyBlock(g, g.players[0])!, /Tleilaxu absent/);
|
|
g.players.pop();
|
|
|
|
g.phase = 5;
|
|
assert.match(tleilaxuSecretAllyBlock(g, g.players[0])!, /during Revival/);
|
|
assert.throws(
|
|
() =>
|
|
quoteTleilaxuSecretAlly(g, g.players[0], {
|
|
leader: g.players[0].leaders[0].id,
|
|
amount: 0,
|
|
elite: 0,
|
|
}),
|
|
(error: unknown) =>
|
|
error instanceof TleilaxuSecretAllyError && error.reason === 'timing',
|
|
);
|
|
g.phase = 4;
|
|
g.status = 'lobby';
|
|
assert.match(tleilaxuSecretAllyBlock(g, g.players[0])!, /started game/);
|
|
g.status = 'playing';
|
|
|
|
g.pendingCapture = { player: 'a', loser: 'h', territory: 'arrakeen' };
|
|
assert.match(tleilaxuSecretAllyBlock(g, g.players[0])!, /Finish the current interaction/);
|
|
g.pendingCapture = null;
|
|
assert.equal(tleilaxuSecretAllyBlock(g, g.players[0]), null);
|
|
});
|
|
|
|
void test('the roster waiver belongs to the card alone and never opens the ordinary cycle', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
kill(p, [0]);
|
|
// Ordinary rules still refuse: the cohort has never been opened (cycle 0).
|
|
assert.deepEqual(leaderRevivalOptions(g, p).leaders, []);
|
|
assert.equal(leaderRevivalOptions(g, p).cycle, 0);
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.deepEqual(
|
|
offer.leaders.map((leader) => [leader.id, leader.kind, leader.early]),
|
|
[[p.leaders[0].id, 'leader', true]],
|
|
);
|
|
// The synthetic cycle is a local projection only.
|
|
assert.equal(p.revivalCycle, 0);
|
|
assert.deepEqual(leaderRevivalOptions(g, p).leaders, []);
|
|
assert.deepEqual(g.revivalRules!.earlyBlocked, []);
|
|
|
|
// A second death is not returned early while the cohort has never been opened,
|
|
// and the same disc is an ordinary candidate once that cohort level is open.
|
|
const second = fixture();
|
|
const seat = second.players[0];
|
|
kill(seat, [0], 2);
|
|
const withheld = tleilaxuSecretAllyOptions(second, seat);
|
|
assert.deepEqual(withheld.leaders, []);
|
|
assert.match(withheld.blocked!, /one own dead leader/);
|
|
seat.revivalCycle = 2;
|
|
assert.deepEqual(
|
|
tleilaxuSecretAllyOptions(second, seat).leaders.map((leader) => [leader.id, leader.early]),
|
|
[[seat.leaders[0].id, false]],
|
|
);
|
|
});
|
|
|
|
void test('the Tleilaxu waiver reaches an unopened saved Ecaz cohort without reopening a previously returned disc', () => {
|
|
const g = fixture(false, 'ecaz'), p = g.players[0];
|
|
g.dukeVidal = createDukeVidal();
|
|
kill(p, [0], 2);
|
|
kill(p, [1]);
|
|
p.ecazRevivalCohort = { cycle: 0, returned: [p.leaders[0].id] };
|
|
const before = structuredClone(g);
|
|
assert.deepEqual(leaderRevivalOptions(g, p).leaders, []);
|
|
assert.deepEqual(tleilaxuSecretAllyOptions(g, p).leaders.map(l => l.id), [p.leaders[1].id]);
|
|
const quote = quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[1].id, amount: 0, elite: 0 });
|
|
assert.equal(quote.kind, 'leader');
|
|
assert.equal(quote.cost, 0);
|
|
assert.throws(() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 0, elite: 0 }));
|
|
assert.deepEqual(leaderRevivalOptions(g, p).leaders, []);
|
|
assert.deepEqual(g, before);
|
|
});
|
|
|
|
void test('an opened ordinary cohort keeps canonical eligibility and is never marked early', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
for (const leader of p.leaders) {
|
|
leader.dead = true;
|
|
leader.deaths = 1;
|
|
}
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.deepEqual(
|
|
offer.leaders.map((leader) => leader.id),
|
|
p.leaders.map((leader) => leader.id),
|
|
);
|
|
assert.ok(offer.leaders.every((leader) => leader.kind === 'leader' && !leader.early));
|
|
});
|
|
|
|
void test('one own dead leader is mandatory: zero forces is legal, a force-only return is not, and the used slot closes it', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.deepEqual(offer.leaders, []);
|
|
assert.match(offer.blocked!, /one own dead leader/);
|
|
assert.ok(offer.maxForces > 0);
|
|
assert.throws(
|
|
() =>
|
|
quoteTleilaxuSecretAlly(g, p, {
|
|
leader: p.leaders[0].id,
|
|
amount: 0,
|
|
elite: 0,
|
|
}),
|
|
(error: unknown) =>
|
|
error instanceof TleilaxuSecretAllyError && error.reason === 'leader',
|
|
);
|
|
kill(p, [0]);
|
|
assert.equal(quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 0, elite: 0 }).cost, 0);
|
|
p.leaderRevived = true;
|
|
assert.deepEqual(tleilaxuSecretAllyOptions(g, p).leaders, []);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 0, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
});
|
|
|
|
void test('exact paid typed counters: one bank spice each, physical Tanks, and the Advanced one-per-turn cap', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
p.elites!.tanks = 3;
|
|
kill(p, [0]);
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.equal(offer.maxForces, NEXUS_TLEILAXU_FORCE_GROUP_CAP);
|
|
assert.equal(offer.maxElite, 1);
|
|
assert.deepEqual(
|
|
quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 4, elite: 1 }),
|
|
{ leader: p.leaders[0].id, kind: 'leader', amount: 4, elite: 1, cost: 4, limit: 5 },
|
|
);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 4, elite: 2 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 1, elite: 2 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 6, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
p.elites!.tanks = 0;
|
|
kill(p, [1, 2, 3, 4]);
|
|
p.tanks = 2;
|
|
assert.equal(tleilaxuSecretAllyForceBounds(g, p).maxForces, 2);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 3, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
});
|
|
|
|
void test('Ixian Cyborgs and Basic typed counters keep identity without the Advanced cap or ordinary pricing', () => {
|
|
const advanced = fixture(true, 'ixians');
|
|
advanced.players[0].elites = { reserves: 0, tanks: 3, forces: {}, revived: 4 };
|
|
kill(advanced.players[0], [0]);
|
|
const ix = tleilaxuSecretAllyOptions(advanced, advanced.players[0]);
|
|
assert.equal(ix.maxElite, 3);
|
|
assert.equal(
|
|
quoteTleilaxuSecretAlly(advanced, advanced.players[0], {
|
|
leader: advanced.players[0].leaders[0].id,
|
|
amount: 5,
|
|
elite: 3,
|
|
}).cost,
|
|
5,
|
|
);
|
|
const basic = fixture(false, 'emperor');
|
|
basic.players[0].elites = { reserves: 0, tanks: 2, forces: {}, revived: 5 };
|
|
kill(basic.players[0], [0]);
|
|
const emperor = tleilaxuSecretAllyOptions(basic, basic.players[0]);
|
|
assert.equal(emperor.maxForces, 5);
|
|
assert.equal(emperor.maxElite, 2);
|
|
});
|
|
|
|
void test('the shared ordinary ceiling is raised to five, counts earlier returns, and never doubles or shrinks a native limit', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
kill(p, [0]);
|
|
p.revived = 4;
|
|
assert.equal(tleilaxuSecretAllyOptions(g, p).maxForces, 1);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 2, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
p.revived = 5;
|
|
const spent = tleilaxuSecretAllyOptions(g, p);
|
|
assert.equal(spent.maxForces, 0);
|
|
assert.equal(spent.blocked, null);
|
|
assert.equal(quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 0, elite: 0 }).cost, 0);
|
|
assert.equal(tleilaxuSecretAllyForceBounds(g, p).ceiling, 5);
|
|
|
|
// The group is not an independent extra five: its paid counters come out of
|
|
// the same ordinary allowance, in either order.
|
|
const ordered = fixture();
|
|
const holder = ordered.players[0];
|
|
kill(holder, [0]);
|
|
const ordinary = forceRevivalRemaining(ordered, holder);
|
|
assert.equal(ordinary, 3);
|
|
const group = quoteTleilaxuSecretAlly(ordered, holder, {
|
|
leader: holder.leaders[0].id,
|
|
amount: 2,
|
|
elite: 0,
|
|
});
|
|
holder.revived += group.amount;
|
|
assert.equal(forceRevivalRemaining(ordered, holder), ordinary - 2);
|
|
assert.deepEqual(ordered.revivalRules!.expanded, []);
|
|
|
|
const choam = fixture(true, 'choam');
|
|
kill(choam.players[0], [0]);
|
|
choam.players[0].revived = 6;
|
|
assert.equal(tleilaxuSecretAllyForceBounds(choam, choam.players[0]).ceiling, 20);
|
|
assert.equal(tleilaxuSecretAllyOptions(choam, choam.players[0]).maxForces, NEXUS_TLEILAXU_FORCE_GROUP_CAP);
|
|
|
|
const recruits = fixture();
|
|
kill(recruits.players[0], [0]);
|
|
recruits.recruits = { turn: recruits.turn, player: 'h', card: 'ecaz-recruits' };
|
|
assert.equal(tleilaxuSecretAllyForceBounds(recruits, recruits.players[0]).ceiling, 7);
|
|
assert.equal(tleilaxuSecretAllyOptions(recruits, recruits.players[0]).maxForces, NEXUS_TLEILAXU_FORCE_GROUP_CAP);
|
|
assert.equal(
|
|
quoteTleilaxuSecretAlly(recruits, recruits.players[0], {
|
|
leader: recruits.players[0].leaders[0].id,
|
|
amount: 5,
|
|
elite: 0,
|
|
}).cost,
|
|
5,
|
|
);
|
|
});
|
|
|
|
void test('a native Fremen free-rate restriction never rejects the explicit paid grant', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
p.faction = 'fremen';
|
|
p.leaders = leaders('fremen');
|
|
p.revived = 3;
|
|
p.freeForcesRevived = 3;
|
|
kill(p, [0]);
|
|
// The ordinary paid Fremen path is closed without Tleilaxu in play.
|
|
assert.equal(forceRevivalRemaining(g, p), 0);
|
|
assert.equal(
|
|
quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 2, elite: 0 }).cost,
|
|
2,
|
|
);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: p.leaders[0].id, amount: 3, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
});
|
|
|
|
void test('Kwisatz and Auditor keep their own canonical return cycles inside the grant', () => {
|
|
const g = fixture(true, 'atreides');
|
|
const p = g.players[0];
|
|
p.kwisatz = { dead: true, revivalCycle: 1 };
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.deepEqual(offer.leaders, [
|
|
{ id: 'kwisatz', name: 'Kwisatz Haderach', kind: 'kwisatz', early: true },
|
|
]);
|
|
assert.equal(
|
|
quoteTleilaxuSecretAlly(g, p, { leader: 'kwisatz', amount: 0, elite: 0 }).kind,
|
|
'kwisatz',
|
|
);
|
|
p.kwisatz = { dead: true, revivalCycle: 2 };
|
|
assert.deepEqual(tleilaxuSecretAllyOptions(g, p).leaders, []);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: 'kwisatz', amount: 0, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
p.kwisatz = { dead: true, revivalCycle: 1 };
|
|
g.revivalRules!.earlyBlocked.push('a:kwisatz');
|
|
assert.deepEqual(tleilaxuSecretAllyOptions(g, p).leaders, []);
|
|
g.revivalRules!.earlyBlocked.length = 0;
|
|
p.kwisatz = { dead: false, revivalCycle: 1 };
|
|
kill(p, [0]);
|
|
g.revivalRules!.earlyBlocked.push(`a:${p.leaders[0].id}`);
|
|
assert.deepEqual(tleilaxuSecretAllyOptions(g, p).leaders, []);
|
|
|
|
const choam = fixture(true, 'choam');
|
|
const seat = choam.players[0];
|
|
const auditor = createAuditorLeader();
|
|
auditor.dead = true;
|
|
auditor.deaths = 1;
|
|
seat.leaders.push(auditor);
|
|
kill(seat, [0]);
|
|
assert.deepEqual(tleilaxuSecretAllyOptions(choam, seat).leaders, [
|
|
{ id: auditor.id, name: 'Auditor', kind: 'leader', early: false },
|
|
{ id: seat.leaders[0].id, name: seat.leaders[0].name, kind: 'leader', early: true },
|
|
]);
|
|
});
|
|
|
|
void test('the shared Ecaz Duke is one more own identity and its custody blocks are named, not silently narrowed', () => {
|
|
const shared = fixture(false, 'ecaz');
|
|
shared.dukeVidal = createDukeVidal();
|
|
shared.dukeVidal!.leader.dead = true;
|
|
shared.dukeVidal!.leader.deaths = 1;
|
|
const p = shared.players[0];
|
|
const offer = tleilaxuSecretAllyOptions(shared, p);
|
|
assert.deepEqual(offer.leaders, [
|
|
{ id: DUKE_VIDAL_ID, name: shared.dukeVidal!.leader.name, kind: 'duke', early: false },
|
|
]);
|
|
assert.equal(
|
|
quoteTleilaxuSecretAlly(shared, p, { leader: DUKE_VIDAL_ID, amount: 1, elite: 0 }).cost,
|
|
1,
|
|
);
|
|
|
|
const contested = fixture(true, 'ecaz');
|
|
contested.dukeVidal = createDukeVidal();
|
|
contested.dukeVidal!.leader.dead = true;
|
|
contested.dukeVidal!.leader.deaths = 1;
|
|
const seat = contested.players[0];
|
|
assert.match(tleilaxuSecretAllyOptions(contested, seat).blocked!, /Advanced Harkonnen/);
|
|
kill(seat, [0]);
|
|
const withLeader = tleilaxuSecretAllyOptions(contested, seat);
|
|
assert.equal(withLeader.blocked, null);
|
|
assert.ok(withLeader.leaders.every((leader) => leader.kind === 'leader'));
|
|
});
|
|
|
|
void test('living, captured, foreign ghola, wrong-faction and Cheap Hero custody are never candidates', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
kill(p, [0]);
|
|
p.leaders[1].dead = true;
|
|
p.leaders[1].deaths = 1;
|
|
p.leaders[1].capturedBy = 'h';
|
|
p.leaders[2].dead = true;
|
|
p.leaders[2].deaths = 1;
|
|
p.leaders[2].gholaBy = 'h';
|
|
p.leaders.push({
|
|
id: CHEAP_HERO,
|
|
name: 'Cheap Hero',
|
|
strength: 0,
|
|
faction: 'atreides',
|
|
dead: true,
|
|
deaths: 1,
|
|
});
|
|
p.leaders.push({
|
|
id: 'harkonnen-0',
|
|
name: 'Foreign Disc',
|
|
strength: 6,
|
|
faction: 'harkonnen',
|
|
dead: true,
|
|
deaths: 1,
|
|
});
|
|
const before = structuredClone(g);
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.deepEqual(
|
|
offer.leaders.map((leader) => leader.id),
|
|
[p.leaders[0].id],
|
|
);
|
|
assert.deepEqual(g, before);
|
|
for (const id of [CHEAP_HERO, p.leaders[1].id, p.leaders[2].id, 'harkonnen-0', 'nobody'])
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: id, amount: 0, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
});
|
|
|
|
void test('every offered candidate and bound quotes successfully and every overage is refused', () => {
|
|
const g = fixture();
|
|
const p = g.players[0];
|
|
p.elites!.tanks = 3;
|
|
kill(p, [0, 1]);
|
|
const offer = tleilaxuSecretAllyOptions(g, p);
|
|
assert.equal(offer.blocked, null);
|
|
assert.deepEqual(
|
|
offer.leaders.map((leader) => leader.id),
|
|
[p.leaders[0].id, p.leaders[1].id],
|
|
);
|
|
for (const leader of offer.leaders)
|
|
for (let amount = 0; amount <= offer.maxForces; amount++)
|
|
for (let elite = 0; elite <= Math.min(amount, offer.maxElite); elite++) {
|
|
const quote = quoteTleilaxuSecretAlly(g, p, { leader: leader.id, amount, elite });
|
|
assert.equal(quote.kind, leader.kind);
|
|
assert.equal(quote.cost, amount);
|
|
assert.equal(quote.limit, offer.maxForces);
|
|
}
|
|
const target = offer.leaders[0].id;
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: target, amount: offer.maxForces + 1, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: target, amount: 1.5, elite: 0 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: target, amount: -1, elite: -1 }),
|
|
TleilaxuSecretAllyError,
|
|
);
|
|
p.spice = 0;
|
|
assert.throws(
|
|
() => quoteTleilaxuSecretAlly(g, p, { leader: target, amount: 1, elite: 0 }),
|
|
(error: unknown) =>
|
|
error instanceof TleilaxuSecretAllyError && error.reason === 'price',
|
|
);
|
|
assert.equal(quoteTleilaxuSecretAlly(g, p, { leader: target, amount: 0, elite: 0 }).cost, 0);
|
|
});
|