80 lines
5.6 KiB
TypeScript
80 lines
5.6 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import {applyAction, createGame, joinGame, newPlayer, viewGame, type Action, type Game} from '../game/engine';
|
|
import {advanceSemutaNexusTrade} from './fixture-semuta-nexus-trade';
|
|
import {beforeIxSpiceDraw, orderIxSpiceCards} from './fixture-ix-spice-cards';
|
|
import {SEMUTA_DRUG_ID} from '../game/semuta-drug';
|
|
|
|
const act=(game:Game, actor:string, action:Action)=>applyAction(game,actor,action);
|
|
const seat=(game:Game,id:string)=>{const player=game.players.find(row=>row.id===id);assert.ok(player);return player;};
|
|
const clean=(game:Game)=>!game.response&&!game.decision&&!game.phaseOpening&&!game.truthtrance&&!game.pendingTreacheryDiscard;
|
|
function inventory(game:Game):string[] {
|
|
const cards=[...game.deck,...game.discard,...(game.richeseCache??[]),...(game.richeseRemoved??[]),
|
|
...(game.auction?.cards.slice(game.auction.index+Number(game.currentAuctionSale?.origin==='normal'))??[]),
|
|
...(game.ixAuction?.cards??[]),...game.players.flatMap(player=>player.hand)].map(card=>card.id).sort();
|
|
assert.equal(new Set(cards).size,cards.length);return cards;
|
|
}
|
|
function acquire(game:Game, owner:string, predicate:(card:Game['deck'][number])=>boolean) {
|
|
const player=seat(game,owner), held=player.hand.find(predicate);if(held)return held;
|
|
const sources=[game.deck,game.discard,game.richeseCache!,...game.players.filter(row=>row.id!==owner).map(row=>row.hand)];
|
|
const source=sources.find(zone=>zone.some(predicate));assert.ok(source);
|
|
const index=source.findIndex(predicate),card=source[index], displaced=player.hand.pop();
|
|
source.splice(index,1,...(displaced?[displaced]:[]));player.hand.push(card);return card;
|
|
}
|
|
/** Actual public expansion setup, second-turn worm and mutual alliance.
|
|
* Unopened Spice order, board forces, wallets and physical cards are staged. */
|
|
function fixture() {
|
|
let game=createGame('BASICNATIVEGIFT',newPlayer('c','CHOAM','choam'),false,['choam','ix']);
|
|
for(const [id,faction] of [['r','richese'],['e','emperor'],['h','harkonnen'],['a','atreides']] as const)
|
|
joinGame(game,newPlayer(id,faction,faction));
|
|
for(const player of game.players) game=act(game,player.id,{type:'ready'});
|
|
game=act(game,'c',{type:'start'});assert.equal(game.semutaPreview,true);assert.equal(game.kullPreview,true);
|
|
game=advanceSemutaNexusTrade(game,state=>state.status==='playing');
|
|
const original=inventory(game);
|
|
for(const player of game.players) {player.reserves+=Object.values(player.forces).reduce((a,b)=>a+b,0);player.forces={};}
|
|
game=advanceSemutaNexusTrade(game,beforeIxSpiceDraw);orderIxSpiceCards(game,['land','land']);
|
|
game=advanceSemutaNexusTrade(game,state=>state.turn===2&&beforeIxSpiceDraw(state));
|
|
orderIxSpiceCards(game,['worm','land','land']);
|
|
game=advanceSemutaNexusTrade(game,state=>!!state.nexus&&!state.spiceWindow&&clean(state));
|
|
game=act(game,'e',{type:'alliance',target:'h'});game=act(game,'h',{type:'alliance',target:'e'});
|
|
game=advanceSemutaNexusTrade(game,state=>state.phase===3&&!!state.auction&&!state.richeseAuction&&clean(state));
|
|
while(game.auction!.active!=='h')game=act(game,game.auction!.active,{type:'passBid'});
|
|
seat(game,'e').spice=8;seat(game,'h').spice=4;
|
|
const counter=acquire(game,'a',card=>card.effect==='karama');
|
|
const semuta=acquire(game,'r',card=>card.id===SEMUTA_DRUG_ID);
|
|
assert.deepEqual(inventory(game),original);
|
|
game=act(game,'e',{type:'pledgeAid',amount:2});
|
|
const lot=game.auction!.cards[game.auction!.index].id;
|
|
game=act(game,'h',{type:'bid',amount:6,allyPayment:2});
|
|
for(let n=0;game.auction&&!game.response&&!game.decision&&n<15;n++)game=act(game,game.auction.active,{type:'passBid'});
|
|
assert.equal(game.decision?.kind,'auctionPayment');
|
|
const offer=viewGame(game,'e').emperorAuctionGift;assert.ok(offer);
|
|
return {game,original,counter:counter.id,semuta:semuta.id,lot,event:offer.event};
|
|
}
|
|
function passResponse(game:Game) {
|
|
for(let n=0;game.response&&n<20;n++) {const p=game.players.find(row=>!game.response!.passed.includes(row.id));assert.ok(p);game=act(game,p.id,{type:'passResponse'});}
|
|
return game;
|
|
}
|
|
for(const mode of ['allow','cancel-claim','cancel-pass'] as const) void test(`Basic native CHOAM/Richese ally auction gift ${mode} preserves payment`,()=>{
|
|
const cancel=mode!=='allow';
|
|
const f=fixture();let game=act(f.game,'e',{type:'emperorGift',amount:2});
|
|
if(cancel) {
|
|
game=act(game,'a',{type:'card',mode:'cancel',card:f.counter});
|
|
assert.ok(game.pendingKull,'The original printed Kull may interrupt this Karama');
|
|
game=act(game,'c',{type:'kullDecision',event:game.pendingKull.event,decline:true});
|
|
assert.equal(game.pendingTreacheryDiscard?.reaction?.stage,'offer','The used Karama is offered to Semuta');
|
|
const event=game.pendingTreacheryDiscard!.batch.event;
|
|
if(mode==='cancel-claim')game=act(game,'r',{type:'semutaCommit',event});
|
|
else for(const player of game.players)game=act(game,player.id,{type:'semutaPass',event});
|
|
assert.equal(seat(game,'r').hand.some(card=>card.id===f.counter),mode==='cancel-claim');
|
|
assert.equal(game.discard.some(card=>card.id===f.semuta),mode==='cancel-claim');
|
|
} else game=passResponse(game);
|
|
assert.equal(game.response,null);assert.equal(viewGame(game,'e').emperorAuctionGift!.event,f.event);
|
|
assert.equal(seat(game,'h').spice,cancel?4:6);assert.equal(seat(game,'e').spice,cancel?6:4);
|
|
game=act(game,'e',{type:'emperorAuctionGiftContinue',event:f.event});
|
|
if(game.decision?.kind==='auctionPayment')game=act(game,'h',{type:'decision',karama:false});
|
|
game=passResponse(game);
|
|
assert.ok(seat(game,'h').hand.some(card=>card.id===f.lot));
|
|
assert.deepEqual(inventory(game),f.original);
|
|
});
|