134 lines
7.7 KiB
TypeScript
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);
|
|
});
|