Support Sapho Truth order during Richese cache auctions
This commit is contained in:
1 parent
8e16542784
commit
6a31315c95
17 files changed
+420
-25
No files matched your search
@@ -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);
|
||||
}
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
);
|
||||
@@ -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);
|
||||
|
||||
@@ -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,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);
|
||||
|
||||
Reference in new issue
Block a user