Offer Ixian Nexus replacement after native BG purchases

This commit is contained in:
Quintine committed 2026-10-16 03:10:59 +11:00
1 parent 5902286db5
commit f268a36a11
9 files changed
+203 -32

No files matched your search

+79
View File
@@ -0,0 +1,79 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame } from '../game/engine';
import { createOriginalBgIxianReplacement, allowBgIxianConversion } from './fixture-bg-ixian-replacement';
import { settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement';
for (const use of ['purchase', 'auctionPayment'] as const) {
void test(`native BG ${use} gives the exact purchased card its independent Ixian replacement without another fee`, () => {
const f = createOriginalBgIxianReplacement(use);
const game = allowBgIxianConversion(f.game);
const offer = viewGame(game, f.buyer).nexusIxianReplacement;
assert.ok(offer?.canUse);
assert.equal(offer.purchased?.id, f.lot.id);
assert.equal(game.pendingNexusIxianReplacement?.free, true);
assert.equal(game.pendingNexusIxianReplacement?.amount, use === 'purchase' ? 0 : 2);
const before = f.beforeCost.players.find(p => p.id === f.buyer)!;
const emperorSpice = f.beforeCost.players.find(p => p.faction === 'emperor')!.spice;
for (const replace of [false, true]) {
const done = settleIxianReplacementFixture(applyAction(game, f.buyer,
{ type: replace ? 'nexusIxianReplacementUse' : 'nexusIxianReplacementPass', event: offer.event }));
const owner = done.players.find(p => p.id === f.buyer)!;
assert.equal(owner.spice, before.spice);
assert.equal(done.players.find(p => p.faction === 'emperor')!.spice, emperorSpice);
assert.equal(owner.hand.some(c => c.id === f.cost.id), false);
assert.equal(done.discard.filter(c => c.id === f.cost.id).length, 1);
assert.equal(owner.hand.some(c => c.id === f.lot.id), !replace);
assert.equal(owner.hand.some(c => c.id === f.next.id), replace);
assert.equal(done.discard.filter(c => c.id === f.lot.id).length, replace ? 1 : 0);
assert.equal(done.nexusCards!.cards!.hands[f.buyer], replace ? null : 'ixians');
assert.equal(done.nexusCards!.cards!.discard.filter(c => c === 'ixians').length, replace ? 1 : 0);
assert.equal(done.nexusIxianReplacementHistory!.length, 2);
assert.equal(done.nexusIxianReplacementHistory!.at(-1)!.source.card.id, f.lot.id);
assert.equal(done.pendingKarama, null);
assert.equal(done.pendingNexusIxianReplacement, null);
assert.equal(done.auction!.index, f.beforeCost.auction!.index + 1);
}
});
void test(`canceled native BG ${use} preserves the native unpurchased lot or funded payment`, () => {
const f = createOriginalBgIxianReplacement(use);
const parent = structuredClone(f.beforeCost);
const owner = parent.players.find(p => p.id === f.buyer)!;
const emperor = parent.players.find(p => p.faction === 'emperor')!;
// CONTROLLED counter custody only: move the conserved unused starting
// Karama to Emperor, leaving native auctions, wallets and Nexus untouched.
const index = owner.hand.findIndex(c => c.effect === 'karama'); assert.ok(index >= 0);
const counter = owner.hand.splice(index, 1)[0]; emperor.hand.push(counter);
const balance = owner.spice;
let game = applyAction(parent, f.buyer, f.action);
assert.equal(game.response?.kind, 'worthlessKarama');
assert.equal(game.pendingNexusIxianReplacement, null);
game = applyAction(game, emperor.id, { type: 'card', card: counter.id, mode: 'cancel' });
game = settleIxianReplacementFixture(game);
if (use === 'purchase') {
assert.equal(game.pendingNexusIxianReplacement, null);
assert.equal(game.nexusIxianReplacementHistory!.length, 1);
assert.equal(game.nexusCards!.cards!.hands[f.buyer], 'ixians');
assert.equal(game.players.find(p => p.id === f.buyer)!.spice, balance);
assert.equal(game.players.find(p => p.id === f.buyer)!.hand.some(c => c.id === f.lot.id), false);
assert.equal(game.auction!.cards[game.auction!.index].id, f.lot.id);
} else {
// The native winning-bid fallback automatically pays a funded bid;
// that real paid purchase, not the denied free benefit, earns the choice.
const source = game.pendingNexusIxianReplacement!;
assert.equal(source.free, false);
assert.equal(source.amount, 2);
assert.equal(source.card.id, f.lot.id);
assert.equal(game.players.find(p => p.id === f.buyer)!.spice, balance - 2);
game = settleIxianReplacementFixture(applyAction(game, f.buyer,
{ type: 'nexusIxianReplacementUse', event: source.event }));
assert.equal(game.players.find(p => p.id === f.buyer)!.spice, balance - 2);
assert.equal(game.players.find(p => p.faction === 'emperor')!.spice, emperor.spice + 2);
assert.equal(game.players.find(p => p.id === f.buyer)!.hand.some(c => c.id === f.next.id), true);
assert.equal(game.nexusCards!.cards!.hands[f.buyer], null);
}
assert.equal(game.discard.filter(c => c.id === f.cost.id).length, 1);
assert.equal(game.discard.filter(c => c.id === counter.id).length, 1);
});
}
+38
View File
@@ -0,0 +1,38 @@
import assert from 'node:assert/strict';
import { applyAction, createGame, joinGame, newPlayer, type Action, type Game } from '../game/engine';
import { createIxianNexusReplacementFixture, settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement';
/** Original Advanced BG setup, native worm/alliance/Nexus deal and paid
* Worthless purchase. Keep that first lot, then spend it on a second actual
* normal auction lot. Only original undealt deck/Nexus order is selected. */
export function createOriginalBgIxianReplacement(use: 'purchase' | 'auctionPayment' = 'purchase') {
const lobby = createGame('BGIXIANREPLACE', newPlayer('b', 'Bene Gesserit', 'beneGesserit'), true, []);
joinGame(lobby, newPlayer('e', 'Emperor', 'emperor'));
joinGame(lobby, newPlayer('g', 'Guild', 'guild'));
const first = createIxianNexusReplacementFixture({ initial: lobby, purchasedKind: 'worthless' });
let game = settleIxianReplacementFixture(applyAction(first.game, first.buyer,
{ type: 'nexusIxianReplacementPass', event: first.event }));
assert.ok(game.auction && !game.response && !game.decision && !game.phaseOpening);
const cost = game.players.find(p => p.id === first.buyer)!.hand.find(c => c.id === first.purchased.id)!;
assert.equal(cost.kind, 'worthless');
const lot = game.auction.cards[game.auction.index];
const next = game.deck[0]; assert.ok(next);
if (use === 'auctionPayment') {
for (let n = 0; game.auction!.active !== first.buyer && n < 10; n++)
game = applyAction(game, game.auction!.active, { type: 'passBid' });
game = applyAction(game, first.buyer, { type: 'bid', amount: 2 });
for (let n = 0; !game.decision && n < 10; n++)
game = applyAction(game, game.auction!.active, { type: 'passBid' });
assert.deepEqual(game.decision, { kind: 'auctionPayment', player: first.buyer });
}
const beforeCost = structuredClone(game);
const action: Action = use === 'purchase'
? { type: 'card', card: cost.id, mode: 'purchase' }
: { type: 'decision', karama: true, card: cost.id };
game = applyAction(game, first.buyer, action);
return { game, beforeCost, buyer: first.buyer, cost, lot, next, action, use,
provenance: 'Original setup, qualifying Nexus draw, paid Worthless purchase, original first-lot Pass, then native second-lot BG conversion; no played state staged.' };
}
export function allowBgIxianConversion(game: Game): Game {
return settleIxianReplacementFixture(game);
}
@@ -322,24 +322,3 @@ void test('direct printed Karama acquisition preserves the original zero bid and
inventory(use);
});
void test('nonprinted Bene Gesserit Karama substitution continues native sale without enabling the bounded offer',() => {
const lobby = createGame('BGCONVERT',newPlayer('b','Bene Gesserit','beneGesserit'),true,[]);
joinGame(lobby,newPlayer('e','Emperor','emperor'));
joinGame(lobby,newPlayer('g','Guild','guild'));
const f = createIxianNexusReplacementFixture({initial:lobby});
const parent = reload(f.beforePayment);
const index = parent.deck.findIndex(c => c.kind === 'worthless');
assert.ok(index >= 0);
const worthless = parent.deck.splice(index,1)[0];
owner(parent,f.buyer).hand.push(worthless);
const balance = owner(parent,f.buyer).spice;
let game = applyAction(parent,f.buyer,{type:'decision',karama:true,card:worthless.id});
game = settleIxianReplacementFixture(game);
assert.equal(game.pendingNexusIxianReplacement,null);
assert.equal(viewGame(game,f.buyer).nexusIxianReplacement,null);
assert.equal(game.nexusCards!.cards!.hands[f.buyer],'ixians');
assert.equal(owner(game,f.buyer).spice,balance);
assert.ok(owner(game,f.buyer).hand.some(c => c.id === f.purchased.id));
assert.equal(game.nexusIxianReplacementHistory!.length,0);
inventory(game);
});