107 lines
6.2 KiB
TypeScript
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);
|
|
});
|
|
}
|