Allow Distrans between completed auction bids
Verify and publish checkpoint container / container (push) Successful in 10m44s
Verify and publish checkpoint container / container (push) Successful in 10m44s
This commit is contained in:
1 parent
ab7a947833
commit
ccc4c7bb40
16 files changed
+250
-80
No files matched your search
@@ -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);
|
||||
});
|
||||
|
||||
@@ -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',()=>{
|
||||
|
||||
@@ -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/);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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=/);
|
||||
});
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -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: '' },
|
||||
|
||||
Reference in new issue
Block a user