113 lines
6.7 KiB
TypeScript
113 lines
6.7 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 { DIFFICULTIES } from '../game/bot-profiles';
|
|
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
|
|
import { nextRicheseSkillsTechNativeStep } from './fixture-richese-skills-tech';
|
|
import { semutaSkillsInventory } from './fixture-semuta-skills';
|
|
import { finishOriginalTruthQuestions, finishTruthOrder } from './fixture-sapho-truth-order';
|
|
|
|
const SAPHO = 'richese-juice-of-sapho';
|
|
const copy = (game: Game): Game => JSON.parse(JSON.stringify(game));
|
|
const seat = (game: Game, id: string) => {
|
|
const player = game.players.find(row => row.id === id); assert.ok(player); return player;
|
|
};
|
|
function step(game: Game) {
|
|
if (!game.phaseOpening && !game.response && !game.decision && game.phase === 2 && game.charityOrder) {
|
|
const actor = game.charityOrder.remaining[0]; assert.ok(actor);
|
|
return {actor, action:{type:'ready'}};
|
|
}
|
|
return nextRicheseSkillsTechNativeStep(game);
|
|
}
|
|
/** Public CHOAM/Richese lobby, original setup and native Movement. Only the
|
|
* order of two unopened ordinary Spice Cards and actual card locations are
|
|
* staged. Every held card returns to its original deck before selecting four
|
|
* existing physical cards, preserving the complete inventory. */
|
|
function fixture(module: 'leaderSkills' | 'discoveries', advanced: boolean) {
|
|
let game = createGame('SEMUTATRUTHPUBLIC', newPlayer('r','Richese','richese'), advanced, ['choam']);
|
|
joinGame(game, newPlayer('a','Atreides','atreides'));
|
|
joinGame(game, newPlayer('e','Emperor','emperor'));
|
|
game = applyAction(game, game.host, {type:'moduleOption',module,enabled:true});
|
|
for (const p of game.players) game = applyAction(game,p.id,{type:'ready'});
|
|
game = applyAction(game,game.host,{type:'start',...(advanced ? {advancedPreview:true} : {})});
|
|
assert.equal(game.semutaPreview,true);
|
|
for (let n=0; game.status==='setup' && n<300; n++) {
|
|
if (game.setupStage==='leaderSkills') {
|
|
const actor=Object.keys(game.leaderSkills!.offers)[0];
|
|
const offer=game.leaderSkills!.offers[actor], own=viewGame(game,actor).leaderSkills!;
|
|
const skill=offer.cards.find(id=>!own.unavailableSkills?.[id]);
|
|
const leader=own.eligibleLeaders[0]; assert.ok(skill && leader);
|
|
game=applyAction(game,actor,{type:'leaderSkill',event:offer.event,skill,leader:leader.id});
|
|
} else { const next=step(game); game=applyAction(game,next.actor,next.action); }
|
|
}
|
|
assert.equal(game.status,'playing');
|
|
for (const position of [0,1]) {
|
|
const index=game.spiceDeck.findIndex((card,i)=>i>=position && 'territory' in card && !card.discovery);
|
|
assert.ok(index>=position); game.spiceDeck.splice(position,0,game.spiceDeck.splice(index,1)[0]);
|
|
}
|
|
for (let n=0;n<1600 && !(game.phase===5 && !game.phaseOpening && !game.response && !game.decision);n++) {
|
|
assert.equal(game.turn,1); const next=step(game); game=applyAction(game,next.actor,next.action);
|
|
}
|
|
assert.equal(game.phase,5); assert.ok(!game.phaseOpening && !game.response && !game.decision);
|
|
const inventory=semutaSkillsInventory(game);
|
|
for (const p of game.players) game.deck.push(...p.hand.splice(0));
|
|
const give=(id:string,identity:string) => {
|
|
const zones=[game.deck,game.discard,game.richeseCache!,...game.players.map(p=>p.hand)];
|
|
const source=zones.find(zone=>zone.some(card=>card.id===identity || card.effect===identity));
|
|
assert.ok(source,`Missing original ${identity}`);
|
|
const index=source.findIndex(card=>card.id===identity || card.effect===identity);
|
|
const [card]=source.splice(index,1); seat(game,id).hand.push(card); return card.id;
|
|
};
|
|
const others=game.order.filter(id=>id==='a' || id==='e'); assert.equal(others.length,2);
|
|
const holder=others[1], other=others[0];
|
|
give('r',SEMUTA_DRUG_ID); give(holder,SAPHO);
|
|
const truth=give(holder,'truthtrance'), otherTruth=give(other,'truthtrance');
|
|
assert.deepEqual(semutaSkillsInventory(game),inventory);
|
|
const parent={turn:game.turn,phase:game.phase,active:game.active,order:[...game.order],
|
|
movementRemaining:structuredClone(game.movementRemaining)};
|
|
for (const [id,card] of [[holder,truth],[other,otherTruth]])
|
|
game=applyAction(game,id,{type:'card',card});
|
|
while (game.truthtrance?.stage==='priority') {
|
|
const actor=game.players.find(p=>!game.truthtrance!.passed.includes(p.id)); assert.ok(actor);
|
|
game=applyAction(game,actor.id,{type:'truthPass'});
|
|
}
|
|
assert.equal(game.truthtrance?.stage,'order');
|
|
return {game,holder,other,truth,otherTruth,inventory,parent};
|
|
}
|
|
|
|
for (const selectedModule of ['leaderSkills','discoveries'] as const) for (const advanced of [false,true])
|
|
void test(`${advanced?'Advanced':'Basic'} public ${selectedModule} preserves Sapho Truth-order through Semuta`,()=>{
|
|
const f=fixture(selectedModule,advanced);
|
|
const frame=f.game.truthtrance!.order!;
|
|
assert.ok(viewGame(f.game,f.holder).saphoTruthOrderChoices.some(choice=>choice.card===SAPHO && choice.mode==='first'));
|
|
const action={type:'saphoTruthOrder',event:frame.event,revision:frame.revision,card:SAPHO,mode:'first'};
|
|
const pending=applyAction(copy(f.game),f.holder,action);
|
|
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind,'saphoTruthOrderDiscard');
|
|
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage,'offer');
|
|
assert.equal(pending.truthtrance,null);
|
|
const event=pending.pendingTreacheryDiscard!.batch.event;
|
|
for (const level of DIFFICULTIES) {
|
|
const own=viewGame(pending,'r'); own.players.find(p=>p.id==='r')!.bot=level;
|
|
const bot=botActions(own).find(row=>row.type==='semutaCommit' || row.type==='semutaPass');
|
|
assert.ok(bot,`${level} can answer this neutral reaction`);
|
|
assert.doesNotThrow(()=>applyAction(copy(pending),'r',bot));
|
|
}
|
|
for (const claim of [false,true]) {
|
|
let game=copy(pending);
|
|
if (claim) game=applyAction(game,'r',{type:'semutaCommit',event});
|
|
else for (const p of game.players) game=applyAction(game,p.id,{type:'semutaPass',event});
|
|
assert.equal(game.pendingTreacheryDiscard,null);
|
|
assert.equal(seat(game,'r').hand.some(card=>card.id===SAPHO),claim);
|
|
assert.equal(seat(game,'r').hand.some(card=>card.id===SEMUTA_DRUG_ID),!claim);
|
|
game=finishTruthOrder(game);
|
|
assert.equal(game.truthtrance!.queue[0].player,f.holder);
|
|
assert.deepEqual(game.order,f.parent.order); assert.equal(game.turn,f.parent.turn);
|
|
assert.equal(game.phase,f.parent.phase); assert.equal(game.active,f.parent.active);
|
|
assert.deepEqual(game.movementRemaining,f.parent.movementRemaining);
|
|
game=finishOriginalTruthQuestions(game);
|
|
assert.equal(game.truthtrance,null);
|
|
assert.deepEqual(semutaSkillsInventory(game),f.inventory);
|
|
}
|
|
});
|