Files
Dune/tests/semuta-truth-order-public-modules.test.ts
T
Quintine 49f9ebdea0
Verify and publish checkpoint container / container (push) Successful in 11m13s
Enable Semuta in public Homeworld games
2026-10-29 09:36:52 +11:00

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);
}
});