Files
Dune/tests/semuta-richese-gift-karama-engine.test.ts

164 lines
11 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
createSemutaRicheseGiftKaramaFixture as fixture,
actSemutaRicheseGiftKarama as act, passSemutaRicheseGiftKarama as pass,
claimSemutaRicheseGiftKarama as claim, finishSemutaRicheseGiftKarama as finish,
advanceSemutaRicheseGiftKarama as advance, semutaRicheseGiftKaramaClean as clean,
semutaRicheseGiftKaramaInventory as inventory, semutaRicheseGiftKaramaPlayer as player,
type SemutaRicheseGiftKaramaFixture,
} from './fixture-semuta-richese-gift-karama';
const resources = (game: Game) => ({ players: game.players.map(p => ({ id: p.id, spice: p.spice,
reserves: p.reserves, tanks: p.tanks, forces: p.forces, elites: p.elites, moved: p.moved, shipped: p.shipped,
ally: p.ally, revived: p.revived })), order: game.order, active: game.active, ready: game.ready,
turn: game.turn, phase: game.phase, movementRemaining: game.movementRemaining });
function reject(game: Game, actor: string, action: Action) {
const before = structuredClone(game); assert.throws(() => applyAction(game, actor, action)); assert.deepEqual(game, before);
}
function nativeSource(f: SemutaRicheseGiftKaramaFixture) {
assert.equal(f.game.advanced, f.advanced); assert.equal(f.setup.advanced, f.advanced);
assert.equal(f.setup.status, 'setup'); assert.equal(f.setup.semutaPreview, true);
assert.ok(f.beforeStartingDeal.players.every(p => p.hand.length === 0));
assert.ok(player(f.afterSetup, f.spender).hand.some(card => card.id === f.cost));
assert.ok(player(f.afterSetup, f.observer).hand.some(card => card.id === f.parentCounter));
assert.notEqual(f.cost, f.parentCounter);
for (const acquisition of [f.semutaPurchase, f.giftPurchase]) {
assert.equal(acquisition.price, 1);
const bids = acquisition.steps.filter(step => step.action.type === 'richeseBid');
assert.equal(bids.length, f.game.players.length);
assert.equal(bids.filter(step => step.actor === f.donor && step.action.amount === 1).length, 1);
assert.ok(bids.filter(step => step.actor !== f.donor).every(step => step.action.amount === 0));
assert.equal(player(acquisition.before, f.donor).spice - player(acquisition.after, f.donor).spice, 1);
assert.ok(player(acquisition.after, f.donor).hand.some(card => card.id === acquisition.card));
}
assert.equal(f.alliance.before.nexus, true);
assert.deepEqual(f.alliance.steps, [
{ actor: f.donor, action: { type: 'alliance', target: f.recipient } },
{ actor: f.recipient, action: { type: 'alliance', target: f.donor } },
]);
assert.equal(player(f.game, f.donor).ally, f.recipient); assert.equal(player(f.game, f.recipient).ally, f.donor);
assert.equal(f.game.response?.kind, 'richeseGift'); assert.equal(f.game.response?.recipient, f.recipient);
assert.equal(f.game.pendingRicheseGift?.intent.cardId, f.gift);
assert.deepEqual(f.game.pendingRicheseGift?.resume, f.originalParent);
assert.equal(f.game.discard.some(card => card.id === f.cost), false);
assert.ok(player(f.game, f.holder).hand.some(card => card.id === f.semuta));
assert.ok(player(f.game, f.holder).hand.some(card => card.id === f.gift));
assert.equal(player(f.game, f.recipient).hand.some(card => card.id === f.gift), false);
assert.equal(f.sourceActionCount, f.actions.length); assert.deepEqual(inventory(f.game), f.originalInventory);
}
function open(f: SemutaRicheseGiftKaramaFixture) {
const offered = act(structuredClone(f.game), f.spender, f.action), pending = offered.pendingTreacheryDiscard;
assert.ok(pending); assert.equal(pending.continuation.kind, 'karamaRicheseGiftDiscard');
assert.deepEqual(pending.batch.entries.map(entry => entry.card.id), [f.cost]);
assert.deepEqual(pending.batch.entries.map(entry => entry.discardedBy), [f.spender]);
assert.equal(offered.discard.filter(card => card.id === f.cost).length, 1);
assert.equal(player(offered, f.spender).hand.some(card => card.id === f.cost), false);
assert.deepEqual(resources(offered), resources(f.game)); assert.deepEqual(inventory(offered), f.originalInventory);
assert.ok(offered.pendingRicheseGift, 'Original prevention waits for the used physical cost reaction');
assert.deepEqual(offered.pendingRicheseGift.intent, f.game.pendingRicheseGift!.intent);
assert.deepEqual(offered.pendingRicheseGift.resume, f.originalParent);
assert.equal(viewGame(offered, f.holder).semutaReaction?.canCommit, true);
assert.equal(offered.richeseGiftBlocked?.cards.includes(f.gift) ?? false, false);
return offered;
}
function prevented(game: Game, f: SemutaRicheseGiftKaramaFixture, claimed: boolean) {
const done = finish(game);
assert.ok(!done.pendingTreacheryDiscard); assert.ok(!done.pendingRicheseGift);
assert.deepEqual(resources(done), resources(f.game)); assert.deepEqual(inventory(done), f.originalInventory);
assert.equal(player(done, f.donor).hand.filter(card => card.id === f.gift).length, 1);
assert.equal(player(done, f.recipient).hand.some(card => card.id === f.gift), false);
assert.equal(player(done, f.spender).hand.some(card => card.id === f.cost), false);
assert.equal(player(done, f.holder).hand.filter(card => card.id === f.cost).length, Number(claimed));
assert.equal(done.discard.filter(card => card.id === f.cost).length, Number(!claimed));
assert.equal(player(done, f.holder).hand.filter(card => card.id === f.semuta).length, Number(!claimed));
assert.equal(done.discard.filter(card => card.id === f.semuta).length, Number(claimed));
assert.ok(player(done, f.observer).hand.some(card => card.id === f.parentCounter));
assert.deepEqual(done.richeseGiftBlocked, { turn: f.game.turn, phase: f.game.phase, cards: [f.gift] });
assert.deepEqual(done.response, f.originalParent.response); assert.deepEqual(done.decision, f.originalParent.decision);
assert.deepEqual(done.phaseOpening, f.originalParent.phaseOpening);
assert.deepEqual(done.pendingKarama, f.originalParent.pendingKarama);
assert.ok(viewGame(done, f.donor).richeseGift?.cards.some(card => card.id === f.gift));
const preserved = structuredClone(done);
const retry = applyAction(structuredClone(done), f.donor, f.giftAction);
assert.equal(retry.response?.kind, 'richeseGift');
assert.equal(retry.pendingRicheseGift?.intent.cardId, f.gift);
assert.deepEqual(done, preserved);
return done;
}
for (const advanced of [false, true]) for (const claimed of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} genuine paid Semuta ${claimed ? 'claims' : 'passes'} the original printed Karama before allied Richese gift prevention completes`, () => {
const f = fixture({ advanced }); nativeSource(f);
const offered = open(f), event = offered.pendingTreacheryDiscard!.batch.event;
const done = prevented(claimed ? claim(offered, f.holder) : pass(offered), f, claimed);
assert.equal(player(done, f.holder).hand.length, 3);
reject(done, f.holder, { type: 'semutaCommit', event });
reject(done, f.spender, f.action);
},
);
for (const advanced of [false, true]) void test(
`${advanced ? 'Advanced' : 'Basic'} native normal-one purchase fills Richese to four before the Semuta singleton exchange`, () => {
const f = fixture({ advanced, fullHand: true }); nativeSource(f);
const normalOrder = f.normalOrder; assert.ok(normalOrder);
assert.ok(f.actions.some(step => step.actor === f.donor && step.action.type === 'bid' && step.action.amount === 1));
assert.ok(player(f.afterFirstBidding, f.holder).hand.some(card => card.id === normalOrder.card));
assert.equal(player(f.game, f.holder).hand.length, 4);
const done = prevented(claim(open(f), f.holder), f, true);
assert.equal(player(done, f.holder).hand.length, 4);
assert.ok(player(done, f.holder).hand.some(card => card.id === normalOrder.card));
},
);
for (const claimed of [false, true]) void test(
`native pending cache-purchase Emperor income survives gift-cost Semuta ${claimed ? 'claim' : 'pass'} then settles once`, () => {
const f = fixture({ advanced: true, parent: 'emperorIncome' }); nativeSource(f);
assert.equal(f.originalParent.response?.kind, 'emperorIncome');
assert.equal(f.beforeGift.currentAuctionSale?.amount ?? f.beforeGift.auction?.bid, 1);
const offered = open(f), done = prevented(claimed ? claim(offered, f.holder) : pass(offered), f, claimed);
assert.equal(done.response?.kind, 'emperorIncome');
const before = player(done, f.recipient).spice, remaining = advance(done, clean);
assert.equal(player(remaining, f.recipient).spice, before + 1);
assert.ok(!remaining.pendingRicheseGift); assert.ok(!remaining.pendingTreacheryDiscard);
assert.equal(player(remaining, f.donor).hand.filter(card => card.id === f.gift).length, 1);
assert.equal(player(remaining, f.recipient).hand.some(card => card.id === f.gift), false);
assert.deepEqual(remaining.richeseGiftBlocked, done.richeseGiftBlocked);
assert.deepEqual(inventory(remaining), f.originalInventory);
},
);
void test('all four minimal profiles legally answer the exact original Richese-gift Karama discard in both rule bands', () => {
for (const advanced of [false, true]) {
const f = fixture({ advanced }), offered = open(f), event = offered.pendingTreacheryDiscard!.batch.event;
for (const difficulty of DIFFICULTIES) {
const own = viewGame(offered, f.holder); own.players.find(p => p.id === f.holder)!.bot = difficulty;
const action = botActions(own)[0]; assert.ok(action, difficulty);
assert.ok(action.type === 'semutaCommit' || action.type === 'semutaPass'); assert.equal(action.event, event);
let done = act(structuredClone(offered), f.holder, action);
const claimed = action.type === 'semutaCommit';
if (done.pendingTreacheryDiscard?.reaction?.stage === 'select') {
const selecting = viewGame(done, f.holder); selecting.players.find(p => p.id === f.holder)!.bot = difficulty;
const selected = botActions(selecting)[0]; assert.ok(selected);
assert.equal(selected.type, 'semutaSelect'); assert.equal(selected.card, f.cost);
done = act(done, f.holder, selected);
} else if (done.pendingTreacheryDiscard) done = pass(done);
prevented(done, f, claimed);
}
}
});
void test('the original fresh cost rejects stale, foreign and underlying gift replies without consuming another card', () => {
const f = fixture(), offered = open(f), event = offered.pendingTreacheryDiscard!.batch.event;
reject(offered, f.holder, { type: 'semutaCommit', event: `${event}:stale` });
reject(offered, f.spender, { type: 'semutaCommit', event });
reject(offered, f.spender, f.action); reject(offered, f.donor, f.giftAction);
const done = prevented(pass(offered), f, false);
assert.equal(done.discard.filter(card => card.id === f.cost).length, 1);
assert.equal(done.richeseGiftBlocked!.cards.filter(card => card === f.gift).length, 1);
assert.ok(player(done, f.holder).hand.some(card => card.id === f.semuta));
});