155 lines
9.3 KiB
TypeScript
155 lines
9.3 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import { applyAction, viewGame, type Game } from '../game/engine';
|
|
import { botArrivalBlock } from '../game/bot-arrival';
|
|
import { guildTransportQuote } from '../game/transport-quote';
|
|
import {
|
|
allowGuildAmbassadorIncomeArrival, cancelGuildAmbassadorIncomeArrival,
|
|
completeGuildAmbassadorIncomeArrival,
|
|
createGuildAmbassadorIncomeArrival, declareGuildAmbassadorIncomeArrival,
|
|
guildAmbassadorIncomeChoice, passNativeGuildTransportPermission,
|
|
} from './fixture-guild-ambassador-income-arrival';
|
|
import { pairedE3NexusPolicy, pairedE3NexusInventory as guildAmbassadorIncomeInventory,
|
|
pairedE3NexusPlayer as player, pairedE3NexusReload as reload } from './fixture-paired-e3-nexus-modules';
|
|
|
|
function rejectUnchanged(game: Game, actor: string, action: Parameters<typeof applyAction>[2]) {
|
|
const before = reload(game);
|
|
assert.throws(() => applyAction(game, actor, action));
|
|
assert.deepEqual(game, before);
|
|
}
|
|
|
|
for (const trigger of [true, false]) {
|
|
for (const cancelIncome of [false, true]) {
|
|
void test(`native allied Guild cross-transport ${cancelIncome ? 'cancels' : 'allows'} its real income before Ecaz ${trigger ? 'acquires Duke' : 'declines entry'}`, () => {
|
|
const f = createGuildAmbassadorIncomeArrival();
|
|
const start = f.beforeAction, payer = player(start, f.entrant), guild = player(start, f.guild);
|
|
assert.equal(start.turn, 2);
|
|
assert.equal(start.phase, 5);
|
|
assert.equal(start.active, f.entrant);
|
|
assert.equal(payer.shipped, false);
|
|
assert.equal(payer.moved, 0);
|
|
assert.equal(payer.ally, f.guild);
|
|
assert.equal(guild.ally, f.entrant);
|
|
assert.equal(f.beforeAlliance.nexus, true);
|
|
assert.equal(player(f.beforeAlliance, f.entrant).ally, null);
|
|
assert.equal(player(f.alliance, f.entrant).ally, f.guild);
|
|
assert.equal(player(f.purchase.after, f.ecaz).spice, player(f.purchase.before, f.ecaz).spice - 1);
|
|
assert.equal(player(f.placement.after, f.ecaz).spice, player(f.placement.before, f.ecaz).spice - 1);
|
|
assert.ok(player(start, f.ecaz).hand.some(c => c.id === f.karama));
|
|
assert.equal(start.ecazAmbassadors!.tokens.find(t => t.id === f.token)!.zone, 'placed');
|
|
assert.equal(start.ecazAmbassadors!.tokens.find(t => t.id === f.token)!.effect, 'ecaz');
|
|
const view = viewGame(reload(start), f.entrant);
|
|
const quote = guildTransportQuote(view, f.action);
|
|
assert.deepEqual(quote.unavailableReasons, []);
|
|
assert.deepEqual(quote.quote, { cost: 1, normalCost: 1, ownPayment: 1, pledgedPayment: 0 });
|
|
assert.equal(quote.physicalForces, 1);
|
|
assert.equal(botArrivalBlock(view, f.action), null,
|
|
'A future earned receipt is not an already-active arrival response');
|
|
|
|
// Permission is a distinct unpaid window; income follows actual allowance.
|
|
const declaration = declareGuildAmbassadorIncomeArrival(f);
|
|
const paid = passNativeGuildTransportPermission(declaration.after).game;
|
|
assert.ok(!paid.pendingGuildTransport);
|
|
assert.equal(paid.response?.kind, 'guildIncome');
|
|
assert.equal(paid.response?.amount, 1);
|
|
assert.equal(paid.response?.owner, f.guild);
|
|
assert.equal(player(paid, f.entrant).spice, payer.spice - 1);
|
|
assert.equal(player(paid, f.guild).spice, guild.spice);
|
|
assert.equal(player(paid, f.entrant).reserves, payer.reserves);
|
|
assert.equal(player(paid, f.entrant).forces[f.source], payer.forces[f.source] - 1);
|
|
assert.equal(player(paid, f.entrant).forces[f.target], (payer.forces[f.target] ?? 0) + 1);
|
|
assert.equal(player(paid, f.entrant).shipped, true);
|
|
assert.equal(player(paid, f.entrant).moved, payer.moved);
|
|
assert.equal(paid.active, f.entrant);
|
|
assert.deepEqual(paid.pendingArrivalReaction?.map(e => ({ entrant: e.entrant, territory: e.territory,
|
|
sector: e.sector, amount: e.amount, cause: e.cause, turn: e.turn, phase: e.phase })),
|
|
[{ entrant: f.entrant, territory: f.destination, sector: f.sector,
|
|
amount: 1, cause: 'guildTransport', turn: start.turn, phase: 5 }]);
|
|
assert.ok(!paid.pendingAmbassador);
|
|
const counter = viewGame(reload(paid), f.ecaz).responseControls;
|
|
assert.ok(counter && !counter.hasPassed && counter.cancelCards.includes(f.karama));
|
|
rejectUnchanged(paid, f.entrant, f.action);
|
|
rejectUnchanged(paid, f.entrant, { type: 'endMovement' });
|
|
guildAmbassadorIncomeInventory(paid);
|
|
|
|
const offered = cancelIncome
|
|
? cancelGuildAmbassadorIncomeArrival(reload(paid), f.ecaz, f.karama).after
|
|
: allowGuildAmbassadorIncomeArrival(reload(paid)).game;
|
|
assert.equal(offered.response, null);
|
|
assert.equal(offered.pendingArrivalReaction, null);
|
|
assert.equal(offered.decision?.kind, 'ecazAmbassador');
|
|
assert.equal(offered.pendingAmbassador?.entrant, f.entrant);
|
|
assert.equal(offered.pendingAmbassador?.token, f.token);
|
|
assert.equal(offered.pendingAmbassador?.territory, f.destination);
|
|
assert.equal(offered.pendingAmbassador?.stage, 'offer');
|
|
assert.equal(player(offered, f.guild).spice, guild.spice + (cancelIncome ? 0 : 1));
|
|
assert.equal(player(offered, f.entrant).spice, payer.spice - 1);
|
|
assert.deepEqual(player(offered, f.entrant).forces, player(paid, f.entrant).forces);
|
|
assert.equal(offered.discard.filter(c => c.id === f.karama).length, cancelIncome ? 1 : 0);
|
|
assert.equal(player(offered, f.ecaz).hand.some(c => c.id === f.karama), !cancelIncome);
|
|
assert.equal(offered.active, f.entrant);
|
|
rejectUnchanged(offered, f.entrant, guildAmbassadorIncomeChoice(offered, trigger).action);
|
|
const completed = completeGuildAmbassadorIncomeArrival(reload(offered), trigger);
|
|
const done = completed.after;
|
|
assert.equal(done.pendingAmbassador, null);
|
|
assert.equal(done.pendingArrivalReaction, null);
|
|
assert.equal(done.response, null);
|
|
assert.equal(done.decision, null);
|
|
assert.equal(done.active, f.entrant);
|
|
assert.equal(done.turn, start.turn);
|
|
assert.equal(done.phase, start.phase);
|
|
assert.equal(player(done, f.entrant).moved, payer.moved);
|
|
assert.equal(player(done, f.entrant).shipped, true);
|
|
assert.deepEqual(player(done, f.entrant).forces, player(paid, f.entrant).forces);
|
|
assert.equal(player(done, f.entrant).reserves, payer.reserves);
|
|
assert.equal(player(done, f.entrant).spice, payer.spice - 1);
|
|
assert.equal(player(done, f.guild).spice, guild.spice + (cancelIncome ? 0 : 1));
|
|
assert.equal(done.ecazAmbassadors!.tokens.find(t => t.id === f.token)!.zone, trigger ? 'supply' : 'placed');
|
|
assert.deepEqual(done.ecazAmbassadors!.cohort, start.ecazAmbassadors!.cohort);
|
|
assert.equal(done.dukeVidal!.controller, trigger ? f.ecaz : start.dukeVidal!.controller);
|
|
if (trigger) assert.equal(done.dukeVidal!.source, 'ecaz');
|
|
assert.deepEqual(done.players.map(p => p.hand), offered.players.map(p => p.hand));
|
|
assert.deepEqual(done.players.map(p => p.leaders), offered.players.map(p => p.leaders));
|
|
assert.deepEqual(done.deck, offered.deck);
|
|
assert.deepEqual(done.discard, offered.discard);
|
|
rejectUnchanged(done, completed.step.actor, completed.step.action);
|
|
rejectUnchanged(done, f.entrant, f.action);
|
|
guildAmbassadorIncomeInventory(done);
|
|
|
|
const next = applyAction(reload(done), completed.continuation.actor, completed.continuation.action);
|
|
assert.notEqual(next.active, f.entrant);
|
|
assert.equal(next.pendingAmbassador, null);
|
|
assert.equal(next.pendingArrivalReaction, null);
|
|
assert.deepEqual(player(next, f.entrant).forces, player(done, f.entrant).forces);
|
|
assert.equal(player(next, f.entrant).spice, payer.spice - 1);
|
|
assert.equal(player(next, f.guild).spice, guild.spice + (cancelIncome ? 0 : 1));
|
|
assert.equal(next.discard.filter(c => c.id === f.karama).length, cancelIncome ? 1 : 0);
|
|
guildAmbassadorIncomeInventory(next);
|
|
});
|
|
}
|
|
}
|
|
|
|
void test('actual Ecaz owner-view policies complete the queued native Guild entry without replaying payment or counters', () => {
|
|
const f = createGuildAmbassadorIncomeArrival();
|
|
const paid = passNativeGuildTransportPermission(declareGuildAmbassadorIncomeArrival(f).after).game;
|
|
const offered = allowGuildAmbassadorIncomeArrival(paid).game;
|
|
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
const action = pairedE3NexusPolicy(reload(offered), f.ecaz, difficulty)[0];
|
|
assert.ok(action, `The actual ${difficulty} Ecaz owner needs a legal response`);
|
|
const done = applyAction(reload(offered), f.ecaz, action);
|
|
assert.equal(done.pendingAmbassador, null);
|
|
assert.equal(done.pendingArrivalReaction, null);
|
|
assert.equal(done.decision, null);
|
|
assert.equal(done.response, null);
|
|
assert.equal(done.active, f.entrant);
|
|
assert.deepEqual(player(done, f.entrant).forces, player(paid, f.entrant).forces);
|
|
assert.equal(player(done, f.entrant).spice, player(paid, f.entrant).spice);
|
|
assert.equal(player(done, f.guild).spice, player(paid, f.guild).spice + 1);
|
|
assert.equal(player(done, f.entrant).moved, player(paid, f.entrant).moved);
|
|
guildAmbassadorIncomeInventory(done);
|
|
const next = applyAction(reload(done), f.entrant, { type: 'endMovement' });
|
|
assert.notEqual(next.active, f.entrant);
|
|
assert.deepEqual(player(next, f.entrant).forces, player(paid, f.entrant).forces);
|
|
}
|
|
});
|