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