412 lines
21 KiB
TypeScript
412 lines
21 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction,
|
|
createGame,
|
|
newPlayer,
|
|
normalizeAutomaticGame,
|
|
viewGame,
|
|
type Action,
|
|
type Game,
|
|
} from '../game/engine';
|
|
import { baseDeck } from '../game/cards';
|
|
import { botActions } from '../game/bots';
|
|
import { territory } from '../game/board';
|
|
import { passNativeGuildTransportPermission } from './fixture-guild-ambassador-income-arrival';
|
|
|
|
const player = (g: Game, id: string) => g.players.find(seat => seat.id === id)!;
|
|
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
|
|
|
|
function fixture(shipper: 'g' | 'e' | 'f' = 'g', advanced = false) {
|
|
const g = createGame('GUILDTRANSPORT', newPlayer('g', 'Guild', 'guild'), advanced);
|
|
g.players.push(newPlayer('e', 'Emperor', 'emperor'), newPlayer('f', 'Fremen', 'fremen'),
|
|
newPlayer('a', 'Atreides', 'atreides'));
|
|
g.status = 'playing';
|
|
g.phase = 5;
|
|
g.turn = 2;
|
|
g.storm = 18;
|
|
g.order = ['g', 'e', 'f', 'a'];
|
|
g.movementRemaining = [...g.order];
|
|
g.active = shipper;
|
|
g.deck = baseDeck();
|
|
for (const seat of g.players) {
|
|
seat.spice = 20;
|
|
seat.reserves = 20;
|
|
seat.forces = {};
|
|
seat.hand = [];
|
|
}
|
|
if (shipper !== 'g') {
|
|
player(g, shipper).ally = 'g';
|
|
player(g, 'g').ally = shipper;
|
|
}
|
|
const karama = g.deck.splice(g.deck.findIndex(card => card.effect === 'karama'), 1)[0];
|
|
player(g, 'a').hand.push(karama);
|
|
return { g, karama };
|
|
}
|
|
|
|
function stationed(g: Game, id: string, sources: Record<string, number>) {
|
|
const seat = player(g, id);
|
|
seat.forces = { ...sources };
|
|
seat.reserves -= Object.values(sources).reduce((sum, n) => sum + n, 0);
|
|
}
|
|
function declare(g: Game, id: string, sources: Record<string, number> | 'reserves',
|
|
destination: string, sector = 0, allyPayment = 0, eliteForces?: Record<string, number>): Game {
|
|
const selection = sources === 'reserves'
|
|
? { from: 'reserves', amount: 3 }
|
|
: { forces: sources, ...(eliteForces ? { eliteForces } : {}) };
|
|
const permission = applyAction(g, id, {
|
|
type: 'guildShip', ...selection, territory: destination, sector, allyPayment,
|
|
});
|
|
return passNativeGuildTransportPermission(permission).game;
|
|
}
|
|
function canceled(g: Game, card: string): Game {
|
|
assert.equal(g.response?.kind, 'guildRate');
|
|
return applyAction(reload(g), 'a', { type: 'card', mode: 'cancel', card });
|
|
}
|
|
function rejectUnchanged(g: Game, actor: string, action: Action) {
|
|
const before = structuredClone(g);
|
|
assert.throws(() => applyAction(g, actor, action));
|
|
assert.deepEqual(g, before);
|
|
}
|
|
function physical(g: Game, id: string) {
|
|
const seat = player(g, id);
|
|
return { spice: seat.spice, reserves: seat.reserves, forces: seat.forces,
|
|
elites: seat.elites, shipped: seat.shipped };
|
|
}
|
|
|
|
void test('multi-sector Guild cross-shipment waits unpaid, then Karama charges full for the exact selected forces', () => {
|
|
const { g: initial, karama } = fixture();
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 2, 'pasty_mesa:6': 3 });
|
|
const before = physical(initial, 'g');
|
|
const offered = declare(initial, 'g', { 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, 'carthag', 11);
|
|
assert.equal(offered.response?.kind, 'guildRate');
|
|
assert.equal(offered.pendingGuildTransport?.cost, 2);
|
|
assert.equal(offered.pendingGuildTransport?.amount, 3);
|
|
assert.deepEqual(physical(offered, 'g'), before);
|
|
assert.equal(viewGame(offered, 'a').responseControls?.cancelCards.includes(karama.id), true);
|
|
assert.equal(viewGame(offered, 'a').players.find(seat => seat.id === 'g')?.spice, undefined);
|
|
assert.deepEqual(normalizeAutomaticGame(reload(offered)), reload(offered));
|
|
const done = canceled(offered, karama.id);
|
|
assert.equal(done.pendingGuildTransport, null);
|
|
assert.equal(done.response, null);
|
|
assert.equal(player(done, 'g').spice, 17);
|
|
assert.equal(player(done, 'g').reserves, 15);
|
|
assert.equal(player(done, 'g').forces['pasty_mesa:5'], 1);
|
|
assert.equal(player(done, 'g').forces['pasty_mesa:6'], 1);
|
|
assert.equal(player(done, 'g').forces['carthag:11'], 3);
|
|
assert.equal(player(done, 'g').shipped, true);
|
|
assert.equal(done.discard.filter(card => card.id === karama.id).length, 1);
|
|
rejectUnchanged(done, 'a', { type: 'card', mode: 'cancel', card: karama.id });
|
|
});
|
|
|
|
void test('Guild return to abstract reserves is still a cancelable shipment and restores its exact force group', () => {
|
|
const { g: initial, karama } = fixture();
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 2, 'pasty_mesa:6': 3 });
|
|
const offered = declare(initial, 'g', { 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, 'reserves');
|
|
assert.equal(offered.response?.kind, 'guildRate');
|
|
assert.equal(offered.pendingGuildTransport?.cost, 2);
|
|
assert.equal(player(offered, 'g').reserves, 15);
|
|
assert.equal(player(offered, 'g').spice, 20);
|
|
const done = canceled(offered, karama.id);
|
|
assert.equal(player(done, 'g').spice, 17);
|
|
assert.equal(player(done, 'g').reserves, 18);
|
|
assert.equal(player(done, 'g').forces['pasty_mesa:5'], 1);
|
|
assert.equal(player(done, 'g').forces['pasty_mesa:6'], 1);
|
|
assert.equal(player(done, 'g').shipped, true);
|
|
});
|
|
|
|
void test('passing a Guild transport rate response retains the half tariff and its Guild income', () => {
|
|
const { g: initial } = fixture('e');
|
|
stationed(initial, 'e', { 'imperial_basin:10': 2, 'imperial_basin:11': 3 });
|
|
const offered = declare(initial, 'e', { 'imperial_basin:10': 1, 'imperial_basin:11': 2 }, 'red_chasm', 7);
|
|
assert.equal(offered.pendingGuildTransport?.cost, 3);
|
|
assert.equal(player(offered, 'e').spice, 20);
|
|
assert.equal(player(offered, 'g').spice, 20);
|
|
const pendingIncome = applyAction(reload(offered), 'a', { type: 'passResponse' });
|
|
assert.equal(pendingIncome.pendingGuildTransport, null);
|
|
assert.equal(pendingIncome.response?.kind, 'guildIncome');
|
|
assert.equal(player(pendingIncome, 'e').spice, 17);
|
|
assert.equal(player(pendingIncome, 'g').spice, 20);
|
|
const done = applyAction(reload(pendingIncome), 'a', { type: 'passResponse' });
|
|
assert.equal(player(done, 'g').spice, 23);
|
|
assert.equal(player(done, 'e').forces['red_chasm:7'], 3);
|
|
assert.equal(player(done, 'e').shipped, true);
|
|
});
|
|
|
|
void test('the reciprocal Guild ally retains elite selections and original aid split at canceled full desert price', () => {
|
|
const { g: initial, karama } = fixture('e');
|
|
stationed(initial, 'e', { 'imperial_basin:10': 2, 'imperial_basin:11': 3 });
|
|
player(initial, 'e').elites = { reserves: 3, tanks: 0, forces: { 'imperial_basin:10': 1, 'imperial_basin:11': 1 }, revived: 0 };
|
|
player(initial, 'e').spice = 5;
|
|
const funded = applyAction(initial, 'g', { type: 'pledgeAid', amount: 2 });
|
|
const offered = declare(funded, 'e', { 'imperial_basin:10': 1, 'imperial_basin:11': 2 },
|
|
'red_chasm', 7, 1, { 'imperial_basin:10': 1, 'imperial_basin:11': 1 });
|
|
assert.equal(offered.pendingGuildTransport?.cost, 3);
|
|
assert.equal(offered.pendingGuildTransport?.allyPayment, 1);
|
|
assert.equal(player(offered, 'e').spice, 5);
|
|
assert.equal(player(offered, 'e').elites?.forces['imperial_basin:10'], 1);
|
|
const done = canceled(offered, karama.id);
|
|
assert.equal(player(done, 'e').spice, 0);
|
|
assert.equal(done.aid.g.amount, 1);
|
|
assert.equal(player(done, 'e').forces['imperial_basin:10'], 1);
|
|
assert.equal(player(done, 'e').forces['imperial_basin:11'], 1);
|
|
assert.equal(player(done, 'e').forces['red_chasm:7'], 3);
|
|
assert.equal(player(done, 'e').elites?.forces['red_chasm:7'], 2);
|
|
assert.equal(player(done, 'e').elites?.forces['imperial_basin:10'] ?? 0, 0);
|
|
assert.equal(player(done, 'e').elites?.forces['imperial_basin:11'] ?? 0, 0);
|
|
assert.equal(player(done, 'g').spice, 23);
|
|
assert.equal(done.guildRateBlocked, undefined);
|
|
});
|
|
|
|
void test('an unfunded full-price return withdraws intact; its replacement pays full using newly approved aid', () => {
|
|
const { g: initial, karama } = fixture();
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 2, 'pasty_mesa:6': 3 });
|
|
player(initial, 'g').spice = 2;
|
|
player(initial, 'g').ally = 'e';
|
|
player(initial, 'e').ally = 'g';
|
|
const funded = applyAction(initial, 'e', { type: 'pledgeAid', amount: 2 });
|
|
const offered = declare(funded, 'g', { 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, 'reserves');
|
|
assert.equal(offered.pendingGuildTransport?.allyPayment, 0);
|
|
const untouched = physical(offered, 'g');
|
|
const withdrawn = canceled(offered, karama.id);
|
|
assert.deepEqual(physical(withdrawn, 'g'), untouched);
|
|
assert.equal(withdrawn.aid.e.amount, 2);
|
|
assert.equal(withdrawn.pendingGuildTransport, null);
|
|
assert.equal(withdrawn.guildRateBlocked?.player, 'g');
|
|
assert.equal(viewGame(withdrawn, 'g').guildRateCanceled, true);
|
|
assert.equal(viewGame(withdrawn, 'e').guildRateCanceled, false);
|
|
assert.equal(withdrawn.discard.filter(card => card.id === karama.id).length, 1);
|
|
rejectUnchanged(withdrawn, 'g', {
|
|
type: 'guildShip', forces: { 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, territory: 'reserves', allyPayment: 0,
|
|
});
|
|
const replacement = declare(reload(withdrawn), 'g',
|
|
{ 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, 'reserves', 0, 1);
|
|
assert.notEqual(replacement.response?.kind, 'guildRate');
|
|
assert.equal(player(replacement, 'g').spice, 1);
|
|
assert.equal(player(replacement, 'g').reserves, 18);
|
|
assert.equal(replacement.aid.e.amount, 1);
|
|
assert.equal(player(replacement, 'g').shipped, true);
|
|
});
|
|
|
|
void test('an allied Fremen southern-reserve cross uses the cancelable Guild rate, not off-planet reserves', () => {
|
|
const { g: initial, karama } = fixture('f');
|
|
const offered = declare(initial, 'f', 'reserves', 'carthag', 11);
|
|
assert.equal(offered.response?.kind, 'guildRate');
|
|
assert.equal(offered.pendingGuildTransport?.cost, 2);
|
|
assert.equal(player(offered, 'f').reserves, 20);
|
|
assert.equal(player(offered, 'f').spice, 20);
|
|
const done = canceled(offered, karama.id);
|
|
assert.equal(player(done, 'f').reserves, 17);
|
|
assert.equal(player(done, 'f').spice, 17);
|
|
assert.equal(player(done, 'f').forces['carthag:11'], 3);
|
|
assert.equal(player(done, 'g').spice, 23);
|
|
assert.equal(player(done, 'f').shipped, true);
|
|
});
|
|
|
|
void test('a withdrawn Fremen southern-reserve declaration preserves its native forces and requires full-price replacement', () => {
|
|
const { g: initial, karama } = fixture('f');
|
|
player(initial, 'f').spice = 2;
|
|
const offered = declare(initial, 'f', 'reserves', 'carthag', 11);
|
|
const withdrawn = canceled(offered, karama.id);
|
|
assert.equal(withdrawn.pendingGuildTransport, null);
|
|
assert.equal(player(withdrawn, 'f').reserves, 20);
|
|
assert.equal(player(withdrawn, 'f').forces['carthag:11'] ?? 0, 0);
|
|
assert.equal(player(withdrawn, 'f').spice, 2);
|
|
assert.equal(player(withdrawn, 'f').shipped, false);
|
|
assert.equal(viewGame(withdrawn, 'f').guildRateCanceled, true);
|
|
rejectUnchanged(withdrawn, 'f', {
|
|
type: 'guildShip', from: 'reserves', amount: 3, territory: 'carthag', sector: 11,
|
|
});
|
|
const replacement = applyAction(reload(withdrawn), 'f', {
|
|
type: 'guildShip', from: 'reserves', amount: 2, territory: 'carthag', sector: 11,
|
|
});
|
|
assert.notEqual(replacement.response?.kind, 'guildRate');
|
|
assert.equal(player(replacement, 'f').spice, 0);
|
|
assert.equal(player(replacement, 'f').reserves, 18);
|
|
assert.equal(player(replacement, 'f').forces['carthag:11'], 2);
|
|
});
|
|
|
|
void test('Advanced cross-transport retains the Guild rate response separately from the reserve shipment stop', () => {
|
|
const { g: initial, karama } = fixture('e', true);
|
|
stationed(initial, 'e', { 'imperial_basin:10': 3 });
|
|
const offered = declare(initial, 'e', { 'imperial_basin:10': 3 }, 'arrakeen', 10);
|
|
assert.equal(offered.decision, null);
|
|
assert.equal(offered.response?.kind, 'guildRate');
|
|
assert.equal(player(offered, 'e').forces['arrakeen:10'] ?? 0, 0);
|
|
const done = canceled(offered, karama.id);
|
|
assert.equal(player(done, 'e').spice, 17);
|
|
assert.equal(player(done, 'e').forces['arrakeen:10'], 3);
|
|
assert.equal(player(done, 'e').shipped, true);
|
|
});
|
|
|
|
void test('independently purchased Karama shipping remains half-price and bank-paid, not Guild-cancelable', () => {
|
|
const { g: initial, karama } = fixture('e');
|
|
player(initial, 'a').hand.splice(player(initial, 'a').hand.indexOf(karama), 1);
|
|
player(initial, 'e').hand.push(karama);
|
|
stationed(initial, 'e', { 'imperial_basin:10': 3 });
|
|
const purchased = applyAction(initial, 'e', { type: 'card', mode: 'shipment', card: karama.id });
|
|
assert.equal(purchased.karamaShipping?.player, 'e');
|
|
const done = declare(purchased, 'e', { 'imperial_basin:10': 3 }, 'arrakeen', 10);
|
|
assert.notEqual(done.response?.kind, 'guildRate');
|
|
assert.equal(player(done, 'e').spice, 18);
|
|
assert.equal(player(done, 'g').spice, 20);
|
|
assert.equal(player(done, 'e').forces['arrakeen:10'], 3);
|
|
assert.equal(done.karamaShipping, null);
|
|
});
|
|
|
|
void test('BG Worthless conversion preserves the signed cross-shipment while its cancellation waits', () => {
|
|
const { g: initial, karama } = fixture('g', true);
|
|
initial.players.push(newPlayer('b', 'Bene Gesserit', 'beneGesserit'));
|
|
initial.order.push('b');
|
|
initial.movementRemaining!.push('b');
|
|
const bg = player(initial, 'b');
|
|
bg.spice = 20;
|
|
bg.reserves = 20;
|
|
bg.forces = {};
|
|
const index = initial.deck.findIndex(card => card.kind === 'worthless');
|
|
const worthless = initial.deck.splice(index, 1)[0];
|
|
bg.hand.push(worthless);
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 3 });
|
|
let g = declare(initial, 'g', { 'pasty_mesa:5': 3 }, 'carthag', 11);
|
|
const signature = g.pendingGuildTransport!.signature;
|
|
g = applyAction(reload(g), 'b', { type: 'card', mode: 'cancel', card: worthless.id });
|
|
assert.equal(g.response?.kind, 'worthlessKarama');
|
|
assert.equal(g.pendingKarama?.use.kind === 'cancel'
|
|
? g.pendingKarama.use.response.kind : null, 'guildRate');
|
|
assert.equal(g.pendingGuildTransport?.signature, signature);
|
|
assert.equal(viewGame(reload(g), 'a').response?.kind, 'worthlessKarama');
|
|
for (let turn = 0; g.response && turn < 12; turn++) {
|
|
const responder = g.players.find(seat => !g.response!.passed.includes(seat.id))!;
|
|
g = applyAction(reload(g), responder.id, { type: 'passResponse' });
|
|
}
|
|
assert.equal(g.pendingGuildTransport, null);
|
|
assert.equal(g.response, null);
|
|
assert.equal(player(g, 'g').spice, 17);
|
|
assert.equal(player(g, 'g').forces['carthag:11'], 3);
|
|
assert.equal(g.discard.filter(card => card.id === worthless.id).length, 1);
|
|
assert.equal(player(g, 'a').hand.some(card => card.id === karama.id), true);
|
|
});
|
|
|
|
void test('a restored pending cross rejects stale or tampered physical and response data without mutation', () => {
|
|
const { g: initial, karama } = fixture();
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 2, 'pasty_mesa:6': 3 });
|
|
const offered = reload(declare(initial, 'g',
|
|
{ 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, 'carthag', 11));
|
|
const cancel: Action = { type: 'card', mode: 'cancel', card: karama.id };
|
|
rejectUnchanged(offered, 'g', { type: 'guildShip', from: 'pasty_mesa:5', amount: 1, territory: 'reserves' });
|
|
for (const corrupt of [
|
|
(g: Game) => { g.response!.guildRateEvent = 'stale'; },
|
|
(g: Game) => { g.pendingGuildTransport!.signature = 'stale'; },
|
|
(g: Game) => { g.pendingGuildTransport!.turn = g.turn - 1; },
|
|
(g: Game) => { g.pendingGuildTransport!.group[0][1] = 2; },
|
|
(g: Game) => { g.pendingGuildTransport!.cost = 1; },
|
|
]) {
|
|
const stale = reload(offered);
|
|
corrupt(stale);
|
|
const before = structuredClone(stale);
|
|
assert.throws(() => viewGame(stale, 'a'));
|
|
assert.throws(() => normalizeAutomaticGame(stale));
|
|
assert.deepEqual(stale, before);
|
|
rejectUnchanged(stale, 'a', cancel);
|
|
}
|
|
});
|
|
|
|
void test('a depleted saved source cannot spend or arrive from the obsolete force group', () => {
|
|
const { g: initial } = fixture();
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 2, 'pasty_mesa:6': 3 });
|
|
const offered = reload(declare(initial, 'g',
|
|
{ 'pasty_mesa:5': 1, 'pasty_mesa:6': 2 }, 'carthag', 11));
|
|
player(offered, 'g').forces['pasty_mesa:5'] = 0;
|
|
const spice = player(offered, 'g').spice;
|
|
const remaining = structuredClone(player(offered, 'g').forces);
|
|
const released = applyAction(offered, 'a', { type: 'passResponse' });
|
|
assert.equal(released.pendingGuildTransport, null);
|
|
assert.equal(player(released, 'g').spice, spice);
|
|
assert.deepEqual(player(released, 'g').forces, remaining);
|
|
assert.equal(player(released, 'g').forces['carthag:11'] ?? 0, 0);
|
|
assert.equal(player(released, 'g').shipped, false);
|
|
});
|
|
|
|
void test('all AI profiles pass pending transport and a blocked ally still selects a legal full-price shipment', () => {
|
|
for (const level of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
const { g: initial } = fixture();
|
|
player(initial, 'a').bot = level;
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 3 });
|
|
const offered = declare(initial, 'g', { 'pasty_mesa:5': 3 }, 'carthag', 11);
|
|
const reply = botActions(viewGame(offered, 'a'))[0];
|
|
assert.ok(reply, `${level} must respond to a legal Guild transport`);
|
|
const completed = applyAction(reload(offered), 'a', reply);
|
|
assert.equal(completed.pendingGuildTransport, null);
|
|
assert.equal(player(completed, 'g').forces['carthag:11'], 3);
|
|
assert.equal(player(completed, 'g').spice, 18);
|
|
}
|
|
const { g: initial, karama } = fixture('e');
|
|
stationed(initial, 'e', { 'imperial_basin:10': 4 });
|
|
player(initial, 'e').spice = 2;
|
|
const withdrawn = canceled(declare(initial, 'e',
|
|
{ 'imperial_basin:10': 4 }, 'arrakeen', 10), karama.id);
|
|
player(withdrawn, 'e').bot = 'Easy';
|
|
const candidate = botActions(viewGame(withdrawn, 'e')).find(action =>
|
|
action.type === 'ship' || action.type === 'guildShip');
|
|
assert.ok(candidate, 'A canceled discount leaves the allied bot a legal full-price shipment.');
|
|
const done = applyAction(reload(withdrawn), 'e', candidate);
|
|
assert.equal(player(done, 'e').shipped, true);
|
|
assert.ok(player(done, 'e').spice < 2);
|
|
});
|
|
|
|
void test('controlled southern-reserve permission denial preserves typed Fremen counters and their separate free reinforcement', () => {
|
|
const { g: initial, karama } = fixture('f', true);
|
|
player(initial, 'f').elites = { reserves: 3, tanks: 0, forces: {}, revived: 0 };
|
|
const offered = applyAction(reload(initial), 'f', { type: 'guildShip', from: 'reserves',
|
|
territory: 'carthag', sector: 11, amount: 3, elite: 1, allyPayment: 0 });
|
|
assert.equal(offered.response?.kind, 'guildTransportPermission');
|
|
const denied = applyAction(offered, 'a', { type: 'card', mode: 'cancel', card: karama.id });
|
|
assert.deepEqual(physical(denied, 'f'), physical(initial, 'f'));
|
|
assert.equal(viewGame(denied, 'f').guildTransportCanceled, true);
|
|
assert.equal(viewGame(denied, 'f').guildRateCanceled, false);
|
|
const sector = territory('the_great_flat').sectors.find(value => value !== denied.storm)!;
|
|
const done = applyAction(denied, 'f', { type: 'ship', territory: 'the_great_flat',
|
|
sector, amount: 3, elite: 1, allyPayment: 0 });
|
|
assert.equal(player(done, 'f').spice, player(initial, 'f').spice);
|
|
assert.equal(player(done, 'g').spice, player(initial, 'g').spice);
|
|
assert.equal(player(done, 'f').reserves, 17);
|
|
assert.equal(player(done, 'f').elites?.reserves, 2);
|
|
assert.equal(player(done, 'f').forces[`the_great_flat:${sector}`], 3);
|
|
assert.equal(player(done, 'f').elites?.forces[`the_great_flat:${sector}`], 1);
|
|
assert.equal(player(done, 'f').shipped, true);
|
|
});
|
|
|
|
for (const counterConversion of [false, true]) {
|
|
void test(`controlled BG conversion ${counterConversion ? 'countered' : 'allowed'} keeps the original native permission source and physical group`, () => {
|
|
const { g: initial, karama } = fixture('g', true);
|
|
stationed(initial, 'g', { 'pasty_mesa:5': 5 });
|
|
initial.players.push(newPlayer('b', 'Bene Gesserit', 'beneGesserit'));
|
|
initial.order.push('b'); initial.movementRemaining!.push('b');
|
|
const index = initial.deck.findIndex(card => card.kind === 'worthless');
|
|
assert.ok(index >= 0);
|
|
const worthless = initial.deck.splice(index, 1)[0];
|
|
player(initial, 'b').hand.push(worthless);
|
|
let g = applyAction(reload(initial), 'g', { type: 'guildShip', from: 'pasty_mesa:5',
|
|
territory: 'carthag', sector: 11, amount: 3, elite: 0, allyPayment: 0 });
|
|
assert.equal(g.response?.kind, 'guildTransportPermission');
|
|
g = applyAction(g, 'b', { type: 'card', mode: 'cancel', card: worthless.id });
|
|
assert.equal(g.response?.kind, 'worthlessKarama');
|
|
assert.deepEqual(physical(g, 'g'), physical(initial, 'g'));
|
|
g = applyAction(g, 'a', counterConversion
|
|
? { type: 'card', mode: 'cancel', card: karama.id } : { type: 'passResponse' });
|
|
if (counterConversion) {
|
|
assert.equal(player(g, 'g').spice, 18);
|
|
assert.equal(player(g, 'g').forces['pasty_mesa:5'], 2);
|
|
assert.equal(player(g, 'g').forces['carthag:11'], 3);
|
|
assert.equal(player(g, 'g').shipped, true);
|
|
assert.ok(!viewGame(g, 'g').guildTransportCanceled);
|
|
} else {
|
|
assert.deepEqual(physical(g, 'g'), physical(initial, 'g'));
|
|
assert.equal(viewGame(g, 'g').guildTransportCanceled, true);
|
|
assert.ok(player(g, 'a').hand.some(card => card.id === karama.id));
|
|
}
|
|
assert.equal(g.discard.filter(card => card.id === worthless.id).length, 1);
|
|
assert.equal(g.discard.filter(card => card.id === karama.id).length, counterConversion ? 1 : 0);
|
|
});
|
|
}
|