Files
Dune/tests/auction-clock-runtime.test.ts
T

141 lines
9.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, advanceAuctionClock, hasAuctionClockOpportunity, synchronizeAuctionClock,
viewGame } from '../game/engine';
import { createTruthBidFixture as fixture, declareTruthBid as declare, askTruthBid as ask,
answerTruthBid as answer, bindTruthBid as bind, continueTruthBidLot as closeLot,
truthBidInventory as inventory, truthBidPlayer as player } from './fixture-truth-bid';
import { createTruthTurnPreparationFixture as preparation, bindTruthTurnPreparation as bindPreparation,
truthTurnPreparationBid as bid, truthTurnPreparationGroup as group,
truthTurnPreparationPayment as payment, settleTruthTurnPreparation as settle } from './fixture-truth-turn-preparation';
const epoch = '0:0';
void test('native Basic and Advanced original auction lots expire at ten seconds through the canonical pass', () => {
for (const advanced of [false, true]) {
const f = fixture({ advanced }), before = structuredClone(f.game), actor = f.game.auction!.active;
assert.equal(f.game.auction!.clockVersion, 1); assert.equal(f.game.auctionClock, undefined);
assert.equal(hasAuctionClockOpportunity(f.game), true);
const armed = synchronizeAuctionClock(f.game, 1_000, epoch);
const early = advanceAuctionClock(armed, 10_999, epoch);
assert.deepEqual(early, armed); assert.deepEqual(f.game, before);
const expected = applyAction(f.game, actor, { type: 'passBid' });
const expired = advanceAuctionClock(early, 11_000, epoch);
assert.deepEqual(expired.auction, expected.auction); assert.equal(expired.active, expected.active);
assert.notEqual(expired.auctionClock!.event, armed.auctionClock!.event);
assert.equal(expired.auctionClock!.remainingMs, 10_000); assert.equal(expired.auctionClock!.startedAt, 11_000);
assert.match(expired.log.at(-1)!.text, /ten-second bidding time expired; they passed/);
assert.deepEqual(inventory(expired), f.originalCards);
}
});
void test('a real raised bid starts the next bidder clock while unchanged polls cannot refresh it', () => {
const f = fixture(), actor = f.game.auction!.active;
let game = synchronizeAuctionClock(f.game, 1_000, epoch);
const original = game.auctionClock!.event;
game = applyAction(game, actor, { type: 'bid', amount: 1, allyPayment: 0 });
game = synchronizeAuctionClock(game, 5_000, epoch);
const next = game.auctionClock!; assert.notEqual(next.event, original);
assert.equal(next.player, game.auction!.active); assert.equal(next.startedAt, 5_000);
const observed = advanceAuctionClock(game, 14_999, epoch); assert.equal(observed, game);
assert.equal(observed.auctionClock, next);
const expired = advanceAuctionClock(observed, 15_000, epoch);
assert.ok(expired.auction?.passed.includes(next.player) || expired.auction?.index !== game.auction!.index);
assert.deepEqual(inventory(expired), f.originalCards);
});
void test('actual Truthtrance suspends the remaining bid budget and the resolved native question resumes it', () => {
const f = fixture({ window: 'active' });
let game = synchronizeAuctionClock(f.game, 1_000, epoch);
game = declare(f, game).game; game = synchronizeAuctionClock(game, 3_000, epoch);
assert.equal(game.auctionClock!.remainingMs, 8_000); assert.equal(game.auctionClock!.startedAt, null);
const event = game.auctionClock!.event, waiting = advanceAuctionClock(game, 50_000, epoch);
assert.deepEqual(waiting, game); assert.equal(waiting.auction!.active, f.target);
game = ask(waiting, f.asker, f.target, 4); game = answer(game, f.target, 'no');
game = synchronizeAuctionClock(game, 50_000, epoch);
assert.equal(game.auctionClock!.event, event); assert.equal(game.auctionClock!.remainingMs, 8_000);
assert.equal(game.auctionClock!.startedAt, 50_000);
assert.equal(advanceAuctionClock(game, 57_999, epoch), game);
const expired = advanceAuctionClock(game, 58_000, epoch);
assert.match(expired.log.at(-1)!.text, /passed this bid/); assert.deepEqual(inventory(expired), f.originalCards);
});
void test('a timeout cannot evade a native Truthtrance Yes and submits only its required bid before normal paid purchase', () => {
const f = fixture({ window: 'active' });
let game = bind(f, 'yes', 4).game; game = synchronizeAuctionClock(game, 1_000, epoch);
const before = structuredClone(game); game = advanceAuctionClock(game, 11_000, epoch);
assert.equal(before.auction!.bid, 0); assert.equal(game.auction!.bid, 4); assert.equal(game.auction!.bidder, f.target);
assert.equal(player(game, f.target).spice, 10, 'The bid is a declaration, not a free or prepaid purchase');
assert.match(game.log.at(-1)!.text, /legal bid required by their binding Truthtrance answer was submitted/);
game = closeLot(game); assert.equal(player(game, f.target).spice, 6);
assert.ok(player(game, f.target).hand.some(card => card.id === f.lotCard));
assert.ok(game.bidPromises!.every(promise => promise.fulfilled)); assert.deepEqual(inventory(game), f.originalCards);
});
void test('a whole bid AND payment deadline selects the required bid even when its voluntary guide offers payment first', () => {
const f = preparation();
const mixed = group('and', bid(f.lot!, 3), payment(f.recipient));
let game = bindPreparation(f, 'yes', mixed).game;
assert.equal(viewGame(game, f.target).turnActionGuide!.action.type, 'bribe');
game = synchronizeAuctionClock(game, 1_000, epoch); game = advanceAuctionClock(game, 11_000, epoch);
assert.equal(game.auction!.bidder, f.target); assert.equal(game.auction!.bid, 3);
assert.equal(game.turnActionPromises![0].preparation!.bid!.maximum, 3);
assert.equal(game.turnActionPromises![0].fulfilled, false);
game = applyAction(game, f.target, f.payment); assert.equal(game.turnActionPromises![0].fulfilled, true);
game = closeLot(game); assert.equal(player(game, f.target).spice, 5);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('a whole bid OR payment keeps its legal timeout pass and the remaining real payment alternative', () => {
const f = preparation(), mixed = group('or', bid(f.lot!, 3), payment(f.recipient));
let game = bindPreparation(f, 'yes', mixed).game;
game = synchronizeAuctionClock(game, 1_000, epoch);
const nativePass = applyAction(game, f.target, { type: 'passBid' });
game = advanceAuctionClock(game, 11_000, epoch);
assert.deepEqual(game.auction, nativePass.auction); assert.equal(game.phase, nativePass.phase);
assert.notEqual(game.auction?.bidder, f.target);
assert.match(game.log.at(-1)!.text, /passed this bid/);
assert.equal(game.turnActionPromises![0].fulfilled, false); assert.equal(game.turnActionPromises![0].released, false);
game = settle(game); game = applyAction(game, f.target, f.payment);
assert.equal(game.turnActionPromises![0].fulfilled, true);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('a compelled bid uses the lowest legal whole-answer alternative regardless of question order', () => {
const f = preparation();
const mixed = group('or', bid(f.lot!, 5), bid(f.lot!, 3));
let game = bindPreparation(f, 'yes', mixed).game;
assert.equal(viewGame(game, f.target).turnActionGuide!.action.amount, 5);
game = synchronizeAuctionClock(game, 1_000, epoch);
game = advanceAuctionClock(game, 11_000, epoch);
assert.equal(game.auction!.bid, 3); assert.equal(game.auction!.bidder, f.target);
assert.equal(game.turnActionPromises![0].fulfilled, true);
game = closeLot(game); assert.equal(player(game, f.target).spice, 7);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('a controlled legacy open lot remains untimed and its actual newly retired next lot adopts the clock', () => {
const f = fixture(), legacy = structuredClone(f.game); delete legacy.auction!.clockVersion;
assert.equal(hasAuctionClockOpportunity(legacy), false);
assert.equal(synchronizeAuctionClock(legacy, 1_000, epoch), legacy);
assert.equal(advanceAuctionClock(legacy, 1_000_000, epoch), legacy);
assert.equal(viewGame(legacy, f.asker).auctionClock, null);
let game = applyAction(legacy, legacy.auction!.active, { type: 'bid', amount: 1, allyPayment: 0 });
game = closeLot(game); assert.equal(game.auction!.index, 1); assert.equal(game.auction!.clockVersion, 1);
assert.equal(hasAuctionClockOpportunity(game), true); assert.equal(synchronizeAuctionClock(game, 1_000_000, epoch).auctionClock!.remainingMs, 10_000);
assert.deepEqual(inventory(game), f.originalCards);
});
void test('only public bidder time is projected and the private physical-card source never enters the clock view', () => {
const f = fixture(); const game = synchronizeAuctionClock(f.game, 1_000, epoch);
for (const owner of game.players) {
const view = viewGame(game, owner.id, 3_250);
assert.deepEqual(Object.keys(view.auctionClock!).sort(), ['event', 'player', 'remainingMs', 'running']);
assert.equal(view.auctionClock!.remainingMs, 7_750); assert.equal(view.auctionClock!.player, game.auction!.active);
assert.ok(!JSON.stringify(view.auctionClock).includes(f.lotCard));
assert.equal(view.auction?.clockVersion, 1);
}
const rearmed = synchronizeAuctionClock(game, 100_000, '2:0');
assert.equal(rearmed.auctionClock!.remainingMs, 10_000); assert.equal(rearmed.auctionClock!.startedAt, 100_000);
assert.equal(game.auctionClock!.startedAt, 1_000);
});