90 lines
4.4 KiB
TypeScript
90 lines
4.4 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import {
|
|
applyAction, createGame, initializeHomeworldOccupationGameForAudit,
|
|
joinGame, newPlayer, viewGame, type Game,
|
|
} from '../game/engine';
|
|
import { FACTIONS, type FactionId } from '../game/catalog';
|
|
import { botActions } from '../game/bots';
|
|
import { assertDefenseInventory } from './fixture-homeworld-occupied-defenses';
|
|
|
|
export const biddingPlayer = (game: Game, id: string) => game.players.find(player => player.id === id)!;
|
|
|
|
/** Original lobby, selected expansion decks and full original setup pipeline.
|
|
* All later conserved positions are labelled controlled fixtures, not gameplay. */
|
|
export function freshBiddingGame(native: FactionId, ally: FactionId = 'emperor', advanced = true): Game {
|
|
const roster: [string, FactionId][] = [
|
|
['native', native], ['occupier', 'guild'], ['ally', ally],
|
|
['observer', 'beneGesserit'], ['fremen', 'fremen'],
|
|
];
|
|
const expansions = [...new Set(roster.map(([, faction]) =>
|
|
FACTIONS.find(card => card.id === faction)!.expansion))].filter(expansion => expansion !== 'base');
|
|
let game = createGame('OCCUPIEDBIDDING', newPlayer('native', native, native), advanced, expansions);
|
|
for (const [id, faction] of roster.slice(1)) joinGame(game, newPlayer(id, id, faction));
|
|
game = applyAction(game, 'native', { type: 'homeworlds', enabled: true });
|
|
for (const player of game.players) game = applyAction(game, player.id, { type: 'ready' });
|
|
game = initializeHomeworldOccupationGameForAudit(game);
|
|
for (let step = 0; game.status === 'setup' && step < 100; step++) {
|
|
let progressed = false;
|
|
for (const player of game.players) {
|
|
const view = viewGame(game, player.id);
|
|
view.players.find(seat => seat.id === player.id)!.bot = 'Easy';
|
|
const action = botActions(view)[0];
|
|
if (!action) continue;
|
|
game = applyAction(game, player.id, action);
|
|
progressed = true;
|
|
break;
|
|
}
|
|
assert.ok(progressed, 'original Bidding fixture setup has a legal next choice');
|
|
}
|
|
assert.equal(game.status, 'playing');
|
|
assert.equal(game.homeworldOccupationPreview, true);
|
|
assert.deepEqual(game.homeworldOccupationHistory!.qualifications, []);
|
|
assertDefenseInventory(game);
|
|
assert.equal(new Set(biddingPhysicalIds(game)).size, biddingPhysicalIds(game).length);
|
|
return game;
|
|
}
|
|
|
|
/** Live physical ownership only: original knowledge aliases are not cards. */
|
|
export function biddingPhysicalIds(game: Game): string[] {
|
|
return [
|
|
...game.players.flatMap(player => player.hand), ...game.deck, ...game.discard,
|
|
...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
|
|
...(game.ixSetupCards ?? []), ...(game.ixAuction?.cards ?? []),
|
|
...(game.ornithopter ? [game.ornithopter.card] : []),
|
|
...(game.auction?.cards.slice(game.auction.index + Number(game.currentAuctionSale?.origin === 'normal')) ?? []),
|
|
].map(card => card.id).sort();
|
|
}
|
|
|
|
/** Controlled original-card transfers to stage receiver capacity. No fake
|
|
* Treachery card, duplicate or destruction is used to manufacture a full hand. */
|
|
export function stageBiddingHandSize(game: Game, id: string, count: number) {
|
|
const original = biddingPhysicalIds(game);
|
|
const player = biddingPlayer(game, id);
|
|
while (player.hand.length > count) game.deck.push(player.hand.pop()!);
|
|
while (player.hand.length < count) {
|
|
const card = game.deck.shift() ?? game.discard.shift();
|
|
assert.ok(card, 'capacity fixture requires an original drawable card');
|
|
player.hand.push(card);
|
|
}
|
|
assert.equal(player.hand.length, count);
|
|
assert.deepEqual(biddingPhysicalIds(game), original, 'controlled hand transfer conserves all original physical IDs');
|
|
}
|
|
|
|
/** Labelled conserved transfer from original stock, including already-dealt
|
|
* cards. Random setup may deal every Karama; never fabricate a replacement. */
|
|
export function stageBiddingKarama(game: Game, id: string) {
|
|
const original = biddingPhysicalIds(game), receiver = biddingPlayer(game, id);
|
|
const held = receiver.hand.find(card => card.effect === 'karama');
|
|
if (held) return held;
|
|
const stock = [game.deck, game.discard, ...game.players.filter(player => player.id !== id).map(player => player.hand)];
|
|
for (const source of stock) {
|
|
const index = source.findIndex(card => card.effect === 'karama');
|
|
if (index < 0) continue;
|
|
const card = source.splice(index, 1)[0];
|
|
receiver.hand.push(card);
|
|
assert.deepEqual(biddingPhysicalIds(game), original);
|
|
return card;
|
|
}
|
|
throw new Error('Original unused fixture stock has no Karama.');
|
|
}
|