229 lines
8.5 KiB
TypeScript
229 lines
8.5 KiB
TypeScript
import test from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {
|
|
createRicheseAuction,
|
|
submitRicheseBid,
|
|
projectRicheseAuction,
|
|
moveRicheseBidderLast,
|
|
type RicheseAuction,
|
|
type RicheseAuctionInput,
|
|
} from '../game/richese-auction';
|
|
|
|
const create = (overrides: Partial<RicheseAuctionInput> = {}) =>
|
|
createRicheseAuction({
|
|
event: 'lot-1',
|
|
cardId: 'physical-1',
|
|
source: 'cache',
|
|
owner: 'r',
|
|
method: 'onceAround',
|
|
eligible: ['r', 'a', 'b'],
|
|
order: ['r', 'a', 'b'],
|
|
tieOrder: ['b', 'r', 'a'],
|
|
...overrides,
|
|
});
|
|
const bid = (
|
|
state: RicheseAuction,
|
|
actor: string,
|
|
amount: number | null,
|
|
max = 20,
|
|
) => submitRicheseBid(state, { event: state.event, actor, amount }, max);
|
|
|
|
void test('Once Around follows supplied physical direction, skips ineligible seats and offers owner last', () => {
|
|
let state = create({ order: ['r', 'empty', 'b', 'a'] });
|
|
assert.deepEqual(state.order, ['b', 'a', 'r']);
|
|
state = bid(state, 'b', 2);
|
|
state = bid(state, 'a', 4);
|
|
assert.equal(state.active, 'r');
|
|
assert.throws(() => bid(state, 'b', 5), /Wait/);
|
|
state = bid(state, 'r', 5);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'r', amount: 5 });
|
|
});
|
|
|
|
void test('Once Around owner decline preserves highest other bid and excludes an ineligible seller', () => {
|
|
let state = bid(create(), 'a', 3);
|
|
state = bid(state, 'b', null);
|
|
state = bid(state, 'r', null);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'a', amount: 3 });
|
|
state = create({ eligible: ['a', 'b'] });
|
|
state = bid(state, 'a', 2);
|
|
state = bid(state, 'b', null);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'a', amount: 2 });
|
|
});
|
|
|
|
void test('Once Around all-other-pass becomes unbid without a redundant owner acknowledgment', () => {
|
|
for (const source of ['cache', 'blackMarket'] as const) {
|
|
let state = create({ source });
|
|
state = bid(state, 'a', null);
|
|
state = bid(state, 'b', null);
|
|
assert.deepEqual(state.outcome, { kind: 'unbid' });
|
|
assert.equal(state.active, null);
|
|
assert.deepEqual(state.acted, ['a', 'b']);
|
|
}
|
|
assert.deepEqual(create({ eligible: ['r'] }).outcome, { kind: 'unbid' });
|
|
});
|
|
|
|
void test('normal auctions allow a prior passer to reenter after a raise and skip the high bidder', () => {
|
|
let state = create({
|
|
method: 'normal',
|
|
source: 'blackMarket',
|
|
order: ['a', 'b', 'r'],
|
|
});
|
|
state = bid(state, 'a', null);
|
|
state = bid(state, 'b', 2);
|
|
state = bid(state, 'r', null);
|
|
assert.equal(state.active, 'a');
|
|
state = bid(state, 'a', 4);
|
|
assert.deepEqual(state.passed, []);
|
|
assert.equal(state.active, 'b');
|
|
state = bid(state, 'b', null);
|
|
state = bid(state, 'r', null);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'a', amount: 4 });
|
|
});
|
|
|
|
void test('normal all-pass, empty eligibility and single-bidder outcomes terminate correctly', () => {
|
|
let state = create({ method: 'normal', source: 'blackMarket' });
|
|
for (const actor of ['r', 'a', 'b']) state = bid(state, actor, null);
|
|
assert.deepEqual(state.outcome, { kind: 'unbid' });
|
|
for (const method of ['normal', 'onceAround', 'silent'] as const)
|
|
assert.deepEqual(
|
|
create({ method, source: 'blackMarket', eligible: [] }).outcome,
|
|
{
|
|
kind: 'unbid',
|
|
},
|
|
);
|
|
state = bid(
|
|
create({ method: 'normal', source: 'blackMarket', eligible: ['a'] }),
|
|
'a',
|
|
1,
|
|
);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'a', amount: 1 });
|
|
});
|
|
|
|
void test('Silent commitments accept arbitrary submission order and break ties by storm order', () => {
|
|
let state = create({ method: 'silent' });
|
|
state = bid(state, 'r', 6);
|
|
state = bid(state, 'a', 6);
|
|
assert.equal(state.outcome, null);
|
|
assert.equal(state.bid, 0);
|
|
assert.equal(state.bidder, null);
|
|
state = bid(state, 'b', 6);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'b', amount: 6 });
|
|
assert.deepEqual(projectRicheseAuction(state, 'observer').revealedBids, {
|
|
r: 6,
|
|
a: 6,
|
|
b: 6,
|
|
});
|
|
});
|
|
|
|
void test('Silent zero bids are valid declines and never produce a zero-spice winner', () => {
|
|
let state = create({ method: 'silent' });
|
|
assert.throws(() => bid(state, 'a', null), /zero/);
|
|
for (const actor of ['a', 'r', 'b']) state = bid(state, actor, 0, 0);
|
|
assert.deepEqual(state.outcome, { kind: 'unbid' });
|
|
assert.deepEqual(projectRicheseAuction(state, 'r').revealedBids, {
|
|
a: 0,
|
|
r: 0,
|
|
b: 0,
|
|
});
|
|
});
|
|
|
|
void test('private projections are invariant to opponent sealed amounts before reveal', () => {
|
|
const low = bid(create({ method: 'silent' }), 'a', 1);
|
|
const high = bid(create({ method: 'silent' }), 'a', 17);
|
|
for (const viewer of ['r', 'b', 'observer']) {
|
|
assert.deepEqual(
|
|
projectRicheseAuction(low, viewer),
|
|
projectRicheseAuction(high, viewer),
|
|
);
|
|
assert.equal('sealed' in projectRicheseAuction(high, viewer), false);
|
|
}
|
|
assert.equal(projectRicheseAuction(high, 'a').ownBid, 17);
|
|
assert.deepEqual(projectRicheseAuction(high, 'b').submitted, ['a']);
|
|
const view = projectRicheseAuction(high, 'a');
|
|
view.order.reverse();
|
|
assert.deepEqual(high.order, ['r', 'a', 'b']);
|
|
});
|
|
|
|
void test('stale events, duplicate commitments, wrong actors and invalid funded bids reject without mutation', () => {
|
|
const state = bid(create({ method: 'silent' }), 'a', 3);
|
|
const original = JSON.stringify(state);
|
|
for (const amount of [-1, 1.5, NaN, Infinity, Number.MAX_SAFE_INTEGER + 1])
|
|
assert.throws(() => bid(state, 'b', amount));
|
|
assert.throws(() => bid(state, 'b', 6, 5), /funding/);
|
|
assert.throws(() => bid(state, 'b', 1, -1));
|
|
assert.throws(() => bid(state, 'a', 4), /already submitted/);
|
|
assert.throws(() => bid(state, 'outsider', 1), /cannot bid/);
|
|
assert.throws(
|
|
() => submitRicheseBid(state, { event: 'old', actor: 'b', amount: 1 }, 20),
|
|
/expired/,
|
|
);
|
|
assert.equal(JSON.stringify(state), original);
|
|
const ordinary = bid(
|
|
create({ method: 'normal', source: 'blackMarket' }),
|
|
'r',
|
|
3,
|
|
);
|
|
assert.throws(() => bid(ordinary, 'a', 3), /Raise/);
|
|
assert.throws(() => bid(ordinary, 'r', 4), /Wait/);
|
|
});
|
|
|
|
void test('a restored auction finishes once and replayed or stale terminal submissions fail', () => {
|
|
const original = bid(create(), 'a', 2);
|
|
const snapshot = JSON.stringify(original);
|
|
let state: RicheseAuction = JSON.parse(snapshot);
|
|
state = bid(state, 'b', null);
|
|
state = bid(state, 'r', null);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'a', amount: 2 });
|
|
assert.equal(JSON.stringify(original), snapshot);
|
|
assert.throws(() => bid(state, 'r', 3), /already complete/);
|
|
const clone = JSON.parse(JSON.stringify(state));
|
|
assert.deepEqual(
|
|
projectRicheseAuction(clone, 'r'),
|
|
projectRicheseAuction(state, 'r'),
|
|
);
|
|
});
|
|
|
|
void test('legal last-position override can put an unacted opponent after Richese without repeating completed bids', () => {
|
|
let state = bid(create(), 'a', 2);
|
|
const original = structuredClone(state);
|
|
state = moveRicheseBidderLast(state, state.event, 'b');
|
|
assert.deepEqual(state.order, ['a', 'r', 'b']);
|
|
assert.equal(state.active, 'r');
|
|
state = bid(state, 'r', 3);
|
|
assert.equal(state.active, 'b');
|
|
state = bid(state, 'b', 4);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'b', amount: 4 });
|
|
assert.deepEqual(original.order, ['a', 'b', 'r']);
|
|
assert.throws(
|
|
() => moveRicheseBidderLast(original, original.event, 'a'),
|
|
/has not bid/,
|
|
);
|
|
assert.throws(() => moveRicheseBidderLast(original, 'old', 'b'), /expired/);
|
|
assert.throws(() => moveRicheseBidderLast(state, state.event, 'b'), /open/);
|
|
});
|
|
|
|
void test('an unacted bidder moved after owner prevents premature all-pass closure', () => {
|
|
let state = bid(create(), 'a', null);
|
|
state = moveRicheseBidderLast(state, state.event, 'b');
|
|
assert.equal(state.active, 'r');
|
|
assert.equal(state.outcome, null);
|
|
state = bid(state, 'r', null);
|
|
assert.equal(state.active, 'b');
|
|
state = bid(state, 'b', 1);
|
|
assert.deepEqual(state.outcome, { kind: 'sold', winner: 'b', amount: 1 });
|
|
});
|
|
|
|
void test('construction rejects ambiguous seat ordering and does not select seller participation', () => {
|
|
assert.throws(() => create({ eligible: ['a', 'a'] }), /distinct/);
|
|
assert.throws(() => create({ order: ['a', 'a', 'b', 'r'] }), /distinct/);
|
|
assert.throws(() => create({ tieOrder: ['a', 'b'] }), /position/);
|
|
assert.throws(() => create({ event: '' }), /event/);
|
|
const state = create({
|
|
method: 'silent',
|
|
source: 'blackMarket',
|
|
eligible: ['a', 'b'],
|
|
});
|
|
assert.throws(() => bid(state, 'r', 1), /cannot bid/);
|
|
assert.deepEqual(state.eligible, ['a', 'b']);
|
|
});
|