Files
Dune/tests/fixture-richese-betrayal.ts
T

130 lines
5.9 KiB
TypeScript

import assert from 'node:assert/strict';
import {
applyAction, createGame, initializeRicheseBetrayalGameForAudit, joinGame,
newPlayer, viewGame, type Game,
} from '../game/engine';
import { botActions } from '../game/bots';
import { drawNexusCard, type NexusState } from '../game/nexus-cards';
import { orderNexusSpice } from './fixture-nexus-cards';
export const RICHESE_BETRAYAL_FIXTURE_FACTIONS = ['richese','choam','harkonnen','fremen'] as const;
export type RicheseBetrayalFixtureOptions = {
seatIds?: string[];
source?: 'cache' | 'blackMarket';
advanced?: boolean;
holderNexus?: 'richese' | 'choam';
allyPayment?: number;
allyIsSeller?: boolean;
allySpice?: number;
beforeFinalBid?: (g: Game) => void;
bonusCounter?: boolean;
};
export type RicheseBetrayalFixture = {
game: Game;
holder: string;
other: string;
target: string;
buyer: string;
card: string;
event: string;
ally: string | null;
counterCard: string | null;
};
function step(g: Game): Game {
for (const p of g.players) {
const v = viewGame(g,p.id);
v.players.find(seat => seat.id === p.id)!.bot = 'Easy';
const action = botActions(v)[0];
if (action) return applyAction(g,p.id,action);
}
throw new Error(`Richese Betrayal setup stalled at ${g.status}/${g.phase}/${g.decision?.kind}`);
}
function holdNexus(g: Game, owner: string, card: 'richese' | 'choam' | 'guild') {
const cards = g.nexusCards!.cards!;
assert.ok(cards.deck.includes(card));
const ordered: NexusState = {...cards,deck:[card,...cards.deck.filter(c => c !== card)]};
g.nexusCards!.cards = drawNexusCard(ordered,owner,g.players,() => 0);
}
/** Genuine setup and completed Spice phase, conserved physical Nexus draws and
* real declaration/bid actions. Arrange resources only before the auction, never
* rewrite a paused receipt. Seat IDs are fixed before setup for authenticated SQL. */
export function createRicheseBetrayalFixture(
kind: 'purchase' | 'sale', options: RicheseBetrayalFixtureOptions = {},
): RicheseBetrayalFixture {
const ids = options.seatIds ?? ['richese','holder','buyer','other'];
assert.equal(ids.length,4);
const [target,holder,harkonnen,other] = ids;
const source = options.source ?? (kind === 'purchase' ? 'cache' : 'blackMarket');
assert.ok(kind !== 'purchase' || source === 'cache');
const advanced = options.advanced ?? source === 'blackMarket';
assert.ok(source !== 'blackMarket' || advanced);
let g = createGame('RICHESEBETRAYAL',newPlayer(target,'Richese','richese'),advanced,['choam']);
for (let i = 1; i < ids.length; i++)
joinGame(g,newPlayer(ids[i],RICHESE_BETRAYAL_FIXTURE_FACTIONS[i],RICHESE_BETRAYAL_FIXTURE_FACTIONS[i]));
g.nexusCards = {cards:null,phase:null};
for (const p of g.players) g = applyAction(g,p.id,{type:'ready'});
g = initializeRicheseBetrayalGameForAudit(g);
for (let i = 0; g.status === 'setup' && i < 100; i++) g = step(g);
assert.equal(g.status,'playing');
for (const p of g.players) {g.deck.push(...p.hand.splice(0));p.spice = 20;}
for (let i = 0; g.phase === 0 && i < 100; i++) g = step(g);
assert.equal(g.phase,1);
orderNexusSpice(g,['land','land']);
for (let i = 0; g.phase === 1 && i < 100; i++) g = step(g);
assert.equal(g.phase,2);
assert.equal(g.nexusCards!.phase!.stage,'complete');
holdNexus(g,holder,options.holderNexus ?? 'richese');
const contribution = options.allyPayment ?? 0;
const buyer = kind === 'purchase' ? target : harkonnen;
let ally: string | null = null;
if (contribution) {
assert.equal(kind,'sale');
assert.ok(contribution > 0 && contribution <= 6);
ally = options.allyIsSeller ? target : other;
g.players.find(p => p.id === harkonnen)!.ally = ally;
g.players.find(p => p.id === ally)!.ally = harkonnen;
} else holdNexus(g,other,'guild');
const card = source === 'cache' ? g.richeseCache![0].id : (() => {
const index = g.deck.findIndex(c => c.kind === 'shield');
assert.ok(index >= 0);
const selected = g.deck.splice(index,1)[0];
g.players.find(p => p.id === target)!.hand.push(selected);
return selected.id;
})();
for (const id of g.order) g = applyAction(g,id,{type:'ready'});
while (g.decision?.kind === 'choamMarket')
g = applyAction(g,g.decision.player,{type:'decision',done:true});
assert.equal(g.phase,3);
if (source === 'cache') {
if (g.decision?.kind === 'richeseBlackMarket')
g = applyAction(g,target,{type:'decision',event:g.richeseBidding!.event,decline:true});
g = applyAction(g,target,{type:'decision',event:g.richeseBidding!.event,position:'first'});
}
assert.equal(g.decision?.kind,source === 'cache' ? 'richeseCache' : 'richeseBlackMarket');
g = applyAction(g,target,{type:'decision',event:g.richeseBidding!.event,card,method:'onceAround',direction:'clockwise'});
assert.equal(g.response,null);
let counterCard: string | null = null;
if (options.bonusCounter) {
const index = g.deck.findIndex(c => c.effect === 'karama');
assert.ok(index >= 0);
const counter = g.deck.splice(index,1)[0];
g.players.find(p => p.id === other)!.hand.push(counter);
counterCard = counter.id;
}
if (ally) {
if (options.allySpice !== undefined) g.players.find(p => p.id === ally)!.spice = options.allySpice;
g = applyAction(g,ally,{type:'pledgeAid',amount:contribution});
}
for (let i = 0; !g.pendingRicheseBetrayal && i < 10; i++) {
const lot = g.richeseAuction!;
assert.ok(lot && !lot.outcome && lot.active);
if (lot.acted.length === lot.order.length - 1) options.beforeFinalBid?.(g);
g = applyAction(g,lot.active,{type:'richeseBid',event:lot.event,
amount:lot.active === buyer ? 6 : kind === 'purchase' && !lot.bidder ? 1 : null,
...(lot.active === buyer ? {allyPayment:contribution} : {})});
}
assert.ok(g.pendingRicheseBetrayal);
assert.equal(g.pendingRicheseBetrayal.receipt.invoice.kind,kind);
return {game:g,holder,other,target,buyer,card,event:g.pendingRicheseBetrayal.receipt.event,ally,counterCard};
}