Files
Dune/tests/early-richese-bid.test.ts

85 lines
5.2 KiB
TypeScript

import assert from 'node:assert/strict';
import test from 'node:test';
import {
EarlyRicheseBidError, attachEarlyRicheseBids, bindEarlyRicheseBid,
earlyRicheseBidAnswerOptions, earlyRicheseBidClaimText,
liveEarlyRicheseBidPromises, parseEarlyRicheseBidClaim,
reconcileEarlyRicheseBids, validateEarlyRicheseBidPromises,
type EarlyRicheseBidPromise,
} from '../game/early-richese-bid';
const claim = { turn: 3, minimum: 4 };
const bind = (rows: EarlyRicheseBidPromise[], minimum: number, answer: boolean, id: string, maximum = 8) =>
bindEarlyRicheseBid(rows, { turn: 3, minimum }, 'target', 'asker', answer, id, maximum);
void test('the early claim binds a turn and positive bid without a speculative lot', () => {
assert.deepEqual(parseEarlyRicheseBidClaim({ ...claim, event: 'discarded' }), claim);
assert.match(earlyRicheseBidClaimText(claim), /first Richese cache auction/);
for (const input of [null, {}, { turn: 0, minimum: 1 }, { turn: 2, minimum: 0 },
{ turn: 2, minimum: 1.5 }, { turn: 2, minimum: Infinity }])
assert.throws(() => parseEarlyRicheseBidClaim(input), EarlyRicheseBidError);
});
void test('candidate answers preserve all jointly feasible waiting promises', () => {
assert.deepEqual(earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', 8), [true, false]);
assert.deepEqual(earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', 3), [false]);
assert.deepEqual(earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', 0), [false]);
const yes = bind([], 4, true, 'yes');
assert.deepEqual(earlyRicheseBidAnswerOptions(yes, { turn: 3, minimum: 4 }, 'target', 'asker', 8), [true]);
const no = bind([], 5, false, 'no');
assert.deepEqual(earlyRicheseBidAnswerOptions(no, { turn: 3, minimum: 4 }, 'target', 'asker', 8), [true, false]);
assert.deepEqual(earlyRicheseBidAnswerOptions(no, { turn: 3, minimum: 5 }, 'target', 'asker', 8), [false]);
assert.throws(() => bind(yes, 4, false, 'no'), /compatible/);
assert.throws(() => bind(yes, 4, true, 'yes'), /distinct receipt/);
assert.throws(() => earlyRicheseBidAnswerOptions([], claim, 'target', 'target', 8));
assert.throws(() => earlyRicheseBidAnswerOptions([], claim, 'target', 'asker', -1));
});
void test('first actual cache event attaches waiting receipts once without changing other turns', () => {
const source = [...bind([], 4, true, 'yes'),
{ ...claim, turn: 2, id: 'old', player: 'target', asker: 'asker', answer: false },
{ ...claim, id: 'closed', player: 'target', asker: 'asker', answer: true, released: true }];
const before = structuredClone(source);
const attached = attachEarlyRicheseBids(source, 3, 'real-cache-event');
assert.deepEqual(source, before);
assert.deepEqual(attached.bound, [{ ...claim, id: 'yes', player: 'target', asker: 'asker', answer: true, event: 'real-cache-event' }]);
assert.equal(attached.promises[0].boundEvent, 'real-cache-event');
assert.deepEqual(liveEarlyRicheseBidPromises(attached.promises, 'target', 3), []);
assert.deepEqual(attachEarlyRicheseBids(attached.promises, 3, 'another-event').bound, []);
validateEarlyRicheseBidPromises(attached.promises, ['target', 'asker']);
});
void test('voluntary losses cannot abandon a Yes, external losses release only impossible promises', () => {
const rows = bind(bind([], 4, true, 'y4'), 7, true, 'y7');
const witness = { cachePending: true, canBid: true, maximum: 5, change: 'voluntary' as const };
assert.throws(() => reconcileEarlyRicheseBids(rows, 'target', 3, witness), /last funded/);
assert.deepEqual(rows, bind(bind([], 4, true, 'y4'), 7, true, 'y7'));
const external = reconcileEarlyRicheseBids(rows, 'target', 3, { ...witness, change: 'external' });
assert.equal(external[0].released, undefined);
assert.equal(external[1].released, true);
const noCapacity = reconcileEarlyRicheseBids(external, 'target', 3,
{ cachePending: true, canBid: false, maximum: 5, change: 'external' });
assert.equal(noCapacity[0].released, true);
});
void test('closing unavailable cache releases Yes and establishes No', () => {
const rows = bind(bind([], 4, true, 'yes'), 7, false, 'no');
assert.throws(() => reconcileEarlyRicheseBids(rows, 'target', 3,
{ cachePending: false, canBid: false, maximum: 0, change: 'voluntary' }));
const closed = reconcileEarlyRicheseBids(rows, 'target', 3,
{ cachePending: false, canBid: false, maximum: 0, change: 'external' });
assert.equal(closed[0].released, true);
assert.equal(closed[1].fulfilled, true);
assert.deepEqual(attachEarlyRicheseBids(closed, 3, 'later-cache').bound, []);
validateEarlyRicheseBidPromises(closed, ['target', 'asker']);
});
void test('validation rejects malformed or duplicated receipts and conflicting states', () => {
const valid = bind([], 4, true, 'one');
validateEarlyRicheseBidPromises(valid, ['target', 'asker']);
assert.throws(() => validateEarlyRicheseBidPromises([...valid, valid[0]], ['target', 'asker']));
assert.throws(() => validateEarlyRicheseBidPromises(valid, ['target']));
assert.throws(() => validateEarlyRicheseBidPromises([{ ...valid[0], fulfilled: true, released: true }], ['target', 'asker']));
assert.throws(() => validateEarlyRicheseBidPromises([{ ...valid[0], boundEvent: 'event', released: true }], ['target', 'asker']));
});