90 lines
6.0 KiB
TypeScript
90 lines
6.0 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {applyAction,createGame,newPlayer,viewGame,type Game} from '../game/engine';
|
|
import {baseDeck} from '../game/cards';
|
|
import {FACTIONS,type FactionId} from '../game/catalog';
|
|
import {richeseCards} from '../game/richese-cards';
|
|
function fixture(faction: FactionId = 'richese', advanced = false, phase = 4) {
|
|
const g = createGame('DISTRANSENGINE',newPlayer('d','Donor',faction),advanced,['choam']);
|
|
const others=FACTIONS.filter(f=>f.id!==faction).slice(0,2);
|
|
g.players.push(newPlayer('t','Target',others[0].id),newPlayer('o','Observer',others[1].id));
|
|
g.status='playing';g.phase=phase;g.turn=2;g.order=['d','t','o'];g.active='t';
|
|
g.deck=baseDeck();g.richeseCache=richeseCards();
|
|
for(const p of g.players){p.hand=[];p.spice=10;p.forces={};p.ally=null;}
|
|
const distrans=g.richeseCache.find(c=>c.effect==='distrans')!;
|
|
g.richeseCache=g.richeseCache.filter(c=>c.id!==distrans.id);g.players[0].hand.push(distrans);
|
|
const card=g.deck.splice(g.deck.findIndex(c=>c.name==='Shield'),1)[0];g.players[0].hand.push(card);
|
|
return {g,card};
|
|
}
|
|
const play=(g:Game,card:string)=>applyAction(g,'d',{type:'card',card:'richese-distrans',target:'t',give:card});
|
|
const reload=(g:Game):Game=>JSON.parse(JSON.stringify(g));
|
|
void test('all twelve faction holders can transfer to a nonally across nine phases in both modes with exact disposal and no purchase effects',()=>{
|
|
for(const f of FACTIONS)for(const advanced of [false,true])for(let phase=0;phase<9;phase++){
|
|
const {g,card}=fixture(f.id,advanced,phase),before=structuredClone(g),done=play(g,card.id);
|
|
assert.deepEqual(g,before);assert.deepEqual(done.players[0].hand,[]);assert.deepEqual(done.players[1].hand,[card]);
|
|
assert.deepEqual(done.discard.map(c=>c.id),['richese-distrans']);assert.deepEqual(done.deck,g.deck);assert.deepEqual(done.richeseCache,g.richeseCache);
|
|
assert.deepEqual(done.players.map(p=>p.spice),[10,10,10]);assert.equal(done.phase,phase);assert.equal(done.response,null);
|
|
assert.throws(()=>play(reload(done),card.id));
|
|
}
|
|
});
|
|
void test('projection exposes choices only to the holder and public history reveals Distrans but not the transferred identity',()=>{
|
|
const {g,card}=fixture();
|
|
assert.ok(viewGame(g,'d').distrans!.choices.find(c=>c.recipient==='t')!.cards.some(c=>c.id===card.id));
|
|
assert.equal(viewGame(g,'t').distrans,null);assert.equal(viewGame(g,'o').distrans,null);
|
|
const done=play(g,card.id);
|
|
assert.equal(viewGame(done,'d').distrans,null);
|
|
assert.equal(viewGame(done,'t').players.find(p=>p.id==='t')!.hand![0].id,card.id);
|
|
assert.equal(JSON.stringify(viewGame(done,'o')).includes(card.id),false);
|
|
assert.match(done.log.at(-1)!.text,/Distrans/);assert.equal(done.log.some(l=>l.text.includes(card.name)),false);
|
|
});
|
|
void test('ordinary lots permit Distrans between completed bids and passes',()=>{
|
|
const {g,card}=fixture('richese',false,3);
|
|
assert.deepEqual(play(g,card.id).players[1].hand,[card]);
|
|
g.auction={cards:[g.deck.shift()!],index:0,bid:0,bidder:null,active:'t',passed:[],opener:0};
|
|
assert.equal(viewGame(g,'d').distrans!.blocked,null);
|
|
const transferred=play(g,card.id);
|
|
assert.deepEqual(transferred.auction,g.auction);
|
|
assert.deepEqual(transferred.players[1].hand,[card]);
|
|
for(const progress of ['bid','pass'] as const){
|
|
const during=reload(g);
|
|
if(progress==='bid'){during.auction!.bid=1;during.auction!.bidder='t';}
|
|
else during.auction!.passed=['o'];
|
|
const before=structuredClone(during);
|
|
assert.equal(viewGame(during,'d').distrans!.blocked,null);
|
|
const after=play(during,card.id);
|
|
assert.deepEqual(during,before);assert.deepEqual(after.auction,before.auction);
|
|
}
|
|
});
|
|
void test('an already-paid purchase response allows Distrans without changing its completed sale or creating a second response',()=>{
|
|
const {g,card}=fixture('richese',true,3);
|
|
const k=g.deck.splice(g.deck.findIndex(c=>c.effect==='karama'),1)[0];g.players[0].hand.push(k);
|
|
const purchased=g.deck.shift()!;g.players[1].hand.push(purchased);
|
|
g.auction={cards:[purchased],index:0,bid:2,bidder:'t',active:'t',passed:[],opener:0};
|
|
g.currentAuctionSale={winner:'t',amount:2,free:false,origin:'normal',seller:null};
|
|
g.response={kind:'emperorIncome',owner:'o',passed:[]};
|
|
const done=play(g,card.id);
|
|
assert.deepEqual(done.response,g.response);assert.deepEqual(done.currentAuctionSale,g.currentAuctionSale);assert.deepEqual(done.auction,g.auction);
|
|
assert.equal(done.players[2].spice,10);assert.deepEqual(done.players[1].hand,[purchased,card]);
|
|
});
|
|
void test('phase opening and an interrupted response retain ownership and passes; a new recipient Karama may answer the original power',()=>{
|
|
const {g,card}=fixture('richese',true,5);
|
|
// This suspended income belongs to the Guild during Shipment & Movement.
|
|
g.players[2].faction='guild';
|
|
const k=g.deck.splice(g.deck.findIndex(c=>c.effect==='karama'),1)[0];g.players[0].hand.push(k);
|
|
g.response={kind:'guildIncome',owner:'o',amount:3,passed:['d']};g.phaseOpening={passed:['d'],initialize:false};
|
|
const done=play(g,k.id);
|
|
assert.deepEqual(done.response,g.response);assert.deepEqual(done.phaseOpening,g.phaseOpening);assert.deepEqual(done.players[0].hand,[card]);
|
|
let next=applyAction(reload(done),'t',{type:'ready'});next=applyAction(next,'o',{type:'ready'});
|
|
next=applyAction(next,'t',{type:'card',card:k.id,mode:'cancel'});
|
|
assert.equal(next.response,null);assert.equal(next.players[2].spice,10);
|
|
assert.deepEqual(next.discard.map(c=>c.id),['richese-distrans',k.id]);
|
|
});
|
|
void test('full recipient, self-transfer and forged activation reject atomically and no mode can bypass validation',()=>{
|
|
for(const change of ['full','self','forged','mode'] as const){
|
|
const {g,card}=fixture();if(change==='full')g.players[1].hand=g.deck.splice(0,4);if(change==='forged')g.players[0].hand[0].name='Forged';
|
|
const before=structuredClone(g);
|
|
assert.throws(()=>applyAction(g,'d',{type:'card',card:'richese-distrans',target:'t',give:change==='self'?'richese-distrans':card.id,...(change==='mode'?{mode:'special'}:{})}));
|
|
assert.deepEqual(g,before);
|
|
}
|
|
});
|