Connect classic allied Bureaucrat payment redirection
This commit is contained in:
1 parent
ec574ab0f5
commit
c3d1896673
17 files changed
+1057
-220
No files matched your search
@@ -1,19 +1,10 @@
|
||||
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 { BureaucratPayment } from '../components/bureaucrat-payment';
|
||||
import type { GameView } from '../game/engine';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(
|
||||
specifier === 'next/image' ? 'vinext/shims/image' : specifier,
|
||||
context,
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
function view(overrides: Record<string, unknown> = {}) {
|
||||
return {
|
||||
@@ -50,15 +41,6 @@ function markup(game: GameView, busy = false) {
|
||||
);
|
||||
}
|
||||
|
||||
void test('owned matching Bureaucrat decision shows both payment costs and public names', () => {
|
||||
const html = markup(view({ me: 'owner' }));
|
||||
assert.match(html, /payment from House Payer to House Payee is 5 spice/);
|
||||
assert.match(html, /House Payee receives 3 spice/);
|
||||
assert.match(html, /Bank receives 2/);
|
||||
assert.match(html, /still pays the full amount/);
|
||||
assert.match(html, /Redirect 2 spice to the Bank/);
|
||||
assert.match(html, /Allow full 5-spice payment/);
|
||||
});
|
||||
|
||||
void test('non-owner and stale decisions render no Bureaucrat controls', () => {
|
||||
assert.equal(markup(view()), '');
|
||||
@@ -73,13 +55,3 @@ void test('busy Bureaucrat controls are disabled', () => {
|
||||
assert.ok((html.match(/disabled=""/g) ?? []).length >= 2);
|
||||
});
|
||||
|
||||
void test('bribe redirection keeps the remaining payment unavailable until Mentat', () => {
|
||||
const game = view({ me: 'owner' });
|
||||
game.bureaucrat.pending!.kind = 'bribe';
|
||||
const html = markup(game);
|
||||
assert.match(html, /recipient keeps this spice in front of their shield until the next Mentat Pause/);
|
||||
assert.match(html, /cannot be spent before then/);
|
||||
assert.match(html, /House Payee receives 3 spice/);
|
||||
});
|
||||
|
||||
aliases.deregister();
|
||||
@@ -121,6 +121,26 @@ void test('Bureaucrat requires two other players and tracks the unique physical
|
||||
);
|
||||
});
|
||||
|
||||
void test('allied receipts qualify per other payer, redirect only two once, and reject invalid contribution totals', () => {
|
||||
const base = {owner:'b',payer:'p',payee:'g',amount:8,turn:2,phase:5,used:[]};
|
||||
const contributions = [{payer:'p',amount:3},{payer:'f',amount:5}];
|
||||
assert.deepEqual(quoteBureaucratPayment({...base,contributions}), {redirect:2,income:6});
|
||||
assert.equal(quoteBureaucratPayment({...base,owner:'f',contributions}), null);
|
||||
assert.deepEqual(quoteBureaucratPayment({...base,owner:'p',contributions}), {redirect:2,income:6});
|
||||
assert.equal(quoteBureaucratPayment({...base,payee:'b',contributions}), null);
|
||||
assert.equal(quoteBureaucratPayment({...base,amount:6,
|
||||
contributions:[{payer:'p',amount:3},{payer:'f',amount:3}]}), null);
|
||||
assert.deepEqual(quoteBureaucratPayment({...base,amount:10,
|
||||
contributions:[{payer:'p',amount:5},{payer:'f',amount:5}]}), {redirect:2,income:8});
|
||||
assert.deepEqual(quoteBureaucratPayment({...base,payer:'f',amount:5,
|
||||
contributions:[{payer:'f',amount:5}]}), {redirect:2,income:3});
|
||||
for (const invalid of [
|
||||
[{payer:'p',amount:3},{payer:'f',amount:4}],
|
||||
[{payer:'p',amount:3},{payer:'p',amount:5}],
|
||||
[{payer:'p',amount:3},{payer:'f',amount:-1}],
|
||||
]) assert.throws(() => quoteBureaucratPayment({...base,contributions:invalid}));
|
||||
});
|
||||
|
||||
void test('Emperor gifts keep private amounts and do not consume Bureaucrat before a later qualifying payment', () => {
|
||||
let game = bureaucratPaymentGame({ advanced: true });
|
||||
bureaucratPlayer(game, 'e').ally = 'g';
|
||||
@@ -294,22 +314,6 @@ void test('a real simultaneous Bene Gesserit advisor choice survives the deferre
|
||||
assert.deepEqual(done.decision, original);
|
||||
assert.equal(bureaucratPlayer(done, 'g').spice, 33);
|
||||
});
|
||||
void test('individually qualifying mixed or fully allied-funded payments retain the integration guard before commitment', () => {
|
||||
for (const allyPayment of [1, 6]) {
|
||||
let g = bureaucratPaymentGame();
|
||||
bureaucratPlayer(g, 'p').ally = 'e';
|
||||
bureaucratPlayer(g, 'e').ally = 'p';
|
||||
g = applyAction(g, 'e', { type: 'pledgeAid', amount: 6 });
|
||||
reject(g, 'p', { ...ship, amount: 6, allyPayment });
|
||||
g = bureaucratPaymentGame();
|
||||
bureaucratPlayer(g, 'p').ally = 'g';
|
||||
bureaucratPlayer(g, 'g').ally = 'p';
|
||||
g = beginBureaucratAuction(g);
|
||||
g = applyAction(g, 'g', { type: 'pledgeAid', amount: 6 });
|
||||
g = applyAction(g, 'b', { type: 'passBid' });
|
||||
reject(g, 'p', { type: 'bid', amount: 6, allyPayment });
|
||||
}
|
||||
});
|
||||
void test('saved Bureaucrat receipt, source, unique decision, continuation and phase-use mutations reject immutably before reads', () => {
|
||||
const pending = applyAction(bureaucratPaymentGame(), 'p', ship),
|
||||
done = choose(pending);
|
||||
|
||||
@@ -0,0 +1,262 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { advanceBureaucratQualifyingSplit } from './fixture-bureaucrat-qualifying-split';
|
||||
import {
|
||||
createBureaucratQualifyingAuctionFixture, payBureaucratQualifyingAuction,
|
||||
allowQualifyingAuctionResponses, chooseQualifyingAuction, qualifyingAuctionPlayer,
|
||||
qualifyingAuctionPhysicalCards, type BureaucratQualifyingAuctionFixture,
|
||||
} from './fixture-bureaucrat-qualifying-auction';
|
||||
|
||||
function rejectUnchanged(game: Game, actor: string, action: Action) {
|
||||
const before = JSON.stringify(game);
|
||||
assert.throws(() => applyAction(game, actor, action));
|
||||
assert.equal(JSON.stringify(game), before);
|
||||
}
|
||||
function assertPaid(fixture: BureaucratQualifyingAuctionFixture, game: Game) {
|
||||
const payer = qualifyingAuctionPlayer(game, fixture.payer), donor = qualifyingAuctionPlayer(game, fixture.donor);
|
||||
assert.equal(payer.spice, qualifyingAuctionPlayer(fixture.beforePledge, fixture.payer).spice - fixture.payerShare);
|
||||
assert.equal(donor.spice, qualifyingAuctionPlayer(fixture.beforePledge, fixture.donor).spice - fixture.donorShare);
|
||||
assert.equal(game.aid[fixture.donor].amount, 0);
|
||||
assert.equal(payer.hand.length, qualifyingAuctionPlayer(fixture.beforeAction, fixture.payer).hand.length + 1);
|
||||
assert.equal(payer.hand.filter(card => card.id === fixture.reservedCard).length, 1);
|
||||
assert.equal(qualifyingAuctionPlayer(game, fixture.emperor).spice, qualifyingAuctionPlayer(fixture.beforeAction, fixture.emperor).spice);
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(game), fixture.physical);
|
||||
assert.equal(game.auction!.index, fixture.beforeAction.auction!.index);
|
||||
assert.deepEqual(game.auction!.cards, fixture.sourceSetup.auction!.cards);
|
||||
for (const original of fixture.beforeAction.players) {
|
||||
const actual = qualifyingAuctionPlayer(game, original.id);
|
||||
assert.deepEqual(actual.forces, original.forces);
|
||||
assert.equal(actual.reserves, original.reserves);
|
||||
}
|
||||
}
|
||||
function assertCompleted(fixture: BureaucratQualifyingAuctionFixture, pending: Game, done: Game, diverted: number) {
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice,
|
||||
qualifyingAuctionPlayer(fixture.beforeAction, fixture.emperor).spice + fixture.amount - diverted);
|
||||
for (const actor of [fixture.payer, fixture.donor, fixture.trainer])
|
||||
assert.equal(qualifyingAuctionPlayer(done, actor).spice, qualifyingAuctionPlayer(pending, actor).spice);
|
||||
const payer = qualifyingAuctionPlayer(done, fixture.payer);
|
||||
assert.deepEqual(payer.hand.map(card => card.id), [
|
||||
...qualifyingAuctionPlayer(pending, fixture.payer).hand.map(card => card.id), fixture.bonusCard,
|
||||
]);
|
||||
assert.deepEqual(done.deck.map(card => card.id), pending.deck.slice(1).map(card => card.id));
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
|
||||
assert.equal(done.currentAuctionSale, null);
|
||||
assert.equal(done.auction!.index, fixture.beforeAction.auction!.index + 1);
|
||||
assert.equal(done.auction!.cards[done.auction!.index].id, fixture.beforeAction.auction!.cards[fixture.beforeAction.auction!.index + 1].id);
|
||||
assert.equal(done.auction!.bid, 0);
|
||||
assert.equal(done.auction!.bidder, null);
|
||||
assert.equal(done.bureaucratPayments?.used.length ?? 0, diverted ? 1 : 0);
|
||||
assert.equal(viewGame(done, fixture.trainer).bureaucrat.usedThisPhase, diverted === 2);
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) {
|
||||
for (const [payerShare, donorShare] of [[5, 3], [3, 5], [5, 5], [0, 5]]) {
|
||||
for (const redirect of [true, false]) {
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} native ordinary auction ${payerShare}+${donorShare}: ${redirect ? 'Bank two' : 'full Emperor income'} and one Harkonnen bonus`, () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ advanced, payerShare, donorShare });
|
||||
const paid = payBureaucratQualifyingAuction(fixture);
|
||||
assertPaid(fixture, paid);
|
||||
const pending = allowQualifyingAuctionResponses(paid);
|
||||
assert.equal(pending.decision?.kind, 'bureaucratPayment');
|
||||
assertPaid(fixture, pending);
|
||||
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(JSON.parse(JSON.stringify(pending)) as Game, redirect));
|
||||
assertCompleted(fixture, pending, done, redirect ? 2 : 0);
|
||||
rejectUnchanged(done, fixture.trainer, { type: 'decision', event: pending.bureaucratPaymentEvent, redirect: true });
|
||||
});
|
||||
}
|
||||
}
|
||||
for (const [payerShare, donorShare] of [[3, 3], [4, 3]]) {
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} native ordinary auction ${payerShare}+${donorShare} cannot pool sub-five shares`, () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ advanced, payerShare, donorShare });
|
||||
const paid = payBureaucratQualifyingAuction(fixture);
|
||||
assertPaid(fixture, paid);
|
||||
const done = allowQualifyingAuctionResponses(paid);
|
||||
assertCompleted(fixture, paid, done, 0);
|
||||
assert.equal(viewGame(done, fixture.trainer).bureaucrat.pending, null);
|
||||
});
|
||||
}
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} original Emperor Karama cancellation banks the whole allied invoice, not just the primary contribution`, () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ advanced, payerShare: 5, donorShare: 5, counter: true });
|
||||
const paid = payBureaucratQualifyingAuction(fixture);
|
||||
assertPaid(fixture, paid);
|
||||
assert.equal(paid.response?.kind, 'emperorIncome');
|
||||
assert.equal(viewGame(paid, fixture.trainer).bureaucrat.pending, null);
|
||||
const canceled = applyAction(paid, fixture.trainer, { type: 'card', mode: 'cancel', card: fixture.counterCard });
|
||||
const done = allowQualifyingAuctionResponses(canceled);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice, qualifyingAuctionPlayer(fixture.beforeAction, fixture.emperor).spice);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).spice, qualifyingAuctionPlayer(paid, fixture.payer).spice);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.donor).spice, qualifyingAuctionPlayer(paid, fixture.donor).spice);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).hand.length, qualifyingAuctionPlayer(paid, fixture.payer).hand.length + 1);
|
||||
assert.equal(done.discard.filter(card => card.id === fixture.counterCard).length, 1);
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
|
||||
assert.equal(done.auction!.index, 1);
|
||||
assert.equal(done.bureaucratPayments?.used.length ?? 0, 0);
|
||||
assert.equal(viewGame(done, fixture.trainer).bureaucrat.pending, null);
|
||||
});
|
||||
}
|
||||
|
||||
void test('equal public eight-spice invoices conceal which allied contribution qualified from Bureaucrat and outside audiences', () => {
|
||||
const primary = createBureaucratQualifyingAuctionFixture({ payerShare: 5, donorShare: 3 });
|
||||
const ally = createBureaucratQualifyingAuctionFixture({ payerShare: 3, donorShare: 5 });
|
||||
const a = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(primary));
|
||||
const b = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(ally));
|
||||
for (const viewer of [primary.trainer, primary.guild, primary.emperor]) {
|
||||
const left = viewGame(a, viewer), right = viewGame(b, viewer);
|
||||
assert.equal(left.bureaucrat.pending!.payer, null);
|
||||
assert.equal(left.bureaucrat.pending!.amount, 8);
|
||||
assert.equal(left.bureaucrat.pending!.payee, primary.emperor);
|
||||
assert.equal(left.auction!.bid, 8);
|
||||
assert.equal(left.auction!.bidder, primary.payer);
|
||||
assert.equal(left.auction!.allyPayment, undefined);
|
||||
const { event: leftEvent, ...leftPayment } = left.bureaucrat.pending!;
|
||||
const { event: rightEvent, ...rightPayment } = right.bureaucrat.pending!;
|
||||
assert.notEqual(leftEvent, rightEvent);
|
||||
assert.deepEqual(leftPayment, rightPayment);
|
||||
assert.equal('contributions' in leftPayment, false);
|
||||
assert.equal('contributions' in rightPayment, false);
|
||||
assert.deepEqual(left.log.slice(primary.beforePledge.log.length).map(entry => entry.text),
|
||||
right.log.slice(ally.beforePledge.log.length).map(entry => entry.text));
|
||||
}
|
||||
});
|
||||
|
||||
void test('a real original pledge is private funding, not a gift or a second income/payment action', () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 3, donorShare: 5 });
|
||||
assert.equal(qualifyingAuctionPlayer(fixture.pledged, fixture.donor).spice, 1);
|
||||
assert.equal(qualifyingAuctionPlayer(fixture.pledged, fixture.payer).spice, qualifyingAuctionPlayer(fixture.beforePledge, fixture.payer).spice);
|
||||
assert.equal(qualifyingAuctionPlayer(fixture.pledged, fixture.payer).bribes, 0);
|
||||
assert.equal(fixture.pledged.aid[fixture.donor].amount, 5);
|
||||
assert.equal(fixture.pledged.decision, null);
|
||||
assert.equal(fixture.pledged.response, null);
|
||||
assert.equal(fixture.pledged.bureaucratPayments?.used.length ?? 0, 0);
|
||||
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
|
||||
assert.equal(pending.decision?.kind, 'bureaucratPayment');
|
||||
assertPaid(fixture, pending);
|
||||
});
|
||||
|
||||
void test('stale and cross-actor choices cannot spend, refund, duplicate, or modify the reserved ordinary lot', () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture();
|
||||
const bid = applyAction(fixture.game, fixture.payer, fixture.action);
|
||||
rejectUnchanged(bid, fixture.donor, { type: 'pledgeAid', amount: fixture.donorShare - 1 });
|
||||
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
|
||||
rejectUnchanged(pending, fixture.donor, { type: 'decision', event: pending.bureaucratPaymentEvent, redirect: true });
|
||||
rejectUnchanged(pending, fixture.trainer, { type: 'decision', event: 'old-auction-invoice', redirect: true });
|
||||
rejectUnchanged(pending, fixture.payer, { type: 'bid', amount: 1 });
|
||||
rejectUnchanged(pending, fixture.donor, { type: 'pledgeAid', amount: 0 });
|
||||
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending));
|
||||
assertCompleted(fixture, pending, done, 2);
|
||||
});
|
||||
|
||||
void test('an original purchase using the last hand space has no premature or overflow Harkonnen bonus', () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ lastHandSpace: true });
|
||||
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
|
||||
assertPaid(fixture, pending);
|
||||
assert.equal(qualifyingAuctionPlayer(pending, fixture.payer).hand.length, handLimit(qualifyingAuctionPlayer(pending, fixture.payer)));
|
||||
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending));
|
||||
assert.deepEqual(qualifyingAuctionPlayer(done, fixture.payer).hand, qualifyingAuctionPlayer(pending, fixture.payer).hand);
|
||||
assert.deepEqual(done.deck, pending.deck);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice, qualifyingAuctionPlayer(pending, fixture.emperor).spice + 6);
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
|
||||
assert.equal(done.auction!.index, 1);
|
||||
assert.equal(done.bureaucratPayments!.used.length, 1);
|
||||
});
|
||||
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
void test(`${difficulty} Bureaucrat bot resolves the actual concealed allied invoice and hands control to the native next lot`, () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 5, donorShare: 5 });
|
||||
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
|
||||
const view = viewGame(pending, fixture.trainer);
|
||||
view.players.find(player => player.id === fixture.trainer)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
const done = allowQualifyingAuctionResponses(applyAction(pending, fixture.trainer, action));
|
||||
assertCompleted(fixture, pending, done, action.redirect ? 2 : 0);
|
||||
const next = advanceBureaucratQualifyingSplit(done, game => !game.decision && !game.response && !!game.auction?.peekKnown);
|
||||
assert.equal(next.auction!.index, 1);
|
||||
assert.equal(next.active, next.auction!.active);
|
||||
assert.ok(next.order.includes(next.active!));
|
||||
assert.equal(viewGame(next, fixture.trainer).bureaucrat.pending, null);
|
||||
});
|
||||
}
|
||||
|
||||
void test('one split use covers both qualifying shares, blocks a later same-phase native payment, and refreshes only at a real phase boundary', () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 5, donorShare: 5 });
|
||||
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
|
||||
let game = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending));
|
||||
const use = structuredClone(game.bureaucratPayments!.used);
|
||||
const secondLot = game.auction!.cards[1].id;
|
||||
game = advanceBureaucratQualifyingSplit(game, state => !!state.auction && state.auction.active === fixture.guild && !state.decision && !state.response);
|
||||
const guildBefore = qualifyingAuctionPlayer(game, fixture.guild).spice;
|
||||
const emperorBefore = qualifyingAuctionPlayer(game, fixture.emperor).spice;
|
||||
game = applyAction(game, fixture.guild, { type: 'bid', amount: 5 });
|
||||
for (let n = 0; game.auction?.index === 1 && !game.response && !game.decision && n < game.players.length + 2; n++)
|
||||
game = applyAction(game, game.auction!.active, { type: 'passBid' });
|
||||
if (game.decision?.kind === 'auctionPayment') game = applyAction(game, fixture.guild, { type: 'decision', karama: false });
|
||||
game = allowQualifyingAuctionResponses(game);
|
||||
assert.equal(viewGame(game, fixture.trainer).bureaucrat.pending, null);
|
||||
assert.deepEqual(game.bureaucratPayments!.used, use);
|
||||
assert.equal(qualifyingAuctionPlayer(game, fixture.guild).spice, guildBefore - 5);
|
||||
assert.equal(qualifyingAuctionPlayer(game, fixture.guild).hand.filter(card => card.id === secondLot).length, 1);
|
||||
assert.equal(qualifyingAuctionPlayer(game, fixture.emperor).spice, emperorBefore + 5);
|
||||
assert.equal(game.auction!.index, 2);
|
||||
game = advanceBureaucratQualifyingSplit(game, state => state.phase === 5 && !state.phaseOpening && !state.response && !state.decision);
|
||||
assert.equal(game.turn, pending.turn);
|
||||
assert.equal(viewGame(game, fixture.trainer).bureaucrat.usedThisPhase, false);
|
||||
const fresh = applyAction(game, fixture.emperor, { type: 'bribe', target: fixture.guild, amount: 5 });
|
||||
assert.equal(fresh.decision?.kind, 'bureaucratPayment');
|
||||
assert.notEqual(fresh.bureaucratPaymentEvent, pending.bureaucratPaymentEvent);
|
||||
const done = chooseQualifyingAuction(fresh);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.guild).bribes, 3);
|
||||
assert.equal(done.bureaucratPayments!.used.length, 2);
|
||||
assert.equal(viewGame(done, fixture.trainer).bureaucrat.usedThisPhase, true);
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
|
||||
});
|
||||
|
||||
for (const donor of ['b', 'e'] as const) {
|
||||
for (const [payerShare, donorShare] of [[3, 5], [5, 3]]) {
|
||||
void test(`native worm alliance with ${donor === 'b' ? 'Bureaucrat owner' : 'Emperor payee'} excludes only that contributor's threshold`, () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ donor, payerShare, donorShare });
|
||||
assert.equal(qualifyingAuctionPlayer(fixture.alliance, fixture.payer).ally, donor);
|
||||
assert.equal(qualifyingAuctionPlayer(fixture.alliance, donor).ally, fixture.payer);
|
||||
const paid = payBureaucratQualifyingAuction(fixture);
|
||||
assertPaid(fixture, paid);
|
||||
const allowed = allowQualifyingAuctionResponses(paid);
|
||||
const qualifies = payerShare === 5;
|
||||
assert.equal(allowed.decision?.kind, qualifies ? 'bureaucratPayment' : undefined);
|
||||
const done = qualifies ? allowQualifyingAuctionResponses(chooseQualifyingAuction(allowed)) : allowed;
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).spice,
|
||||
qualifyingAuctionPlayer(fixture.beforePledge, fixture.payer).spice - payerShare);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.emperor).spice,
|
||||
qualifyingAuctionPlayer(fixture.beforePledge, fixture.emperor).spice - (donor === 'e' ? donorShare : 0) +
|
||||
fixture.amount - (qualifies ? 2 : 0));
|
||||
if (donor === 'b') assert.equal(qualifyingAuctionPlayer(done, donor).spice,
|
||||
qualifyingAuctionPlayer(fixture.beforePledge, donor).spice - donorShare);
|
||||
assert.equal(done.bureaucratPayments?.used.length ?? 0, qualifies ? 1 : 0);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).hand.filter(card => card.id === fixture.reservedCard).length, 1);
|
||||
assert.equal(qualifyingAuctionPlayer(done, fixture.payer).hand.filter(card => card.id === fixture.bonusCard).length, 1);
|
||||
assert.equal(done.auction!.index, 1);
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(done), fixture.physical);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
void test('declining a native split invoice leaves the original card available for a distinct payment in the same Bidding phase', () => {
|
||||
const fixture = createBureaucratQualifyingAuctionFixture({ payerShare: 3, donorShare: 5 });
|
||||
const pending = allowQualifyingAuctionResponses(payBureaucratQualifyingAuction(fixture));
|
||||
const done = allowQualifyingAuctionResponses(chooseQualifyingAuction(pending, false));
|
||||
assertCompleted(fixture, pending, done, 0);
|
||||
const next = advanceBureaucratQualifyingSplit(done, state => !!state.auction?.peekKnown &&
|
||||
!state.phaseOpening && !state.response && !state.decision);
|
||||
assert.equal(next.auction!.index, 1);
|
||||
const paid = applyAction(next, fixture.emperor, { type: 'bribe', target: fixture.guild, amount: 5 });
|
||||
assert.equal(paid.phase, pending.phase);
|
||||
assert.equal(paid.decision?.kind, 'bureaucratPayment');
|
||||
assert.notEqual(paid.bureaucratPaymentEvent, pending.bureaucratPaymentEvent);
|
||||
const redirected = chooseQualifyingAuction(paid);
|
||||
assert.equal(qualifyingAuctionPlayer(redirected, fixture.guild).bribes, 3);
|
||||
assert.equal(redirected.bureaucratPayments!.used.length, 1);
|
||||
assert.equal(redirected.auction!.index, 1);
|
||||
assert.equal(qualifyingAuctionPlayer(redirected, fixture.payer).spice, qualifyingAuctionPlayer(done, fixture.payer).spice);
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(redirected), fixture.physical);
|
||||
});
|
||||
@@ -0,0 +1,247 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import {
|
||||
allowBureaucratQualifyingShipmentIncome, createBureaucratQualifyingShipmentFixture,
|
||||
declareBureaucratQualifyingShipment,
|
||||
} from './fixture-bureaucrat-qualifying-shipment';
|
||||
|
||||
const seat = (game: Game, id: string) => game.players.find(p => p.id === id)!;
|
||||
const physical = (game: Game) => game.players.map(p => ({ id: p.id, forces: p.forces,
|
||||
reserves: p.reserves, tanks: p.tanks, elites: p.elites, shipped: p.shipped, moved: p.moved }));
|
||||
const wallets = (game: Game) => game.players.map(p => ({ id: p.id, spice: p.spice, bribes: p.bribes }));
|
||||
function rejectUnchanged(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
function ownerChoice(game: Game, redirect: boolean) {
|
||||
assert.equal(game.decision?.kind, 'bureaucratPayment');
|
||||
return applyAction(structuredClone(game), 'b', {
|
||||
type: 'decision', event: game.decision!.event, redirect,
|
||||
});
|
||||
}
|
||||
|
||||
void test('native Basic and Advanced allied shipments pay every original leg and divert at most two from Guild income', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const [primary, donor] of [[5, 3], [3, 5], [5, 5], [0, 5]])
|
||||
for (const redirect of [false, true]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary, donor });
|
||||
const original = f.beforePledge;
|
||||
assert.equal(seat(original, 'f').spice, 6);
|
||||
assert.equal(seat(f.beforeAction, 'f').spice, 6 - donor);
|
||||
assert.equal(f.beforeAction.aid.f.amount, donor);
|
||||
assert.deepEqual(physical(f.beforeAction), physical(original));
|
||||
const { declared, paid } = declareBureaucratQualifyingShipment(f);
|
||||
if (declared.decision?.kind === 'guildShipment') {
|
||||
assert.deepEqual(wallets(declared), wallets(f.beforeAction));
|
||||
assert.deepEqual(physical(declared), physical(f.beforeAction));
|
||||
assert.equal(declared.decision.player, 'g');
|
||||
}
|
||||
assert.equal(paid.response?.kind, 'guildIncome');
|
||||
assert.equal(paid.response?.amount, primary + donor);
|
||||
assert.equal(seat(paid, 'p').spice, seat(original, 'p').spice - primary);
|
||||
assert.equal(seat(paid, 'f').spice, 6 - donor);
|
||||
assert.equal(paid.aid.f.amount, 0);
|
||||
assert.equal(seat(paid, 'g').spice, advanced ? 6 : 5);
|
||||
assert.equal(seat(paid, 'p').reserves, 10 - f.amount);
|
||||
assert.deepEqual(seat(paid, 'p').forces, { 'carthag:11': 10, [f.target]: f.amount });
|
||||
assert.equal(seat(paid, 'p').shipped, true);
|
||||
for (const id of ['b', 'f', 'e', 'g']) {
|
||||
assert.deepEqual(seat(paid, id).forces, seat(original, id).forces);
|
||||
assert.equal(seat(paid, id).reserves, seat(original, id).reserves);
|
||||
}
|
||||
const pending = allowBureaucratQualifyingShipmentIncome(paid);
|
||||
assert.equal(pending.decision?.kind, 'bureaucratPayment');
|
||||
assert.deepEqual(physical(pending), physical(paid));
|
||||
assert.deepEqual(wallets(pending), wallets(paid));
|
||||
const after = ownerChoice(pending, redirect);
|
||||
assert.equal(seat(after, 'g').spice, (advanced ? 6 : 5) + f.amount - (redirect ? 2 : 0));
|
||||
for (const id of ['p', 'f', 'b', 'e']) {
|
||||
assert.equal(seat(after, id).spice, seat(paid, id).spice);
|
||||
assert.equal(seat(after, id).bribes, seat(paid, id).bribes);
|
||||
}
|
||||
assert.equal(after.players.reduce((sum, p) => sum + p.spice, 0),
|
||||
original.players.reduce((sum, p) => sum + p.spice, 0) - (redirect ? 2 : 0));
|
||||
assert.deepEqual(physical(after), physical(paid));
|
||||
assert.equal(after.bureaucratPayments?.used.length ?? 0, Number(redirect));
|
||||
assert.equal(after.bureaucratPayments?.pending, undefined);
|
||||
assert.deepEqual(after.deck, pending.deck);
|
||||
assert.deepEqual(after.discard, pending.discard);
|
||||
assert.deepEqual(after.players.map(p => p.hand), pending.players.map(p => p.hand));
|
||||
rejectUnchanged(after, 'b', { type: 'decision', event: pending.decision!.event, redirect });
|
||||
rejectUnchanged(after, 'p', f.action);
|
||||
const ended = applyAction(structuredClone(after), 'p', { type: 'endMovement' });
|
||||
assert.notEqual(ended.active, 'p');
|
||||
assert.deepEqual(physical(ended), physical(after));
|
||||
rejectUnchanged(ended, 'p', { type: 'endMovement' });
|
||||
}
|
||||
});
|
||||
|
||||
void test('Karama cancels original gross Guild income after the same shipment and full allied debits, without spending Bureaucrat', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const [primary, donor] of [[5, 3], [3, 5], [5, 5], [0, 5]]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary, donor });
|
||||
const { paid } = declareBureaucratQualifyingShipment(f);
|
||||
assert.equal(paid.response?.amount, f.amount);
|
||||
assert.equal(viewGame(paid, f.responder).responseControls?.cancelCards.includes(f.karama.id), true);
|
||||
const after = applyAction(structuredClone(paid), f.responder, {
|
||||
type: 'card', mode: 'cancel', card: f.karama.id,
|
||||
});
|
||||
assert.equal(seat(after, 'g').spice, seat(f.beforePledge, 'g').spice);
|
||||
assert.equal(seat(after, 'p').spice, seat(f.beforePledge, 'p').spice - primary);
|
||||
assert.equal(seat(after, 'f').spice, 6 - donor);
|
||||
assert.equal(after.players.reduce((sum, p) => sum + p.spice, 0),
|
||||
f.beforePledge.players.reduce((sum, p) => sum + p.spice, 0) - f.amount);
|
||||
assert.deepEqual(physical(after), physical(paid));
|
||||
assert.equal(after.discard.filter(card => card.id === f.karama.id).length, 1);
|
||||
assert.equal(seat(after, f.responder).hand.some(card => card.id === f.karama.id), false);
|
||||
assert.equal(after.bureaucratPayments?.pending, undefined);
|
||||
assert.equal(after.bureaucratPayments?.used.length ?? 0, 0);
|
||||
assert.notEqual(after.decision?.kind, 'bureaucratPayment');
|
||||
assert.equal(after.response, null);
|
||||
rejectUnchanged(after, f.responder, { type: 'card', mode: 'cancel', card: f.karama.id });
|
||||
}
|
||||
});
|
||||
|
||||
void test('neither pooled sub-five shares nor the native Bureaucrat owner contribution qualifies on its own', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const options of [
|
||||
{ primary: 3, donor: 3 }, { primary: 4, donor: 3 },
|
||||
{ primary: 3, donor: 5, ownerFunding: true },
|
||||
]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, ...options });
|
||||
if (options.ownerFunding) {
|
||||
assert.equal(seat(f.beforePledge, 'p').ally, 'b');
|
||||
assert.equal(seat(f.beforePledge, 'b').ally, 'p');
|
||||
assert.equal(seat(f.beforePledge, 'f').ally, null);
|
||||
}
|
||||
const { paid } = declareBureaucratQualifyingShipment(f);
|
||||
const after = allowBureaucratQualifyingShipmentIncome(paid);
|
||||
assert.notEqual(after.decision?.kind, 'bureaucratPayment');
|
||||
assert.equal(after.bureaucratPayments?.used.length ?? 0, 0);
|
||||
assert.equal(seat(after, 'g').spice, seat(f.beforePledge, 'g').spice + f.amount);
|
||||
assert.equal(seat(after, 'p').spice, seat(f.beforePledge, 'p').spice - options.primary);
|
||||
assert.equal(seat(after, f.donor).spice, seat(f.beforePledge, f.donor).spice - options.donor);
|
||||
assert.deepEqual(physical(after), physical(paid));
|
||||
}
|
||||
});
|
||||
|
||||
void test('an excluded owner funding leg does not mask the other actual qualifying third-party payer', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const donor of [3, 5]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary: 5, donor, ownerFunding: true });
|
||||
assert.equal(f.donor, 'b');
|
||||
assert.equal(seat(f.alliance, 'p').ally, 'b');
|
||||
assert.equal(seat(f.alliance, 'b').ally, 'p');
|
||||
const { paid } = declareBureaucratQualifyingShipment(f);
|
||||
const pending = allowBureaucratQualifyingShipmentIncome(paid);
|
||||
const after = ownerChoice(pending, true);
|
||||
assert.equal(seat(after, 'b').spice, seat(f.beforePledge, 'b').spice - donor);
|
||||
assert.equal(seat(after, 'p').spice, seat(f.beforePledge, 'p').spice - 5);
|
||||
assert.equal(seat(after, 'g').spice, seat(f.beforePledge, 'g').spice + f.amount - 2);
|
||||
assert.equal(after.bureaucratPayments?.used.length, 1);
|
||||
assert.deepEqual(physical(after), physical(paid));
|
||||
}
|
||||
});
|
||||
|
||||
void test('pending allied-funded income exposes public gross and recipient, never which private contribution qualified', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const [primary, donor] of [[5, 3], [3, 5], [5, 5], [0, 5]]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary, donor });
|
||||
const { paid } = declareBureaucratQualifyingShipment(f);
|
||||
for (const viewer of paid.players) {
|
||||
const response = viewGame(paid, viewer.id).response;
|
||||
assert.equal(response?.amount, f.amount);
|
||||
assert.equal(response?.owner, 'g');
|
||||
assert.equal(response?.bureaucratPayment, undefined);
|
||||
assert.equal(response?.guildContributions, undefined);
|
||||
assert.equal(response?.occupiedPayment, undefined);
|
||||
}
|
||||
const pending = allowBureaucratQualifyingShipmentIncome(paid);
|
||||
for (const viewer of pending.players) {
|
||||
const view = viewGame(pending, viewer.id);
|
||||
const payment = view.bureaucrat.pending;
|
||||
assert.ok(payment);
|
||||
assert.equal(payment.amount, f.amount);
|
||||
assert.equal(payment.payee, 'g');
|
||||
assert.equal(payment.payer, null);
|
||||
assert.equal('contributions' in payment, false);
|
||||
assert.equal('source' in payment, false);
|
||||
assert.equal('signature' in payment, false);
|
||||
assert.equal('bureaucratPayments' in view, false);
|
||||
assert.equal('bureaucratUseEvents' in view, false);
|
||||
for (const other of view.players.filter(p => p.id !== viewer.id)) {
|
||||
assert.equal(other.spice, undefined);
|
||||
assert.equal(other.hand, undefined);
|
||||
}
|
||||
}
|
||||
for (const id of ['p', 'f', 'e', 'g'])
|
||||
rejectUnchanged(pending, id, { type: 'decision', event: pending.bureaucratPaymentEvent, redirect: true });
|
||||
rejectUnchanged(pending, 'b', { type: 'decision', event: 'stale-income', redirect: true });
|
||||
rejectUnchanged(pending, 'b', { type: 'endMovement' });
|
||||
rejectUnchanged(pending, 'f', { type: 'pledgeAid', amount: 1 });
|
||||
}
|
||||
});
|
||||
|
||||
void test('all four minimal Bureaucrat bot responses settle the original private-funded payment and resume native movement', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const bot of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary: 5, donor: 5 });
|
||||
const pending = allowBureaucratQualifyingShipmentIncome(declareBureaucratQualifyingShipment(f).paid);
|
||||
const view = viewGame(pending, 'b');
|
||||
view.players.find(p => p.id === 'b')!.bot = bot;
|
||||
const actions = botActions(view);
|
||||
assert.equal(actions.length, 1);
|
||||
const after = applyAction(structuredClone(pending), 'b', actions[0]);
|
||||
assert.equal(seat(after, 'g').spice, seat(f.beforePledge, 'g').spice + 8);
|
||||
assert.equal(after.bureaucratPayments?.used.length, 1);
|
||||
assert.deepEqual(physical(after), physical(pending));
|
||||
for (const id of ['b', 'p', 'f', 'e']) assert.equal(seat(after, id).spice, seat(pending, id).spice);
|
||||
const ended = applyAction(after, 'p', { type: 'endMovement' });
|
||||
assert.notEqual(ended.active, 'p');
|
||||
assert.deepEqual(physical(ended), physical(after));
|
||||
rejectUnchanged(ended, 'b', actions[0]);
|
||||
}
|
||||
});
|
||||
|
||||
void test('declining the shipment preserves the physical card for the next real payment, while redirecting spends it once per phase', () => {
|
||||
for (const advanced of [false, true])
|
||||
for (const redirect of [false, true]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary: 5, donor: 3 });
|
||||
const pending = allowBureaucratQualifyingShipmentIncome(declareBureaucratQualifyingShipment(f).paid);
|
||||
const settled = ownerChoice(pending, redirect);
|
||||
const following = applyAction(structuredClone(settled), 'e', { type: 'bribe', target: 'f', amount: 5 });
|
||||
assert.equal(seat(following, 'e').spice, seat(settled, 'e').spice - 5);
|
||||
assert.deepEqual(physical(following), physical(settled));
|
||||
if (redirect) {
|
||||
assert.notEqual(following.decision?.kind, 'bureaucratPayment');
|
||||
assert.equal(seat(following, 'f').bribes, seat(settled, 'f').bribes + 5);
|
||||
assert.equal(following.bureaucratPayments?.used.length, 1);
|
||||
} else {
|
||||
assert.equal(following.decision?.kind, 'bureaucratPayment');
|
||||
rejectUnchanged(following, 'b', { type: 'decision', event: pending.bureaucratPaymentEvent, redirect: true });
|
||||
const second = ownerChoice(following, true);
|
||||
assert.equal(seat(second, 'f').bribes, seat(settled, 'f').bribes + 3);
|
||||
assert.equal(second.bureaucratPayments?.used.length, 1);
|
||||
assert.equal(seat(second, 'g').spice, seat(settled, 'g').spice);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
void test('native shipment funding rejects more than the actual pledged aid without committing payment or counters', () => {
|
||||
for (const advanced of [false, true]) {
|
||||
const f = createBureaucratQualifyingShipmentFixture({ advanced, primary: 5, donor: 3 });
|
||||
assert.equal(viewGame(f.beforeAction, 'p').aid.available, 3);
|
||||
assert.equal(viewGame(f.beforeAction, 'f').aid.pledged, 3);
|
||||
assert.equal(viewGame(f.beforeAction, 'b').aid.available, 0);
|
||||
rejectUnchanged(f.beforeAction, 'p', { ...f.action, allyPayment: 4 });
|
||||
rejectUnchanged(f.beforeAction, 'f', f.action);
|
||||
assert.deepEqual(physical(f.beforeAction), physical(f.beforePledge));
|
||||
assert.equal(seat(f.beforeAction, 'p').spice, seat(f.beforePledge, 'p').spice);
|
||||
assert.equal(seat(f.beforeAction, 'f').spice, 3);
|
||||
assert.equal(f.beforeAction.response, null);
|
||||
assert.equal(f.beforeAction.decision, null);
|
||||
}
|
||||
});
|
||||
@@ -1,80 +0,0 @@
|
||||
import test from 'node:test';
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { nextClassicNexusSkillsPaymentsNativeStep } from './fixture-classic-nexus-skills-payments';
|
||||
import { createBureaucratSplitThresholdFixture, advanceBureaucratThresholdToMovement } from './fixture-bureaucrat-split-threshold';
|
||||
|
||||
function settle(state: Game): Game {
|
||||
let game = state;
|
||||
for (let n = 0; (game.response || game.decision) && n < 40; n++) {
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
|
||||
assert.ok(next);
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
}
|
||||
assert.ok(!game.response && !game.decision);
|
||||
return game;
|
||||
}
|
||||
const player = (game: Game, id: string) => game.players.find(p => p.id === id)!;
|
||||
|
||||
for (const advanced of [false, true]) {
|
||||
for (const personal of [3, 4]) void test(`${advanced ? 'Advanced' : 'Basic'} shipment preserves ${personal}+3 allied payments below Bureaucrat’s individual threshold`, () => {
|
||||
const f = createBureaucratSplitThresholdFixture(advanced);
|
||||
let game = advanceBureaucratThresholdToMovement(f.game, f.payer);
|
||||
const before = structuredClone(game);
|
||||
game = applyAction(game, f.donor, { type: 'pledgeAid', amount: 3 });
|
||||
const amount = personal + 3, key = 'habbanya_ridge_sietch:17';
|
||||
const paid = settle(applyAction(game, f.payer, {
|
||||
type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount, allyPayment: 3,
|
||||
}));
|
||||
assert.equal(player(paid, f.payer).spice, player(before, f.payer).spice - personal);
|
||||
assert.equal(player(paid, f.donor).spice, player(before, f.donor).spice - 3);
|
||||
assert.equal(player(paid, f.guild).spice, player(before, f.guild).spice + amount);
|
||||
assert.equal(player(paid, f.trainer).spice, player(before, f.trainer).spice);
|
||||
assert.equal(player(paid, f.payer).forces[key], amount);
|
||||
assert.equal(player(paid, f.payer).reserves, player(before, f.payer).reserves - amount);
|
||||
assert.equal(paid.aid[f.donor]?.amount ?? 0, 0);
|
||||
assert.equal(viewGame(paid, f.trainer).bureaucrat.usedThisPhase, false);
|
||||
assert.throws(() => applyAction(paid, f.payer, {
|
||||
type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount, allyPayment: 3,
|
||||
}));
|
||||
const finished = applyAction(paid, f.payer, { type: 'endMovement' });
|
||||
assert.ok(!finished.movementRemaining?.includes(f.payer));
|
||||
});
|
||||
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} auction preserves separate three-spice payer costs and full Emperor income`, () => {
|
||||
const f = createBureaucratSplitThresholdFixture(advanced);
|
||||
let game = f.game;
|
||||
while (game.auction!.active !== f.payer) {
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game); assert.ok(next);
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
}
|
||||
const card = game.auction!.cards[game.auction!.index].id;
|
||||
const before = structuredClone(game);
|
||||
game = applyAction(game, f.donor, { type: 'pledgeAid', amount: 3 });
|
||||
game = applyAction(game, f.payer, { type: 'bid', amount: 6, allyPayment: 3 });
|
||||
for (let n = 0; !player(game, f.payer).hand.some(c => c.id === card) && n < 40; n++) {
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game); assert.ok(next);
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
}
|
||||
const paid = settle(game);
|
||||
assert.ok(player(paid, f.payer).hand.some(c => c.id === card));
|
||||
assert.equal(player(paid, f.payer).spice, player(before, f.payer).spice - 3);
|
||||
assert.equal(player(paid, f.donor).spice, player(before, f.donor).spice - 3);
|
||||
assert.equal(player(paid, f.emperor).spice, player(before, f.emperor).spice + 6);
|
||||
assert.equal(player(paid, f.trainer).spice, player(before, f.trainer).spice);
|
||||
assert.equal(viewGame(paid, f.trainer).bureaucrat.usedThisPhase, false);
|
||||
assert.equal(paid.auction!.index, before.auction!.index + 1);
|
||||
assert.ok(!paid.auction!.cards.slice(paid.auction!.index).some(c => c.id === card));
|
||||
});
|
||||
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} an individual five-spice contribution retains the unresolved mixed-payment guard`, () => {
|
||||
const f = createBureaucratSplitThresholdFixture(advanced);
|
||||
let game = advanceBureaucratThresholdToMovement(f.game, f.payer);
|
||||
game = applyAction(game, f.donor, { type: 'pledgeAid', amount: 3 });
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => applyAction(game, f.payer, {
|
||||
type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount: 8, allyPayment: 3,
|
||||
}));
|
||||
assert.deepEqual(game, before, 'No payer, escrow or physical force changes on the unsupported qualifying split');
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { createBureaucratQualifyingSplitSetup, advanceBureaucratQualifyingSplit, type BureaucratQualifyingSplitStep } from './fixture-bureaucrat-qualifying-split';
|
||||
export interface BureaucratQualifyingAuctionFixture {
|
||||
game: Game;
|
||||
setup: Game;
|
||||
beforeBribe: Game;
|
||||
bribe: Game;
|
||||
afterFirstMentat: Game;
|
||||
worm: Game;
|
||||
alliance: Game;
|
||||
sourceSetup: Game;
|
||||
beforePledge: Game;
|
||||
originalPledge: BureaucratQualifyingSplitStep;
|
||||
pledged: Game;
|
||||
beforeAction: Game;
|
||||
action: Action;
|
||||
payer: string;
|
||||
donor: string;
|
||||
trainer: string;
|
||||
guild: string;
|
||||
emperor: string;
|
||||
payerShare: number;
|
||||
donorShare: number;
|
||||
amount: number;
|
||||
counterCard?: string;
|
||||
physical: string[];
|
||||
reservedCard: string;
|
||||
bonusCard?: string;
|
||||
actions: BureaucratQualifyingSplitStep[];
|
||||
staging: string[];
|
||||
}
|
||||
|
||||
|
||||
export const qualifyingAuctionPlayer = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
||||
export function qualifyingAuctionPhysicalCards(game: Game): string[] {
|
||||
return [...game.deck, ...game.discard, ...game.players.flatMap(player => player.hand),
|
||||
...(game.auction?.cards.slice(game.auction.index + (game.currentAuctionSale?.origin === 'normal' ? 1 : 0)) ?? [])]
|
||||
.map(card => card.id).sort();
|
||||
}
|
||||
|
||||
/** Swap conserved physical cards, retaining the original hand size and auction
|
||||
* quantity. Harkonnen's Karama makes the native winning payment a real choice;
|
||||
* no purchase bonus is dealt until the original income continuation finishes. */
|
||||
function positionKarama(game: Game, actor: string, staging: string[]): string {
|
||||
const player = qualifyingAuctionPlayer(game, actor);
|
||||
const held = player.hand.find(card => card.effect === 'karama');
|
||||
if (held) return held.id;
|
||||
const index = game.deck.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(index >= 0, 'An original undealt physical Karama must be available');
|
||||
const outgoing = player.hand[0];
|
||||
assert.ok(outgoing);
|
||||
const incoming = game.deck[index];
|
||||
game.deck[index] = outgoing;
|
||||
player.hand[0] = incoming;
|
||||
staging.push(`Swap original held ${outgoing.id} with original undealt ${incoming.id} for ${actor}; retain hand size and committed lots.`);
|
||||
return incoming.id;
|
||||
}
|
||||
|
||||
export function createBureaucratQualifyingAuctionFixture({
|
||||
advanced = false, payerShare = 5, donorShare = 3, donor = 'f', counter = false, lastHandSpace = false,
|
||||
}: { advanced?: boolean; payerShare?: number; donorShare?: number; donor?: 'f' | 'b' | 'e'; counter?: boolean; lastHandSpace?: boolean } = {}): BureaucratQualifyingAuctionFixture {
|
||||
const native = createBureaucratQualifyingSplitSetup(advanced);
|
||||
let game = native.game, alliance = native.alliance;
|
||||
let actions = native.actions;
|
||||
const staging = native.staging;
|
||||
if (donor !== native.donor) {
|
||||
// Branch at the actual worm, before either mutual alliance proposal.
|
||||
game = structuredClone(native.worm);
|
||||
actions = actions.slice(0, actions.findIndex(step => step.action.type === 'alliance'));
|
||||
for (const [actor, target] of [[native.payer, donor], [donor, native.payer]]) {
|
||||
const action: Action = { type: 'alliance', target };
|
||||
actions.push({ actor, action });
|
||||
game = applyAction(game, actor, action);
|
||||
}
|
||||
alliance = structuredClone(game);
|
||||
game = advanceBureaucratQualifyingSplit(game, state => state.phase === 3 && !!state.auction &&
|
||||
!state.phaseOpening && !state.response && !state.decision, actions);
|
||||
}
|
||||
const sourceSetup = structuredClone(game);
|
||||
const physical = qualifyingAuctionPhysicalCards(game);
|
||||
positionKarama(game, native.payer, staging);
|
||||
const counterCard = counter ? positionKarama(game, native.trainer, staging) : undefined;
|
||||
if (lastHandSpace) {
|
||||
const payer = qualifyingAuctionPlayer(game, native.payer);
|
||||
while (payer.hand.length < handLimit(payer) - 1) {
|
||||
const incoming = game.deck.shift();
|
||||
assert.ok(incoming);
|
||||
payer.hand.push(incoming);
|
||||
staging.push(`Conserve original undealt ${incoming.id} into ${payer.id} after original auction quantity committed.`);
|
||||
}
|
||||
}
|
||||
assert.deepEqual(qualifyingAuctionPhysicalCards(game), physical);
|
||||
const originalPledge: BureaucratQualifyingSplitStep = { actor: donor, action: { type: 'pledgeAid', amount: donorShare } };
|
||||
const beforePledge = structuredClone(game);
|
||||
actions.push(originalPledge);
|
||||
game = applyAction(game, originalPledge.actor, originalPledge.action);
|
||||
const pledged = structuredClone(game);
|
||||
for (let n = 0; game.auction!.active !== native.payer && n < game.players.length; n++) {
|
||||
const step: BureaucratQualifyingSplitStep = { actor: game.auction!.active, action: { type: 'passBid' } };
|
||||
actions.push(step);
|
||||
game = applyAction(game, step.actor, step.action);
|
||||
}
|
||||
assert.equal(game.auction!.active, native.payer);
|
||||
const beforeAction = structuredClone(game), amount = payerShare + donorShare;
|
||||
const action: Action = { type: 'bid', amount, allyPayment: donorShare };
|
||||
return { ...native, donor, alliance, game, sourceSetup, beforePledge, originalPledge, pledged, beforeAction, action,
|
||||
payerShare, donorShare, amount, counterCard, physical, reservedCard: game.auction!.cards[game.auction!.index].id,
|
||||
bonusCard: game.deck[0]?.id, actions, staging };
|
||||
}
|
||||
|
||||
export function payBureaucratQualifyingAuction(fixture: BureaucratQualifyingAuctionFixture, state = fixture.game): Game {
|
||||
let game = applyAction(state, fixture.payer, fixture.action);
|
||||
fixture.actions.push({ actor: fixture.payer, action: fixture.action });
|
||||
for (let n = 0; game.decision?.kind !== 'auctionPayment' && n < game.players.length + 2; n++) {
|
||||
assert.ok(game.auction && !game.currentAuctionSale);
|
||||
const step: BureaucratQualifyingSplitStep = { actor: game.auction.active, action: { type: 'passBid' } };
|
||||
fixture.actions.push(step);
|
||||
game = applyAction(game, step.actor, step.action);
|
||||
}
|
||||
assert.equal(game.decision?.kind, 'auctionPayment');
|
||||
assert.equal(game.decision!.player, fixture.payer);
|
||||
const action: Action = { type: 'decision', karama: false };
|
||||
fixture.actions.push({ actor: fixture.payer, action });
|
||||
return applyAction(game, fixture.payer, action);
|
||||
}
|
||||
|
||||
export function allowQualifyingAuctionResponses(state: Game): Game {
|
||||
let game = state;
|
||||
for (let n = 0; game.response && n < 40; n++) {
|
||||
const actor = game.players.find(player => {
|
||||
const controls = viewGame(game, player.id).responseControls;
|
||||
return controls && !controls.hasPassed && controls.cancelCards.length > 0;
|
||||
});
|
||||
assert.ok(actor, 'The original response must have a legal unpassed counter holder');
|
||||
game = applyAction(game, actor.id, { type: 'passResponse' });
|
||||
}
|
||||
assert.equal(game.response, null);
|
||||
return game;
|
||||
}
|
||||
|
||||
export function chooseQualifyingAuction(game: Game, redirect = true): Game {
|
||||
assert.equal(game.decision?.kind, 'bureaucratPayment');
|
||||
return applyAction(game, game.decision!.player, { type: 'decision', event: game.bureaucratPaymentEvent, redirect });
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
|
||||
import {
|
||||
advanceBureaucratQualifyingSplit, createBureaucratQualifyingSplitSetup,
|
||||
type BureaucratQualifyingSplitStep,
|
||||
} from './fixture-bureaucrat-qualifying-split';
|
||||
|
||||
export interface BureaucratQualifyingShipmentDeclaration {
|
||||
beforeAction: Game;
|
||||
payer: string;
|
||||
action: Action;
|
||||
}
|
||||
|
||||
export function createBureaucratQualifyingShipmentFixture({
|
||||
advanced = false, primary = 5, donor = 3, ownerFunding = false,
|
||||
}: { advanced?: boolean; primary?: number; donor?: number; ownerFunding?: boolean } = {}) {
|
||||
const setup = createBureaucratQualifyingSplitSetup(advanced);
|
||||
const allianceIndex = setup.actions.findIndex(step => step.action.type === 'alliance');
|
||||
const actions = ownerFunding ? setup.actions.slice(0, allianceIndex) : [...setup.actions];
|
||||
const staging = [...setup.staging];
|
||||
const donorId = ownerFunding ? setup.trainer : setup.donor;
|
||||
let game = structuredClone(ownerFunding ? setup.worm : setup.game);
|
||||
if (ownerFunding) {
|
||||
for (const [actor, target] of [[setup.payer, donorId], [donorId, setup.payer]]) {
|
||||
const action: Action = { type: 'alliance', target };
|
||||
actions.push({ actor, action });
|
||||
game = applyAction(game, actor, action);
|
||||
}
|
||||
}
|
||||
const alliance = ownerFunding ? structuredClone(game) : setup.alliance;
|
||||
game = advanceBureaucratQualifyingSplit(game, state => state.phase === 5 &&
|
||||
state.active === setup.payer && !state.phaseOpening && !state.response && !state.decision, actions);
|
||||
assert.equal(game.turn, 2);
|
||||
const sourceSetup = structuredClone(game);
|
||||
const sourceArmy = structuredClone(game.players.find(p => p.id === setup.payer)!);
|
||||
assert.equal(sourceArmy.reserves, 10);
|
||||
assert.deepEqual(sourceArmy.forces, { 'carthag:11': 10 });
|
||||
assert.ok(game.players.every(p => !p.forces['habbanya_ridge_sietch:17']));
|
||||
assert.equal(game.players.find(p => p.id === setup.donor)!.spice, 6);
|
||||
assert.equal(game.players.find(p => p.id === setup.guild)!.spice, advanced ? 6 : 5);
|
||||
|
||||
// Keep the original singleton in physical custody so the original income
|
||||
// cancellation window is exercised, rather than manufacturing a response.
|
||||
const responder = game.players.find(p => p.id === setup.emperor)!;
|
||||
let karama = responder.hand.find(card => card.effect === 'karama');
|
||||
if (!karama) {
|
||||
const index = game.deck.findIndex(card => card.effect === 'karama');
|
||||
assert.ok(index >= 0);
|
||||
karama = game.deck.splice(index, 1)[0];
|
||||
responder.hand.push(karama);
|
||||
staging.push(`Conserved original ${karama.id}: undealt deck to Emperor hand for native income cancellation.`);
|
||||
}
|
||||
const beforePledge = structuredClone(game);
|
||||
const pledge: Action = { type: 'pledgeAid', amount: donor };
|
||||
actions.push({ actor: donorId, action: pledge });
|
||||
game = applyAction(game, donorId, pledge);
|
||||
const beforeAction = structuredClone(game);
|
||||
const amount = primary + donor;
|
||||
const action: Action = { type: 'ship', territory: 'habbanya_ridge_sietch', sector: 17, amount, allyPayment: donor };
|
||||
assert.equal(game.aid[donorId].amount, donor);
|
||||
return { ...setup, donor: donorId, alliance, game, sourceSetup, sourceArmy, beforePledge, pledge, beforeAction,
|
||||
action, primary, donorAmount: donor, amount, target: 'habbanya_ridge_sietch:17',
|
||||
karama, responder: responder.id, actions, staging };
|
||||
}
|
||||
|
||||
/** Declare once; the original Guild interception, when present, commits it. */
|
||||
export function declareBureaucratQualifyingShipment(
|
||||
fixture: BureaucratQualifyingShipmentDeclaration,
|
||||
) {
|
||||
const actions: BureaucratQualifyingSplitStep[] = [];
|
||||
let game = applyAction(structuredClone(fixture.beforeAction), fixture.payer, fixture.action);
|
||||
actions.push({ actor: fixture.payer, action: fixture.action });
|
||||
const declared = structuredClone(game);
|
||||
if (game.decision?.kind === 'guildShipment') {
|
||||
const actor = game.decision.player;
|
||||
const action: Action = { type: 'decision', allow: true };
|
||||
actions.push({ actor, action });
|
||||
game = applyAction(game, actor, action);
|
||||
}
|
||||
return { game, declared, paid: structuredClone(game), actions };
|
||||
}
|
||||
|
||||
/** Pass only original legal Karama responders; stop at the owner choice. */
|
||||
export function allowBureaucratQualifyingShipmentIncome(state: Game) {
|
||||
let game = structuredClone(state);
|
||||
for (let n = 0; game.response?.kind === 'guildIncome' && n < 12; n++) {
|
||||
const actor = game.players.find(p => {
|
||||
const controls = viewGame(game, p.id).responseControls;
|
||||
return controls && !controls.hasPassed && controls.cancelCards.length > 0;
|
||||
});
|
||||
assert.ok(actor, 'The conserved Karama must retain its original income response');
|
||||
game = applyAction(game, actor.id, { type: 'passResponse' });
|
||||
}
|
||||
assert.notEqual(game.response?.kind, 'guildIncome');
|
||||
return game;
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
||||
import { initializeClassicNexusSkillsPaymentsSetup, nextClassicNexusSkillsPaymentsNativeStep } from './fixture-classic-nexus-skills-payments';
|
||||
|
||||
export type BureaucratQualifyingSplitStep = { actor: string; action: Action };
|
||||
const clean = (game: Game) => !game.phaseOpening && !game.response && !game.decision;
|
||||
|
||||
/** Keep every original army and wallet. A real three-spice bribe collects into
|
||||
* Fremen's shield at the first Mentat; the actual second-turn worm forms H/F. */
|
||||
export function createBureaucratQualifyingSplitSetup(advanced = false) {
|
||||
const lobby = createGame('BUREAUCRATQUALIFY', newPlayer('b', 'Bureaucrat', 'atreides'), advanced);
|
||||
for (const [id, faction] of [['p', 'harkonnen'], ['f', 'fremen'], ['e', 'emperor'], ['g', 'guild']] as const)
|
||||
joinGame(lobby, newPlayer(id, faction, faction));
|
||||
let game = initializeClassicNexusSkillsPaymentsSetup({ initial: lobby, skill: 'bureaucrat' });
|
||||
const actions: BureaucratQualifyingSplitStep[] = [];
|
||||
const staging: string[] = [];
|
||||
const act = (actor: string, action: Action) => {
|
||||
actions.push({ actor, action });
|
||||
game = applyAction(game, actor, action);
|
||||
};
|
||||
const advance = (until: (state: Game) => boolean) => {
|
||||
for (let n = 0; !until(game) && n < 2400; n++) {
|
||||
assert.notEqual(game.status, 'finished');
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
|
||||
assert.ok(next, 'Original skill and phase controls must reach the payment boundary');
|
||||
act(next.actor, next.action);
|
||||
}
|
||||
assert.ok(until(game));
|
||||
};
|
||||
const orderSpice = (kinds: ('land' | 'worm')[]) => {
|
||||
for (const [position, kind] of kinds.entries()) {
|
||||
const index = game.spiceDeck.findIndex((card, i) => i >= position &&
|
||||
(kind === 'land' ? 'territory' in card : 'worm' in card));
|
||||
assert.ok(index >= position);
|
||||
game.spiceDeck.splice(position, 0, game.spiceDeck.splice(index, 1)[0]);
|
||||
}
|
||||
staging.push('Reorder only original still-unplayed Spice Cards.');
|
||||
};
|
||||
advance(state => state.status === 'playing');
|
||||
const setup = structuredClone(game);
|
||||
assert.ok(game.leaderSkills!.assignments.some(a => a.owner === 'b' && a.skill === 'bureaucrat'));
|
||||
orderSpice(['land', 'land']);
|
||||
advance(state => state.phase === 5 && state.active === 'p' && clean(state));
|
||||
const beforeBribe = structuredClone(game);
|
||||
act('p', { type: 'bribe', target: 'f', amount: 3 });
|
||||
const bribe = structuredClone(game);
|
||||
assert.equal(game.players.find(p => p.id === 'f')!.bribes, 3);
|
||||
advance(state => state.turn === 2 && state.phase === 1 && clean(state));
|
||||
const afterFirstMentat = structuredClone(game);
|
||||
assert.equal(game.players.find(p => p.id === 'f')!.spice, 6);
|
||||
orderSpice(['worm', 'land', 'land']);
|
||||
advance(state => state.nexus && !state.spiceWindow && !state.spiceResolution && clean(state));
|
||||
const worm = structuredClone(game);
|
||||
act('p', { type: 'alliance', target: 'f' });
|
||||
act('f', { type: 'alliance', target: 'p' });
|
||||
const alliance = structuredClone(game);
|
||||
advance(state => state.phase === 3 && !!state.auction && clean(state));
|
||||
return { game, setup, beforeBribe, bribe, afterFirstMentat, worm, alliance,
|
||||
payer: 'p', donor: 'f', trainer: 'b', guild: 'g', emperor: 'e', actions, staging };
|
||||
}
|
||||
|
||||
export function advanceBureaucratQualifyingSplit(state: Game, until: (game: Game) => boolean,
|
||||
actions?: BureaucratQualifyingSplitStep[]): Game {
|
||||
let game = state;
|
||||
for (let n = 0; !until(game) && n < 1600; n++) {
|
||||
assert.notEqual(game.status, 'finished');
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
|
||||
assert.ok(next);
|
||||
actions?.push(next);
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
}
|
||||
assert.ok(until(game));
|
||||
return game;
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
|
||||
import { initializeClassicNexusSkillsPaymentsSetup, nextClassicNexusSkillsPaymentsNativeStep } from './fixture-classic-nexus-skills-payments';
|
||||
|
||||
/** Original skill offers/setup and actual turn-two worm alliance. Conserved
|
||||
* starting counters are staged into reserves to isolate payment, not combat.
|
||||
* Wallets, alliances, clocks and payment records are never assigned. */
|
||||
export function createBureaucratSplitThresholdFixture(advanced = false) {
|
||||
const lobby = createGame('BUREAUCRATTHRESHOLD', newPlayer('b', 'Bureaucrat', 'atreides'), advanced);
|
||||
for (const [id, faction] of [['p', 'harkonnen'], ['f', 'fremen'], ['e', 'emperor'], ['g', 'guild']] as const)
|
||||
joinGame(lobby, newPlayer(id, faction, faction));
|
||||
let game = initializeClassicNexusSkillsPaymentsSetup({ initial: lobby, skill: 'bureaucrat' });
|
||||
const actions: { actor: string; action: Action }[] = [];
|
||||
const step = () => {
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
|
||||
assert.ok(next, `Native payment setup/progression stalled at ${game.phase}/${game.setupStage}`);
|
||||
actions.push(next);
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
};
|
||||
for (let n = 0; game.status === 'setup' && n < 240; n++) step();
|
||||
assert.equal(game.status, 'playing');
|
||||
assert.ok(game.leaderSkills!.assignments.some(a => a.owner === 'b' && a.skill === 'bureaucrat'));
|
||||
for (const player of game.players) {
|
||||
player.reserves += Object.values(player.forces).reduce((a, b) => a + b, 0);
|
||||
player.forces = {};
|
||||
if (player.elites) {
|
||||
player.elites.reserves += Object.values(player.elites.forces).reduce((a, b) => a + b, 0);
|
||||
player.elites.forces = {};
|
||||
}
|
||||
if (player.advisors) player.advisors = {};
|
||||
}
|
||||
const orderSpice = (kinds: ('land' | 'worm')[]) => {
|
||||
const front = kinds.map(kind => {
|
||||
const index = game.spiceDeck.findIndex(card => kind === 'land' ? 'territory' in card : 'worm' in card);
|
||||
assert.ok(index >= 0);
|
||||
return game.spiceDeck.splice(index, 1)[0];
|
||||
});
|
||||
game.spiceDeck.unshift(...front);
|
||||
};
|
||||
orderSpice(['land', 'land']);
|
||||
for (let n = 0; !(game.turn === 2 && game.phase === 1) && n < 1800; n++) step();
|
||||
assert.equal(game.turn, 2); assert.equal(game.phase, 1);
|
||||
orderSpice(['worm', 'land', 'land']);
|
||||
let allied = false;
|
||||
for (let n = 0; !(allied && game.phase === 3 && game.auction && !game.phaseOpening && !game.response && !game.decision) && n < 600; n++) {
|
||||
if (game.nexus && !game.phaseOpening && !game.response && !game.decision && !game.spiceWindow && !allied) {
|
||||
for (const [actor, target] of [['p', 'f'], ['f', 'p']]) {
|
||||
const action: Action = { type: 'alliance', target };
|
||||
actions.push({ actor, action }); game = applyAction(game, actor, action);
|
||||
}
|
||||
allied = true;
|
||||
} else step();
|
||||
}
|
||||
assert.ok(allied && game.auction);
|
||||
assert.equal(game.players.find(p => p.id === 'p')!.ally, 'f');
|
||||
assert.equal(game.players.find(p => p.id === 'f')!.ally, 'p');
|
||||
return { game, actions, payer: 'p', donor: 'f', trainer: 'b', guild: 'g', emperor: 'e' };
|
||||
}
|
||||
|
||||
export function advanceBureaucratThresholdToMovement(state: Game, actor: string): Game {
|
||||
let game = state;
|
||||
for (let n = 0; !(game.phase === 5 && game.active === actor && !game.phaseOpening && !game.response && !game.decision) && n < 600; n++) {
|
||||
const next = nextClassicNexusSkillsPaymentsNativeStep(game);
|
||||
assert.ok(next, 'Native payment continuation stalled');
|
||||
game = applyAction(game, next.actor, next.action);
|
||||
}
|
||||
assert.equal(game.phase, 5); assert.equal(game.active, actor);
|
||||
return game;
|
||||
}
|
||||
Reference in new issue
Block a user