Files
Dune/tests/fixture-homeworld-occupied-private-lots.ts

120 lines
6.0 KiB
TypeScript

import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeHomeworldOccupationGameForAudit,
joinGame, newPlayer, type Game,
} from '../game/engine';
import { FACTIONS, type FactionId } from '../game/catalog';
import {
assertDefenseInventory, qualifyDefensePosition,
} from './fixture-homeworld-occupied-defenses';
import { biddingPhysicalIds, biddingPlayer, stageBiddingKarama } from './fixture-homeworld-occupied-bidding';
import { advanceOriginal } from './fixture-homeworld-occupied-producers';
export const privateLotPlayer = biddingPlayer;
export const privateLotPhysicalIds = biddingPhysicalIds;
export const privateLotReload = (game: Game): Game => JSON.parse(JSON.stringify(game));
/** Real fresh audit setup with the originally selected faction decks. Only the
* subsequent conserved qualification/card positions are controlled fixtures. */
export function freshPrivateLotGame(excludeCardId?: string) {
const roster: [string, FactionId][] = [
['nativeRichese', 'richese'], ['nativeAt', 'atreides'], ['occupier', 'guild'],
['ally', 'emperor'], ['rival', 'beneGesserit'], ['fremen', 'fremen'],
];
const expansions = [...new Set(roster.map(([, faction]) =>
FACTIONS.find(card => card.id === faction)!.expansion))].filter(expansion => expansion !== 'base');
let game = createGame('OCCUPIEDPRIVATELOTS', newPlayer('nativeRichese', 'Richese', 'richese'), true, expansions);
for (const [id, faction] of roster.slice(1)) joinGame(game, newPlayer(id, id, faction));
game = applyAction(game, 'nativeRichese', { type: 'homeworlds', enabled: true });
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
game = initializeHomeworldOccupationGameForAudit(game);
game = advanceOriginal(game, game => game.status === 'playing');
assert.equal(game.homeworldOccupationPreview, true);
assert.deepEqual(game.homeworldOccupationHistory!.qualifications, []);
assertDefenseInventory(game);
const original = privateLotPhysicalIds(game);
assert.equal(new Set(original).size, original.length);
// Controlled source-helper qualification: original native counters go to
// their own Tanks and one original Guild reserve becomes the sole visitor.
qualifyDefensePosition(game, 'homeworld:atreides');
privateLotPlayer(game, 'occupier').ally = 'ally';
privateLotPlayer(game, 'ally').ally = 'occupier';
// Original unused physical stock, not a manufactured face or cache alias.
const cardAt = game.deck.findIndex(card => card.effect !== 'karama' && card.id !== excludeCardId);
assert.ok(cardAt >= 0);
const card = game.deck.splice(cardAt, 1)[0];
privateLotPlayer(game, 'nativeRichese').hand.push(card);
const karama = stageBiddingKarama(game, 'rival');
assert.deepEqual(privateLotPhysicalIds(game), original);
assertDefenseInventory(game);
game = advanceOriginal(game, game => game.decision?.kind === 'richeseBlackMarket');
assert.equal(game.decision!.player, 'nativeRichese');
game = applyAction(game, 'nativeRichese', {
type: 'decision', event: game.richeseBidding!.event,
card: card.id, method: 'silent',
});
assert.equal(game.richeseAuction!.source, 'blackMarket');
assert.equal(game.richeseAuction!.owner, 'nativeRichese');
assert.equal(game.response?.kind, 'richeseBlackMarket');
assert.deepEqual(privateLotPhysicalIds(game), original);
return { game, card, karama, original };
}
/** Complete only the actual source response, without bot decisions/bids or
* phase-clock mutation. Every original eligible seat passes once. */
export function passPrivateLotWindow(game: Game, kind: 'richeseBlackMarket' | 'richeseAuction' | 'atreidesAuction'): Game {
assert.equal(game.response?.kind, kind);
for (let step = 0; game.response?.kind === kind && step < game.players.length; step++) {
const actor = game.players.find(player => !game.response!.passed.includes(player.id));
assert.ok(actor);
game = applyAction(game, actor.id, { type: 'passResponse' });
}
assert.notEqual(game.response?.kind, kind);
return game;
}
/** Pay the original funded Silent invoice: native Richese cannot self-bid;
* the Guild buyer pays its own real spice, every other eligible seat bids zero. */
export function sellPrivateBlackMarketLot(game: Game, amount = 2): Game {
assert.equal(game.response, null);
assert.equal(game.richeseAuction!.source, 'blackMarket');
assert.equal(game.richeseAuction!.method, 'silent');
const event = game.richeseAuction!.event;
const eligible = [...game.richeseAuction!.eligible];
assert.ok(eligible.includes('occupier'));
assert.ok(privateLotPlayer(game, 'occupier').spice >= amount);
for (const actor of eligible) game = applyAction(game, actor, {
type: 'richeseBid', event, amount: actor === 'occupier' ? amount : 0, allyPayment: 0,
});
assert.equal(game.decision?.kind, 'richeseDeclaration');
assert.equal(game.richeseBidding!.blackMarketSold, true);
assert.equal(game.currentAuctionSale, null);
assert.equal(game.richeseAuction, null);
return game;
}
/** Controlled conserved recycle position, NOT a claim that a game effect
* returns Black Market purchases. The next pool/lot is produced by the actual
* native declaration, original draw and original inspection handler. */
export function recyclePrivatePurchaseIntoOriginalNormalLot(game: Game, cardId: string): Game {
assert.equal(game.decision?.kind, 'richeseDeclaration');
const original = privateLotPhysicalIds(game);
const buyer = privateLotPlayer(game, 'occupier');
const at = buyer.hand.findIndex(card => card.id === cardId);
assert.ok(at >= 0);
game.deck.unshift(buyer.hand.splice(at, 1)[0]);
assert.deepEqual(privateLotPhysicalIds(game), original);
game = applyAction(game, 'nativeRichese', {
type: 'decision', event: game.richeseBidding!.event, position: 'last',
});
game = passPrivateLotWindow(game, 'richeseAuction');
assert.equal(game.response?.kind, 'atreidesAuction');
assert.equal(game.auction!.cards[game.auction!.index].id, cardId);
assert.equal(game.auction!.peekKnown, false);
assert.deepEqual(privateLotPhysicalIds(game), original);
return game;
}