Resume Richese auctions after Distrans Semuta reaction
Verify and publish checkpoint container / container (push) Successful in 11m24s

This commit is contained in:
Quintine committed 2026-10-29 10:38:59 +11:00
1 parent ccc4c7bb40
commit f8078ed5e2
9 files changed
+245 -56

No files matched your search

+48 -5
View File
@@ -1,6 +1,8 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Action, type Game } from '../game/engine';
import { handLimit, viewGame, type Action, type Game } from '../game/engine';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share';
import {
actDistransPrebid, advanceDistransPrebid, createDistransPrebidFixture,
distransPrebidClean, distransPrebidInventory, distransPrebidPlayer as player,
@@ -67,13 +69,54 @@ void test('genuine Advanced Black Market normal lot remains unbid through earned
});
for (const amount of [null, 1] as const) {
void test(`genuine Advanced Black Market first ${amount === null ? 'pass' : 'bid'} keeps Distrans guarded with the Semuta preview`, () => {
void test(`genuine Advanced Black Market first ${amount === null ? 'pass' : 'bid'} allows Distrans while preserving the lot`, () => {
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.match(viewGame(game, f.holder).distrans!.blocked!, /Semuta preview/);
rejected(game, f.holder, f.action);
assert.ok(player(game, f.seller).hand.some(c => c.id === f.physical.id));
assert.equal(viewGame(game, f.holder).distrans!.blocked, null);
const after = actDistransPrebid(game, f.holder, f.action);
assert.deepEqual(after.richeseAuction, game.richeseAuction);
assert.deepEqual(after.richeseFunding, game.richeseFunding);
assert.ok(player(after, f.seller).hand.some(c => c.id === f.physical.id));
assert.ok(player(after, f.recipient).hand.some(c => c.id === f.give));
assert.deepEqual(distransPrebidInventory(after), f.originalInventory);
});
}
for (const claim of [false, true]) {
void test(`Advanced Black Market resumes a started lot after Semuta ${claim ? 'claims' : 'passes'} Distrans`, () => {
const f = source(), game = structuredClone(f.game);
// Controlled physical custody supplement: move the one existing Semuta to
// the seller. The lot, prices, bidders and other hands remain native.
assert.notEqual(f.physical.id, SEMUTA_DRUG_ID);
assert.ok(player(game, f.seller).hand.length < handLimit(player(game, f.seller)));
moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, f.seller);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
const lot = game.richeseAuction!, actor = lot.active!;
const started = actDistransPrebid(game, actor, { type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0 });
const originalLot = structuredClone(started.richeseAuction);
const originalFunding = structuredClone(started.richeseFunding);
const pending = actDistransPrebid(started, f.holder, f.action);
const frame = pending.pendingTreacheryDiscard; assert.ok(frame);
assert.equal(frame.continuation.kind, 'distransDiscard');
assert.equal(frame.reaction?.stage, 'offer');
assert.equal(viewGame(pending, f.seller).semutaReaction?.canCommit, true);
assert.deepEqual(pending.richeseAuction, originalLot);
assert.deepEqual(pending.richeseFunding, originalFunding);
let resumed = pending;
if (claim) resumed = actDistransPrebid(resumed, f.seller, { type: 'semutaCommit', event: frame.batch.event });
else for (const seat of resumed.players)
resumed = actDistransPrebid(resumed, seat.id, { type: 'semutaPass', event: frame.batch.event });
assert.equal(resumed.pendingTreacheryDiscard, null);
assert.deepEqual(resumed.richeseAuction, originalLot);
assert.deepEqual(resumed.richeseFunding, originalFunding);
assert.equal(player(resumed, f.seller).hand.some(c => c.id === f.card), claim);
assert.equal(resumed.discard.some(c => c.id === SEMUTA_DRUG_ID), claim);
assert.equal(resumed.discard.some(c => c.id === f.card), !claim);
assert.ok(player(resumed, f.seller).hand.some(c => c.id === f.physical.id));
assert.ok(player(resumed, f.recipient).hand.some(c => c.id === f.give));
assert.deepEqual(distransPrebidInventory(resumed), f.originalInventory);
const complete = advanceDistransPrebid(resumed, state => state.richeseAuction?.event !== lot.event);
assert.deepEqual(distransPrebidInventory(complete), f.originalInventory);
});
}
+3 -3
View File
@@ -67,8 +67,8 @@ for (const advanced of [false, true]) for (const method of ['onceAround', 'silen
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 guarded = viewGame(started, f.holder);
assert.match(guarded.distrans!.blocked!, /Semuta preview/);
assert.match(transferButton(markup(guarded)), /\sdisabled=/);
const available = viewGame(started, f.holder);
assert.equal(available.distrans!.blocked, null);
assert.doesNotMatch(transferButton(markup(available)), /\sdisabled=/);
});
}
+8 -6
View File
@@ -63,16 +63,17 @@ for (const advanced of [false, true]) for (const method of ['onceAround', 'silen
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'} keeps Distrans guarded with the Semuta preview`, () => {
void test(`${prefix} first ${first === null ? 'pass' : first === 0 ? 'sealed zero' : 'bid'} allows Distrans without changing the original special lot`, () => {
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.match(viewGame(game, f.holder).distrans!.blocked!, /Semuta preview/);
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);
assert.equal(viewGame(game, f.holder).distrans!.blocked, null);
const after = actDistransPrebid(game, f.holder, f.action);
assert.deepEqual(after.richeseAuction, game.richeseAuction);
assert.deepEqual(after.richeseFunding, game.richeseFunding);
assert.ok(player(after, f.recipient).hand.some(c => c.id === f.give));
assert.deepEqual(distransPrebidInventory(after), f.originalInventory);
});
}
void test(`${prefix} rejects a full recipient before spending either physical card`, () => {
@@ -118,6 +119,7 @@ for (const advanced of [false, true]) for (const method of ['onceAround', 'silen
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);
game = advanceDistransPrebid(game, distransPrebidClean);
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);
+112 -29
View File
@@ -1,36 +1,119 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame } from '../game/engine';
import { actDistransPrebid, distransPrebidInventory, distransPrebidPlayer as player } from './fixture-distrans-prebid';
import { handLimit, viewGame, type Action } from '../game/engine';
import { SEMUTA_DRUG_ID } from '../game/semuta-drug';
import { actDistransPrebid, advanceDistransPrebid, distransPrebidClean,
distransPrebidInventory, distransPrebidPlayer as player } from './fixture-distrans-prebid';
import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid';
import { distransAuctionBlock } from '../game/distrans-timing';
import { moveSemutaEmperorCard } from './fixture-semuta-emperor-share';
for (const method of ['onceAround', 'silent'] as const) {
void test(`a started ${method} cache auction preserves its commitments while Semuta guards Distrans`, () => {
const f = createDistransRichesePrebidFixture({ method });
const lot = f.game.richeseAuction!;
const bidder = method === 'silent'
? lot.order.find(id => id !== f.holder && player(f.game, id).spice >= 1)!
: lot.active!;
assert.ok(bidder && player(f.game, bidder).spice >= 1);
const game = actDistransPrebid(f.game, bidder, {
type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0,
for (const claim of [false, true]) {
void test(`Basic ${method} started auction resumes its original lot after Semuta ${claim ? 'claims' : 'passes'} Distrans`, () => {
const f = createDistransRichesePrebidFixture({ method });
// The game and bidding phase are reached through native actions. Stage
// only the conserved physical Semuta card into another player's hand
// before the real cache declaration; this is not an earned purchase.
let game = structuredClone(f.beforeDeclaration);
const semutaHolder = 'e';
moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, semutaHolder);
assert.ok(player(game, semutaHolder).hand.length <= handLimit(player(game, semutaHolder)));
const offer = game.richeseCache![0]; assert.ok(offer);
const declaration = { ...f.declaration, card: offer.id } as Action;
game = actDistransPrebid(game, game.decision!.player, declaration);
const lot = game.richeseAuction!;
const bidder = method === 'silent'
? lot.order.find(id => id !== f.holder && player(game, id).spice >= 1)!
: lot.active!;
assert.ok(bidder && player(game, bidder).spice >= 1);
game = actDistransPrebid(game, bidder, {
type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0,
});
assert.ok(game.richeseAuction && !game.richeseAuction.outcome);
assert.ok(game.richeseAuction.acted.includes(bidder));
const recipient = game.players.find(p => p.id !== f.holder && p.id !== bidder &&
p.hand.length < handLimit(p));
assert.ok(recipient);
const transfer = { ...f.action, target: recipient.id } as Action;
const quote = viewGame(game, f.holder).distrans!;
assert.equal(quote.blocked, null);
assert.ok(quote.choices.find(c => c.recipient === recipient.id)!.cards.some(c => c.id === f.give));
const originalLot = structuredClone(game.richeseAuction);
const originalFunding = structuredClone(game.richeseFunding);
const originalInventory = distransPrebidInventory(game);
const originalHand = structuredClone(player(game, recipient.id).hand);
const transferred = player(game, f.holder).hand.find(c => c.id === f.give)!;
game = actDistransPrebid(game, f.holder, transfer);
const pending = game.pendingTreacheryDiscard;
assert.ok(pending?.reaction, 'The public Distrans discard must offer a Semuta reaction');
assert.equal(pending.batch.entries[0].card.id, f.card);
const event = pending.batch.event;
if (method === 'silent' && claim) {
const altered = structuredClone(game);
altered.richeseAuction!.sealed[bidder] = 2;
assert.throws(() => actDistransPrebid(altered, semutaHolder, { type: 'semutaCommit', event }),
/completed private Distrans transfer|Semuta/);
}
assert.equal(viewGame(game, semutaHolder).semutaReaction!.canCommit, true);
if (claim) {
game = actDistransPrebid(game, semutaHolder, { type: 'semutaCommit', event });
} else {
for (const seat of game.players)
game = actDistransPrebid(game, seat.id, { type: 'semutaPass', event });
}
assert.equal(game.pendingTreacheryDiscard, null);
assert.deepEqual(game.richeseAuction, originalLot);
assert.deepEqual(game.richeseFunding, originalFunding);
assert.deepEqual(player(game, recipient.id).hand,
recipient.id === semutaHolder && claim
? [...originalHand.filter(c => c.id !== SEMUTA_DRUG_ID), transferred,
pending.batch.entries[0].card]
: [...originalHand, transferred]);
assert.equal(player(game, semutaHolder).hand.some(c => c.id === f.card), claim);
assert.equal(game.discard.some(c => c.id === SEMUTA_DRUG_ID), claim);
assert.equal(game.discard.some(c => c.id === f.card), !claim);
assert.deepEqual(distransPrebidInventory(game), originalInventory);
if (method === 'silent') {
const observer = game.players.find(p => p.id !== bidder)!;
const hidden = viewGame(game, observer.id).richeseAuction!;
assert.equal(hidden.ownBid, null);
assert.equal(hidden.revealedBids, null);
assert.equal('sealed' in hidden, false);
assert.equal(viewGame(game, bidder).richeseAuction!.ownBid, 1);
}
game = advanceDistransPrebid(game, state => distransPrebidClean(state) &&
state.richeseAuction?.event !== lot.event);
assert.deepEqual(distransPrebidInventory(game), originalInventory);
});
assert.ok(game.richeseAuction && !game.richeseAuction.outcome);
assert.ok(game.richeseAuction.acted.includes(bidder));
assert.equal(distransAuctionBlock(game), null, 'Completed special bid is a valid timing interval');
const before = structuredClone(game), beforeInventory = distransPrebidInventory(game);
assert.match(viewGame(game, f.holder).distrans!.blocked!, /Semuta preview/);
assert.throws(() => actDistransPrebid(game, f.holder, f.action), /Semuta preview/);
assert.deepEqual(game, before);
assert.deepEqual(distransPrebidInventory(game), beforeInventory);
if (method === 'silent') {
const observer = game.players.find(p => p.id !== bidder)!;
const hidden = viewGame(game, observer.id).richeseAuction!;
assert.equal(hidden.ownBid, null);
assert.equal(hidden.revealedBids, null);
assert.equal('sealed' in hidden, false);
assert.equal(viewGame(game, bidder).richeseAuction!.ownBid, 1);
}
});
}
}
void test('a recipient can use the received Semuta to claim Distrans during a started Silent lot', () => {
const f = createDistransRichesePrebidFixture({ method: 'silent' });
let game = structuredClone(f.beforeDeclaration);
// Stage the one physical Semuta card in the donor's hand before the real
// lot declaration; the recipient acquires it only through native Distrans.
moveSemutaEmperorCard(game, SEMUTA_DRUG_ID, f.holder);
assert.ok(player(game, f.holder).hand.length <= handLimit(player(game, f.holder)));
game = actDistransPrebid(game, game.decision!.player,
{ ...f.declaration, card: game.richeseCache![0].id } as Action);
const lot = game.richeseAuction!;
const bidder = lot.order.find(id => id !== f.holder && id !== f.recipient &&
player(game, id).spice >= 1)!;
assert.ok(bidder);
game = actDistransPrebid(game, bidder,
{ type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0 });
const originalLot = structuredClone(game.richeseAuction), funding = structuredClone(game.richeseFunding);
const inventory = distransPrebidInventory(game);
game = actDistransPrebid(game, f.holder, { ...f.action, give: SEMUTA_DRUG_ID } as Action);
const pending = game.pendingTreacheryDiscard!;
assert.ok(pending?.reaction);
assert.equal(viewGame(game, f.recipient).semutaReaction!.canCommit, true);
game = actDistransPrebid(game, f.recipient, { type: 'semutaCommit', event: pending.batch.event });
assert.equal(game.pendingTreacheryDiscard, null);
assert.ok(player(game, f.recipient).hand.some(c => c.id === f.card));
assert.ok(game.discard.some(c => c.id === SEMUTA_DRUG_ID));
assert.deepEqual(game.richeseAuction, originalLot);
assert.deepEqual(game.richeseFunding, funding);
assert.deepEqual(distransPrebidInventory(game), inventory);
});