106 lines
5.9 KiB
TypeScript
106 lines
5.9 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { applyAction, handLimit, type Action, type Game } from '../game/engine';
|
|
import type { FactionId } from '../game/catalog';
|
|
import type { LeaderSkillId } from '../game/leader-skill-cards';
|
|
import { createIndependentCardModulesFixture } from './fixture-independent-card-modules';
|
|
import { homeworldEcazClean, nextHomeworldEcazStep } from './fixture-homeworld-ecaz-occupy';
|
|
import {
|
|
allowRicheseBureaucratResponses, richeseBureaucratCards, richeseBureaucratPlayer,
|
|
} from './fixture-bureaucrat-richese-funding';
|
|
|
|
/** Original Advanced setup and purchases, not a full-hand state injection.
|
|
* Only the conserved, unopened turn-two worm is moved to the next Spice reveal. */
|
|
export function createRicheseFullHandSelfPurchaseGame(options: {
|
|
skills?: boolean;
|
|
emperor?: boolean;
|
|
owner?: 'beneGesserit' | 'atreides' | 'emperor' | 'richese';
|
|
} = {}): Game {
|
|
const emperor = options.emperor !== false, owner = options.owner ?? 'beneGesserit';
|
|
const roster: FactionId[] = ['richese', 'atreides', 'beneGesserit', emperor ? 'emperor' : 'guild'];
|
|
assert.ok(roster.includes(owner));
|
|
const training: Partial<Record<FactionId, LeaderSkillId>> = {
|
|
richese: 'smuggler', atreides: 'planetologist', beneGesserit: 'warmaster',
|
|
[emperor ? 'emperor' : 'guild']: 'spice-banker', [owner]: 'bureaucrat',
|
|
};
|
|
if (owner === 'emperor') training.beneGesserit = 'spice-banker';
|
|
let game = createIndependentCardModulesFixture({ advanced: true, factions: roster, expansions: ['choam'],
|
|
skills: options.skills ?? true, training, independent: false, tech: false }).game;
|
|
const stock = richeseBureaucratCards(game);
|
|
assert.equal(stock.length, 45);
|
|
assert.equal(new Set(stock).size, stock.length);
|
|
const original = richeseBureaucratPlayer(game, 'richese').hand.map(c => c.id);
|
|
assert.equal(original.length, 1);
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').spice, 5);
|
|
const act = (actor: string, action: Action) => { game = applyAction(game, actor, action); };
|
|
const choose = (values: Record<string, unknown>) => act('richese',
|
|
{ type: 'decision', event: game.richeseBidding!.event, ...values } as Action);
|
|
const next = () => {
|
|
if (game.turn === 1 && !game.response && !game.phaseOpening) {
|
|
if (game.decision?.kind === 'richeseBlackMarket') { choose({ decline: true }); return; }
|
|
if (game.decision?.kind === 'richeseDeclaration') { choose({ position: 'first' }); return; }
|
|
if (game.decision?.kind === 'richeseCache') {
|
|
choose({ card: game.richeseCache![0].id, method: 'silent' }); return;
|
|
}
|
|
if (game.decision?.kind === 'auctionPayment') {
|
|
act(game.decision.player, { type: 'decision', karama: false }); return;
|
|
}
|
|
if (!game.decision && game.richeseAuction && !game.richeseAuction.outcome) {
|
|
const lot = game.richeseAuction;
|
|
assert.equal(lot.source, 'cache');
|
|
const actor = lot.order.find(id => !lot.acted.includes(id));
|
|
assert.ok(actor);
|
|
act(actor, { type: 'richeseBid', event: lot.event, amount: actor === 'richese' ? 1 : 0 });
|
|
return;
|
|
}
|
|
if (!game.decision && game.auction) {
|
|
const actor = game.auction.active;
|
|
act(actor, actor === 'richese' && richeseBureaucratPlayer(game, actor).hand.length < 4
|
|
? { type: 'bid', amount: 1 } : { type: 'passBid' });
|
|
return;
|
|
}
|
|
}
|
|
if (game.phase === 5 && homeworldEcazClean(game)) act(game.active!, { type: 'endMovement' });
|
|
else {
|
|
const step = nextHomeworldEcazStep(game);
|
|
assert.ok(step, `Missing original full-hand action at ${game.turn}/${game.phase}/${game.decision?.kind}`);
|
|
act(step.actor, step.action);
|
|
}
|
|
};
|
|
const reach = (until: (state: Game) => boolean) => {
|
|
for (let n = 0; !until(game) && n < 800; n++) next();
|
|
assert.ok(until(game), 'Reach the original full-hand purchase boundary');
|
|
};
|
|
act('atreides', { type: 'bribe', target: 'richese', amount: 4 });
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').spice, 5, 'A promised bribe is not spendable');
|
|
reach(g => g.phase === 3 && g.richeseBidding?.stage === 'normal' && homeworldEcazClean(g));
|
|
assert.equal(game.richeseBidding!.position, 'first');
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').hand.length, 2);
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').spice, 4);
|
|
reach(g => g.phase === 4 && homeworldEcazClean(g));
|
|
const full = richeseBureaucratPlayer(game, 'richese');
|
|
assert.equal(full.hand.length, handLimit(full));
|
|
assert.equal(full.hand.length, 4);
|
|
assert.equal(full.hand[0].id, original[0]);
|
|
assert.equal(full.spice, 2, 'The cache and two ordinary acquisitions each cost one');
|
|
reach(g => g.phase === 8 && homeworldEcazClean(g));
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').spice, 6, 'Only first Mentat collects the four-spice bribe');
|
|
reach(g => g.turn === 2 && g.phase === 1 && homeworldEcazClean(g));
|
|
const worm = game.spiceDeck.findIndex(c => 'worm' in c && !c.greatMaker && !c.suppressed);
|
|
assert.ok(worm >= 0);
|
|
game.spiceDeck.unshift(game.spiceDeck.splice(worm, 1)[0]);
|
|
reach(g => !!g.nexus && !g.spiceWindow && !g.spiceResolution && homeworldEcazClean(g));
|
|
act('richese', { type: 'alliance', target: 'atreides' });
|
|
act('atreides', { type: 'alliance', target: 'richese' });
|
|
reach(g => g.phase === 3 && g.decision?.kind === 'richeseBlackMarket');
|
|
choose({ card: richeseBureaucratPlayer(game, 'richese').hand[0].id, method: 'silent' });
|
|
game = allowRicheseBureaucratResponses(game);
|
|
assert.ok(game.richeseAuction && !game.richeseAuction.outcome);
|
|
assert.equal(game.richeseAuction.acted.length, 0);
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').hand.length, 4);
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').spice, 6);
|
|
assert.equal(richeseBureaucratPlayer(game, 'richese').ally, 'atreides');
|
|
assert.equal(richeseBureaucratPlayer(game, 'atreides').ally, 'richese');
|
|
assert.deepEqual(richeseBureaucratCards(game), stock);
|
|
return game;
|
|
}
|