Provisionally apply first/last to the current ordinary or native Advanced normal Black Market lot. Preserve bids, funding and passes; skip passed seats until a real raise. Use a local full cycle and actual post-discard hand capacity. Keep the original Black Market opener in both settlement consumers and clear ordinary overrides for the next lot. Connect persistent controls, native disposal and existing minimal AI options. Validation:10-file134/134, types/lint/build and eight original-action/rendered-table continuations. Public setup/cache1, real lobby circle choice, native Black Market and paid full hand preserve prior1,2/3/4 raises, one payment4 and original next opener or final-lot Revival. Eight minimal policies are legal. Corrected fixture opener/completion assumptions and external default-history/final-lot assertions; only runtime was rerun against identical source. Bounded consumer review found no concrete defect. Other timing scopes, direct combined Semuta claim/capture, full modes, final assurance and browser/deployed acceptance remain open. No saved-room/reset/restart/deployment operation.
162 lines
9.7 KiB
TypeScript
162 lines
9.7 KiB
TypeScript
import assert from 'node:assert/strict';
|
|
import { test } from 'node:test';
|
|
import { viewGame } from '../game/engine';
|
|
import { botActions } from '../game/bots';
|
|
import {
|
|
actSaphoCyclic, completeSaphoCyclicAuction, createSaphoCyclicAuctionFixture, saphoCyclicAction,
|
|
saphoCyclicBid, saphoCyclicLot, saphoCyclicPlayer as player, type SaphoCyclicAuctionFixture,
|
|
} from './fixture-sapho-cyclic-auction';
|
|
|
|
function source(f: SaphoCyclicAuctionFixture) {
|
|
assert.ok(f.actions.some(step => step.actor === f.owner && step.action.type === 'richeseBid' && step.action.amount === 1));
|
|
assert.ok(player(f.acquired).hand.some(card => card.id === f.card));
|
|
assert.equal(f.sourceActionCount, f.actions.length);
|
|
assert.equal(saphoCyclicLot(f.game).bid, 0);
|
|
assert.equal(player(f.game).hand.some(card => card.id === f.card), true);
|
|
if (f.kind === 'blackMarket') {
|
|
assert.equal(f.game.advanced, true);
|
|
assert.ok(f.actions.some(step => step.actor === f.owner && step.action.type === 'seatPosition' && step.action.position === 5));
|
|
assert.equal(f.lobby.playerPositions![f.owner], 5,
|
|
'The actual pre-start circle choice leaves genuine forward first/last options after the native second Storm');
|
|
assert.equal(f.blackMarketStep!.action.method, 'normal');
|
|
assert.ok(player(f.afterSetup, f.seller).hand.some(card => card.id === f.target));
|
|
assert.ok(player(f.game, f.seller).hand.some(card => card.id === f.target));
|
|
}
|
|
}
|
|
function finished(f: SaphoCyclicAuctionFixture, result: ReturnType<typeof completeSaphoCyclicAuction>) {
|
|
const before = saphoCyclicLot(result.beforeSapho), ordered = saphoCyclicLot(result.ordered);
|
|
assert.equal(before.bid, 1);
|
|
assert.equal(ordered.bid, before.bid);
|
|
assert.equal(ordered.bidder, before.bidder);
|
|
assert.deepEqual(ordered.passed, before.passed);
|
|
assert.deepEqual(ordered.funding, before.funding);
|
|
assert.deepEqual(result.ordered.order, result.beforeSapho.order);
|
|
assert.deepEqual(result.ordered.players.map(player => player.spice), result.beforeSapho.players.map(player => player.spice));
|
|
assert.equal(result.ordered.discard.filter(card => card.id === f.card).length, 1);
|
|
assert.equal(player(result.ordered).hand.some(card => card.id === f.card), false);
|
|
assert.equal(player(result.beforeSapho).spice - player(result.game).spice, 4,
|
|
'Native sale debits the final strict winning bid once');
|
|
assert.equal(player(result.game).hand.filter(card => card.id === f.target).length, 1);
|
|
assert.equal(result.game.discard.filter(card => card.id === f.card).length, 1);
|
|
assert.deepEqual(result.game.order, f.game.order);
|
|
const raises = result.bids.filter(bid => bid.step.actor === f.owner && bid.step.action.amount !== undefined && bid.step.action.amount !== null);
|
|
assert.deepEqual(raises.map(bid => bid.step.action.amount), [2, 4], 'Going first/last does not remove ordinary cyclic bidding on a later wrap');
|
|
assert.throws(() => actSaphoCyclic(result.game, f.owner, result.action), /Sapho|card|hand|opportunity|expired/i);
|
|
}
|
|
|
|
void test('genuine paid Sapho first and last preserve a prior funded raise and ordinary next-lot rotation in Basic and Advanced', () => {
|
|
for (const advanced of [false, true]) {
|
|
const f = createSaphoCyclicAuctionFixture({ advanced });
|
|
source(f);
|
|
for (const mode of ['first', 'last'] as const) {
|
|
const result = completeSaphoCyclicAuction(f, mode);
|
|
finished(f, result);
|
|
assert.equal(result.action.scope, 'cyclicAuction');
|
|
assert.equal(saphoCyclicLot(result.ordered).active === f.owner, mode === 'first');
|
|
assert.equal(result.ordered.auction!.opener, f.originalOpener);
|
|
assert.equal(result.game.auction!.index, f.game.auction!.index + 1);
|
|
assert.equal(result.game.auction!.opener, (f.originalOpener + 1) % f.game.order.length);
|
|
assert.equal(result.game.auction!.active, f.game.order[(f.originalOpener + 1) % f.game.order.length]);
|
|
assert.equal(result.game.auction!.bid, 0);
|
|
assert.equal(result.game.auction!.bidder, null);
|
|
assert.deepEqual(result.game.auction!.passed, []);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('genuine Advanced Black Market Sapho keeps the original sale price, seller payment and following normal opener', () => {
|
|
const f = createSaphoCyclicAuctionFixture({ advanced: true, blackMarket: true });
|
|
source(f);
|
|
for (const mode of ['first', 'last'] as const) {
|
|
const result = completeSaphoCyclicAuction(f, mode);
|
|
finished(f, result);
|
|
assert.equal(player(result.game, f.seller).hand.some(card => card.id === f.target), false);
|
|
assert.equal(player(result.game, f.seller).spice - player(result.beforeSapho, f.seller).spice, 4,
|
|
'The native original seller receives the full sale once');
|
|
assert.equal(result.game.richeseBidding!.blackMarketSold, true);
|
|
assert.equal(result.game.auction!.opener, (f.originalOpener + 1) % f.game.order.length);
|
|
assert.equal(result.game.auction!.active, f.game.order[(f.originalOpener + 1) % f.game.order.length]);
|
|
assert.equal(result.game.auction!.index, 0);
|
|
}
|
|
});
|
|
|
|
void test('actual current-first, highest-bidder and passed-holder positions cannot spend Sapho for a no-op or renewed chance', () => {
|
|
const f = createSaphoCyclicAuctionFixture();
|
|
const initial = saphoCyclicAction(f.game, 'first');
|
|
let current = structuredClone(f.game);
|
|
for (let count = 0; saphoCyclicLot(current).active !== f.owner && count < 8; count++) {
|
|
const step = saphoCyclicBid(current, null); current = actSaphoCyclic(current, step.actor, step.action);
|
|
}
|
|
assert.equal(saphoCyclicLot(current).active, f.owner);
|
|
assert.equal(viewGame(current, f.owner).saphoOptions.some(option => option.scope === 'cyclicAuction' && option.mode === 'first'), false);
|
|
assert.throws(() => actSaphoCyclic(current, f.owner, initial), /Sapho|opportunity|expired|no.op/i);
|
|
const highStep = saphoCyclicBid(current, 1), highest = actSaphoCyclic(current, highStep.actor, highStep.action);
|
|
assert.equal(saphoCyclicLot(highest).bidder, f.owner);
|
|
assert.equal(viewGame(highest, f.owner).saphoOptions.some(option => option.scope === 'cyclicAuction'), false);
|
|
assert.throws(() => actSaphoCyclic(highest, f.owner, initial), /Sapho|opportunity|expired|bid/i);
|
|
const passStep = saphoCyclicBid(current, null), passed = actSaphoCyclic(current, passStep.actor, passStep.action);
|
|
assert.ok(saphoCyclicLot(passed).passed.includes(f.owner));
|
|
assert.equal(viewGame(passed, f.owner).saphoOptions.some(option => option.scope === 'cyclicAuction'), false);
|
|
assert.throws(() => actSaphoCyclic(passed, f.owner, initial), /Sapho|opportunity|expired|pass/i);
|
|
for (const game of [current, highest, passed]) {
|
|
assert.ok(player(game).hand.some(card => card.id === f.card));
|
|
assert.equal(game.discard.some(card => card.id === f.card), false);
|
|
}
|
|
});
|
|
|
|
void test('a genuinely full hand from paid purchases gains one current-lot slot by discarding Sapho', () => {
|
|
const f = createSaphoCyclicAuctionFixture({ fullHand: true });
|
|
source(f);
|
|
assert.equal(player(f.game).hand.length, 4);
|
|
assert.equal(f.actions.filter(step => step.actor === f.owner && step.action.type === 'bid' && step.action.amount === 1).length, 2);
|
|
const result = completeSaphoCyclicAuction(f, 'first');
|
|
finished(f, result);
|
|
assert.equal(player(result.ordered).hand.length, 3);
|
|
assert.equal(saphoCyclicLot(result.ordered).active, f.owner);
|
|
assert.equal(player(result.game).hand.length, 4);
|
|
});
|
|
|
|
void test('all four minimal policies use legal original ordinary and Black Market cyclic Sapho options', () => {
|
|
for (const blackMarket of [false, true]) {
|
|
const f = createSaphoCyclicAuctionFixture({ advanced: blackMarket, blackMarket });
|
|
const raise = saphoCyclicBid(f.game, 1), game = actSaphoCyclic(f.game, raise.actor, raise.action);
|
|
for (const difficulty of ['Easy', 'Medium', 'Hard', 'Brutal'] as const) {
|
|
const view = viewGame(game, f.owner);
|
|
view.players.find(player => player.id === f.owner)!.bot = difficulty;
|
|
const action = botActions(view).find(action => action.type === 'card' && action.card === f.card && action.scope === 'cyclicAuction');
|
|
assert.ok(action, `${difficulty} retains the owned original cyclic Sapho option`);
|
|
const next = actSaphoCyclic(structuredClone(game), f.owner, action);
|
|
assert.deepEqual(next.order, game.order);
|
|
assert.equal(saphoCyclicLot(next).bid, 1);
|
|
assert.equal(saphoCyclicLot(next).bidder, saphoCyclicLot(game).bidder);
|
|
assert.equal(next.discard.filter(card => card.id === f.card).length, 1);
|
|
}
|
|
}
|
|
});
|
|
|
|
void test('genuine pending passes survive Sapho and cannot receive a repeat turn before a real raise', () => {
|
|
for (const blackMarket of [false,true]) {
|
|
const f = createSaphoCyclicAuctionFixture({advanced:blackMarket,blackMarket});
|
|
const raised = saphoCyclicBid(f.game,1);
|
|
let game = actSaphoCyclic(f.game,raised.actor,raised.action);
|
|
const earlier = saphoCyclicBid(game,null);
|
|
game = actSaphoCyclic(game,earlier.actor,earlier.action);
|
|
assert.equal(saphoCyclicLot(game).active,f.owner);
|
|
const before = saphoCyclicLot(game);
|
|
game = actSaphoCyclic(game,f.owner,saphoCyclicAction(game,'last'));
|
|
assert.deepEqual(saphoCyclicLot(game).passed,before.passed);
|
|
const nextPass = saphoCyclicBid(game,null);
|
|
game = actSaphoCyclic(game,nextPass.actor,nextPass.action);
|
|
assert.equal(saphoCyclicLot(game).active,f.owner,
|
|
'The earlier passed seat is skipped in the changed cycle');
|
|
const standingPrice = saphoCyclicBid(game,1), snapshot = structuredClone(game);
|
|
assert.throws(() => actSaphoCyclic(game,standingPrice.actor,standingPrice.action),/bid|raise/i);
|
|
assert.deepEqual(game,snapshot);
|
|
const newRaise = saphoCyclicBid(game,2);
|
|
game = actSaphoCyclic(game,newRaise.actor,newRaise.action);
|
|
assert.deepEqual(saphoCyclicLot(game).passed,[],
|
|
'A genuine raise reopens the previously passed native opportunity');
|
|
assert.deepEqual(game.order,f.game.order);
|
|
}
|
|
});
|