Support Sapho Truth order during Richese cache auctions

This commit is contained in:
Quintine committed 2026-10-27 18:55:42 +11:00
1 parent 8e16542784
commit 6a31315c95
17 files changed
+420 -25

No files matched your search

+73
View File
@@ -0,0 +1,73 @@
import assert from 'node:assert/strict';
import { type Game } from '../game/engine';
import { createTruthRicheseBidFixture } from './fixture-truth-richese-bid';
import { createTruthRicheseSilentFixture } from './fixture-truth-richese-silent';
import { act, inventory, player } from './fixture-truth-richese-bid';
import { truthOrderFrame } from './fixture-sapho-truth-order';
export { act, inventory, player };
/** Native public R/A/E setup and actual started first cache lot. Controlled,
* conserved custody supplements: swap the original cached Sapho for Emperor's
* dealt card; move an undealt original Truth to his vacant hand slot; optionally
* swap original cached Semuta for Richese's dealt card. None is represented as
* an earned purchase. The other actual dealt Truth, lot and funds stay native. */
export function createSaphoTruthRicheseFixture(options: {
advanced?: boolean; method?: 'onceAround' | 'silent'; semuta?: boolean; standingBid?: boolean;
} = {}) {
const advanced = options.advanced ?? false, method = options.method ?? 'onceAround';
const source = method === 'silent' ? createTruthRicheseSilentFixture(advanced) : createTruthRicheseBidFixture(advanced);
let game = structuredClone(source.game);
const owner = source.target, other = source.asker, semutaHolder = source.owner, lot = source.lot;
const cache = game.richeseCache!;
const saphoAt = cache.findIndex(card => card.effect === 'juiceOfSapho');
assert.ok(saphoAt >= 0, 'The original cache must contain Sapho');
const receiver = player(game,owner);
assert.ok(receiver.hand.length);
const [sapho] = cache.splice(saphoAt,1), [exchange] = receiver.hand.splice(0,1);
receiver.hand.push(sapho); cache.push(exchange);
const truthAt = game.deck.findIndex(card => card.effect === 'truthtrance');
assert.ok(truthAt >= 0, 'A second original Truth is available in the undealt deck');
const [truth] = game.deck.splice(truthAt,1); receiver.hand.push(truth);
let semuta: string | null = null;
if (options.semuta) {
const at = cache.findIndex(card => card.effect === 'semutaDrug'); assert.ok(at >= 0);
const claimant = player(game,semutaHolder); assert.ok(claimant.hand.length);
const [drug] = cache.splice(at,1), [held] = claimant.hand.splice(0,1);
claimant.hand.push(drug); cache.push(held); semuta = drug.id;
}
assert.deepEqual(inventory(game),source.originalCards);
if (options.standingBid) {
const bidder = method === 'silent' ? semutaHolder : game.richeseAuction!.active!;
game = act(game,bidder,{type:'richeseBid',event:lot,amount:1,allyPayment:0});
assert.ok(game.richeseAuction && !game.richeseAuction.outcome);
}
const mode = game.order.indexOf(owner) > game.order.indexOf(other) ? 'first' as const : 'last' as const;
const parent = {
turn:game.turn, phase:game.phase, active:game.active, order:structuredClone(game.order),
auction:structuredClone(game.richeseAuction), funding:structuredClone(game.richeseFunding),
bidding:structuredClone(game.richeseBidding), aid:structuredClone(game.aid),
offered:structuredClone(game.richeseOfferedCard),
players:game.players.map(row => ({id:row.id,spice:row.spice,reserves:row.reserves,tanks:row.tanks})),
};
const beforeTruth = structuredClone(game);
for (const [actor,card] of [[owner,truth.id],[other,source.truth]] as const)
game = act(game,actor,{type:'card',card});
while (game.truthtrance?.stage === 'priority') {
const next = game.players.find(row => !game.truthtrance!.passed.includes(row.id)); assert.ok(next);
game = act(game,next.id,{type:'truthPass'});
}
assert.deepEqual([...truthOrderFrame(game).holders].sort(),[owner,other].sort());
assertRicheseParent(game,parent);
return {game,source,advanced,method,owner,other,semutaHolder,card:sapho.id,semuta,truth:truth.id,
otherTruth:source.truth,lot,mode,parent,beforeTruth,originalCards:source.originalCards,
disclosure:'Actual public setup and native cache lot; labelled conserved cache/hand swaps and undealt Truth placement.'};
}
export type SaphoTruthRicheseFixture = ReturnType<typeof createSaphoTruthRicheseFixture>;
export function assertRicheseParent(game: Game, parent: SaphoTruthRicheseFixture['parent']) {
assert.equal(game.turn,parent.turn); assert.equal(game.phase,parent.phase); assert.equal(game.active,parent.active);
assert.deepEqual(game.order,parent.order); assert.deepEqual(game.richeseAuction,parent.auction);
assert.deepEqual(game.richeseFunding,parent.funding); assert.deepEqual(game.richeseBidding,parent.bidding);
assert.deepEqual(game.aid,parent.aid); assert.deepEqual(game.richeseOfferedCard,parent.offered);
assert.deepEqual(game.players.map(row => ({id:row.id,spice:row.spice,reserves:row.reserves,tanks:row.tanks})),parent.players);
}
+3 -2
View File
@@ -4,12 +4,13 @@ import {
actTruthtranceRichesePromises as act,
advanceTruthtranceRichesePromises as advance,
createTruthtranceRichesePromisesFixture,
passTruthRicheseFreshDiscard,
truthtranceRichesePromisesClean as clean,
truthtranceRichesePromisesInventory as inventory,
truthtranceRichesePromisesPlayer as player,
} from './fixture-truthtrance-richese-promises';
export { act, advance, clean, inventory, player };
export { act, advance, clean, inventory, player, passTruthRicheseFreshDiscard };
/** Actual public R/A/E Basic or Advanced setup. The parent fixture selects only
* unopened original deal/Spice positions and native scalar lotteries. Use its
@@ -60,7 +61,7 @@ export function bindTruthRicheseBid(f: TruthRicheseBidFixture, answer: 'yes' | '
source = f.game, target = f.target) {
const asked = askTruthRicheseBid(f, target, minimum, source);
assert.ok(viewGame(asked, target).truthRicheseBidAnswers?.includes(answer));
const game = act(asked, target, { type: 'truthAnswer', answer });
const game = passTruthRicheseFreshDiscard(act(asked, target, { type: 'truthAnswer', answer }));
return { game, asked };
}
export function rejectTruthRicheseBid(game: Game, actor: string, action: Action) {
+3 -2
View File
@@ -4,12 +4,13 @@ import {
actTruthtranceRichesePromises as act,
advanceTruthtranceRichesePromises as advance,
createTruthtranceRichesePromisesFixture,
passTruthRicheseFreshDiscard,
truthtranceRichesePromisesClean as clean,
truthtranceRichesePromisesInventory as inventory,
truthtranceRichesePromisesPlayer as player,
} from './fixture-truthtrance-richese-promises';
export { act, inventory, player };
export { act, inventory, player, passTruthRicheseFreshDiscard };
/** Original public Basic/Advanced Richese/Atreides/Emperor setup, genuine
* Charity prefix and actual first Silent cache declaration. The parent fixture
@@ -45,7 +46,7 @@ export function askTruthRicheseSilent(f: TruthRicheseSilentFixture, minimum = 4,
export function bindTruthRicheseSilent(f: TruthRicheseSilentFixture, answer: 'yes'|'no', minimum = 4, source = f.game) {
const asked = askTruthRicheseSilent(f, minimum, source);
assert.ok(viewGame(asked, f.target).truthRicheseBidAnswers?.includes(answer));
return { asked, game:act(asked, f.target, {type:'truthAnswer', answer}) };
return { asked, game:passTruthRicheseFreshDiscard(act(asked, f.target, {type:'truthAnswer', answer})) };
}
export function rejectTruthRicheseSilent(game: Game, actor: string, action: Action) {
const before = structuredClone(game);
@@ -15,6 +15,20 @@ export type TruthtranceRichesePromisesTransition = {
};
export const actTruthtranceRichesePromises = actTruthMovement;
export const truthtranceRichesePromisesClean = truthMovementClean;
/** Pass the neutral Basic Semuta offer following a completed Truth discard.
* This uses real player actions rather than bypassing the continuation. */
export function passTruthRicheseFreshDiscard(state: Game): Game {
let game = state;
for (let count = 0; game.pendingTreacheryDiscard?.reaction?.stage === 'offer' && count < game.players.length; count++) {
const pending = game.pendingTreacheryDiscard, reaction = pending.reaction!;
assert.equal(reaction.stage, 'offer');
if (reaction.stage !== 'offer') throw Error('The Semuta response must remain an offer');
const actor = game.players.find(row => !reaction.passed.includes(row.id)); assert.ok(actor);
game = actTruthtranceRichesePromises(game, actor.id, {type:'semutaPass', event:pending.batch.event});
}
assert.ok(!game.pendingTreacheryDiscard?.reaction || game.pendingTreacheryDiscard.reaction.stage !== 'offer');
return game;
}
export const truthtranceRichesePromisesPlayer = (game: Game, actor = 'richese') => truthMovementPlayer(game, actor);
export function truthtranceRichesePromisesInventory(game: Game) {
const start = (game.auction?.index ?? 0) + Number(game.currentAuctionSale?.origin === 'normal');
+105
View File
@@ -0,0 +1,105 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { registerHooks } from 'node:module';
import { readFileSync } from 'node:fs';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { viewGame, type Action, type Game, type GameView } from '../game/engine';
import { SaphoTruthOrderPass, SaphoTruthOrderChoiceButton } from '../components/sapho-truth-order';
import { act, inventory, player, createSaphoTruthRicheseFixture, assertRicheseParent } from './fixture-sapho-truth-richese';
import { passFreshDiscard, finishOriginalTruthQuestions } from './fixture-sapho-truth-order';
const aliases = registerHooks({
resolve(specifier, context, next) {
return next(specifier === 'next/image' ? 'vinext/shims/image' : specifier === 'next/link' ? 'vinext/shims/link' : specifier, context);
},
load(url, context, next) {
if (!url.endsWith('.module.css')) return next(url, context);
const css = readFileSync(new URL(url), 'utf8');
const classes = Object.fromEntries([...css.matchAll(/\.([a-zA-Z_][\w-]*)/g)].map(match => [match[1], match[1]]));
return {format:'module', source:`export default ${JSON.stringify(classes)}`, shortCircuit:true};
},
});
const { GameTable } = await import('../components/game-table');
const { Truthtrance } = await import('../components/truthtrance');
aliases.deregister();
const table = (game: GameView) => renderToStaticMarkup(createElement(GameTable,
{game,busy:false,send:async()=>{},onExit() {}}));
const truth = (game: GameView) => renderToStaticMarkup(createElement(Truthtrance,
{game,busy:false,act() {}}));
/** Never project another player's sealed amount or the collective results
* before every eligible player submits. Public submitted identities remain. */
function checkSilentPrivacy(game: Game, observer: string) {
const lot = game.richeseAuction; assert.ok(lot); assert.equal(lot.method,'silent');
const view = viewGame(game,observer).richeseAuction; assert.ok(view);
assert.equal(view.revealedBids,null);
if (!Object.hasOwn(lot.sealed,observer)) assert.equal(view.ownBid,null);
for (const other of Object.keys(lot.sealed).filter(id => id !== observer))
assert.ok(view.submitted.includes(other));
}
for (const method of ['onceAround','silent'] as const) void test(
`Basic ${method} actual Truth-order controls preserve the Richese lot and Semuta continuation`, () => {
// Genuine public R/A/E setup and a real native Richese cache lot. The
// fixture explicitly stages conserved original Sapho/Truth/Semuta card
// custody; it does not claim those card acquisitions were earned.
const f = createSaphoTruthRicheseFixture({advanced:false,method,semuta:true,standingBid:true});
let game = f.game;
assertRicheseParent(game,f.parent);
if (method === 'silent') checkSilentPrivacy(game,f.other);
const other = viewGame(game,f.other), frame = other.truthtrance?.order;
assert.ok(frame); assert.deepEqual(other.saphoTruthOrderChoices,[]);
assert.match(truth(other),/Continue without changing question order/);
assert.match(table(other),/Continue without changing question order/);
assert.doesNotMatch(truth(other),/Use Juice of Sapho to ask/);
assert.doesNotMatch(truth(other),new RegExp(f.card));
// Render real table controls, then invoke their actual hook-free leaf
// callbacks. Server rendering does not operate a browser or hydration.
const sent: Action[] = [];
const pass = SaphoTruthOrderPass({frame,me:f.other,choices:other.saphoTruthOrderChoices,
busy:false,act(action) {sent.push(action);}});
assert.equal(pass.props.disabled,false); pass.props.onClick();
assert.deepEqual([...sent],[{type:'truthOrderPass',event:frame.event,revision:frame.revision}]);
game = act(game,f.other,sent.pop()!); assertRicheseParent(game,f.parent);
const own = viewGame(game,f.owner), current = own.truthtrance?.order;
assert.ok(current); assert.ok(current.passed.includes(f.other));
const choice = own.saphoTruthOrderChoices.find(row => row.card === f.card && row.mode === f.mode);
assert.ok(choice); assert.match(truth(own),new RegExp(`Use Juice of Sapho to ask ${f.mode}`));
assert.match(table(own),new RegExp(`Use Juice of Sapho to ask ${f.mode}`));
const use = SaphoTruthOrderChoiceButton({frame:current,me:f.owner,choices:own.saphoTruthOrderChoices,
choice,busy:false,act(action) {sent.push(action);}});
assert.equal(use.props.disabled,false); use.props.onClick();
assert.deepEqual([...sent],[{type:'saphoTruthOrder',event:current.event,revision:current.revision,
card:f.card,mode:f.mode}]);
game = act(game,f.owner,sent.pop()!); assertRicheseParent(game,f.parent);
assert.ok(game.pendingTreacheryDiscard?.reaction,'The fresh Sapho discard opens neutral Semuta');
if (method === 'silent') checkSilentPrivacy(game,f.other);
if (method === 'silent') {
const event = game.pendingTreacheryDiscard.batch.event;
assert.equal(viewGame(game,f.semutaHolder).semutaReaction?.canCommit,true);
game = act(game,f.semutaHolder,{type:'semutaCommit',event});
assert.ok(player(game,f.semutaHolder).hand.some(card => card.id === f.card));
assert.ok(game.discard.some(card => card.id === f.semuta));
} else game = passFreshDiscard(game);
assertRicheseParent(game,f.parent);
assert.equal(game.truthtrance?.stage,'ask');
if (method === 'silent') checkSilentPrivacy(game,f.other);
assert.deepEqual(inventory(game),f.originalCards);
game = finishOriginalTruthQuestions(game);
assert.equal(game.truthtrance,null); assertRicheseParent(game,f.parent);
if (method === 'silent') checkSilentPrivacy(game,f.other);
assert.deepEqual(inventory(game),f.originalCards);
const resumed = game.richeseAuction; assert.ok(resumed);
const bidder = method === 'silent'
? resumed.eligible.find(id => !Object.hasOwn(resumed.sealed,id))
: resumed.active;
assert.ok(bidder);
game = act(game,bidder,{type:'richeseBid',event:resumed.event,
amount:method === 'silent' ? 0 : null,allyPayment:0});
assert.deepEqual(inventory(game),f.originalCards);
},
);
+104
View File
@@ -0,0 +1,104 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import { viewGame, type Game } from '../game/engine';
import { createSaphoTruthRicheseFixture as fixture, assertRicheseParent, act, inventory, player } from './fixture-sapho-truth-richese';
import { truthOrderFrame as frame, passTruthOrder as pass, playSaphoTruthOrder as play,
finishTruthOrder as finish, finishOriginalTruthQuestions as answer, passFreshDiscard,
rejectSaphoTruthOrder as reject, ownOrderChoices as choices } from './fixture-sapho-truth-order';
function assertSealedPrivacy(game: Game, bidder: string, observer: string) {
const own = viewGame(game,bidder).richeseAuction!, other = viewGame(game,observer).richeseAuction!;
assert.equal(own.ownBid,1); assert.equal(other.ownBid,null);
assert.equal(other.revealedBids,null);
assert.equal('sealed' in other,false);
}
function continueBidding(game: Game, method: 'onceAround'|'silent', lot: string) {
const actor = method === 'onceAround' ? game.richeseAuction!.active! :
game.richeseAuction!.order.find(id => !Object.hasOwn(game.richeseAuction!.sealed,id))!;
assert.ok(actor);
return act(game,actor,{type:'richeseBid',event:lot,amount:method === 'silent' ? 0 : null,allyPayment:0});
}
for (const advanced of [false,true]) for (const method of ['onceAround','silent'] as const) {
void test(`${advanced ? 'Advanced' : 'Basic'} ${method} Sapho preserves the funded native cache lot while ordering Truth`, () => {
const f = fixture({advanced,method,standingBid:true}), state = f.game, current = frame(state);
assertRicheseParent(state,f.parent);
if (method === 'silent') assertSealedPrivacy(state,f.semutaHolder,f.owner);
assert.ok(choices(state,f.owner).some(row => row.card === f.card && row.mode === f.mode));
const noOp = f.mode === 'first' ? 'last' : 'first';
reject(state,f.owner,{type:'saphoTruthOrder',event:current.event,revision:current.revision,card:f.card,mode:noOp});
reject(state,f.owner,{type:'saphoTruthOrder',event:'stale',revision:current.revision,card:f.card,mode:f.mode});
reject(state,f.owner,{type:'saphoTruthOrder',event:current.event,revision:current.revision + 1,card:f.card,mode:f.mode});
let game = play(state,f.owner,f.card,f.mode);
assertRicheseParent(game,f.parent);
if (method === 'silent') assertSealedPrivacy(game,f.semutaHolder,f.owner);
game = passFreshDiscard(game);
assertRicheseParent(game,f.parent);
reject(game,f.owner,{type:'saphoTruthOrder',event:current.event,revision:current.revision,card:f.card,mode:f.mode});
game = finish(game);
assert.equal(f.mode === 'first' ? game.truthtrance!.queue[0].player : game.truthtrance!.queue.at(-1)!.player,f.owner);
assertRicheseParent(game,f.parent);
game = answer(game);
assert.equal(game.truthtrance,null); assertRicheseParent(game,f.parent);
if (method === 'silent') assertSealedPrivacy(game,f.semutaHolder,f.owner);
assert.deepEqual(inventory(game),f.originalCards);
game = continueBidding(game,method,f.lot);
assert.equal(game.richeseAuction?.event,f.lot);
assert.deepEqual(inventory(game),f.originalCards);
});
}
for (const method of ['onceAround','silent'] as const) for (const claim of [false,true]) {
void test(`Basic ${method} Sapho Semuta ${claim ? 'claim' : 'pass'} restores the original funded cache lot`, () => {
const f = fixture({method,standingBid:true,semuta:true});
assert.ok(f.semuta); assertRicheseParent(f.game,f.parent);
let game = pass(f.game,f.other);
game = play(game,f.owner,f.card,f.mode);
const pending = game.pendingTreacheryDiscard; assert.ok(pending?.reaction);
assert.equal(pending.continuation.kind,'saphoTruthOrderDiscard');
assert.equal(pending.batch.entries[0].card.id,f.card);
assert.equal(viewGame(game,f.semutaHolder).semutaReaction?.canCommit,true);
assertRicheseParent(game,f.parent);
if (method === 'silent') assertSealedPrivacy(game,f.semutaHolder,f.owner);
if (claim) game = act(game,f.semutaHolder,{type:'semutaCommit',event:pending.batch.event});
else game = passFreshDiscard(game);
assert.equal(game.pendingTreacheryDiscard,null); assert.equal(game.truthtrance?.stage,'ask');
assertRicheseParent(game,f.parent);
assert.equal(player(game,f.semutaHolder).hand.some(card => card.id === f.card),claim);
assert.equal(game.discard.some(card => card.id === f.semuta),claim);
assert.deepEqual(inventory(game),f.originalCards);
game = answer(game); assert.equal(game.truthtrance,null); assertRicheseParent(game,f.parent);
game = continueBidding(game,method,f.lot);
assert.equal(game.richeseAuction?.event,f.lot); assert.deepEqual(inventory(game),f.originalCards);
});
}
for (const method of ['onceAround','silent'] as const) {
void test(`Basic ${method} definite Truth cost offers a fresh Semuta reaction after Sapho was passed`, () => {
const f = fixture({method,standingBid:true,semuta:true});
let game = pass(f.game,f.other);
game = play(game,f.owner,f.card,f.mode);
assert.equal(game.pendingTreacheryDiscard?.continuation.kind,'saphoTruthOrderDiscard');
game = passFreshDiscard(game);
assert.equal(game.truthtrance?.stage,'ask'); assertRicheseParent(game,f.parent);
const asker = game.truthtrance!.queue[0].player;
const target = game.players.find(row => row.id !== asker)!.id;
const consumed = game.truthtrance!.queue[0].card;
game = act(game,asker,{type:'truthAsk',question:{kind:'fact',target,fact:{kind:'spice',compare:'gte',value:0}}});
game = act(game,target,{type:'truthAnswer',answer:'yes'});
const pending = game.pendingTreacheryDiscard; assert.ok(pending?.reaction);
assert.equal(pending.continuation.kind,'truthtranceDiscard');
assert.equal(pending.batch.entries[0].card.id,consumed);
assert.equal(viewGame(game,f.semutaHolder).semutaReaction?.canCommit,true);
assertRicheseParent(game,f.parent);
if (method === 'silent') assertSealedPrivacy(game,f.semutaHolder,f.owner);
game = act(game,f.semutaHolder,{type:'semutaCommit',event:pending.batch.event});
assert.equal(game.pendingTreacheryDiscard,null);
assert.ok(player(game,f.semutaHolder).hand.some(card => card.id === consumed));
assertRicheseParent(game,f.parent);
game = answer(game); assert.equal(game.truthtrance,null); assertRicheseParent(game,f.parent);
assert.deepEqual(inventory(game),f.originalCards);
game = continueBidding(game,method,f.lot);
assert.equal(game.richeseAuction?.event,f.lot); assert.deepEqual(inventory(game),f.originalCards);
});
}
@@ -6,7 +6,7 @@ import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { viewGame, type Action, type GameView } from '../game/engine';
import { RicheseBidPromiseGuide } from '../components/richese-bid-promise-panel';
import { act, inventory, createTruthRicheseBidFixture, declareTruthRicheseBid,
import { act, inventory, passTruthRicheseFreshDiscard, createTruthRicheseBidFixture, declareTruthRicheseBid,
askTruthRicheseBid } from './fixture-truth-richese-bid';
const aliases = registerHooks({
@@ -61,7 +61,7 @@ for (const advanced of [false, true]) void test(
assert.match(auction(respondent), /Richese auction/);
for (const answer of ['yes', 'no'] as const) {
let game = act(asked, f.target, { type: 'truthAnswer', answer });
let game = passTruthRicheseFreshDiscard(act(asked, f.target, { type: 'truthAnswer', answer }));
const own = viewGame(game, f.target), sent: Action[] = [];
assert.ok(own.richeseBidPromises.some(promise => promise.player === f.target && promise.answer === (answer === 'yes')));
assert.ok(own.richeseBidPromiseGuide);
+2 -2
View File
@@ -4,7 +4,7 @@ import { viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import {
act, inventory, player, createTruthRicheseBidFixture, askTruthRicheseBid,
act, inventory, player, passTruthRicheseFreshDiscard, createTruthRicheseBidFixture, askTruthRicheseBid,
bindTruthRicheseBid, rejectTruthRicheseBid as reject,
} from './fixture-truth-richese-bid';
@@ -85,7 +85,7 @@ for (const advanced of [false, true]) {
const answer = botActions(answerView)[0]; assert.ok(answer, difficulty);
assert.equal(answer.type, 'truthAnswer');
assert.ok(['yes', 'no'].includes(String(answer.answer)));
const bound = act(structuredClone(asked), f.target, answer);
const bound = passTruthRicheseFreshDiscard(act(structuredClone(asked), f.target, answer));
const bidView = viewGame(bound, f.target);
bidView.players.find(row => row.id === f.target)!.bot = difficulty;
const guide = bidView.richeseBidPromiseGuide; assert.ok(guide, difficulty);
@@ -6,7 +6,7 @@ import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { viewGame, type Action, type Game, type GameView } from '../game/engine';
import { RicheseBidPromiseGuide } from '../components/richese-bid-promise-panel';
import { act, inventory, createTruthRicheseSilentFixture, askTruthRicheseSilent } from './fixture-truth-richese-silent';
import { act, inventory, passTruthRicheseFreshDiscard, createTruthRicheseSilentFixture, askTruthRicheseSilent } from './fixture-truth-richese-silent';
const aliases = registerHooks({
resolve(specifier, context, next) {
@@ -68,7 +68,7 @@ for (const advanced of [false, true]) void test(
assert.match(auction(respondent), /Silent auction/);
for (const answer of ['yes', 'no'] as const) {
let game = act(asked, f.target, {type:'truthAnswer', answer});
let game = passTruthRicheseFreshDiscard(act(asked, f.target, {type:'truthAnswer', answer}));
const own = viewGame(game, f.target), sent: Action[] = [];
assert.ok(own.richeseBidPromises.some(row => row.player === f.target && row.answer === (answer === 'yes')));
assert.ok(own.richeseBidPromiseGuide);
+3 -3
View File
@@ -3,7 +3,7 @@ import test from 'node:test';
import { handLimit, normalizeAutomaticGame, viewGame } from '../game/engine';
import { botActions } from '../game/bots';
import { DIFFICULTIES } from '../game/bot-profiles';
import { act, inventory, player, createTruthRicheseSilentFixture, askTruthRicheseSilent,
import { act, inventory, player, passTruthRicheseFreshDiscard, createTruthRicheseSilentFixture, askTruthRicheseSilent,
bindTruthRicheseSilent, rejectTruthRicheseSilent as reject } from './fixture-truth-richese-silent';
for (const advanced of [false, true]) {
@@ -59,7 +59,7 @@ for (const advanced of [false, true]) {
assert.equal(viewGame(source, f.target).richeseAuction?.ownBid, null);
const asked = askTruthRicheseSilent(f, 4, source);
assert.ok(viewGame(asked, f.target).truthRicheseBidAnswers?.includes('yes'));
const bound = act(asked, f.target, {type:'truthAnswer', answer:'yes'});
const bound = passTruthRicheseFreshDiscard(act(asked, f.target, {type:'truthAnswer', answer:'yes'}));
const guide = viewGame(bound, f.target).richeseBidPromiseGuide; assert.ok(guide);
assert.equal(guide.action.amount, 4);
const submitted = act(bound, f.target, guide.action);
@@ -84,7 +84,7 @@ for (const advanced of [false, true]) {
const answer = botActions(answerView)[0]; assert.ok(answer, difficulty);
assert.equal(answer.type, 'truthAnswer');
assert.ok(['yes','no'].includes(String(answer.answer)));
const bound = act(structuredClone(asked), f.target, answer);
const bound = passTruthRicheseFreshDiscard(act(structuredClone(asked), f.target, answer));
const bidView = viewGame(bound, f.target);
bidView.players.find(row => row.id === f.target)!.bot = difficulty;
const guide = bidView.richeseBidPromiseGuide; assert.ok(guide, difficulty);