Compose Harkonnen Ixian replacement before native purchase bonus
This commit is contained in:
1 parent
f268a36a11
commit
2c6bb02ccb
12 files changed
+229
-55
No files matched your search
@@ -0,0 +1,36 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import { applyAction, createGame, joinGame, newPlayer, type Action } from '../game/engine';
|
||||
import { createIxianNexusReplacementFixture, settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement';
|
||||
|
||||
/** Original four-seat setup, qualifying alliance/Nexus draw and normal paid
|
||||
* purchases. Atreides retains its original starting Karama as a real counter.
|
||||
* Only undealt deck/Nexus order is selected; no played state is staged. */
|
||||
export function createOriginalHarkonnenReplacement(advanced = false) {
|
||||
const lobby = createGame('HARKREPLACE', newPlayer('a', 'Atreides', 'atreides'), advanced, []);
|
||||
joinGame(lobby, newPlayer('e', 'Emperor', 'emperor'));
|
||||
joinGame(lobby, newPlayer('h', 'Harkonnen', 'harkonnen'));
|
||||
joinGame(lobby, newPlayer('g', 'Guild', 'guild'));
|
||||
const first = createIxianNexusReplacementFixture({ initial: lobby, face: 'richese',
|
||||
otherDraw: { player: 'h', face: 'ixians' } });
|
||||
let game = settleIxianReplacementFixture(applyAction(first.game, first.buyer,
|
||||
{ type: 'nexusIxianReplacementPass', event: first.event }));
|
||||
for (let n = 0; game.auction!.active !== 'h' && n < 20; n++)
|
||||
game = applyAction(game, game.auction!.active, { type: 'passBid' });
|
||||
const lot = game.auction!.cards[game.auction!.index];
|
||||
const beforeBid = structuredClone(game);
|
||||
game = applyAction(game, 'h', { type: 'bid', amount: 2 });
|
||||
let beforePurchase = structuredClone(game);
|
||||
let purchaseAction: Action = { type: 'passBid' }, actor = '';
|
||||
for (let n = 0; !game.currentAuctionSale && n < 30; n++) {
|
||||
beforePurchase = structuredClone(game);
|
||||
actor = game.decision?.player ?? game.auction!.active;
|
||||
purchaseAction = game.decision?.kind === 'auctionPayment'
|
||||
? { type: 'decision', karama: false } : { type: 'passBid' };
|
||||
game = applyAction(game, actor, purchaseAction);
|
||||
}
|
||||
assert.ok(game.currentAuctionSale);
|
||||
assert.equal(game.currentAuctionSale.winner, 'h');
|
||||
return { game, beforeBid, beforePurchase, purchaseAction, actor, lot,
|
||||
buyer: 'h', counter: 'a', next: game.deck[0], afterNext: game.deck[1],
|
||||
provenance: 'Original four-seat setup, native worm/alliance and two natural Nexus draws, actual Atreides first purchase/Keep then paid Harkonnen second lot; no played-state staging.' };
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { applyAction, viewGame, type Game } from '../game/engine';
|
||||
import { createOriginalHarkonnenReplacement } from './fixture-harkonnen-ixian-replacement';
|
||||
import { settleIxianReplacementFixture } from './fixture-nexus-ixian-replacement';
|
||||
|
||||
const player = (game: Game, id: string) => game.players.find(p => p.id === id)!;
|
||||
function passIncome(state: Game): Game {
|
||||
let game = state;
|
||||
for (let n = 0; game.response?.kind === 'emperorIncome' && n < 10; n++) {
|
||||
const actor = game.players.find(p => !game.response!.passed.includes(p.id))!;
|
||||
game = applyAction(game, actor.id, { type: 'passResponse' });
|
||||
}
|
||||
return game;
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) {
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} Harkonnen replacement precedes one original cancelable purchase bonus`, () => {
|
||||
for (const replace of [false, true]) for (const cancelBonus of [false, true]) {
|
||||
const f = createOriginalHarkonnenReplacement(advanced);
|
||||
const source = f.game.pendingNexusIxianReplacement!;
|
||||
assert.equal(source.card.id, f.lot.id);
|
||||
assert.equal(player(f.game, 'h').hand.length, 3, 'No bonus is drawn before the replacement choice');
|
||||
assert.ok(viewGame(f.game, 'h').nexusIxianReplacement!.canUse);
|
||||
assert.equal(f.game.response, null);
|
||||
let game = applyAction(f.game, 'h', { type: replace ? 'nexusIxianReplacementUse' : 'nexusIxianReplacementPass', event: source.event });
|
||||
assert.equal(player(game, 'h').hand.some(c => c.id === f.lot.id), !replace);
|
||||
assert.equal(player(game, 'h').hand.some(c => c.id === f.next.id), replace);
|
||||
game = passIncome(game);
|
||||
assert.equal(game.response?.kind, 'harkonnenBonus');
|
||||
assert.equal(player(game, 'h').hand.length, 3);
|
||||
const bonus = replace ? f.afterNext : f.next;
|
||||
assert.ok(bonus);
|
||||
if (cancelBonus) {
|
||||
const counter = player(game, f.counter).hand.find(c => c.effect === 'karama')!;
|
||||
game = applyAction(game, f.counter, { type: 'card', card: counter.id, mode: 'cancel' });
|
||||
}
|
||||
game = settleIxianReplacementFixture(game);
|
||||
assert.equal(player(game, 'h').hand.length, cancelBonus ? 3 : 4);
|
||||
assert.equal(player(game, 'h').hand.some(c => c.id === bonus.id), !cancelBonus);
|
||||
assert.equal(player(game, 'h').spice, player(f.beforeBid, 'h').spice - 2);
|
||||
assert.equal(player(game, 'e').spice, player(f.beforeBid, 'e').spice + 2);
|
||||
assert.equal(game.nexusCards!.cards!.hands.h, replace ? null : 'ixians');
|
||||
assert.equal(game.nexusIxianReplacementHistory!.length, 2);
|
||||
assert.equal(game.nexusIxianReplacementHistory!.at(-1)!.source.card.id, f.lot.id);
|
||||
assert.equal(game.pendingNexusIxianReplacement, null);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void test('Harkonnen replacement at eight cards cannot manufacture a purchase-bonus slot', () => {
|
||||
for (const advanced of [false, true]) for (const replace of [false, true]) {
|
||||
const f = createOriginalHarkonnenReplacement(advanced);
|
||||
const parent = structuredClone(f.beforePurchase), owner = player(parent, 'h');
|
||||
// CONTROLLED capacity boundary: conserve original undealt cards into
|
||||
// Harkonnen custody without changing the actual auction, money or Nexus.
|
||||
while (owner.hand.length < 7) {
|
||||
const index = parent.deck.findIndex(c => c.effect !== 'karama'); assert.ok(index >= 0);
|
||||
owner.hand.push(parent.deck.splice(index, 1)[0]);
|
||||
}
|
||||
let game = applyAction(parent, f.actor, f.purchaseAction);
|
||||
assert.equal(player(game, 'h').hand.length, 8);
|
||||
const count = game.deck.length;
|
||||
const source = game.pendingNexusIxianReplacement!; assert.equal(source.card.id, f.lot.id);
|
||||
game = settleIxianReplacementFixture(applyAction(game, 'h',
|
||||
{ type: replace ? 'nexusIxianReplacementUse' : 'nexusIxianReplacementPass', event: source.event }));
|
||||
assert.equal(player(game, 'h').hand.length, 8);
|
||||
assert.equal(game.deck.length, count - Number(replace));
|
||||
assert.equal(player(game, 'h').hand.some(c => c.id === f.lot.id), !replace);
|
||||
assert.equal(player(game, 'h').spice, owner.spice - 2);
|
||||
assert.equal(game.nexusIxianReplacementHistory!.length, 2);
|
||||
}
|
||||
});
|
||||
@@ -270,36 +270,6 @@ void test('a later genuine purchase creates a new source while the closed prior
|
||||
inventory(use);
|
||||
});
|
||||
|
||||
void test('a genuine Harkonnen buyer holding Ixian Nexus gets native purchase/bonus only, never an unresolved prototype offer',() => {
|
||||
const lobby = createGame('HARKBUYER',newPlayer('a','Atreides','atreides'),false,[]);
|
||||
joinGame(lobby,newPlayer('e','Emperor','emperor'));
|
||||
joinGame(lobby,newPlayer('h','Harkonnen','harkonnen'));
|
||||
joinGame(lobby,newPlayer('g','Guild','guild'));
|
||||
const f = createIxianNexusReplacementFixture({initial:lobby,face:'richese',otherDraw:{player:'h',face:'ixians'}});
|
||||
let game = settleIxianReplacementFixture(applyAction(f.game,f.buyer,{type:'nexusIxianReplacementPass',event:f.event}));
|
||||
assert.equal(game.nexusCards!.cards!.hands.h,'ixians');
|
||||
for (let step = 0; game.auction!.active !== 'h' && step < 20; step++)
|
||||
game = applyAction(game,game.auction!.active,{type:'passBid'});
|
||||
const balance = owner(game,'h').spice;
|
||||
const purchased = game.auction!.cards[game.auction!.index].id;
|
||||
game = applyAction(game,'h',{type:'bid',amount:2});
|
||||
for (let step = 0; game.auction?.bidder === 'h' && !game.currentAuctionSale &&
|
||||
game.auction.index === f.auctionIndex + 1 && step < 30; step++) {
|
||||
if (game.decision?.kind === 'auctionPayment')
|
||||
game = applyAction(game,'h',{type:'decision',karama:false});
|
||||
else game = applyAction(game,game.auction.active,{type:'passBid'});
|
||||
}
|
||||
assert.equal(game.pendingNexusIxianReplacement,null);
|
||||
assert.equal(viewGame(game,'a').nexusIxianReplacement,null);
|
||||
assert.equal(viewGame(game,'h').nexusIxianReplacement,null);
|
||||
game = settleIxianReplacementFixture(game);
|
||||
assert.equal(owner(game,'h').spice,balance - 2);
|
||||
assert.ok(owner(game,'h').hand.some(c => c.id === purchased));
|
||||
assert.equal(owner(game,'h').hand.length,4,'The actual native Harkonnen bonus remains active.');
|
||||
assert.equal(game.nexusCards!.cards!.hands.h,'ixians');
|
||||
assert.equal(game.nexusIxianReplacementHistory!.length,1);
|
||||
inventory(game);
|
||||
});
|
||||
|
||||
void test('direct printed Karama acquisition preserves the original zero bid and native free continuation',() => {
|
||||
const f = createIxianNexusReplacementFixture();
|
||||
|
||||
Reference in new issue
Block a user