Files
Dune/tests/truth-revival-early-runtime.test.ts

210 lines
14 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
actTruthRevivalEarly as act, advanceTruthRevivalEarly as advance, bindTruthRevivalEarly as bind,
answerTruthRevivalEarly as answer, askTruthRevivalEarly as ask, declareTruthRevivalEarly as declare,
completeTruthRevivalEarly as complete, createTruthRevivalEarlyFixture as fixture,
finishTruthRevivalEarly as finish, settleTruthRevivalEarly as settle,
truthRevivalEarlyClean as clean, truthRevivalEarlyInventory as inventory, truthRevivalEarlyPlayer as player,
} from './fixture-truth-revival-early';
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game); assert.throws(() => act(game, actor, action)); assert.deepEqual(game, before);
}
const promise = (game: Game, target: string) => game.revivalPromises!.find(promise => promise.player === target)!;
for (const advanced of [false, true]) void test(`native ${advanced ? 'Advanced typed' : 'Basic'} post-Storm Truth Revival Yes survives actual Spice Charity and Bidding`, () => {
const f = fixture({ advanced }); assert.equal(f.game.turn, 2); assert.equal(f.game.phase, 1);
assert.deepEqual(f.originalPostStorm, f.base.storm.after);
assert.deepEqual(f.sourceActions, f.base.sourceActions.slice(0, f.postStormActionCount));
assert.equal(f.sourceActionCount, f.sourceActions.length); assert.deepEqual(f.staging, []);
assert.equal(player(f.game, f.target).tanks, 3); assert.equal(player(f.game, f.target).spice, 4);
let game = bind(f).game; assert.equal(promise(game, f.target).progress, 0);
assert.equal(viewGame(game, f.target).revivalPromiseGuide, null, 'Quiet Spice is not a fabricated owned guide opportunity');
const actual = complete(f, game); game = actual.game;
assert.ok(actual.actions.some(step => step.action.type === 'passBid'));
assert.equal(game.phase, 4); assert.equal(promise(game, f.target).progress, 0);
if (advanced) assert.equal(game.revivalOrder!.remaining[0], f.target);
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide); assert.equal(guide.phase, 4);
game = settle(act(game, f.target, guide.action));
assert.equal(promise(game, f.target).progress, 3); assert.equal(promise(game, f.target).fulfilled, true);
assert.equal(player(game, f.target).spice, 0); assert.equal(player(game, f.target).tanks, 0);
assert.equal(player(game, f.target).freeForcesRevived, 1);
assert.equal(player(game, f.target).elites?.revived ?? 0, advanced ? 1 : 0);
assert.deepEqual(inventory(finish(game)), f.originalCards);
});
void test('native Storm-phase promise uses the next real Revival reset and excludes a prior-turn ordinary return', () => {
const f = fixture({ phase: 0, phase0PriorReturn: true });
assert.equal(f.game.turn, 3); assert.equal(f.game.phase, 0); assert.ok(f.priorReturn);
assert.equal(player(f.game, f.target).tanks, 2); assert.equal(player(f.game, f.target).revived, 1);
assert.equal(player(f.game, f.target).freeForcesRevived, 1);
let game = bind(f).game; assert.equal(promise(game, f.target).progress, 0);
game = complete(f, game).game;
assert.equal(player(game, f.target).revived, 0); assert.equal(player(game, f.target).freeForcesRevived, 0);
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide); assert.equal(guide.action.amount, 2);
game = act(game, f.target, guide.action);
assert.equal(promise(game, f.target).progress, 2); assert.equal(player(game, f.target).spice, 2);
assert.equal(player(game, f.target).freeForcesRevived, 1); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native Charity-funded early Yes requires its actual two-spice claim before owned readiness or spending', () => {
const f = fixture({ phase: 2, poor: true }); assert.ok(f.funding);
assert.equal(f.funding.steps[0].action.type, 'bribe'); assert.equal(player(f.game, f.target).spice, 0);
assert.equal(player(f.game, f.asker).bribes, 4);
let game = bind(f).game;
rejected(game, f.target, { type: 'ready' });
const charity = viewGame(game, f.target).revivalPromiseGuide; assert.ok(charity); assert.equal(charity.phase, 2);
assert.equal(charity.action.type, 'charity'); game = act(game, f.target, charity.action);
assert.equal(player(game, f.target).spice, 2); assert.equal(promise(game, f.target).progress, 0);
rejected(game, f.target, { type: 'bribe', target: f.asker, amount: 1 });
game = advance(game, state => state.phase === 3 && clean(state) && state.auction?.active === f.target);
rejected(game, f.target, { type: 'bid', amount: 1 });
const bidGuide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(bidGuide); assert.equal(bidGuide.phase, 3);
assert.equal(bidGuide.action.type, 'passBid'); game = act(game, f.target, bidGuide.action);
game = complete(f, game).game;
const returned = viewGame(game, f.target).revivalPromiseGuide; assert.ok(returned); assert.equal(returned.phase, 4);
game = act(game, f.target, returned.action);
assert.equal(player(game, f.target).spice, 0); assert.equal(player(game, f.target).tanks, 1);
assert.equal(promise(game, f.target).progress, 2); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native early No remains binding through Bidding and limits the actual later ordinary return', () => {
const f = fixture(); let game = bind(f, 'no', 2).game;
game = complete(f, game).game;
game = act(game, f.target, { type: 'revive', amount: 1, elite: 0 });
assert.equal(promise(game, f.target).progress, 1);
rejected(game, f.target, { type: 'revive', amount: 1, elite: 0 });
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide); assert.equal(guide.action.type, 'ready');
game = act(game, f.target, guide.action); game = finish(game);
assert.equal(player(game, f.target).revived, 1); assert.equal(player(game, f.target).tanks, 2);
assert.equal(promise(game, f.target).fulfilled, true); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native early Yes reserves its paid return funding against a new owned auction bid', () => {
const f = fixture({ phase: 3 }); let game = bind(f).game;
game = advance(game, state => clean(state) && state.auction?.active === f.target);
assert.equal(player(game, f.target).spice, 4);
rejected(game, f.target, { type: 'bid', amount: 1 });
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide);
assert.equal(guide.phase, 3); assert.equal(guide.action.type, 'passBid');
game = act(game, f.target, guide.action); game = complete(f, game).game;
assert.equal(player(game, f.target).spice, 4); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native existing winning bid and its real payment reserve the early Revival invoice once', () => {
const f = fixture({ phase: 3 }); let game = f.game;
game = advance(game, state => clean(state) && state.auction?.active === f.target);
const card = game.auction!.cards[game.auction!.index].id;
game = act(game, f.target, { type: 'bid', amount: 2 });
assert.equal(game.auction!.bidder, f.target); assert.equal(player(game, f.target).spice, 4);
game = bind(f, 'yes', 2, 'total', game).game;
assert.equal(promise(game, f.target).progress, 0);
game = complete(f, game).game;
assert.equal(player(game, f.target).spice, 2); assert.ok(player(game, f.target).hand.some(held => held.id === card));
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide); assert.equal(guide.action.amount, 2);
game = act(game, f.target, guide.action);
assert.equal(player(game, f.target).spice, 0); assert.equal(promise(game, f.target).progress, 2);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('native owned Karama auction-payment guide preserves the actual funds for the earlier Revival Yes', () => {
const f = fixture({ phase: 3, targetKarama: true }); assert.ok(f.auctionKarama);
let game = bind(f).game;
game = advance(game, state => clean(state) && state.auction?.active === f.target);
game = act(game, f.target, { type: 'bid', amount: 5 });
assert.equal(player(game, f.target).spice, 4);
game = advance(game, state => !state.response && !state.phaseOpening && state.decision?.kind === 'auctionPayment' && state.decision.player === f.target);
const payment = viewGame(game, f.target).revivalPromiseGuide; assert.ok(payment); assert.equal(payment.phase, 3);
assert.equal(payment.action.type, 'decision'); assert.equal(payment.action.karama, true);
assert.equal(payment.action.card, f.auctionKarama);
game = act(game, f.target, payment.action);
assert.equal(player(game, f.target).spice, 4); assert.equal(promise(game, f.target).progress, 0);
assert.equal(game.discard.filter(card => card.id === f.auctionKarama).length, 1);
game = complete(f, game).game;
const revival = viewGame(game, f.target).revivalPromiseGuide; assert.ok(revival); assert.equal(revival.phase, 4);
game = act(game, f.target, revival.action);
assert.equal(player(game, f.target).spice, 0); assert.equal(player(game, f.target).tanks, 0);
assert.equal(promise(game, f.target).progress, 3); assert.deepEqual(inventory(game), f.originalCards);
});
void test('native earlier-phase typed Yes releases only after a real later Tleilaxu prevention', () => {
const f = fixture({ advanced: true, stopper: true }); let game = bind(f).game; assert.ok(f.stopper && f.special);
game = complete(f, game).game;
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide);
game = act(game, f.target, guide.action); assert.equal(game.decision?.kind, 'revivalStop');
assert.equal(promise(game, f.target).progress, 0);
game = settle(act(game, f.stopper, f.special));
assert.equal(promise(game, f.target).released, true); assert.equal(promise(game, f.target).progress, 0);
assert.equal(player(game, f.target).tanks, 3); assert.equal(player(game, f.target).spice, 4);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('two genuine early Truth answers preserve both a funded bid and the later ordinary return', () => {
const f = fixture({ phase: 3 }), declared = declare(f, f.game, { copies: 2 });
let game = ask(declared.game, f.asker, f.target, 2); game = answer(game, f.target, 'yes');
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'bid', target: f.target,
turn: game.turn, lot: game.auction!.index + 1, minimum: 1 } });
assert.ok(viewGame(game, f.target).truthBidAnswers!.includes('yes'));
game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
game = advance(game, state => clean(state) && state.auction?.active === f.target);
assert.notEqual(viewGame(game, f.target).revivalPromiseGuide?.action.type, 'passBid');
const bid = viewGame(game, f.target).bidPromiseGuide; assert.ok(bid); assert.equal(bid.action.amount, 1);
game = act(game, f.target, bid.action); game = complete(f, game).game;
assert.equal(player(game, f.target).spice, 3); assert.equal(promise(game, f.target).progress, 0);
const revival = viewGame(game, f.target).revivalPromiseGuide; assert.ok(revival); assert.equal(revival.action.amount, 2);
game = act(game, f.target, revival.action);
assert.equal(player(game, f.target).spice, 1); assert.equal(promise(game, f.target).progress, 2);
assert.ok(game.bidPromises!.every(promise => promise.fulfilled || promise.released));
for (const truth of f.truths) assert.equal(game.discard.filter(card => card.id === truth).length, 1);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('a genuine early Revival Yes cannot also promise an unfunded ordinary bid', () => {
const f = fixture({ phase: 3 }), declared = declare(f, f.game, { copies: 2 });
let game = ask(declared.game, f.asker, f.target, 3); game = answer(game, f.target, 'yes');
game = act(game, f.asker, { type: 'truthAsk', question: { kind: 'bid', target: f.target,
turn: game.turn, lot: game.auction!.index + 1, minimum: 1 } });
assert.deepEqual(viewGame(game, f.target).truthBidAnswers, ['no']);
rejected(game, f.target, { type: 'truthAnswer', answer: 'yes' });
game = act(game, f.target, { type: 'truthAnswer', answer: 'no' });
assert.equal(promise(game, f.target).answer, true); assert.equal(game.bidPromises![0].answer, false);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('a genuine earlier bid Yes excludes an ordinary Revival Yes that would consume its last bid funding', () => {
const f = fixture({ phase: 3 }), declared = declare(f, f.game, { copies: 2 });
let game = act(declared.game, f.asker, { type: 'truthAsk', question: { kind: 'bid', target: f.target,
turn: f.game.turn, lot: f.game.auction!.index + 1, minimum: 1 } });
game = act(game, f.target, { type: 'truthAnswer', answer: 'yes' });
game = ask(game, f.asker, f.target, 3);
assert.deepEqual(viewGame(game, f.target).truthRevivalAnswers, ['no']);
rejected(game, f.target, { type: 'truthAnswer', answer: 'yes' });
game = answer(game, f.target, 'no');
assert.equal(promise(game, f.target).answer, false); assert.equal(game.bidPromises![0].answer, true);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('native early guide acts only for its real Charity owner and cannot replay in a later phase', () => {
const f = fixture({ phase: 2, poor: true }); let game = bind(f).game;
const guide = viewGame(game, f.target).revivalPromiseGuide; assert.ok(guide); assert.equal(guide.phase, 2);
assert.equal(viewGame(game, f.asker).revivalPromiseGuide, null);
rejected(game, f.asker, guide.action);
game = act(game, f.target, guide.action); game = complete(f, game).game;
rejected(game, f.target, guide.action); assert.deepEqual(inventory(game), f.originalCards);
});
void test('all four minimal policies submit the actual needed Charity before the early Revival return', () => {
const f = fixture({ phase: 2, poor: true }), game = bind(f).game;
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, f.target); view.players.find(seat => seat.id === f.target)!.bot = difficulty;
const action = botActions(view).find(action => action.type === 'charity'); assert.ok(action, difficulty);
const accepted = act(structuredClone(game), f.target, action);
assert.equal(player(accepted, f.target).spice, 2); assert.equal(promise(accepted, f.target).progress, 0);
assert.deepEqual(inventory(accepted), f.originalCards);
}
});