113 lines
6.5 KiB
TypeScript
113 lines
6.5 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { applyAction, createGame, joinGame, newPlayer, viewGame, type Game } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import { nextHarkonnenBetrayalNativeStep } from './fixture-nexus-harkonnen-betrayal';
|
|
import { nextRicheseStrongholdsNativeStep } from './fixture-richese-strongholds';
|
|
|
|
function inventory(game: Game): string[] {
|
|
return [...game.deck, ...game.discard, ...(game.richeseCache ?? []), ...(game.richeseRemoved ?? []),
|
|
...(game.auction?.cards.slice(game.auction.index) ?? []), ...(game.ixAuction?.cards ?? []),
|
|
...game.players.flatMap(player => player.hand)].map(card => card.id).sort();
|
|
}
|
|
|
|
function advance(game:Game, until:(state:Game)=>boolean):Game {
|
|
for(let i=0;i<1800;i++) {
|
|
if(until(game)) return game;
|
|
if(game.pendingGuildBetrayal) {
|
|
const pending=game.pendingGuildBetrayal;
|
|
const actor=pending.required.find(id=>!pending.passed.includes(id)); assert.ok(actor);
|
|
game=applyAction(game,actor,{type:'guildBetrayalPass',event:pending.invoice.event});
|
|
continue;
|
|
}
|
|
const next=(!game.response && !game.phaseOpening &&
|
|
(game.decision?.kind.startsWith('richese') || game.richeseAuction && !game.richeseAuction.outcome))
|
|
? nextRicheseStrongholdsNativeStep(game) : nextHarkonnenBetrayalNativeStep(game);
|
|
assert.ok(next);
|
|
try { game=applyAction(game,next.actor,next.action); }
|
|
catch(error) { throw Error(`Native step ${game.status}/${game.turn}/${game.phase}/${game.decision?.kind} ${JSON.stringify(next)}: ${String(error)}`); }
|
|
}
|
|
throw Error(`Native Ixian journey stalled at ${game.status}/${game.turn}/${game.phase}/${game.decision?.kind}`);
|
|
}
|
|
|
|
/** Public start and real setup, Nexus draw and Ixian Bidding attempt. Conserved
|
|
* unopened Spice and Nexus cards are reordered to reach this window; no hand,
|
|
* phase or pending event is injected. */
|
|
function fixture() {
|
|
let game = createGame('IXSELECTED', newPlayer('ix', 'Ixians', 'ixians'), false, ['ix', 'choam', 'ecaz']);
|
|
for (const [id, faction] of [['a', 'atreides'], ['r', 'richese'], ['g', 'guild']] as const)
|
|
joinGame(game, newPlayer(id, faction, faction));
|
|
game = applyAction(game, 'ix', {type:'moduleOption', module:'nexusCards', enabled:true});
|
|
for (const player of game.players) game = applyAction(game, player.id, {type:'ready'});
|
|
game = applyAction(game, 'ix', {type:'start'});
|
|
assert.equal(game.nexusIxianBetrayalPreview, true);
|
|
assert.ok(game.richeseCache?.length);
|
|
assert.ok(Object.values(game.nexusCards!.cards!.hands).every(card => card === null));
|
|
game=advance(game,state=>state.status==='playing');
|
|
const nx=game.nexusCards!.cards!;
|
|
const face=nx.deck.indexOf('ixians'); assert.ok(face>=0);
|
|
nx.deck.unshift(nx.deck.splice(face,1)[0]);
|
|
for(let pos=0;pos<2;pos++) {
|
|
const index=game.spiceDeck.findIndex((card,i)=>i>=pos&&'territory'in card); assert.ok(index>=pos);
|
|
game.spiceDeck.splice(pos,0,game.spiceDeck.splice(index,1)[0]);
|
|
}
|
|
game=advance(game,state=>state.turn===2&&state.phase===1&&!state.response&&!state.decision);
|
|
const worm=game.spiceDeck.findIndex(card=>'worm'in card&&!card.greatMaker&&!card.suppressed); assert.ok(worm>=0);
|
|
game.spiceDeck.unshift(game.spiceDeck.splice(worm,1)[0]);
|
|
game=advance(game,state=>state.nexus&&!state.spiceWindow&&!state.spiceResolution&&!state.response&&!state.decision);
|
|
game=applyAction(game,'ix',{type:'alliance',target:'r'});
|
|
game=applyAction(game,'r',{type:'alliance',target:'ix'});
|
|
game=advance(game,state=>state.nexusCards?.phase?.stage==='drawing');
|
|
assert.ok(game.nexusCards!.phase!.eligible.includes('a'));
|
|
game=applyAction(game,'a',{type:'nexusCardChoice',turn:game.turn,card:null,choice:'draw',ownRedraws:0});
|
|
game=advance(game,state=>state.response?.kind==='ixAuction'||!!state.pendingNexusIxianBetrayal);
|
|
const nativeCount=game.ixAuction?.count; assert.ok(nativeCount);
|
|
game=advance(game,state=>!!state.pendingNexusIxianBetrayal);
|
|
const event=game.pendingNexusIxianBetrayal!.source.event;
|
|
return {game,event,nativeCount,provider:'ix',holder:'a'};
|
|
}
|
|
|
|
for (const use of [false, true]) void test(
|
|
`Basic public mixed selected decks and Richese retain Ixian Nexus Betrayal ${use ? 'use' : 'pass'}`, () => {
|
|
const f = fixture(), before = f.game, original = inventory(before);
|
|
assert.equal(f.provider, 'ix'); assert.equal(f.holder, 'a');
|
|
assert.equal(before.nexusCards!.cards!.hands.a, 'ixians');
|
|
assert.equal(viewGame(before, 'a').nexusIxianBetrayalReaction?.canUse, true);
|
|
const botView = viewGame(before, 'a');
|
|
botView.players.find(player => player.id === 'a')!.bot = 'Easy';
|
|
const bot = botActions(botView).find(action => action.type === 'nexusIxianBetrayalUse' || action.type === 'nexusIxianBetrayalPass');
|
|
assert.ok(bot); assert.doesNotThrow(() => applyAction(before, 'a', bot));
|
|
assert.ok(before.richeseCache?.length);
|
|
let game = structuredClone(before);
|
|
if (use) game = applyAction(game, f.holder, {type:'nexusIxianBetrayalUse', event:f.event});
|
|
else while (game.pendingNexusIxianBetrayal) {
|
|
const pending = game.pendingNexusIxianBetrayal;
|
|
const actor = pending.required.find(id => !pending.passed.includes(id)); assert.ok(actor);
|
|
game = applyAction(game, actor, {type:'nexusIxianBetrayalPass', event:f.event});
|
|
}
|
|
assert.equal(game.pendingNexusIxianBetrayal, null);
|
|
assert.equal(game.nexusIxianBetrayalHistory!.at(-1)!.outcome, use ? 'use' : 'pass');
|
|
assert.equal(game.nexusIxianBetrayalCursor!.sequence, before.nexusIxianBetrayalCursor!.sequence + 1);
|
|
assert.equal(game.nexusCards!.cards!.hands.a, use ? null : 'ixians');
|
|
assert.equal(game.nexusCards!.cards!.discard.length, before.nexusCards!.cards!.discard.length + Number(use));
|
|
assert.equal(before.deck.length - game.deck.length, f.nativeCount + Number(!use));
|
|
if (use) {
|
|
assert.equal(game.ixAuction, null);
|
|
assert.equal(game.auction!.cards.length, f.nativeCount);
|
|
} else {
|
|
assert.equal(game.decision?.kind, 'ixAuction');
|
|
assert.equal(game.ixAuction!.cards.length, f.nativeCount + 1);
|
|
assert.equal(game.auction, null);
|
|
}
|
|
assert.deepEqual(inventory(game), original);
|
|
assert.deepEqual(game.richeseCache, before.richeseCache);
|
|
if (!use) {
|
|
const returned = game.ixAuction!.cards[0].id;
|
|
game = applyAction(game, 'ix', {type:'decision', card:returned, position:'top'});
|
|
assert.equal(game.ixAuction, null);
|
|
assert.equal(game.auction!.cards.length, f.nativeCount);
|
|
assert.equal(game.deck[0].id, returned);
|
|
assert.deepEqual(inventory(game), original);
|
|
}
|
|
});
|