141 lines
8.3 KiB
TypeScript
141 lines
8.3 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import test from 'node:test';
|
|
import { createGame, joinGame, newPlayer, viewGame } from '../game/engine';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import {
|
|
actSemutaIxAllyCost as act, advanceSemutaIxAllyCost as advance,
|
|
allowSemutaIxAllyCost as allow, finishSemutaIxAllyCost as finish,
|
|
passSemutaIxAllyCost as pass, claimSemutaIxAllyCost as claim,
|
|
semutaIxAllyCostPlayer as player, semutaIxAllyCostInventory as inventory,
|
|
} from './fixture-semuta-ix-ally-cost';
|
|
import { nextSemutaIxAllyCost } from './fixture-semuta-ix-ally-cost';
|
|
|
|
/** Public Basic R/C/I/F/G game. Only unopened initial deal and Spice Deck
|
|
* positions are selected. CHOAM really buys Semuta; Ixians and Fremen form
|
|
* a real turn-two Nexus alliance; Fremen pays for the current ordinary lot and
|
|
* requests the native Ixian replacement. No resources or pending state are
|
|
* installed. CHOAM holds both the initial Kull and purchased Semuta, so it can
|
|
* recover another player's cost but cannot recover its own Kull discard. */
|
|
function source() {
|
|
let game=createGame('KULLIXALLY',newPlayer('richese','Richese','richese'),false,['choam','ix']);
|
|
for (const id of ['choam','ixians','fremen','guild'] as const)
|
|
joinGame(game,newPlayer(id,id,id));
|
|
for (const seat of game.players) game=act(game,seat.id,{type:'ready'});
|
|
game=act(game,'richese',{type:'start'});
|
|
assert.equal(game.status,'setup'); assert.equal(game.kullPreview,true); assert.equal(game.semutaPreview,true);
|
|
const original=inventory(game);
|
|
const worthless=game.deck.find(card=>card.kind==='worthless' && card.id!=='ix-kull-wahad');
|
|
const kull=game.deck.find(card=>card.id==='ix-kull-wahad');
|
|
const shield=game.deck.find(card=>card.kind==='shield');
|
|
const karamas=game.deck.filter(card=>card.effect==='karama').slice(0,2);
|
|
assert.ok(worthless && kull && shield); assert.equal(karamas.length,2);
|
|
const selected=[worthless,kull,shield,...karamas];
|
|
selected.forEach((card,position)=>{
|
|
const index=game.deck.findIndex(row=>row.id===card.id); assert.ok(index>=position);
|
|
[game.deck[position],game.deck[index]]=[game.deck[index],game.deck[position]];
|
|
});
|
|
game=advance(game,state=>state.status==='playing');
|
|
assert.ok(player(game,'choam').hand.some(card=>card.id===kull.id));
|
|
assert.ok(player(game,'fremen').hand.some(card=>card.id===karamas[0].id));
|
|
assert.ok(player(game,'guild').hand.some(card=>card.id===karamas[1].id));
|
|
function selectSpice(faces:string[]) {
|
|
faces.forEach((face,position)=>{
|
|
const index=game.spiceDeck.findIndex((card,i)=>i>=position && (face==='worm'
|
|
? 'worm' in card && !card.suppressed && !card.thumper && !card.greatMaker
|
|
: 'territory' in card && card.territory===face && !card.discovery));
|
|
assert.ok(index>=position); const [card]=game.spiceDeck.splice(index,1); game.spiceDeck.splice(position,0,card);
|
|
});
|
|
}
|
|
selectSpice(['funeral_plain']);
|
|
game=advance(game,state=>player(state,'choam').hand.some(card=>card.id===SEMUTA_DRUG_ID));
|
|
game=advance(game,state=>state.turn===2 && state.phase===1 && !state.response && !state.phaseOpening &&
|
|
!state.decision && !state.spiceSequence && !state.spiceResolution && !state.spiceWindow && !state.nexus && state.ready.length===0);
|
|
selectSpice(['worm','the_great_flat']);
|
|
game=advance(game,state=>state.turn===2 && state.phase===1 && !!state.nexus &&
|
|
!state.spiceWindow && !state.spiceResolution && !state.response && !state.phaseOpening && !state.decision);
|
|
game=act(game,'ixians',{type:'alliance',target:'fremen'});
|
|
game=act(game,'fremen',{type:'alliance',target:'ixians'});
|
|
game=advance(game,state=>state.turn===2 && state.phase===3 && !!state.auction &&
|
|
state.auction.index===0 && state.auction.active==='fremen' && !state.response && !state.decision && !state.phaseOpening);
|
|
const beforeSpice=player(game,'fremen').spice;
|
|
game=act(game,'fremen',{type:'bid',amount:1,allyPayment:0});
|
|
for (let count=0;game.decision?.kind!=='ixAllyCard' && count<100;count++) {
|
|
if (!game.response && !game.phaseOpening && game.decision?.kind==='auctionPayment')
|
|
game=act(game,'fremen',{type:'decision',karama:false});
|
|
else { const next=nextSemutaIxAllyCost(game); assert.ok(next); game=act(game,next.actor,next.action); }
|
|
}
|
|
assert.equal(game.decision?.kind,'ixAllyCard');
|
|
assert.equal(player(game,'fremen').spice,beforeSpice-1);
|
|
assert.equal(game.currentAuctionSale?.origin,'normal');
|
|
const pending=game.pendingIxAlly; assert.ok(pending);
|
|
const delivered=pending.card, index=game.auction!.index;
|
|
assert.ok(player(game,'fremen').hand.some(card=>card.id===delivered));
|
|
game=act(game,'fremen',{type:'decision',accept:true});
|
|
assert.equal(game.response?.kind,'ixAllyCard');
|
|
assert.deepEqual(inventory(game),original);
|
|
return {game,original,kull:kull.id,counter:karamas[0].id,karama:karamas[1].id,
|
|
delivered,index,paidSpice:player(game,'fremen').spice, replacement:game.deck[0]?.id};
|
|
}
|
|
|
|
for (const outcome of ['decline','success','counter'] as const)
|
|
for (const claimed of [false,true]) {
|
|
// CHOAM cannot recover its own successful Kull discard with Semuta.
|
|
if (outcome==='success' && claimed) continue;
|
|
void test(`paid Ixian ally replacement: Kull ${outcome}, Semuta ${claimed?'claim':'pass'}`,()=>{
|
|
const f=source(); let game=f.game;
|
|
assert.ok(viewGame(game,'guild').responseControls?.cancelCards.includes(f.karama));
|
|
game=act(game,'guild',{type:'card',mode:'cancel',card:f.karama});
|
|
const offer=viewGame(game,'choam').kullReaction; assert.ok(offer);
|
|
assert.ok(offer.plays.some(row=>row.source==='printed' && row.card.id===f.kull));
|
|
game=outcome==='decline' ? act(game,'choam',{type:'kullDecision',event:offer.event,decline:true}) :
|
|
act(game,'choam',{type:'kullDecision',event:offer.event,source:'printed',card:f.kull});
|
|
if (outcome==='counter') {
|
|
assert.ok(viewGame(game,'fremen').responseControls?.cancelCards.includes(f.counter));
|
|
game=act(game,'fremen',{type:'card',mode:'cancel',card:f.counter});
|
|
game=pass(game);
|
|
}
|
|
if (outcome==='success') game=advance(game,state=>!!state.pendingTreacheryDiscard);
|
|
const spent=outcome==='success'?f.kull:f.karama;
|
|
assert.ok(game.pendingTreacheryDiscard?.batch.entries.some(entry=>entry.card.id===spent));
|
|
if (claimed) {
|
|
assert.equal(viewGame(game,'choam').semutaReaction?.canCommit,true);
|
|
game=claim(game,'choam');
|
|
} else game=pass(game);
|
|
assert.equal(game.pendingKull ?? null,null);
|
|
if (outcome==='success') {
|
|
if (game.response?.kind==='ixAllyCard') game=allow(game);
|
|
game=finish(game);
|
|
assert.ok(player(game,'guild').hand.some(card=>card.id===f.karama));
|
|
assert.ok(!player(game,'fremen').hand.some(card=>card.id===f.delivered));
|
|
assert.ok(player(game,'fremen').hand.some(card=>card.id===f.replacement));
|
|
assert.ok(game.discard.some(card=>card.id===f.delivered));
|
|
} else {
|
|
game=finish(game);
|
|
assert.ok(player(game,'fremen').hand.some(card=>card.id===f.delivered));
|
|
assert.ok(!player(game,'guild').hand.some(card=>card.id===f.karama));
|
|
}
|
|
assert.equal(player(game,'fremen').spice,f.paidSpice);
|
|
assert.equal(game.auction?.index,f.index+1);
|
|
assert.equal(player(game,'choam').hand.some(card=>card.id===f.karama),claimed);
|
|
assert.equal(player(game,'choam').hand.some(card=>card.id===SEMUTA_DRUG_ID),!claimed);
|
|
assert.deepEqual(inventory(game),f.original);
|
|
});
|
|
}
|
|
|
|
void test('Semuta may recover the distinct purchased-card discard after Kull preserves Ixian replacement',()=>{
|
|
const f=source(); let game=act(f.game,'guild',{type:'card',mode:'cancel',card:f.karama});
|
|
const offer=viewGame(game,'choam').kullReaction; assert.ok(offer);
|
|
game=act(game,'choam',{type:'kullDecision',event:offer.event,source:'printed',card:f.kull});
|
|
game=advance(game,state=>!!state.pendingTreacheryDiscard);
|
|
game=pass(game);
|
|
if (game.response?.kind==='ixAllyCard') game=allow(game);
|
|
assert.ok(game.pendingTreacheryDiscard?.batch.entries.some(entry=>entry.card.id===f.delivered));
|
|
assert.equal(viewGame(game,'choam').semutaReaction?.canCommit,true);
|
|
game=claim(game,'choam');
|
|
game=finish(game);
|
|
assert.ok(player(game,'choam').hand.some(card=>card.id===f.delivered));
|
|
assert.ok(player(game,'fremen').hand.some(card=>card.id===f.replacement));
|
|
assert.equal(player(game,'fremen').spice,f.paidSpice);
|
|
assert.deepEqual(inventory(game),f.original);
|
|
});
|