Allow Distrans before Richese bidding starts

This commit is contained in:
Quintine committed 2026-10-27 17:12:05 +11:00
1 parent 813a19bb1d
commit f50e436506
14 files changed
+511 -24

No files matched your search

@@ -0,0 +1,81 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import {
actDistransPrebid, advanceDistransPrebid, createDistransPrebidFixture,
distransPrebidClean, distransPrebidInventory, distransPrebidPlayer as player,
distransPrebidPosition, type DistransPrebidStep,
} from './fixture-distrans-prebid';
/** The original Advanced public table actually sells Distrans to Guild for one
* spice in the first cache auction. Continue through real phase actions to the
* next turn; Richese offers its own originally dealt card in the native Black
* Market. Only the underlying fixture's unopened card/Spice order and scalar
* lotteries are selected. No played hand, price, auction or phase is installed. */
function source() {
const f = createDistransPrebidFixture({ advanced: true });
const actions: DistransPrebidStep[] = structuredClone(f.actions);
let game = advanceDistransPrebid(f.game, state => state.turn === 2 && state.phase === 3 &&
!state.response && !state.phaseOpening && !state.pendingTreacheryDiscard &&
state.decision?.kind === 'richeseBlackMarket', actions);
const beforeMarket = structuredClone(game);
const seller = game.decision!.player;
const physical = player(game, seller).hand[0]; assert.ok(physical);
const declaration: Action = { type: 'decision', event: game.richeseBidding!.event,
card: physical.id, method: 'normal', direction: 'clockwise' };
game = actDistransPrebid(game, seller, declaration, actions);
game = advanceDistransPrebid(game, state => distransPrebidClean(state) &&
state.richeseAuction?.source === 'blackMarket' && state.richeseAuction.method === 'normal', actions);
const lot = game.richeseAuction!;
assert.equal(lot.cardId, physical.id);
assert.equal(lot.bid, 0); assert.equal(lot.bidder, null);
assert.deepEqual(lot.passed, []); assert.deepEqual(lot.acted, []);
assert.ok(player(game, seller).hand.some(c => c.id === physical.id));
assert.ok(player(game, f.holder).hand.some(c => c.id === f.card));
assert.ok(player(game, f.holder).hand.some(c => c.id === f.give));
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
return { ...f, game, actions, beforeMarket, seller, physical, declaration };
}
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => actDistransPrebid(game, actor, action));
assert.deepEqual(game, before);
}
void test('genuine Advanced Black Market normal lot remains unbid through earned Distrans transfer', () => {
const f = source(), before = structuredClone(f.game);
assert.equal(f.beforeMarket.decision?.kind, 'richeseBlackMarket');
assert.ok(f.acquisition.actions.some(step => step.actor === f.holder && step.action.type === 'richeseBid' && step.action.amount === 1));
assert.equal(viewGame(before, f.holder).distrans!.blocked, null);
assert.ok(viewGame(before, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!.cards.some(c => c.id === f.give));
let game = actDistransPrebid(f.game, f.holder, f.action);
assert.deepEqual(f.game, before);
game = advanceDistransPrebid(game, distransPrebidClean);
assert.deepEqual(distransPrebidPosition(game), distransPrebidPosition(before));
assert.deepEqual(player(game, f.holder).hand, player(before, f.holder).hand.filter(c => c.id !== f.card && c.id !== f.give));
assert.deepEqual(player(game, f.recipient).hand, [...player(before, f.recipient).hand,
player(before, f.holder).hand.find(c => c.id === f.give)!]);
assert.ok(player(game, f.seller).hand.some(c => c.id === f.physical.id));
assert.equal(game.discard.filter(c => c.id === f.card).length, 1);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
const lot = game.richeseAuction!, actor = lot.active!; assert.ok(actor);
game = actDistransPrebid(game, actor, { type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0 });
assert.equal(game.richeseAuction!.bid, 1);
assert.equal(game.richeseAuction!.bidder, actor);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
rejected(game, f.holder, f.action);
});
for (const amount of [null, 1] as const) {
void test(`genuine Advanced Black Market first ${amount === null ? 'pass' : 'bid'} closes Distrans before either card leaves custody`, () => {
const f = source(), lot = f.game.richeseAuction!, actor = lot.active!; assert.ok(actor);
const game = actDistransPrebid(f.game, actor, { type: 'richeseBid', event: lot.event, amount, allyPayment: 0 });
assert.ok(game.richeseAuction);
assert.ok(viewGame(game, f.holder).distrans!.blocked);
rejected(game, f.holder, f.action);
assert.ok(player(game, f.holder).hand.some(c => c.id === f.card));
assert.ok(player(game, f.holder).hand.some(c => c.id === f.give));
assert.ok(player(game, f.seller).hand.some(c => c.id === f.physical.id));
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
}
+5 -5
View File
@@ -81,8 +81,8 @@ void test('an entitled untouched ordinary lot keeps the transfer enabled in Basi
}
});
void test('live bid, pass-only and special-auction blocks come from the authoritative projection', () => {
for (const context of ['live bid', 'pass-only lot', 'special auction']) {
void test('live bid, pass-only and started special-auction blocks come from the authoritative projection', () => {
for (const context of ['live bid', 'pass-only lot', 'started special auction']) {
const game = projected();
const reason = `Controlled ${context}: Distrans is unavailable until this lot resolves.`;
if (context === 'live bid') { game.auction!.bid = 2; game.auction!.bidder = 't'; }
@@ -161,9 +161,9 @@ void test('brief provisional timing guidance stays visible without an extra conf
for (let render = 0; render < 3; render++) {
const html = markup(game);
assert.match(html, /aria-label="Provisional Distrans bid timing"/);
assert.match(html, /Provisional bid timing: you may transfer before the first bid or pass on an ordinary auction lot/);
assert.match(html, /Once anyone bids or passes, that lot stays blocked while unresolved/);
assert.match(html, /Special auctions keep their existing restrictions/);
assert.match(html, /Provisional bid timing: you may transfer before the first bid or pass on an ordinary, Once Around or normal Black Market lot/);
assert.match(html, /before the first sealed submission in a Silent lot/);
assert.match(html, /Once that bidding has begun, the lot stays blocked while unresolved/);
assert.match(html, /No spice is paid/);
assert.match(html, /without a recipient confirmation or Karama response/);
assert.doesNotMatch(html, /Confirm transfer|Accept transfer|Acknowledge|countdown/);
+74
View File
@@ -0,0 +1,74 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { registerHooks } from 'node:module';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { DistransTransferAction, type DistransTransferActionProps } from '../components/distrans-transfer-action';
import { viewGame, type Action, type GameView } from '../game/engine';
import { actDistransPrebid, distransPrebidInventory, distransPrebidPlayer } from './fixture-distrans-prebid';
import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid';
const aliases = registerHooks({
resolve(specifier, context, next) {
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier, context);
},
});
const { Distrans } = await import('../components/distrans');
aliases.deregister();
function markup(game: GameView) {
return renderToStaticMarkup(createElement(Distrans, { game, busy: false, act() {} }));
}
function transferButton(html: string) {
const button = html.match(/<button\b(?=[^>]*data-testid="distrans-transfer")[^>]*>[\s\S]*?<\/button>/)?.[0];
assert.ok(button, 'The projected transfer action is present');
return button;
}
for (const advanced of [false, true]) for (const method of ['onceAround', 'silent'] as const) {
void test(`${advanced ? 'Advanced' : 'Basic'} ${method} native unopened lot projects and executes the Distrans control`, () => {
const f = createDistransRichesePrebidFixture({ advanced, method });
const before = structuredClone(f.game), view = viewGame(f.game, f.holder), quote = view.distrans;
assert.ok(quote);
assert.equal(quote.blocked, null);
const choice = quote.choices.find(candidate => candidate.recipient === f.recipient);
assert.ok(choice);
assert.equal(choice.blocked, null);
const given = choice.cards.find(card => card.id === f.give);
assert.ok(given);
const html = markup(view);
assert.doesNotMatch(transferButton(html), /\sdisabled=/);
assert.match(html, /ordinary, Once Around or normal Black Market lot/);
assert.match(html, /first sealed submission in a Silent lot/);
// Stateful selectors render above; exercise the real action leaf with the
// exact entitled recipient/card choice from the native engine projection.
const actions: Action[] = [];
const props: DistransTransferActionProps = {
card: quote.card,
recipient: { id: choice.recipient, name: view.players.find(player => player.id === choice.recipient)?.name },
given, reason: null, reasonId: 'native-richese-reason', busy: false,
act(action) { actions.push(action); },
};
const button = DistransTransferAction(props);
button.props.onClick();
assert.deepEqual(actions, [f.action]);
const after = actDistransPrebid(f.game, f.holder, actions[0]);
assert.deepEqual(f.game, before);
assert.ok(!distransPrebidPlayer(after, f.holder).hand.some(card => card.id === f.give || card.id === f.card));
assert.equal(distransPrebidPlayer(after, f.recipient).hand.filter(card => card.id === f.give).length, 1);
assert.equal(after.discard.filter(card => card.id === f.card).length, 1);
assert.deepEqual(distransPrebidInventory(after), f.originalInventory);
assert.equal(after.richeseAuction?.event, before.richeseAuction?.event);
assert.equal(after.richeseAuction?.method, method);
const lot = before.richeseAuction!;
const actor = method === 'onceAround' ? lot.active! :
lot.order.find(id => lot.eligible.includes(id) && !Object.hasOwn(lot.sealed, id))!;
const started = actDistransPrebid(before, actor, { type: 'richeseBid', event: lot.event,
amount: method === 'onceAround' ? null : 0, allyPayment: 0 });
const blocked = viewGame(started, f.holder);
assert.ok(blocked.distrans?.blocked);
assert.match(transferButton(markup(blocked)), /\sdisabled=/);
});
}
+138
View File
@@ -0,0 +1,138 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { handLimit, viewGame, type Action, type Game } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { distransAuctionBlock } from '../game/distrans-timing';
import {
actDistransPrebid, advanceDistransPrebid, distransPrebidClean,
distransPrebidInventory, distransPrebidPlayer as player, distransPrebidPosition,
} from './fixture-distrans-prebid';
import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid';
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
function rejected(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => actDistransPrebid(game, actor, action));
assert.deepEqual(game, before);
}
function firstSubmission(game: Game, amount?: number | null) {
const lot = game.richeseAuction!;
const actor = lot.method === 'silent'
? lot.order.find(id => lot.eligible.includes(id) && !Object.hasOwn(lot.sealed, id))!
: lot.active!;
assert.ok(actor);
return { actor, action: { type: 'richeseBid', event: lot.event,
amount: amount === undefined ? (lot.method === 'silent' ? 0 : null) : amount,
allyPayment: 0 } as Action };
}
for (const advanced of [false, true]) for (const method of ['onceAround', 'silent'] as const) {
const prefix = `${advanced ? 'Advanced' : 'Basic'} native ${method}`;
void test(`${prefix} preserves the unbid original cache lot through a genuine Distrans transfer`, () => {
const f = createDistransRichesePrebidFixture({ advanced, method });
assert.equal(f.initial.status, 'lobby');
assert.equal(f.acquisition.before.richeseCache!.some(card => card.id === f.card), true);
assert.ok(f.acquisition.actions.some(step => step.actor === f.holder && step.action.type === 'richeseBid' && step.action.amount === 1));
assert.equal(f.beforeDeclaration.turn, 2);
assert.equal(f.beforeDeclaration.decision?.kind, 'richeseCache');
const before = structuredClone(f.game);
assert.equal(viewGame(before, f.holder).distrans!.blocked, null);
assert.ok(viewGame(before, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!.cards.some(c => c.id === f.give));
let game = actDistransPrebid(f.game, f.holder, f.action);
assert.deepEqual(f.game, before);
game = advanceDistransPrebid(game, distransPrebidClean);
assert.deepEqual(distransPrebidPosition(game), distransPrebidPosition(before));
assert.deepEqual(player(game, f.holder).hand, player(before, f.holder).hand.filter(c => c.id !== f.card && c.id !== f.give));
assert.deepEqual(player(game, f.recipient).hand, [...player(before, f.recipient).hand,
player(before, f.holder).hand.find(c => c.id === f.give)!]);
assert.equal(game.discard.filter(c => c.id === f.card).length, 1);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
rejected(game, f.holder, f.action);
const start = firstSubmission(game);
for (const difficulty of DIFFICULTIES) {
const view = viewGame(game, start.actor);
view.players.find(p => p.id === start.actor)!.bot = difficulty;
const action = botActions(view)[0];
assert.equal(action?.type, 'richeseBid', `${difficulty} can act on the original lot`);
const next = actDistransPrebid(game, start.actor, action);
assert.ok(next.richeseAuction?.acted.includes(start.actor));
}
game = actDistransPrebid(game, start.actor, start.action);
assert.ok(game.richeseAuction?.acted.includes(start.actor));
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
for (const first of method === 'silent' ? [0, 1] : [null, 1]) {
void test(`${prefix} first ${first === null ? 'pass' : first === 0 ? 'sealed zero' : 'bid'} closes the Distrans window`, () => {
const f = createDistransRichesePrebidFixture({ advanced, method });
const start = firstSubmission(f.game, first);
const game = actDistransPrebid(reload(f.game), start.actor, start.action);
assert.ok(game.richeseAuction?.acted.includes(start.actor));
assert.ok(viewGame(game, f.holder).distrans!.blocked);
rejected(game, f.holder, f.action);
assert.ok(player(game, f.holder).hand.some(c => c.id === f.card));
assert.ok(player(game, f.holder).hand.some(c => c.id === f.give));
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
}
void test(`${prefix} rejects a full recipient before spending either physical card`, () => {
const f = createDistransRichesePrebidFixture({ advanced, method });
// Controlled custody supplement: move real undealt deck cards into the
// recipient hand. This is not claimed as an earned acquisition sequence.
const game = structuredClone(f.game), recipient = player(game, f.recipient);
while (recipient.hand.length < handLimit(recipient)) recipient.hand.push(game.deck.shift()!);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
const choice = viewGame(game, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!;
assert.ok(choice.blocked);
rejected(game, f.holder, f.action);
assert.ok(player(game, f.holder).hand.some(c => c.id === f.card));
assert.ok(player(game, f.holder).hand.some(c => c.id === f.give));
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
void test(`${prefix} fills the active recipient, automatically closes that bid and keeps an originally full donor ineligible`, () => {
const f = createDistransRichesePrebidFixture({ advanced, method });
// Controlled custody supplement: move only actual undealt deck cards to
// hands. These are not claimed as earned purchases. The declaration and
// transfer still use real actions and preserve the physical inventory.
const staged = structuredClone(f.beforeDeclaration), donor = player(staged, f.holder);
while (donor.hand.length < handLimit(donor)) {
const card = staged.deck.shift(); assert.ok(card); donor.hand.push(card);
}
assert.deepEqual(distransPrebidInventory(staged), f.originalInventory);
let game = actDistransPrebid(staged, staged.decision!.player, f.declaration);
const lot = game.richeseAuction!;
assert.ok(lot);
assert.ok(!lot.eligible.includes(f.holder), 'The full donor was excluded at declaration');
const recipientId = method === 'silent'
? lot.order.find(id => lot.eligible.includes(id) && id !== f.holder)!
: lot.active!;
assert.ok(recipientId && recipientId !== f.holder);
const recipient = player(game, recipientId);
while (recipient.hand.length < handLimit(recipient) - 1) {
const card = game.deck.shift(); assert.ok(card); recipient.hand.push(card);
}
assert.equal(recipient.hand.length, handLimit(recipient) - 1);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
const originalLot = structuredClone(lot);
const action: Action = { ...f.action, target: recipientId };
const choice = viewGame(game, f.holder).distrans!.choices.find(c => c.recipient === recipientId)!;
assert.ok(choice.cards.some(c => c.id === f.give));
game = actDistransPrebid(game, f.holder, action);
assert.equal(player(game, recipientId).hand.length, handLimit(recipient));
assert.equal(player(game, f.holder).hand.length, handLimit(donor) - 2);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
const next = game.richeseAuction!;
assert.ok(next);
assert.equal(next.event, originalLot.event);
assert.equal(next.cardId, originalLot.cardId);
assert.equal(next.source, originalLot.source);
assert.deepEqual(next.order, originalLot.order);
assert.deepEqual(next.eligible, originalLot.eligible);
assert.deepEqual(next.tieOrder, originalLot.tieOrder);
assert.ok(!next.eligible.includes(f.holder), 'Freeing space does not add a bidder to this lot');
assert.ok(next.acted.includes(recipientId));
if (method === 'silent') assert.equal(next.sealed[recipientId], 0);
else assert.ok(next.passed.includes(recipientId));
assert.ok(distransAuctionBlock(game), 'The automatic pass or zero closes the original window');
});
}
+46 -5
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { distransAuctionBlock as block } from '../game/distrans-timing';
import { createRicheseAuction, submitRicheseBid } from '../game/richese-auction';
const fresh = () => ({ phase: 3, auction: { cards: ['original-lot'], index: 0,
bid: 0, bidder: null as string | null, active: 'a', passed: [] as string[],
@@ -14,7 +15,7 @@ void test('Distrans quotes a virgin physical ordinary lot without rewriting its
assert.equal(block({ phase: 3, auction: null }), null);
});
void test('Distrans first bid or pass, committed aid and every special unpaid lot retain their timing boundary', () => {
void test('Distrans first bid or pass and committed aid retain the ordinary-lot timing boundary', () => {
const g = fresh();
for (const auction of [
{ ...g.auction, bid: 1, bidder: 'a' },
@@ -27,10 +28,50 @@ void test('Distrans first bid or pass, committed aid and every special unpaid lo
{ ...g.auction, cards: [] },
]) assert.match(block({ ...g, auction })!, /after bidding starts/);
assert.ok(block({ ...g, phase: 4 }));
for (const method of ['onceAround', 'silent']) {
assert.ok(block({ ...g, richeseAuction: { method, bid: 0, acted: [] } }));
assert.ok(block({ phase: 3, auction: null, richeseAuction: { method, bid: 0, acted: [] } }));
}
});
function special(method: 'onceAround' | 'silent' | 'normal', source: 'cache' | 'blackMarket' = 'cache') {
return createRicheseAuction({ event: 'lot-event', cardId: 'physical-card', source,
owner: 'r', method, eligible: ['a', 'r', 'b'], order: ['a', 'r', 'b'],
tieOrder: ['r', 'a', 'b'] });
}
void test('Distrans can be used provisionally before the first decision in a genuine Richese special lot', () => {
for (const method of ['onceAround', 'silent'] as const)
for (const source of ['cache', 'blackMarket'] as const) {
const lot = special(method, source), before = structuredClone(lot);
assert.equal(block({ phase: 3, auction: null, richeseAuction: lot, richeseFunding: {} }), null);
assert.deepEqual(lot, before);
assert.ok(block({ phase: 4, auction: null, richeseAuction: lot }));
assert.ok(block({ ...fresh(), richeseAuction: lot }));
}
const normal = special('normal', 'blackMarket'), before = structuredClone(normal);
assert.equal(block({ phase: 3, auction: null, richeseAuction: normal, richeseFunding: {} }), null);
assert.deepEqual(normal, before);
});
void test('a first pass, bid, zero sealed submission, funding or invalid special lot closes the pre-bid opportunity', () => {
const once = special('onceAround'), silent = special('silent'), normal = special('normal', 'blackMarket');
const pass = submitRicheseBid(once, { event: once.event, actor: once.active!, amount: null }, 0);
const bid = submitRicheseBid(once, { event: once.event, actor: once.active!, amount: 1 }, 1);
const sealed = submitRicheseBid(silent, { event: silent.event, actor: 'a', amount: 0 }, 0);
const normalPass = submitRicheseBid(normal, { event: normal.event, actor: normal.active!, amount: null }, 0);
const normalBid = submitRicheseBid(normal, { event: normal.event, actor: normal.active!, amount: 1 }, 1);
for (const lot of [pass, bid, sealed, normalPass, normalBid,
{ ...once, event: '' },
{ ...once, active: 'b' },
{ ...once, cardId: '' },
{ ...once, order: ['a', 'a', 'r'] },
{ ...once, tieOrder: ['a', 'r', 'r'] },
{ ...once, outcome: { kind: 'unbid' as const } },
{ ...normal, source: 'cache' as const },
{ ...normal, active: 'r' },
{ ...silent, active: 'a' },
{ ...silent, passed: ['a'] },
{ ...silent, acted: ['a'] },
{ ...silent, sealed: { a: 0 } },
]) assert.ok(block({ phase: 3, auction: null, richeseAuction: lot }));
assert.ok(block({ phase: 3, auction: null, richeseAuction: once, richeseFunding: { a: { amount: 0 } } }));
});
void test('Distrans keeps the existing paid-sale allowance independently of the later income response', () => {
+9
View File
@@ -58,6 +58,15 @@ export function actDistransPrebid(game: Game, actor: string, action: Action,
* auction and phase action retains the existing neutral native policy. */
export function nextDistransPrebidStep(game: Game): DistransPrebidStep {
if (!game.response && !game.phaseOpening && !game.pendingTreacheryDiscard) {
// The real Advanced opening has one current owner at a time. Finish that
// owner's optional Charity/Revival opportunity using the public action;
// never stage or reorder the persisted phase frame.
if (!game.decision && !game.truthtrance && !game.pendingNullentropy) {
if (game.phase === 2 && game.charityOrder?.remaining[0])
return { actor: game.charityOrder.remaining[0], action: { type: 'ready' } };
if (game.phase === 4 && game.revivalOrder?.remaining[0])
return { actor: game.revivalOrder.remaining[0], action: { type: 'ready' } };
}
const decision = game.decision;
if (decision?.kind === 'richeseCache' &&
game.richeseCache?.some(card => card.id === DISTRANS_PREBID_CARD))
+42
View File
@@ -0,0 +1,42 @@
import assert from 'node:assert/strict';
import type { Action } from '../game/engine';
import {
actDistransPrebid, advanceDistransPrebid, createDistransPrebidFixture,
distransPrebidClean, distransPrebidInventory, distransPrebidPlayer,
type DistransPrebidStep,
} from './fixture-distrans-prebid';
/** Original public Basic/Advanced table, with Guild's actual one-spice cache
* purchase of Distrans and its original dealt gift card. Continue that native
* table to turn two, then use Richese's real cache declaration; no played hand,
* purse, phase, auction, lot or submission is installed. The underlying fixture
* selects only unopened deal/Spice positions and native scalar lotteries. */
export function createDistransRichesePrebidFixture(options: {
advanced?: boolean;
method: 'onceAround' | 'silent';
}) {
const original = createDistransPrebidFixture({ advanced: options.advanced });
const actions: DistransPrebidStep[] = structuredClone(original.actions);
let game = advanceDistransPrebid(original.game,
state => state.turn === 2 && state.decision?.kind === 'richeseCache' &&
!state.response && !state.phaseOpening && !state.pendingTreacheryDiscard,
actions);
const beforeDeclaration = structuredClone(game);
const actor = game.decision!.player;
const declaration: Action = { type: 'decision', event: game.richeseBidding!.event,
card: game.richeseCache![0].id, method: options.method,
...(options.method === 'onceAround' ? { direction: 'counterclockwise' } : {}) };
game = actDistransPrebid(game, actor, declaration, actions);
assert.ok(distransPrebidClean(game));
assert.ok(game.richeseAuction);
assert.equal(game.richeseAuction.method, options.method);
assert.equal(game.richeseAuction.outcome, null);
assert.equal(game.richeseAuction.acted.length, 0);
assert.equal(Object.keys(game.richeseAuction.sealed).length, 0);
assert.ok(distransPrebidPlayer(game, original.holder).hand.some(card => card.id === original.card));
assert.ok(distransPrebidPlayer(game, original.holder).hand.some(card => card.id === original.give));
assert.deepEqual(distransPrebidInventory(game), original.originalInventory);
return { ...original, game, actions, beforeDeclaration, declaration, method: options.method,
sourceActionCount: actions.length,
staging: [...original.staging, 'The turn-two cache offer and method use actual native actions after the earned Distrans purchase.'] };
}