Files
Dune/tests/semuta-independent-native-retention.test.ts
T
Quintine 59579486a4
Verify and publish checkpoint container / container (push) Canceled after 0s
Connect Semuta with native allied retention
2026-10-29 03:17:07 +11:00

107 lines
6.2 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 { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import type { Card } from '../game/cards';
import { resolveSemutaMoritaniBattle, finishSemutaMoritaniWinner,
chooseSemutaMoritaniRetention, allowSemutaMoritaniResponse,
passSemutaMoritaniDiscard, semutaMoritaniInventory as inventory,
semutaMoritaniResources as resources, semutaMoritaniPlayer as seat,
type SemutaMoritaniFixture } from './fixture-semuta-moritani-retention';
const act = (game: Game, id: string, action: Action) => applyAction(game, id, action);
/** Actual public selected-deck setup with independent Ecaz cards. The later
* reciprocal alliance, held physical cards and conserved battle position are
* deliberately staged; battle plans, retention and Semuta use real actions. */
function fixture(): SemutaMoritaniFixture & { hero: string } {
let game = createGame('SEMUTANATIVERETENTION', newPlayer('r','Richese','richese'), false, ['choam','ecaz']);
joinGame(game,newPlayer('m','Moritani','moritani'));
joinGame(game,newPlayer('w','Emperor','emperor'));
joinGame(game,newPlayer('l','Guild','guild'));
game = act(game,'r',{type:'moduleOption',module:'ecazTreachery',enabled:true});
for (const player of game.players) game = act(game,player.id,{type:'ready'});
game = act(game,'r',{type:'start'});
assert.equal(game.status,'setup');
assert.equal(game.semutaRecruitsPreview,true);
for (let count=0;game.status === 'setup' && count<160;count++) {
let next: {actor:string;action:Action}|undefined;
for (const player of game.players) {
const view = viewGame(game,player.id);
view.players.find(candidate => candidate.id === player.id)!.bot = 'Medium';
const action = botActions(view)[0];
if (action) {next = {actor:player.id,action};break;}
}
assert.ok(next, 'Original setup must offer a legal decision.');
game = act(game,next.actor,next.action);
}
assert.equal(game.status,'playing');
assert.equal(game.ecazTreachery,true);
assert.equal(game.semutaPreview,undefined);
game.deck.push(...game.players.flatMap(player => player.hand));
for (const player of game.players) player.hand = [];
const hold = (owner:string,predicate:(card:Card)=>boolean) => {
for (const zone of [game.deck,game.discard,game.richeseCache!]) {
const index = zone.findIndex(predicate);
if (index >= 0) {const [card] = zone.splice(index,1);seat(game,owner).hand.push(card);return card.id;}
}
throw Error('The original physical card is unavailable.');
};
const semuta = hold('r',card=>card.id === SEMUTA_DRUG_ID);
const weapon = hold('l',card=>card.kind === 'projectile');
const defense = hold('l',card=>card.kind === 'snooper');
const unrelated = hold('l',card=>card.kind === 'worthless');
const hero = hold('l',card=>card.kind === 'hero');
const winnerWeapon = hold('w',card=>card.kind === 'projectile');
const winnerDefense = hold('w',card=>card.kind === 'shield');
for (const player of game.players) {
player.reserves += Object.values(player.forces).reduce((sum,count)=>sum+count,0);
player.forces = {};
assert.equal(player.tanks,0);assert.equal(player.reserves,20);
}
for (const id of ['w','l']) {seat(game,id).reserves-=5;seat(game,id).forces['arrakeen:10']=5;}
seat(game,'m').ally='l';seat(game,'l').ally='m';
Object.assign(game,{turn:2,phase:6,storm:18,active:'w',order:['w','l','m','r'],
ready:[],response:null,decision:null,phaseOpening:null,stormPending:null,movementRemaining:[]});
const winnerLeader = seat(game,'w').leaders.reduce((best,leader)=>leader.strength > best.strength ? leader : best).id;
return {game,semuta,weapon,defense,unrelated,hero,winnerWeapon,winnerDefense,winnerHero:null,karama:null,
cards:inventory(game),winnerLeader,loserLeader:hero,loserStrength:0};
}
for (const initialClaim of [false,true]) for (const finalClaim of [false,true]) {
if (initialClaim && finalClaim) continue; // Claiming spends the only Semuta.
void test(`Basic independent cards: Moritani retention ${initialClaim ? 'claims' : 'passes'} initial Hero and ${finalClaim ? 'claims' : 'passes'} later discard`,()=>{
const f = fixture();
let game = resolveSemutaMoritaniBattle(f);
const initial = game.pendingTreacheryDiscard;assert.ok(initial?.reaction);
assert.equal(initial.batch.cause,'battle:mandatory');
assert.deepEqual(initial.batch.entries.map(entry=>entry.card.id),[f.hero]);
assert.deepEqual(game.moritaniRetention?.eligible,[f.weapon,f.defense]);
const committed = structuredClone(resources(game));
if (!initialClaim) for (const difficulty of DIFFICULTIES) {
const view = viewGame(game,'r');view.players.find(player=>player.id === 'r')!.bot=difficulty;
assert.ok(botActions(view).some(action=>action.type === 'semutaCommit' || action.type === 'semutaPass'));
}
game = initialClaim ? act(game,'r',{type:'semutaCommit',event:initial.batch.event}) : passSemutaMoritaniDiscard(game);
assert.deepEqual(resources(game),committed);
assert.equal(seat(game,'r').hand.some(card=>card.id === f.hero),initialClaim);
game = finishSemutaMoritaniWinner(game);
assert.equal(game.decision?.kind,'moritaniRetention');
game = chooseSemutaMoritaniRetention(game,f.weapon);
game = allowSemutaMoritaniResponse(game);
const later = game.pendingTreacheryDiscard;assert.ok(later?.reaction);
assert.equal(later.batch.cause,'battle:moritani');
assert.deepEqual(later.batch.entries.map(entry=>entry.card.id),[f.defense]);
game = finalClaim ? act(game,'r',{type:'semutaCommit',event:later.batch.event}) : passSemutaMoritaniDiscard(game);
assert.equal(game.pendingTreacheryDiscard,null);
assert.equal(game.moritaniRetention,null);
assert.equal(seat(game,'l').hand.some(card=>card.id === f.weapon),true);
assert.equal(seat(game,'r').hand.some(card=>card.id === f.defense),finalClaim);
assert.equal(game.discard.some(card=>card.id === f.defense),!finalClaim);
assert.deepEqual(resources(game),committed);
assert.deepEqual(inventory(game),f.cards);
});
}