Support Semuta after Box searches during bidding

This commit is contained in:
Quintine committed 2026-10-28 20:40:07 +11:00
1 parent 7dfdf8d593
commit 6bf165a6aa
8 files changed
+312 -2

No files matched your search

+9
View File
@@ -16,6 +16,15 @@ open mode gates.
## Current checkpoint and work
**Semuta after Nullentropy Box during Basic auctions — 28 October. Prototyped /
Partial.** A completed paid Box search in a clean ordinary, Once Around or
Silent cache lot may offer the discarded Box to Semuta. Claim or pass keeps
the selected card, payment and original lot and funding intact. Six related
offline files pass 45 cases, including real auction actions with staged physical
Box and discard cards. The online ordering is provisional; competing sale
parents, optional modules and deployed acceptance remain open.
[Scope](SEMUTA_DISCARD_CONTINUATIONS.md#completed-box-search-during-basic-bidding--28-october-2026).
**In progress: Gitea migration — 28 October.** The local `origin` now tracks
`https://git.juiced.computer/admin/Dune`; its imported `main` is an ancestor of
the local checkout. Git push authentication and an Actions runner are not yet
+9
View File
@@ -1,5 +1,14 @@
# Rule decision index
## Semuta after a Box search during Basic auctions — 28 October 2026
Semuta has no printed general bidding-phase exclusion. A completed Box search
may provisionally offer its discarded Box while an ordinary auction or a
Richese Once Around or Silent cache lot is suspended; payment, private
selection and the original lot resume without replay. The precise online
ordering is a software interpretation rather than a publisher ruling.
[Scope](SEMUTA_DISCARD_CONTINUATIONS.md#completed-box-search-during-basic-bidding--28-october-2026).
## Semuta before Moritani allied retention — 28 October 2026
The printed powers do not specify their combined discard ordering. In the
+24
View File
@@ -2,6 +2,30 @@
Read-only engine audit, 2026-09-07. Read the complete `docs/SEMUTA_DRUG_ENGINE_AUDIT.md`, then inspected every current `discard(...)` call, direct Treachery-discard mutation and `draw(g)` caller in `game/engine.ts`, plus the Truthtrance discard callback and pure Box/Distrans results. Names below are integration anchors; approximate current lines are navigation aids only. This report adopts no additional source ruling and changes no project file.
## Completed Box search during Basic bidding — 28 October 2026
**Prototyped / Partial.** In the supported bare Basic Semuta profile, a
completed paid Nullentropy Box search may offer the discarded physical Box
to Semuta while a clean ordinary auction or a Richese Once Around or Silent
cache lot is active. The owner has already paid two spice, selected the
recovered card and shuffled the remainder. Claim or pass leaves that exchange
and payment committed and resumes the original auction without a second
search or payment.
The new continuation binds the existing auction, bidding order, public or
sealed bids, funding, pledges and player spice. Its auction clock is excluded
because the existing decision pauses it. The Box is the fresh discard; the
privately recovered card and other cards in the shuffled pile are not fresh
Semuta targets. A saved older Box-discard continuation without the optional
auction binding retains its prior behavior. This scope excludes Black Market,
completed sale and other optional-module compositions.
The scoped tests use real auction actions with conserved staging of physical
Box and discard cards. They do not constitute full-game or deployed
acceptance. The rule source does not specify an online suspension or priority
protocol for this combination; the preserved continuation is a software
interpretation. [Source scope](SEMUTA_SOURCE_UPDATE_20260907.md#completed-box-search-during-basic-auctions--28-october-2026).
## Native Moritani retention and initial battle discards — 28 October 2026
In the supported bare Basic Semuta profile, a defeated ally of native Moritani
+22
View File
@@ -2,6 +2,28 @@
7 September 2026. Read `docs/SEMUTA_DRUG_ENGINE_AUDIT.md` in full, inspected the physical card again, checked current capacity/recovery code, and freshly searched publisher sources. No project runtime edits.
## Completed Box search during Basic auctions — 28 October 2026
**Prototyped / Partial.** The native bare Basic Semuta profile may react to
the physical Nullentropy Box after its completed search during a clean
ordinary lot or a Richese Once Around or Silent cache lot. The actual owner
has paid two spice and privately selected a discard before Semuta can claim
the Box or pass. The selected card stays with the searcher and no search,
shuffle or payment repeats. The original lot, committed bids, pledges and
funding resume from their existing state.
New auction-bound receipts validate the original live lot and funding. Older
Box receipts without this optional binding retain their previous continuation;
they do not create new auction reactions. The auction clock follows the
existing pause/resume behavior. Controlled tests use original physical cards
and native bidding actions with explicitly staged Box/discard custody; they do
not certify broader optional-module combinations or deployed play.
Semuta has no printed general bidding-phase exclusion; preserving an auction
through its neutral discard reaction is a software serialization, not a
publisher ruling on the combined timing.
[Continuation scope](SEMUTA_DISCARD_CONTINUATIONS.md#completed-box-search-during-basic-bidding--28-october-2026).
## Printed-use Truthtrance promises — 27 October 2026
The combined current-turn question can name the actual completed Semuta exchange
+25 -2
View File
@@ -1647,6 +1647,8 @@ export type Game = {
finalDiscardSignature: string;
resume: NonNullable<Game['pendingNullentropy']>['resume'];
parentSignature: string;
/** New Basic bidding reactions bind the existing lot and funding. */
auctionBinding?: { kind: 'ordinary' | 'richese'; signature: string };
}
| {
kind: 'ordinaryCardDiscard';
@@ -5115,6 +5117,7 @@ function finishNullentropy(
`${owner.name} recovered one private card with Nullentropy Box, shuffled the remaining discard pile, and placed the used Box on top. The bank was paid when the search began.`,
{ faction: owner.faction, name: 'Nullentropy Box' },
);
const auctionBinding = nullentropyAuctionBinding(g);
stageTreacheryDiscard(
g,
'nullentropyBox',
@@ -5129,6 +5132,7 @@ function finishNullentropy(
finalDiscardSignature: JSON.stringify(g.discard),
resume: pending.resume,
parentSignature: nullentropyParentSignature(g, pending.resume, event),
...(auctionBinding ? {auctionBinding} : {}),
},
);
}
@@ -6157,6 +6161,18 @@ function nullentropyParentSignature(
);
return JSON.stringify({ searchEvent, resume, parents });
}
/** Clock state is deliberately excluded: it pauses and resumes while decisions
* are pending. The lot and committed resources themselves remain unchanged. */
function nullentropyAuctionBinding(g: Game): {kind:'ordinary'|'richese';signature:string} | undefined {
if (!g.semutaPreview || !bareBasicSemutaProfile(g) || g.phase !== 3 || g.currentAuctionSale || g.ixAuction)
return undefined;
const kind = g.auction && !g.richeseAuction ? 'ordinary' : supportedRicheseTruthLot(g) ? 'richese' : undefined;
if (!kind) return undefined;
return {kind, signature: JSON.stringify({turn:g.turn,phase:g.phase,active:g.active,order:g.order,
auction:g.auction,richeseAuction:g.richeseAuction,richeseBidding:g.richeseBidding,
richeseFunding:g.richeseFunding,aid:g.aid,
players:g.players.map(p => ({id:p.id,spice:p.spice,ally:p.ally}))})};
}
type OrdinaryDiscardContinuation = Extract<
NonNullable<Game['pendingTreacheryDiscard']>['continuation'],
{ kind: 'ordinaryCardDiscard' }
@@ -7929,6 +7945,10 @@ function treacheryDiscardIntegrity(g: Game) {
nullentropyParentSignature(g, c.resume, c.searchEvent),
'The completed Box search does not match its suspended controls and obligations.',
);
if (c.auctionBinding) requireRule(
JSON.stringify(c.auctionBinding) === JSON.stringify(nullentropyAuctionBinding(g)),
'The completed Box search no longer matches its original auction and funding.',
);
suspendedControlsIntegrity(g, c.resume);
} else if (continuation?.kind === 'ordinaryCardDiscard') {
const c = continuation,
@@ -9222,10 +9242,13 @@ function semutaOfferSupported(
// native Distrans still cannot produce a transfer after that bid starts.
const ordinaryAuctionDiscard = g.phase === 3 && !!g.auction && !g.richeseAuction && !g.currentAuctionSale &&
(continuation.kind === 'truthtranceDiscard' || continuation.kind === 'distransDiscard' ||
continuation.kind === 'saphoTruthOrderDiscard');
continuation.kind === 'saphoTruthOrderDiscard' ||
continuation.kind === 'nullentropyDiscard' && continuation.auctionBinding?.kind === 'ordinary');
const richeseTruthDiscard = supportedRicheseTruthLot(g) &&
(continuation.kind === 'truthtranceDiscard' || continuation.kind === 'saphoTruthOrderDiscard') &&
!!continuation.richeseLotEvent && continuation.richeseLotEvent === g.richeseAuction?.event;
const richeseBoxDiscard = supportedRicheseTruthLot(g) && continuation.kind === 'nullentropyDiscard' &&
continuation.auctionBinding?.kind === 'richese';
if (continuation.kind === 'truthtranceDiscard' &&
['bribe', 'bribeExpression', 'mixedBribe'].includes(continuation.record.question.kind) &&
(!continuation.bribeBinding || !bareBasicSemutaProfile(g) || !!g.kullPreview)) return false;
@@ -9562,7 +9585,7 @@ function semutaOfferSupported(
(advisorDiscard || auctionCostDiscard || bonusDiscard || auctionAdvantageDiscard || richeseGiftDiscard || bgDiscard || continuation.kind === 'specialKaramaDiscard' || continuation.kind === 'saphoAuctionDiscard' ||
continuation.kind === 'ixAllyCard' ||
(continuation.kind === 'karamaIncomeDiscard' &&
continuation.use.response.kind === 'emperorIncome') || richeseTruthDiscard || !g.richeseAuction) &&
continuation.use.response.kind === 'emperorIncome') || richeseTruthDiscard || richeseBoxDiscard || !g.richeseAuction) &&
(richeseGiftDiscard || combatDiscard || alliedTraitorDiscard || occupyDiscard || continuation.kind === 'saphoAggressorDiscard' ||
bgDiscard ||
continuation.kind === 'specialKaramaDiscard' ||
+1
View File
@@ -1056,6 +1056,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'A pending purchased-shipment cost does not block the search. Under the provisional refund composition, recovering that exact cost preserves its purchased price, creator and beneficiary but ends its later Guild special-stop refund, even if discarded again. Selecting another card leaves the cost’s existing claim status unchanged. Empty or only-Box searches are rejected before payment under the current unsupported-empty-search policy; there is no fallback deck draw.',
'All four AI profiles start only when their own projected availability and spice reserve allow it. They do not inspect unpaid discard contents. Once paid, they choose from their entitled cards, prefer existing functional effects over unfinished Richese faces and complete a legal selection even if only unfinished effects remain. A paid search takes priority over optional actions.',
'Whole Truthtrance questions may name a future printed Box activation. Accepted payment records that play once before private selection; it does not promise the recovered identity. Actual costs share funding with other commitments, while an unpaid unknown recovery cannot supply another effect. Native paid selection and Semuta continuation keep their priority.',
'In supported Basic Semuta games, a completed Box search during an ordinary auction or a Richese Once Around or Silent cache auction can offer the discarded Box to Semuta. The paid search and private selection remain complete; after the reaction, bidding resumes with its original lot, bids and pledged funding. This online ordering is provisional.',
'The paid search and continuations are integrated in development fixtures, with a connected full-hand prototype in both modes. Purchased-shipment cost recovery now uses the provisional nonduplication policy without blocking the search. The explicit Semuta preview can recover the used Box after a clean completed search without replaying payment or shuffle; other discard compositions remain separate. Full Richese starts and complete browser/mobile/full-game acceptance remain gated.',
],
related: [
+119
View File
@@ -0,0 +1,119 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
import {
createSemutaAuctionInspectionFixture,
semutaAuctionInspectionInventory as inventory,
semutaAuctionInspectionPlayer as player,
} from './fixture-semuta-auction-inspection';
const BOX = 'richese-nullentropy-box';
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
function rejectWithoutMutation(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
/** Real public Basic setup, paid cache Semuta acquisition, and a later real
* ordinary auction. This supplemental boundary deliberately stages a mutual
* alliance and relocates the physical Box plus two physical discards; no money,
* auction state, counters, phase or saved continuation is manufactured. */
function fixture() {
const source = createSemutaAuctionInspectionFixture();
let game = structuredClone(source.game);
while (game.response?.kind === 'atreidesAuction') {
const actor = game.players.find(row => !game.response!.passed.includes(row.id));
assert.ok(actor);
game = applyAction(game, actor.id, { type: 'passResponse' });
}
assert.equal(game.response, null);
assert.ok(game.auction && !game.richeseAuction && !game.currentAuctionSale);
const bidder = game.auction.active;
const owner = game.players.find(row => row.id !== bidder && row.id !== source.holder &&
row.spice >= 2 && row.hand.length < handLimit(row));
assert.ok(owner, 'A non-bidding, non-Semuta holder must afford the staged Box.');
const donor = game.players.find(row => row.id !== bidder && row.id !== owner.id && row.spice >= 1);
assert.ok(donor, 'A distinct player must fund the standing auction bid.');
assert.ok(!player(game, bidder).ally && !donor.ally, 'The supplemental alliance must not replace a real pact.');
player(game, bidder).ally = donor.id;
donor.ally = bidder;
game = applyAction(game, donor.id, { type: 'pledgeAid', amount: 1 });
game = applyAction(game, bidder, { type: 'bid', amount: 1, allyPayment: 1 });
assert.equal(game.auction?.bidder, bidder);
assert.equal(game.auction?.bid, 1);
assert.equal(game.auction?.allyPayment, 1);
assert.equal(game.aid[donor.id]?.amount, 1);
const zones = [game.deck, game.discard, game.richeseCache!, game.richeseRemoved!,
...game.players.map(row => row.hand)].filter(Boolean);
const boxZone = zones.find(zone => zone.some(card => card.id === BOX));
assert.ok(boxZone, 'The original physical Box must be in a movable zone.');
const [box] = boxZone.splice(boxZone.findIndex(card => card.id === BOX), 1);
player(game, owner.id).hand.push(box);
const target = game.deck.shift(), other = game.deck.shift();
assert.ok(target && other, 'The original ordinary deck must supply two discard targets.');
game.discard.push(target, other);
assert.deepEqual(inventory(game), source.originalInventory);
return { game, source, owner: owner.id, donor: donor.id, bidder, target: target.id,
lot: structuredClone(game.auction), aid: structuredClone(game.aid),
ownerSpice: player(game, owner.id).spice, originalCards: inventory(game) };
}
for (const claim of [false, true]) {
void test(`Semuta ${claim ? 'claims' : 'passes'} the paid Box during a standing Basic ordinary bid`, () => {
const f = fixture();
const paid = applyAction(f.game, f.owner, { type: 'card', card: BOX });
assert.equal(player(paid, f.owner).spice, f.ownerSpice - 2);
assert.ok(paid.pendingNullentropy);
assert.deepEqual(paid.auction, f.lot);
assert.deepEqual(paid.aid, f.aid);
const searchEvent = paid.pendingNullentropy.event;
const pending = applyAction(paid, f.owner, { type: 'decision', event: searchEvent, card: f.target });
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'nullentropyDiscard');
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
const event = pending.pendingTreacheryDiscard!.batch.event;
assert.deepEqual(pending.pendingTreacheryDiscard!.batch.entries.map(entry =>
[entry.discardedBy, entry.card.id, entry.publicFace]), [[f.owner, BOX, true]]);
assert.deepEqual(pending.auction, f.lot);
assert.deepEqual(pending.aid, f.aid);
assert.equal(player(pending, f.owner).spice, f.ownerSpice - 2);
assert.equal(player(pending, f.owner).hand.filter(card => card.id === f.target).length, 1);
assert.equal(viewGame(pending, f.source.holder).players.find(row => row.id === f.owner)?.hand, undefined);
assert.deepEqual(viewGame(pending, f.source.holder).semutaReaction?.candidates, []);
const shuffled = pending.discard.filter(card => card.id !== BOX).map(card => card.id);
rejectWithoutMutation(pending, f.source.holder, { type: 'semutaCommit', event: 'stale-event' });
rejectWithoutMutation(pending, pending.auction!.active, { type: 'passBid' });
const alteredLot = reload(pending);
alteredLot.auction!.bid += 1;
rejectWithoutMutation(alteredLot, f.source.holder, { type: 'semutaCommit', event });
const alteredAid = reload(pending);
alteredAid.aid[f.donor]!.amount += 1;
rejectWithoutMutation(alteredAid, f.source.holder, { type: 'semutaCommit', event });
let resumed = reload(pending);
if (claim) resumed = applyAction(resumed, f.source.holder, { type: 'semutaCommit', event });
else for (const row of resumed.players)
resumed = applyAction(resumed, row.id, { type: 'semutaPass', event });
assert.equal(resumed.pendingTreacheryDiscard, null);
assert.deepEqual(resumed.auction, f.lot);
assert.deepEqual(resumed.aid, f.aid);
assert.equal(player(resumed, f.owner).spice, f.ownerSpice - 2);
assert.equal(player(resumed, f.owner).hand.filter(card => card.id === f.target).length, 1);
assert.equal(player(resumed, f.source.holder).hand.some(card => card.id === BOX), claim);
assert.equal(resumed.discard.some(card => card.id === BOX), !claim);
assert.equal(resumed.discard.some(card => card.id === f.source.semuta), claim);
assert.deepEqual(resumed.discard.filter(card => card.id !== BOX && card.id !== f.source.semuta)
.map(card => card.id), shuffled);
assert.deepEqual(inventory(resumed), f.originalCards);
rejectWithoutMutation(resumed, f.source.holder, { type: 'semutaCommit', event });
rejectWithoutMutation(resumed, f.owner, { type: 'decision', event: searchEvent, card: f.target });
const active = resumed.auction!.active;
const continued = applyAction(resumed, active, { type: 'passBid' });
assert.notDeepEqual(continued.auction, f.lot, 'The same original auction continues after the reaction.');
assert.equal(player(continued, f.owner).spice, f.ownerSpice - 2);
});
}
+103
View File
@@ -0,0 +1,103 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { applyAction, handLimit, viewGame, type Action, type Game } from '../game/engine';
import {
advanceSemutaRicheseCacheKarama, createSemutaRicheseCacheKaramaFixture,
semutaRicheseCacheKaramaInventory as inventory, semutaRicheseCacheKaramaPlayer as player,
} from './fixture-semuta-richese-cache-karama';
import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share';
const clean = (game: Game) => !game.phaseOpening && !game.response && !game.decision &&
!game.pendingTreacheryDiscard && !game.pendingNullentropy && !game.truthtrance;
const reload = (game: Game): Game => JSON.parse(JSON.stringify(game));
/** A native Basic roster really buys Semuta in turn one and progresses through
* the next Richese declaration. Only the Box and original search candidates
* are staged by conserved card moves; lot, bids, wallets and clocks are real. */
function fixture(method: 'onceAround' | 'silent') {
const native = createSemutaRicheseCacheKaramaFixture();
let game = structuredClone(native.beforeDeclaration);
const box = 'richese-nullentropy-box', owner = native.richese, holder = native.holder;
assert.equal(player(game, holder).hand.some(card => card.id === native.semuta), true);
moveSemutaEmperorCard(game, box, owner);
assert.ok(player(game, owner).hand.length <= handLimit(player(game, owner)));
for (let i = 0; i < 2; i++) {
const index = game.deck.findIndex(card => card.effect !== 'nullentropyBox');
assert.ok(index >= 0);
game.discard.push(game.deck.splice(index, 1)[0]);
}
const physical = inventory(game);
game = applyAction(game, owner, { type: 'decision', event: game.richeseBidding!.event, position: 'first' });
game = advanceSemutaRicheseCacheKarama(game, state => state.decision?.kind === 'richeseCache' && !state.response && !state.phaseOpening);
const lotCard = game.richeseCache![0]; assert.ok(lotCard && lotCard.id !== box);
game = applyAction(game, owner, { type: 'decision', event: game.richeseBidding!.event,
card: lotCard.id, method, ...(method === 'onceAround' ? { direction: 'clockwise' } : {}) });
game = advanceSemutaRicheseCacheKarama(game, state => clean(state) && !!state.richeseAuction && !state.richeseAuction.outcome);
const lot = game.richeseAuction!;
assert.equal(lot.source, 'cache'); assert.equal(lot.method, method);
const bidder = method === 'onceAround' ? lot.active! : lot.eligible[0];
assert.ok(bidder);
game = applyAction(game, bidder, { type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0 });
assert.ok(clean(game) && game.richeseAuction && !game.richeseAuction.outcome);
assert.ok(player(game, owner).spice >= 2);
return { game, owner, holder, box, physical, lotCard: lotCard.id, bidder };
}
function rejectWithoutMutation(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
assert.throws(() => applyAction(game, actor, action));
assert.deepEqual(game, before);
}
for (const method of ['onceAround', 'silent'] as const) for (const claim of [false, true]) {
void test(`Semuta ${claim ? 'claims' : 'passes'} a completed Box during a Basic Richese ${method} lot`, () => {
const f = fixture(method), before = structuredClone(f.game);
const balances = Object.fromEntries(before.players.map(p => [p.id, p.spice]));
const openingDiscard = before.discard.map(card => card.id);
assert.ok(openingDiscard.length >= 2);
const paid = applyAction(before, f.owner, { type: 'card', card: f.box });
assert.equal(player(paid, f.owner).spice, balances[f.owner] - 2);
assert.ok(paid.pendingNullentropy);
assert.deepEqual(paid.richeseAuction, before.richeseAuction);
assert.deepEqual(paid.richeseFunding, before.richeseFunding);
const target = paid.discard[0].id, other = paid.discard[1].id;
rejectWithoutMutation(paid, f.holder, { type: 'decision', event: paid.pendingNullentropy.event, card: target });
const pending = applyAction(paid, f.owner, { type: 'decision', event: paid.pendingNullentropy.event, card: target });
assert.equal(pending.pendingTreacheryDiscard?.continuation.kind, 'nullentropyDiscard');
assert.equal(pending.pendingTreacheryDiscard?.reaction?.stage, 'offer');
assert.deepEqual(pending.richeseAuction, before.richeseAuction);
assert.deepEqual(pending.richeseFunding, before.richeseFunding);
assert.equal(player(pending, f.owner).hand.filter(card => card.id === target).length, 1);
assert.equal(player(pending, f.owner).hand.some(card => card.id === other), false);
assert.equal(pending.discard.filter(card => card.id === f.box).length, 1);
assert.deepEqual(viewGame(pending, f.holder).semutaReaction?.candidates, []);
assert.equal(viewGame(pending, f.holder).semutaReaction?.canCommit, true);
assert.equal(viewGame(pending, f.owner).semutaReaction?.canCommit, false);
assert.deepEqual(inventory(pending), f.physical);
const event = pending.pendingTreacheryDiscard!.batch.event;
rejectWithoutMutation(pending, f.holder, { type: 'semutaCommit', event: 'stale' });
let done = reload(pending);
if (claim) done = applyAction(done, f.holder, { type: 'semutaCommit', event });
else for (const seat of done.players) done = applyAction(done, seat.id, { type: 'semutaPass', event });
assert.equal(done.pendingTreacheryDiscard, null);
assert.deepEqual(done.richeseAuction, before.richeseAuction);
assert.deepEqual(done.richeseFunding, before.richeseFunding);
assert.equal(player(done, f.owner).spice, balances[f.owner] - 2);
assert.equal(player(done, f.owner).hand.filter(card => card.id === target).length, 1);
assert.equal(player(done, f.holder).hand.some(card => card.id === f.box), claim);
assert.equal(done.discard.some(card => card.id === f.box), !claim);
assert.deepEqual(inventory(done), f.physical);
const originalLot = done.richeseAuction!, originalEvent = originalLot.event;
for (let count = 0; done.richeseAuction?.event === originalEvent && !done.richeseAuction.outcome && count < 12; count++) {
const lot = done.richeseAuction;
const actor = method === 'onceAround' ? lot.active : lot.eligible.find(id => lot.sealed[id] === undefined);
assert.ok(actor, 'The original finite lot has an unacted bidder');
done = applyAction(done, actor, { type: 'richeseBid', event: originalEvent,
amount: method === 'onceAround' ? null : 0, allyPayment: 0 });
}
assert.ok(!done.richeseAuction || done.richeseAuction.event !== originalEvent || done.richeseAuction.outcome,
'Original bidding reaches its native settlement without replaying the Box');
assert.equal(player(done, f.owner).hand.some(card => card.id === target), true);
assert.deepEqual(inventory(done), f.physical);
});
}