Allow Distrans between completed auction bids
Verify and publish checkpoint container / container (push) Successful in 10m44s

This commit is contained in:
Quintine committed 2026-10-29 10:34:00 +11:00
1 parent ab7a947833
commit ccc4c7bb40
16 files changed
+250 -80

No files matched your search

+2 -4
View File
@@ -67,14 +67,12 @@ 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'} closes Distrans before either card leaves custody`, () => {
void test(`genuine Advanced Black Market first ${amount === null ? 'pass' : 'bid'} keeps Distrans guarded with the Semuta preview`, () => {
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);
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.ok(player(game, f.seller).hand.some(c => c.id === f.physical.id));
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
+4 -3
View File
@@ -37,7 +37,7 @@ void test('projection exposes choices only to the holder and public history reve
assert.equal(JSON.stringify(viewGame(done,'o')).includes(card.id),false);
assert.match(done.log.at(-1)!.text,/Distrans/);assert.equal(done.log.some(l=>l.text.includes(card.name)),false);
});
void test('an untouched ordinary lot permits Distrans but its first bid or pass closes the provisional window',()=>{
void test('ordinary lots permit Distrans between completed bids and passes',()=>{
const {g,card}=fixture('richese',false,3);
assert.deepEqual(play(g,card.id).players[1].hand,[card]);
g.auction={cards:[g.deck.shift()!],index:0,bid:0,bidder:null,active:'t',passed:[],opener:0};
@@ -50,8 +50,9 @@ void test('an untouched ordinary lot permits Distrans but its first bid or pass
if(progress==='bid'){during.auction!.bid=1;during.auction!.bidder='t';}
else during.auction!.passed=['o'];
const before=structuredClone(during);
assert.throws(()=>play(during,card.id),/after bidding starts/);assert.deepEqual(during,before);
assert.match(viewGame(during,'d').distrans!.blocked!,/after bidding starts/);
assert.equal(viewGame(during,'d').distrans!.blocked,null);
const after=play(during,card.id);
assert.deepEqual(during,before);assert.deepEqual(after.auction,before.auction);
}
});
void test('an already-paid purchase response allows Distrans without changing its completed sale or creating a second response',()=>{
+3 -3
View File
@@ -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, 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, /Provisional bid timing: you may transfer between completed bid actions/);
assert.match(html, /ordinary, Once Around, Silent or normal Black Market lot/);
assert.match(html, /A pending auction decision or other interaction may still limit the choices available/);
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/);
+5 -4
View File
@@ -108,21 +108,22 @@ for (const advanced of [false, true]) {
});
for (const opening of ['bid', 'passBid'] as const) {
void test(`${mode} first real ${opening} closes Distrans until the next untouched ordinary lot`, () => {
void test(`${mode} first real ${opening} leaves the completed lot available for Distrans`, () => {
const f = createDistransPrebidFixture({ advanced });
const actor = f.game.auction!.active;
let game = actDistransPrebid(f.game, actor,
opening === 'bid' ? { type: 'bid', amount: 1, allyPayment: 0 } : { type: 'passBid' });
assert.equal(distransPrebidUntouched(game), false);
assert.ok(viewGame(game, f.holder).distrans!.blocked);
rejected(game, f.holder, f.action);
assert.equal(viewGame(game, f.holder).distrans!.blocked, null);
const offered = actDistransPrebid(game, f.holder, f.action);
assert.deepEqual(offered.auction, game.auction);
let bidder = actor;
if (opening === 'passBid') {
assert.equal(game.auction!.bid, 0);
assert.deepEqual(game.auction!.passed, [actor]);
bidder = game.auction!.active;
game = actDistransPrebid(game, bidder, { type: 'bid', amount: 1, allyPayment: 0 });
rejected(game, f.holder, f.action);
assert.equal(viewGame(game, f.holder).distrans!.blocked, null);
}
game = completeBid(game, bidder);
assert.equal(game.auction!.index, 1);
+5 -5
View File
@@ -38,8 +38,8 @@ for (const advanced of [false, true]) for (const method of ['onceAround', 'silen
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/);
assert.match(html, /ordinary, Once Around, Silent or normal Black Market lot/);
assert.match(html, /between completed bid actions/);
// Stateful selectors render above; exercise the real action leaf with the
// exact entitled recipient/card choice from the native engine projection.
@@ -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 blocked = viewGame(started, f.holder);
assert.ok(blocked.distrans?.blocked);
assert.match(transferButton(markup(blocked)), /\sdisabled=/);
const guarded = viewGame(started, f.holder);
assert.match(guarded.distrans!.blocked!, /Semuta preview/);
assert.match(transferButton(markup(guarded)), /\sdisabled=/);
});
}
+3 -3
View File
@@ -63,12 +63,12 @@ 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'} closes the Distrans window`, () => {
void test(`${prefix} first ${first === null ? 'pass' : first === 0 ? 'sealed zero' : 'bid'} keeps Distrans guarded with the Semuta preview`, () => {
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);
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));
@@ -133,6 +133,6 @@ for (const advanced of [false, true]) for (const method of ['onceAround', 'silen
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');
assert.equal(distransAuctionBlock(game), null, 'The automatic pass or zero is a completed action');
});
}
@@ -0,0 +1,54 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { handLimit, viewGame } from '../game/engine';
import {
actDistransPrebid, advanceDistransPrebid, createDistransPrebidFixture,
distransPrebidClean, distransPrebidInventory, distransPrebidPlayer as player,
distransPrebidPosition, finishDistransPrebidLot,
} from './fixture-distrans-prebid';
for (const advanced of [false, true]) {
const mode = advanced ? 'Advanced' : 'Basic';
void test(`${mode} Distrans between ordinary bids preserves the standing purchase and resumes that lot`, () => {
const f = createDistransPrebidFixture({ advanced });
const bidder = f.game.auction!.active;
let game = actDistransPrebid(f.game, bidder, { type: 'bid', amount: 1, allyPayment: 0 });
assert.equal(game.auction!.bidder, bidder);
assert.equal(game.auction!.bid, 1);
const recipient = game.players.find(seat => seat.id !== f.holder && seat.id !== bidder &&
seat.hand.length < handLimit(seat));
assert.ok(recipient);
const action = { ...f.action, target: recipient.id };
const offered = viewGame(game, f.holder).distrans!;
assert.equal(offered.blocked, null);
assert.ok(offered.choices.find(choice => choice.recipient === recipient.id)!.cards.some(card => card.id === f.give));
const original = structuredClone(game), index = game.auction!.index, turn = game.turn;
const soldCard = game.auction!.cards[index].id;
const bidderSpice = player(game, bidder).spice;
game = actDistransPrebid(game, f.holder, action);
assert.deepEqual(distransPrebidPosition(game), distransPrebidPosition(original));
assert.ok(player(game, recipient.id).hand.some(card => card.id === f.give));
assert.equal(game.discard.filter(card => card.id === f.card).length, 1);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
game = advanceDistransPrebid(game, distransPrebidClean);
assert.deepEqual(game.auction, original.auction);
game = finishDistransPrebidLot(game, turn, index);
assert.ok(player(game, bidder).hand.some(card => card.id === soldCard));
assert.equal(player(game, bidder).spice, bidderSpice - 1);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
void test(`${mode} a standing bidder retains its last hand slot through a Distrans attempt`, () => {
const f = createDistransPrebidFixture({ advanced, capacity: 'recipientLastSlot' });
const bidder = f.game.auction!.active;
assert.equal(bidder, f.recipient);
assert.equal(player(f.game, bidder).hand.length, handLimit(player(f.game, bidder)) - 1);
const game = actDistransPrebid(f.game, bidder, { type: 'bid', amount: 1, allyPayment: 0 });
const before = structuredClone(game);
const option = viewGame(game, f.holder).distrans!.choices.find(choice => choice.recipient === bidder)!;
assert.ok(!option.cards.some(card => card.id === f.give));
assert.throws(() => actDistransPrebid(game, f.holder, f.action));
assert.deepEqual(game, before);
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
});
}
@@ -0,0 +1,36 @@
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 { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid';
import { distransAuctionBlock } from '../game/distrans-timing';
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,
});
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);
}
});
}
+8 -6
View File
@@ -15,18 +15,18 @@ 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 and committed aid retain the ordinary-lot timing boundary', () => {
void test('Distrans permits completed ordinary bids and passes while rejecting invalid lots', () => {
const g = fresh();
for (const auction of [{ ...g.auction, bid: 1, bidder: 'a' }, { ...g.auction, passed: ['a'] },
{ ...g.auction, bid: 2, bidder: 'a', allyPayment: 1 }]) assert.equal(block({ ...g, auction }), null);
for (const auction of [
{ ...g.auction, bid: 1, bidder: 'a' },
{ ...g.auction, passed: ['a'] },
{ ...g.auction, bidder: 'a' },
{ ...g.auction, allyPayment: 1 },
{ ...g.auction, index: -1 },
{ ...g.auction, index: 1 },
{ ...g.auction, index: 0.5 },
{ ...g.auction, cards: [] },
]) assert.match(block({ ...g, auction })!, /after bidding starts/);
]) assert.match(block({ ...g, auction })!, /unresolved auction state/);
assert.ok(block({ ...g, phase: 4 }));
});
@@ -50,14 +50,16 @@ void test('Distrans can be used provisionally before the first decision in a gen
assert.deepEqual(normal, before);
});
void test('a first pass, bid, zero sealed submission, funding or invalid special lot closes the pre-bid opportunity', () => {
void test('completed special bids remain available while malformed lots and funding are rejected', () => {
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,
for (const lot of [pass, bid, sealed, normalPass, normalBid])
assert.equal(block({ phase: 3, auction: null, richeseAuction: lot }), null);
for (const lot of [
{ ...once, event: '' },
{ ...once, active: 'b' },
{ ...once, cardId: '' },