Files
Dune/tests/semuta-independent-ix-ally.test.ts
T
Quintine 3f4925e6e1
Verify and publish checkpoint container / container (push) Canceled after 0s
Offer Semuta after independent-card Ix ally replacement
2026-10-29 06:48:09 +11:00

134 lines
7.7 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
createSemutaIxAllyCostFixture as fixture, actSemutaIxAllyCost as act,
passSemutaIxAllyCost as pass, claimSemutaIxAllyCost as claim,
allowSemutaIxAllyCost as allow, finishSemutaIxAllyCost as finish,
semutaIxAllyCostPlayer as player, semutaIxAllyCostInventory as inventory,
type SemutaIxAllyCostFixture,
} from './fixture-semuta-ix-ally-cost';
/** The producer is a real ordinary paid purchase and native alliance use. */
function source(f: SemutaIxAllyCostFixture) {
assert.equal(f.independent,true); assert.equal(f.setup.semutaRecruitsPreview,true);
assert.equal(f.setup.semutaPreview,undefined); assert.equal(f.setup.ecazTreachery,true);
assert.equal(f.originalInventory.length,60);
assert.ok(f.lobby.players.every(seat => seat.ready));
assert.equal(f.pact.before.nexus,true);
assert.deepEqual(f.pact.steps.map(step => step.action.type),['alliance','alliance']);
assert.equal(player(f.game,f.owner).ally,f.buyer); assert.equal(player(f.game,f.buyer).ally,f.owner);
assert.equal(f.originalSale.origin,'normal'); assert.equal(f.originalSale.amount,1);
assert.equal(player(f.payment.before,f.buyer).spice - player(f.payment.after,f.buyer).spice,1);
assert.ok(f.purchase.steps.some(step => step.actor === f.buyer && step.action.type === 'bid'));
assert.equal(f.game.response?.kind,'ixAllyCard');
assert.ok(player(f.game,f.buyer).hand.some(card => card.id === f.deliveredCardId));
assert.deepEqual(inventory(f.game),f.originalInventory);
}
function open(f: SemutaIxAllyCostFixture, game = structuredClone(f.game)): Game {
game = allow(game);
const pending = game.pendingTreacheryDiscard; assert.ok(pending);
assert.equal(pending.continuation.kind,'ixAllyCard');
assert.equal(pending.reaction?.stage,'offer');
assert.deepEqual(pending.batch.entries.map(entry => ({id:entry.card.id,by:entry.discardedBy,public:entry.publicFace})),
[{id:f.deliveredCardId,by:f.buyer,public:false}]);
assert.equal(game.discard.filter(card => card.id === f.deliveredCardId).length,1);
assert.equal(player(game,f.buyer).hand.some(card => card.id === f.deliveredCardId),false);
assert.deepEqual(game.currentAuctionSale,f.originalSale);
assert.equal(viewGame(game,f.holder).semutaReaction?.canCommit,true);
for (const seat of game.players) {
const reaction = viewGame(game,seat.id).semutaReaction; assert.ok(reaction);
assert.deepEqual(reaction.candidates,[]);
assert.equal(reaction.canCommit,seat.id === f.holder);
}
return game;
}
for (const claiming of [false,true]) void test(
`independent-card native Ix ally discard offers Semuta ${claiming ? 'claim' : 'pass'} before the replacement draw`, () => {
const f = fixture({independent:true,independentLot:true}); source(f);
assert.equal(f.deliveredCardId,'ecaz-recruits'); assert.equal(f.selectedIndependentLot,f.deliveredCardId);
const offered = open(f), event = offered.pendingTreacheryDiscard!.batch.event;
assert.deepEqual(offered.deck,f.originalDeck);
const buyerSpice = player(offered,f.buyer).spice, buyerHand = player(offered,f.buyer).hand.length;
const after = claiming ? claim(offered,f.holder) : pass(offered);
const done = finish(after);
assert.equal(done.pendingTreacheryDiscard ?? null,null);
assert.equal(done.currentAuctionSale ?? null,null); assert.equal(done.pendingIxAlly ?? null,null);
assert.equal(done.auction?.index,f.originalAuction.index + 1);
assert.equal(player(done,f.buyer).spice,buyerSpice);
assert.equal(player(done,f.buyer).hand.length,buyerHand + 1);
assert.equal(player(done,f.buyer).hand.filter(card => card.id === f.replacementTop.id).length,1);
assert.deepEqual(done.deck,f.originalDeck.slice(1));
assert.equal(player(done,f.holder).hand.some(card => card.id === f.deliveredCardId),claiming);
assert.equal(done.discard.some(card => card.id === f.deliveredCardId),!claiming);
assert.equal(done.discard.some(card => card.id === f.semuta),claiming);
assert.deepEqual(inventory(done),f.originalInventory);
const before = structuredClone(done);
assert.throws(() => act(done,f.holder,{type:'semutaCommit',event}));
assert.deepEqual(done,before);
},
);
for (const claiming of [false,true]) void test(
`independent-card Ix ally ${claiming ? 'claim' : 'pass'} precedes a controlled empty-deck reshuffle`, () => {
const f = fixture({independent:true,independentLot:true}); source(f);
const arranged = structuredClone(f.game);
// Controlled conserved shortage after the real paid purchase: original
// undealt deck moves to discard, without touching the delivered lot.
arranged.discard.push(...arranged.deck); arranged.deck = [];
assert.deepEqual(inventory(arranged),f.originalInventory);
const offered = open(f,arranged), originalDiscard = offered.discard.map(card => card.id);
assert.equal(offered.deck.length,0); assert.ok(originalDiscard.includes(f.deliveredCardId));
const buyerHand = player(offered,f.buyer).hand.map(card => card.id);
const buyerSpice = player(offered,f.buyer).spice;
const after = claiming ? claim(offered,f.holder) : pass(offered);
const done = finish(after);
const replacement = player(done,f.buyer).hand.filter(card => !buyerHand.includes(card.id));
assert.equal(replacement.length,1);
const eligible = new Set(originalDiscard.filter(id => !claiming || id !== f.deliveredCardId));
if (claiming) eligible.add(f.semuta);
assert.ok(eligible.has(replacement[0].id));
assert.equal(player(done,f.buyer).spice,buyerSpice);
assert.equal(done.currentAuctionSale ?? null,null); assert.equal(done.pendingIxAlly ?? null,null);
assert.equal(done.auction?.index,f.originalAuction.index + 1);
assert.equal(player(done,f.holder).hand.some(card => card.id === f.deliveredCardId),claiming);
assert.deepEqual(inventory(done),f.originalInventory);
},
);
void test('all minimal bot profiles can legally answer the original independent-card Ix discard offer', () => {
const f = fixture({independent:true}); source(f);
const offered = open(f);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(offered,f.holder);
view.players.find(seat => seat.id === f.holder)!.bot = difficulty;
const action = botActions(view).find(item => item.type === 'semutaPass' || item.type === 'semutaCommit');
assert.ok(action);
const next = act(structuredClone(offered),f.holder,action);
assert.deepEqual(inventory(next),f.originalInventory);
}
});
void test('independent-card native Ix replacement cancellation spends its real Karama without a Semuta cost offer', () => {
const f = fixture({independent:true}); source(f);
let game = act(structuredClone(f.game),f.spender,f.action);
if (game.pendingKull) {
const reaction = viewGame(game,'choam').kullReaction; assert.ok(reaction);
game = act(game,'choam',{type:'kullDecision',event:reaction.event,decline:true});
}
assert.equal(game.pendingTreacheryDiscard ?? null,null);
assert.equal(game.response?.kind === 'ixAllyCard',false);
assert.equal(player(game,f.spender).hand.some(card => card.id === f.cost),false);
assert.equal(game.discard.filter(card => card.id === f.cost).length,1);
const done = finish(game);
assert.equal(done.currentAuctionSale ?? null,null); assert.equal(done.pendingIxAlly ?? null,null);
assert.equal(done.auction?.index,f.originalAuction.index + 1);
assert.deepEqual(done.deck,f.originalDeck);
assert.ok(player(done,f.buyer).hand.some(card => card.id === f.deliveredCardId));
assert.ok(player(done,f.holder).hand.some(card => card.id === f.semuta));
assert.deepEqual(inventory(done),f.originalInventory);
});