Allow Distrans before Richese bidding starts
This commit is contained in:
1 parent
813a19bb1d
commit
f50e436506
14 files changed
+511
-24
No files matched your search
@@ -0,0 +1,81 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { viewGame, type Action, type Game } from '../game/engine';
|
||||
import {
|
||||
actDistransPrebid, advanceDistransPrebid, createDistransPrebidFixture,
|
||||
distransPrebidClean, distransPrebidInventory, distransPrebidPlayer as player,
|
||||
distransPrebidPosition, type DistransPrebidStep,
|
||||
} from './fixture-distrans-prebid';
|
||||
|
||||
/** The original Advanced public table actually sells Distrans to Guild for one
|
||||
* spice in the first cache auction. Continue through real phase actions to the
|
||||
* next turn; Richese offers its own originally dealt card in the native Black
|
||||
* Market. Only the underlying fixture's unopened card/Spice order and scalar
|
||||
* lotteries are selected. No played hand, price, auction or phase is installed. */
|
||||
function source() {
|
||||
const f = createDistransPrebidFixture({ advanced: true });
|
||||
const actions: DistransPrebidStep[] = structuredClone(f.actions);
|
||||
let game = advanceDistransPrebid(f.game, state => state.turn === 2 && state.phase === 3 &&
|
||||
!state.response && !state.phaseOpening && !state.pendingTreacheryDiscard &&
|
||||
state.decision?.kind === 'richeseBlackMarket', actions);
|
||||
const beforeMarket = structuredClone(game);
|
||||
const seller = game.decision!.player;
|
||||
const physical = player(game, seller).hand[0]; assert.ok(physical);
|
||||
const declaration: Action = { type: 'decision', event: game.richeseBidding!.event,
|
||||
card: physical.id, method: 'normal', direction: 'clockwise' };
|
||||
game = actDistransPrebid(game, seller, declaration, actions);
|
||||
game = advanceDistransPrebid(game, state => distransPrebidClean(state) &&
|
||||
state.richeseAuction?.source === 'blackMarket' && state.richeseAuction.method === 'normal', actions);
|
||||
const lot = game.richeseAuction!;
|
||||
assert.equal(lot.cardId, physical.id);
|
||||
assert.equal(lot.bid, 0); assert.equal(lot.bidder, null);
|
||||
assert.deepEqual(lot.passed, []); assert.deepEqual(lot.acted, []);
|
||||
assert.ok(player(game, seller).hand.some(c => c.id === physical.id));
|
||||
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);
|
||||
return { ...f, game, actions, beforeMarket, seller, physical, declaration };
|
||||
}
|
||||
function rejected(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => actDistransPrebid(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
|
||||
void test('genuine Advanced Black Market normal lot remains unbid through earned Distrans transfer', () => {
|
||||
const f = source(), before = structuredClone(f.game);
|
||||
assert.equal(f.beforeMarket.decision?.kind, 'richeseBlackMarket');
|
||||
assert.ok(f.acquisition.actions.some(step => step.actor === f.holder && step.action.type === 'richeseBid' && step.action.amount === 1));
|
||||
assert.equal(viewGame(before, f.holder).distrans!.blocked, null);
|
||||
assert.ok(viewGame(before, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!.cards.some(c => c.id === f.give));
|
||||
let game = actDistransPrebid(f.game, f.holder, f.action);
|
||||
assert.deepEqual(f.game, before);
|
||||
game = advanceDistransPrebid(game, distransPrebidClean);
|
||||
assert.deepEqual(distransPrebidPosition(game), distransPrebidPosition(before));
|
||||
assert.deepEqual(player(game, f.holder).hand, player(before, f.holder).hand.filter(c => c.id !== f.card && c.id !== f.give));
|
||||
assert.deepEqual(player(game, f.recipient).hand, [...player(before, f.recipient).hand,
|
||||
player(before, f.holder).hand.find(c => c.id === f.give)!]);
|
||||
assert.ok(player(game, f.seller).hand.some(c => c.id === f.physical.id));
|
||||
assert.equal(game.discard.filter(c => c.id === f.card).length, 1);
|
||||
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
|
||||
const lot = game.richeseAuction!, actor = lot.active!; assert.ok(actor);
|
||||
game = actDistransPrebid(game, actor, { type: 'richeseBid', event: lot.event, amount: 1, allyPayment: 0 });
|
||||
assert.equal(game.richeseAuction!.bid, 1);
|
||||
assert.equal(game.richeseAuction!.bidder, actor);
|
||||
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
|
||||
rejected(game, f.holder, f.action);
|
||||
});
|
||||
|
||||
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`, () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
@@ -81,8 +81,8 @@ void test('an entitled untouched ordinary lot keeps the transfer enabled in Basi
|
||||
}
|
||||
});
|
||||
|
||||
void test('live bid, pass-only and special-auction blocks come from the authoritative projection', () => {
|
||||
for (const context of ['live bid', 'pass-only lot', 'special auction']) {
|
||||
void test('live bid, pass-only and started special-auction blocks come from the authoritative projection', () => {
|
||||
for (const context of ['live bid', 'pass-only lot', 'started special auction']) {
|
||||
const game = projected();
|
||||
const reason = `Controlled ${context}: Distrans is unavailable until this lot resolves.`;
|
||||
if (context === 'live bid') { game.auction!.bid = 2; game.auction!.bidder = 't'; }
|
||||
@@ -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 auction lot/);
|
||||
assert.match(html, /Once anyone bids or passes, that lot stays blocked while unresolved/);
|
||||
assert.match(html, /Special auctions keep their existing restrictions/);
|
||||
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, /No spice is paid/);
|
||||
assert.match(html, /without a recipient confirmation or Karama response/);
|
||||
assert.doesNotMatch(html, /Confirm transfer|Accept transfer|Acknowledge|countdown/);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { registerHooks } from 'node:module';
|
||||
import { createElement } from 'react';
|
||||
import { renderToStaticMarkup } from 'react-dom/server';
|
||||
import { DistransTransferAction, type DistransTransferActionProps } from '../components/distrans-transfer-action';
|
||||
import { viewGame, type Action, type GameView } from '../game/engine';
|
||||
import { actDistransPrebid, distransPrebidInventory, distransPrebidPlayer } from './fixture-distrans-prebid';
|
||||
import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid';
|
||||
|
||||
const aliases = registerHooks({
|
||||
resolve(specifier, context, next) {
|
||||
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier, context);
|
||||
},
|
||||
});
|
||||
const { Distrans } = await import('../components/distrans');
|
||||
aliases.deregister();
|
||||
|
||||
function markup(game: GameView) {
|
||||
return renderToStaticMarkup(createElement(Distrans, { game, busy: false, act() {} }));
|
||||
}
|
||||
function transferButton(html: string) {
|
||||
const button = html.match(/<button\b(?=[^>]*data-testid="distrans-transfer")[^>]*>[\s\S]*?<\/button>/)?.[0];
|
||||
assert.ok(button, 'The projected transfer action is present');
|
||||
return button;
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) for (const method of ['onceAround', 'silent'] as const) {
|
||||
void test(`${advanced ? 'Advanced' : 'Basic'} ${method} native unopened lot projects and executes the Distrans control`, () => {
|
||||
const f = createDistransRichesePrebidFixture({ advanced, method });
|
||||
const before = structuredClone(f.game), view = viewGame(f.game, f.holder), quote = view.distrans;
|
||||
assert.ok(quote);
|
||||
assert.equal(quote.blocked, null);
|
||||
const choice = quote.choices.find(candidate => candidate.recipient === f.recipient);
|
||||
assert.ok(choice);
|
||||
assert.equal(choice.blocked, null);
|
||||
const given = choice.cards.find(card => card.id === f.give);
|
||||
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/);
|
||||
|
||||
// Stateful selectors render above; exercise the real action leaf with the
|
||||
// exact entitled recipient/card choice from the native engine projection.
|
||||
const actions: Action[] = [];
|
||||
const props: DistransTransferActionProps = {
|
||||
card: quote.card,
|
||||
recipient: { id: choice.recipient, name: view.players.find(player => player.id === choice.recipient)?.name },
|
||||
given, reason: null, reasonId: 'native-richese-reason', busy: false,
|
||||
act(action) { actions.push(action); },
|
||||
};
|
||||
const button = DistransTransferAction(props);
|
||||
button.props.onClick();
|
||||
assert.deepEqual(actions, [f.action]);
|
||||
const after = actDistransPrebid(f.game, f.holder, actions[0]);
|
||||
assert.deepEqual(f.game, before);
|
||||
assert.ok(!distransPrebidPlayer(after, f.holder).hand.some(card => card.id === f.give || card.id === f.card));
|
||||
assert.equal(distransPrebidPlayer(after, f.recipient).hand.filter(card => card.id === f.give).length, 1);
|
||||
assert.equal(after.discard.filter(card => card.id === f.card).length, 1);
|
||||
assert.deepEqual(distransPrebidInventory(after), f.originalInventory);
|
||||
assert.equal(after.richeseAuction?.event, before.richeseAuction?.event);
|
||||
assert.equal(after.richeseAuction?.method, method);
|
||||
|
||||
const lot = before.richeseAuction!;
|
||||
const actor = method === 'onceAround' ? lot.active! :
|
||||
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=/);
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { handLimit, viewGame, type Action, type Game } from '../game/engine';
|
||||
import { botActions } from '../game/bots';
|
||||
import { DIFFICULTIES } from '../game/bot-profiles';
|
||||
import { distransAuctionBlock } from '../game/distrans-timing';
|
||||
import {
|
||||
actDistransPrebid, advanceDistransPrebid, distransPrebidClean,
|
||||
distransPrebidInventory, distransPrebidPlayer as player, distransPrebidPosition,
|
||||
} from './fixture-distrans-prebid';
|
||||
import { createDistransRichesePrebidFixture } from './fixture-distrans-richese-prebid';
|
||||
|
||||
const reload = (g: Game): Game => JSON.parse(JSON.stringify(g));
|
||||
function rejected(game: Game, actor: string, action: Action) {
|
||||
const before = structuredClone(game);
|
||||
assert.throws(() => actDistransPrebid(game, actor, action));
|
||||
assert.deepEqual(game, before);
|
||||
}
|
||||
function firstSubmission(game: Game, amount?: number | null) {
|
||||
const lot = game.richeseAuction!;
|
||||
const actor = lot.method === 'silent'
|
||||
? lot.order.find(id => lot.eligible.includes(id) && !Object.hasOwn(lot.sealed, id))!
|
||||
: lot.active!;
|
||||
assert.ok(actor);
|
||||
return { actor, action: { type: 'richeseBid', event: lot.event,
|
||||
amount: amount === undefined ? (lot.method === 'silent' ? 0 : null) : amount,
|
||||
allyPayment: 0 } as Action };
|
||||
}
|
||||
|
||||
for (const advanced of [false, true]) for (const method of ['onceAround', 'silent'] as const) {
|
||||
const prefix = `${advanced ? 'Advanced' : 'Basic'} native ${method}`;
|
||||
void test(`${prefix} preserves the unbid original cache lot through a genuine Distrans transfer`, () => {
|
||||
const f = createDistransRichesePrebidFixture({ advanced, method });
|
||||
assert.equal(f.initial.status, 'lobby');
|
||||
assert.equal(f.acquisition.before.richeseCache!.some(card => card.id === f.card), true);
|
||||
assert.ok(f.acquisition.actions.some(step => step.actor === f.holder && step.action.type === 'richeseBid' && step.action.amount === 1));
|
||||
assert.equal(f.beforeDeclaration.turn, 2);
|
||||
assert.equal(f.beforeDeclaration.decision?.kind, 'richeseCache');
|
||||
const before = structuredClone(f.game);
|
||||
assert.equal(viewGame(before, f.holder).distrans!.blocked, null);
|
||||
assert.ok(viewGame(before, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!.cards.some(c => c.id === f.give));
|
||||
let game = actDistransPrebid(f.game, f.holder, f.action);
|
||||
assert.deepEqual(f.game, before);
|
||||
game = advanceDistransPrebid(game, distransPrebidClean);
|
||||
assert.deepEqual(distransPrebidPosition(game), distransPrebidPosition(before));
|
||||
assert.deepEqual(player(game, f.holder).hand, player(before, f.holder).hand.filter(c => c.id !== f.card && c.id !== f.give));
|
||||
assert.deepEqual(player(game, f.recipient).hand, [...player(before, f.recipient).hand,
|
||||
player(before, f.holder).hand.find(c => c.id === f.give)!]);
|
||||
assert.equal(game.discard.filter(c => c.id === f.card).length, 1);
|
||||
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
|
||||
rejected(game, f.holder, f.action);
|
||||
const start = firstSubmission(game);
|
||||
for (const difficulty of DIFFICULTIES) {
|
||||
const view = viewGame(game, start.actor);
|
||||
view.players.find(p => p.id === start.actor)!.bot = difficulty;
|
||||
const action = botActions(view)[0];
|
||||
assert.equal(action?.type, 'richeseBid', `${difficulty} can act on the original lot`);
|
||||
const next = actDistransPrebid(game, start.actor, action);
|
||||
assert.ok(next.richeseAuction?.acted.includes(start.actor));
|
||||
}
|
||||
game = actDistransPrebid(game, start.actor, start.action);
|
||||
assert.ok(game.richeseAuction?.acted.includes(start.actor));
|
||||
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`, () => {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
void test(`${prefix} rejects a full recipient before spending either physical card`, () => {
|
||||
const f = createDistransRichesePrebidFixture({ advanced, method });
|
||||
// Controlled custody supplement: move real undealt deck cards into the
|
||||
// recipient hand. This is not claimed as an earned acquisition sequence.
|
||||
const game = structuredClone(f.game), recipient = player(game, f.recipient);
|
||||
while (recipient.hand.length < handLimit(recipient)) recipient.hand.push(game.deck.shift()!);
|
||||
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
|
||||
const choice = viewGame(game, f.holder).distrans!.choices.find(c => c.recipient === f.recipient)!;
|
||||
assert.ok(choice.blocked);
|
||||
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);
|
||||
});
|
||||
void test(`${prefix} fills the active recipient, automatically closes that bid and keeps an originally full donor ineligible`, () => {
|
||||
const f = createDistransRichesePrebidFixture({ advanced, method });
|
||||
// Controlled custody supplement: move only actual undealt deck cards to
|
||||
// hands. These are not claimed as earned purchases. The declaration and
|
||||
// transfer still use real actions and preserve the physical inventory.
|
||||
const staged = structuredClone(f.beforeDeclaration), donor = player(staged, f.holder);
|
||||
while (donor.hand.length < handLimit(donor)) {
|
||||
const card = staged.deck.shift(); assert.ok(card); donor.hand.push(card);
|
||||
}
|
||||
assert.deepEqual(distransPrebidInventory(staged), f.originalInventory);
|
||||
let game = actDistransPrebid(staged, staged.decision!.player, f.declaration);
|
||||
const lot = game.richeseAuction!;
|
||||
assert.ok(lot);
|
||||
assert.ok(!lot.eligible.includes(f.holder), 'The full donor was excluded at declaration');
|
||||
const recipientId = method === 'silent'
|
||||
? lot.order.find(id => lot.eligible.includes(id) && id !== f.holder)!
|
||||
: lot.active!;
|
||||
assert.ok(recipientId && recipientId !== f.holder);
|
||||
const recipient = player(game, recipientId);
|
||||
while (recipient.hand.length < handLimit(recipient) - 1) {
|
||||
const card = game.deck.shift(); assert.ok(card); recipient.hand.push(card);
|
||||
}
|
||||
assert.equal(recipient.hand.length, handLimit(recipient) - 1);
|
||||
assert.deepEqual(distransPrebidInventory(game), f.originalInventory);
|
||||
const originalLot = structuredClone(lot);
|
||||
const action: Action = { ...f.action, target: recipientId };
|
||||
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);
|
||||
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);
|
||||
const next = game.richeseAuction!;
|
||||
assert.ok(next);
|
||||
assert.equal(next.event, originalLot.event);
|
||||
assert.equal(next.cardId, originalLot.cardId);
|
||||
assert.equal(next.source, originalLot.source);
|
||||
assert.deepEqual(next.order, originalLot.order);
|
||||
assert.deepEqual(next.eligible, originalLot.eligible);
|
||||
assert.deepEqual(next.tieOrder, originalLot.tieOrder);
|
||||
assert.ok(!next.eligible.includes(f.holder), 'Freeing space does not add a bidder to this lot');
|
||||
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');
|
||||
});
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import test from 'node:test';
|
||||
import { distransAuctionBlock as block } from '../game/distrans-timing';
|
||||
import { createRicheseAuction, submitRicheseBid } from '../game/richese-auction';
|
||||
|
||||
const fresh = () => ({ phase: 3, auction: { cards: ['original-lot'], index: 0,
|
||||
bid: 0, bidder: null as string | null, active: 'a', passed: [] as string[],
|
||||
@@ -14,7 +15,7 @@ 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, committed aid and every special unpaid lot retain their timing boundary', () => {
|
||||
void test('Distrans first bid or pass and committed aid retain the ordinary-lot timing boundary', () => {
|
||||
const g = fresh();
|
||||
for (const auction of [
|
||||
{ ...g.auction, bid: 1, bidder: 'a' },
|
||||
@@ -27,10 +28,50 @@ void test('Distrans first bid or pass, committed aid and every special unpaid lo
|
||||
{ ...g.auction, cards: [] },
|
||||
]) assert.match(block({ ...g, auction })!, /after bidding starts/);
|
||||
assert.ok(block({ ...g, phase: 4 }));
|
||||
for (const method of ['onceAround', 'silent']) {
|
||||
assert.ok(block({ ...g, richeseAuction: { method, bid: 0, acted: [] } }));
|
||||
assert.ok(block({ phase: 3, auction: null, richeseAuction: { method, bid: 0, acted: [] } }));
|
||||
}
|
||||
});
|
||||
|
||||
function special(method: 'onceAround' | 'silent' | 'normal', source: 'cache' | 'blackMarket' = 'cache') {
|
||||
return createRicheseAuction({ event: 'lot-event', cardId: 'physical-card', source,
|
||||
owner: 'r', method, eligible: ['a', 'r', 'b'], order: ['a', 'r', 'b'],
|
||||
tieOrder: ['r', 'a', 'b'] });
|
||||
}
|
||||
|
||||
void test('Distrans can be used provisionally before the first decision in a genuine Richese special lot', () => {
|
||||
for (const method of ['onceAround', 'silent'] as const)
|
||||
for (const source of ['cache', 'blackMarket'] as const) {
|
||||
const lot = special(method, source), before = structuredClone(lot);
|
||||
assert.equal(block({ phase: 3, auction: null, richeseAuction: lot, richeseFunding: {} }), null);
|
||||
assert.deepEqual(lot, before);
|
||||
assert.ok(block({ phase: 4, auction: null, richeseAuction: lot }));
|
||||
assert.ok(block({ ...fresh(), richeseAuction: lot }));
|
||||
}
|
||||
const normal = special('normal', 'blackMarket'), before = structuredClone(normal);
|
||||
assert.equal(block({ phase: 3, auction: null, richeseAuction: normal, richeseFunding: {} }), null);
|
||||
assert.deepEqual(normal, before);
|
||||
});
|
||||
|
||||
void test('a first pass, bid, zero sealed submission, funding or invalid special lot closes the pre-bid opportunity', () => {
|
||||
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,
|
||||
{ ...once, event: '' },
|
||||
{ ...once, active: 'b' },
|
||||
{ ...once, cardId: '' },
|
||||
{ ...once, order: ['a', 'a', 'r'] },
|
||||
{ ...once, tieOrder: ['a', 'r', 'r'] },
|
||||
{ ...once, outcome: { kind: 'unbid' as const } },
|
||||
{ ...normal, source: 'cache' as const },
|
||||
{ ...normal, active: 'r' },
|
||||
{ ...silent, active: 'a' },
|
||||
{ ...silent, passed: ['a'] },
|
||||
{ ...silent, acted: ['a'] },
|
||||
{ ...silent, sealed: { a: 0 } },
|
||||
]) assert.ok(block({ phase: 3, auction: null, richeseAuction: lot }));
|
||||
assert.ok(block({ phase: 3, auction: null, richeseAuction: once, richeseFunding: { a: { amount: 0 } } }));
|
||||
});
|
||||
|
||||
void test('Distrans keeps the existing paid-sale allowance independently of the later income response', () => {
|
||||
|
||||
@@ -58,6 +58,15 @@ export function actDistransPrebid(game: Game, actor: string, action: Action,
|
||||
* auction and phase action retains the existing neutral native policy. */
|
||||
export function nextDistransPrebidStep(game: Game): DistransPrebidStep {
|
||||
if (!game.response && !game.phaseOpening && !game.pendingTreacheryDiscard) {
|
||||
// The real Advanced opening has one current owner at a time. Finish that
|
||||
// owner's optional Charity/Revival opportunity using the public action;
|
||||
// never stage or reorder the persisted phase frame.
|
||||
if (!game.decision && !game.truthtrance && !game.pendingNullentropy) {
|
||||
if (game.phase === 2 && game.charityOrder?.remaining[0])
|
||||
return { actor: game.charityOrder.remaining[0], action: { type: 'ready' } };
|
||||
if (game.phase === 4 && game.revivalOrder?.remaining[0])
|
||||
return { actor: game.revivalOrder.remaining[0], action: { type: 'ready' } };
|
||||
}
|
||||
const decision = game.decision;
|
||||
if (decision?.kind === 'richeseCache' &&
|
||||
game.richeseCache?.some(card => card.id === DISTRANS_PREBID_CARD))
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
import assert from 'node:assert/strict';
|
||||
import type { Action } from '../game/engine';
|
||||
import {
|
||||
actDistransPrebid, advanceDistransPrebid, createDistransPrebidFixture,
|
||||
distransPrebidClean, distransPrebidInventory, distransPrebidPlayer,
|
||||
type DistransPrebidStep,
|
||||
} from './fixture-distrans-prebid';
|
||||
|
||||
/** Original public Basic/Advanced table, with Guild's actual one-spice cache
|
||||
* purchase of Distrans and its original dealt gift card. Continue that native
|
||||
* table to turn two, then use Richese's real cache declaration; no played hand,
|
||||
* purse, phase, auction, lot or submission is installed. The underlying fixture
|
||||
* selects only unopened deal/Spice positions and native scalar lotteries. */
|
||||
export function createDistransRichesePrebidFixture(options: {
|
||||
advanced?: boolean;
|
||||
method: 'onceAround' | 'silent';
|
||||
}) {
|
||||
const original = createDistransPrebidFixture({ advanced: options.advanced });
|
||||
const actions: DistransPrebidStep[] = structuredClone(original.actions);
|
||||
let game = advanceDistransPrebid(original.game,
|
||||
state => state.turn === 2 && state.decision?.kind === 'richeseCache' &&
|
||||
!state.response && !state.phaseOpening && !state.pendingTreacheryDiscard,
|
||||
actions);
|
||||
const beforeDeclaration = structuredClone(game);
|
||||
const actor = game.decision!.player;
|
||||
const declaration: Action = { type: 'decision', event: game.richeseBidding!.event,
|
||||
card: game.richeseCache![0].id, method: options.method,
|
||||
...(options.method === 'onceAround' ? { direction: 'counterclockwise' } : {}) };
|
||||
game = actDistransPrebid(game, actor, declaration, actions);
|
||||
assert.ok(distransPrebidClean(game));
|
||||
assert.ok(game.richeseAuction);
|
||||
assert.equal(game.richeseAuction.method, options.method);
|
||||
assert.equal(game.richeseAuction.outcome, null);
|
||||
assert.equal(game.richeseAuction.acted.length, 0);
|
||||
assert.equal(Object.keys(game.richeseAuction.sealed).length, 0);
|
||||
assert.ok(distransPrebidPlayer(game, original.holder).hand.some(card => card.id === original.card));
|
||||
assert.ok(distransPrebidPlayer(game, original.holder).hand.some(card => card.id === original.give));
|
||||
assert.deepEqual(distransPrebidInventory(game), original.originalInventory);
|
||||
return { ...original, game, actions, beforeDeclaration, declaration, method: options.method,
|
||||
sourceActionCount: actions.length,
|
||||
staging: [...original.staging, 'The turn-two cache offer and method use actual native actions after the earned Distrans purchase.'] };
|
||||
}
|
||||
Reference in new issue
Block a user