Resume Richese auctions after Distrans Semuta reaction
Verify and publish checkpoint container / container (push) Successful in 11m24s
Verify and publish checkpoint container / container (push) Successful in 11m24s
This commit is contained in:
1 parent
ccc4c7bb40
commit
f8078ed5e2
9 files changed
+245
-56
No files matched your search
@@ -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);
|
||||
});
|
||||
}
|
||||
@@ -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=/);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in new issue
Block a user