Support Truthtrance in Richese Silent bidding
This commit is contained in:
1 parent
090d764f6f
commit
3061dc5004
18 files changed
+442
-50
No files matched your search
+24
-12
@@ -16082,18 +16082,22 @@ function richeseBidRule<T>(run: () => T): T {
|
||||
}
|
||||
function richeseBidWindow(g: Game): boolean {
|
||||
return bidPromiseModeSupported({ ...g, semutaPreview: undefined }) && g.status === 'playing' && g.phase === 3 &&
|
||||
!!g.richeseAuction && g.richeseAuction.method === 'onceAround' && !g.richeseAuction.outcome &&
|
||||
!!g.richeseAuction && ['onceAround','silent'].includes(g.richeseAuction.method) && !g.richeseAuction.outcome &&
|
||||
!g.response && !g.decision && !g.phaseOpening && !g.pendingKarama && !g.pendingTreacheryDiscard;
|
||||
}
|
||||
function richeseBidOpportunity(g: Game, p: Player): RicheseBidOpportunity | null {
|
||||
const lot = g.richeseAuction;
|
||||
if (!lot || !richeseBidWindow(g) || !lot.eligible.includes(p.id) || lot.acted.includes(p.id) ||
|
||||
!richeseBidFitsHand(lot.source,lot.owner,p.id,p.hand.length,handLimit(p)) ||
|
||||
(p.id === lot.owner && lot.active !== p.id && !lot.bidder) || lot.bid >= Number.MAX_SAFE_INTEGER) return null;
|
||||
(lot.method === 'onceAround' && p.id === lot.owner && lot.active !== p.id && !lot.bidder) ||
|
||||
(lot.method === 'onceAround' && lot.bid >= Number.MAX_SAFE_INTEGER)) return null;
|
||||
// Silent submissions have no visible competing price; quote only the
|
||||
// respondent's own funding and an unsubmitted positive bid.
|
||||
const minimum = lot.method === 'silent' ? 1 : lot.bid + 1;
|
||||
const own = uncommittedSpice(g,p) + richeseOwnCommitment(lot,g.richeseFunding ?? {},p.id);
|
||||
const max = Math.min(Number.MAX_SAFE_INTEGER,own + (aidFor(g,p)?.amount ?? 0));
|
||||
if (!Number.isSafeInteger(max) || max < lot.bid + 1) return null;
|
||||
return {turn:g.turn,event:lot.event,player:p.id,minimum:lot.bid + 1,maximum:max};
|
||||
if (!Number.isSafeInteger(max) || max < minimum) return null;
|
||||
return {turn:g.turn,event:lot.event,player:p.id,minimum,maximum:max};
|
||||
}
|
||||
function richeseBidOtherPromises(g: Game, p: Player): boolean {
|
||||
return liveShipmentPromises(g.shipmentPromises ?? [],p.id,g.turn).some(row => row.answer) ||
|
||||
@@ -16129,7 +16133,7 @@ function richeseBidTruthAnswers(g: Game, p: Player, claim: RicheseBidClaim): Tru
|
||||
}
|
||||
function bindRicheseBidTruth(g: Game,p: Player,claim: RicheseBidClaim,answer: boolean,asker: string) {
|
||||
requireRule(richeseBidTruthAnswers(g,p,claim).includes(answer ? 'yes' : 'no'),
|
||||
'Choose an answer compatible with this Once Around bid.');
|
||||
'Choose an answer compatible with this Richese auction bid.');
|
||||
(g.richeseBidPromises ??= []).push({...claim,id:crypto.randomUUID(),player:p.id,asker,answer});
|
||||
}
|
||||
function richeseBidIntegrity(g: Game) {
|
||||
@@ -16152,10 +16156,11 @@ function reconcileRicheseBids(g: Game,voluntary?: {actor:string;action:Action})
|
||||
}
|
||||
function projectedRicheseBidGuide(g: Game, player: string): {action:Action} | null {
|
||||
const lot = g.richeseAuction, p = getPlayer(g,player);
|
||||
if (!lot || lot.active !== player || g.truthtrance || !richeseBidWindow(g)) return null;
|
||||
if (!lot || (lot.method === 'silent' ? Object.hasOwn(lot.sealed,player) : lot.active !== player) ||
|
||||
g.truthtrance || !richeseBidWindow(g)) return null;
|
||||
const context = {turn:g.turn,event:lot.event,player}, live = liveRicheseBidPromises(g.richeseBidPromises ?? [],context);
|
||||
if (!live.length) return null;
|
||||
if (!richeseBidRequired(live,context)) return {action:{type:'richeseBid',event:lot.event,amount:null,allyPayment:0,richeseBidPromiseId:live[0].id}};
|
||||
if (!richeseBidRequired(live,context)) return {action:{type:'richeseBid',event:lot.event,amount:lot.method === 'silent' ? 0 : null,allyPayment:0,richeseBidPromiseId:live[0].id}};
|
||||
const opportunity = richeseBidOpportunity(g,p), range = opportunity && richeseBidCompletionRange(live,opportunity);
|
||||
if (!range) return null;
|
||||
const own = uncommittedSpice(g,p) + richeseOwnCommitment(lot,g.richeseFunding ?? {},player);
|
||||
@@ -18550,6 +18555,13 @@ function finishAutomaticDecision(g: Game): boolean {
|
||||
? active.id
|
||||
: null;
|
||||
if (full) {
|
||||
if ((lot.method === 'onceAround' || lot.method === 'silent') && g.richeseBidPromises?.length) {
|
||||
const context = {turn:g.turn,event:lot.event,player:full};
|
||||
// Capacity loss forces a pass/zero. Release an impossible Yes first,
|
||||
// then settle No against the automatic single declaration.
|
||||
g.richeseBidPromises = richeseBidRule(() => reconcileRicheseBidPromises(g.richeseBidPromises!,context,null,'external'));
|
||||
g.richeseBidPromises = richeseBidRule(() => recordRicheseBid(g.richeseBidPromises!,context,null));
|
||||
}
|
||||
g.richeseAuction = submitRicheseBid(
|
||||
lot,
|
||||
{
|
||||
@@ -18569,8 +18581,8 @@ function finishAutomaticDecision(g: Game): boolean {
|
||||
const player = getPlayer(g, full);
|
||||
log(
|
||||
g,
|
||||
`${player.name} automatically ${lot.method === 'silent' ? 'submitted zero' : 'passed'} because their hand became full before bidding.`,
|
||||
{ faction: player.faction, name: 'Full hand: no bid' },
|
||||
`${player.name} automatically ${lot.method === 'silent' ? 'submitted a sealed bid' : 'passed'} because their hand became full before bidding.`,
|
||||
{ faction: player.faction, name: lot.method === 'silent' ? 'Full hand: sealed submission' : 'Full hand: no bid' },
|
||||
);
|
||||
if (lot.method === 'silent' && g.richeseAuction.outcome)
|
||||
log(
|
||||
@@ -39207,7 +39219,7 @@ export function applyAction(state: Game, id: string, action: Action): Game {
|
||||
const guide = projectedRicheseBidGuide(state,id);
|
||||
requireRule(guide && Object.keys(action).sort().join(',') === Object.keys(guide.action).sort().join(',') &&
|
||||
Object.keys(guide.action).every(key => action[key] === guide.action[key]),
|
||||
'This Once Around bid guide is stale or belongs to another lot.');
|
||||
'This Richese bid guide is stale or belongs to another lot.');
|
||||
}
|
||||
const g = applyActionInner(state, id, action);
|
||||
if (state.phase === 3 && liveTurnActionPromises(state.turnActionPromises ?? [], id, state.turn).some(row => {
|
||||
@@ -42432,9 +42444,9 @@ function applyActionInner(
|
||||
if (parties.richeseSaleAmount !== undefined)
|
||||
checkBureaucratContributors(g,byFaction(g,'emperor')?.id,[{payer:lot.owner,amount:allyPayment}]);
|
||||
}
|
||||
if (lot.method === 'onceAround' && g.richeseBidPromises?.length)
|
||||
if ((lot.method === 'onceAround' || lot.method === 'silent') && g.richeseBidPromises?.length)
|
||||
g.richeseBidPromises = richeseBidRule(() => recordRicheseBid(g.richeseBidPromises!,
|
||||
{turn:g.turn,event:lot.event,player:p.id},amount));
|
||||
{turn:g.turn,event:lot.event,player:p.id},lot.method === 'silent' && amount === 0 ? null : amount));
|
||||
g.richeseAuction = submitRicheseBid(
|
||||
lot,
|
||||
{ event: stringField(action.event), actor: p.id, amount },
|
||||
|
||||
+3
-2
@@ -3656,11 +3656,12 @@ export const RULE_TOPICS: RuleTopic[] = [
|
||||
category: 'Cards',
|
||||
coverage: 'Partial',
|
||||
developmentStage: 'Prototyped',
|
||||
summary: 'Bind a minimum bid in an ordinary lot, the remaining ordinary Bidding phase, or one Richese Once Around lot, preserving native funding and order.',
|
||||
summary: 'Bind a minimum bid in an ordinary lot, the remaining ordinary Bidding phase, or a Richese Once Around or Silent lot, preserving native funding and order.',
|
||||
steps: [
|
||||
'Choose Bind a bid in this ordinary lot, another remaining eligible bidder and a positive whole spice minimum. The turn and lot are fixed. Registered native Basic and Advanced tables without optional modules are supported first; broader compositions remain unfinished.',
|
||||
'In a quiet Richese Once Around auction, choose Bind a bid in this Once Around lot to bind the respondent’s one remaining bid or pass. A waiting Richese owner cannot promise Yes until an actual bid ensures an owner turn. Current hand space, funds and pledged aid remain required; an opposing raise can release an impossible Yes. Silent and Normal special auctions remain outside this prototype.',
|
||||
'In a quiet Richese Once Around auction, choose Bind a bid in this Once Around lot to bind the respondent’s one remaining bid or pass. A waiting Richese owner cannot promise Yes until an actual bid ensures an owner turn. Current hand space, funds and pledged aid remain required; an opposing raise can release an impossible Yes. Normal special auctions remain outside this prototype.',
|
||||
'Alternatively choose Bind a bid this Bidding phase. Yes requires one qualifying accepted ordinary bid after the answer and before Bidding closes; No forbids qualifying later ordinary bids across lots. Earlier bids do not count, and a bid does not promise a win.',
|
||||
'In a Silent Richese lot the question binds the respondent’s single sealed submission. Zero declines; a positive bid remains hidden until the native simultaneous reveal. Available answers use that bidder’s own funding, without using opponents’ concealed amounts.',
|
||||
'The phase-wide guide may wait for a later lot only when the existing funded sale, prepared next card and post-sale space and funds support it. An unbid lot, hypothetical rival raise, cancelable unfunded overbid or Harkonnen bonus slot is not invented as a future opportunity. Before Bidding, current funding and available card count provide a provisional forecast; special auction preparation and combined future commitments remain limited.',
|
||||
'Yes requires one new accepted bid at or above the minimum while possible. An earlier bid does not count. A pass or lower bid cannot rely on another player raising later. No forbids every later qualifying bid until this lot closes.',
|
||||
'The declaration fulfills Yes even if another player later wins. If you win, the original winning invoice and Karama payment rules still apply. A promise creates no spice or free purchase. Current-highest, already-passed and full-hand players have no new opportunity for this structured question.',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/** One future declaration in the exact public Richese Once Around lot.
|
||||
/** One future declaration in the exact public Richese Once Around or Silent lot.
|
||||
* The engine owns native eligibility, timing, hand capacity and funding. */
|
||||
export type RicheseBidClaim = { turn: number; event: string; minimum: number };
|
||||
export type RicheseBidPromise = RicheseBidClaim & {
|
||||
@@ -24,7 +24,7 @@ const identifier = (value: unknown): value is string =>
|
||||
|
||||
export function parseRicheseBidClaim(value: unknown): RicheseBidClaim {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value))
|
||||
throw new RicheseBidPromiseError('Choose a current Once Around bid question.');
|
||||
throw new RicheseBidPromiseError('Choose a current Once Around or Silent bid question.');
|
||||
const input = value as Record<string, unknown>;
|
||||
if (!positive(input.turn) || !identifier(input.event) || !positive(input.minimum))
|
||||
throw new RicheseBidPromiseError('Choose the current turn, auction event and a positive whole bid minimum.');
|
||||
@@ -32,7 +32,7 @@ export function parseRicheseBidClaim(value: unknown): RicheseBidClaim {
|
||||
}
|
||||
|
||||
export function richeseBidClaimText(claim: RicheseBidClaim): string {
|
||||
return `place a bid of at least ${claim.minimum} spice in this Once Around auction`;
|
||||
return `place a bid of at least ${claim.minimum} spice in this Richese auction`;
|
||||
}
|
||||
|
||||
function checkContext(context: RicheseBidContext): void {
|
||||
@@ -42,7 +42,7 @@ function checkContext(context: RicheseBidContext): void {
|
||||
function checkOpportunity(opportunity: RicheseBidOpportunity): void {
|
||||
checkContext(opportunity);
|
||||
if (!positive(opportunity.minimum) || !positive(opportunity.maximum) || opportunity.minimum > opportunity.maximum)
|
||||
throw new RicheseBidPromiseError('Retain an actual funded Once Around bid interval.');
|
||||
throw new RicheseBidPromiseError('Retain an actual funded Once Around or Silent bid interval.');
|
||||
}
|
||||
function sameContext(a: RicheseBidContext, b: RicheseBidContext): boolean {
|
||||
return a.turn === b.turn && a.event === b.event && a.player === b.player;
|
||||
@@ -74,7 +74,7 @@ export function richeseBidRequired(promises: readonly RicheseBidPromise[], conte
|
||||
}
|
||||
|
||||
/** Call inside the detached engine transaction immediately before its native
|
||||
* bid/pass validation. A Once Around actor receives one declaration only; a
|
||||
* bid/pass validation. A Once Around or Silent actor receives one declaration only; a
|
||||
* small bid cannot rely on a later turn. The native action owns the actual
|
||||
* bid amount, funding, eligibility, hand space and event validity. */
|
||||
export function recordRicheseBid<T extends RicheseBidPromise>(promises: readonly T[], context: RicheseBidContext,
|
||||
@@ -107,7 +107,7 @@ export function reconcileRicheseBidPromises<T extends RicheseBidPromise>(promise
|
||||
const range = opportunity ? richeseBidAllowedRange(promises, opportunity) : null;
|
||||
const impossible = live.filter(row => row.answer && (!range || row.minimum > range.maximum));
|
||||
if (impossible.length && change === 'voluntary')
|
||||
throw new RicheseBidPromiseError('This action would remove your last funded Once Around bid opportunity.');
|
||||
throw new RicheseBidPromiseError('This action would remove your last funded Once Around or Silent bid opportunity.');
|
||||
return promises.map(row => impossible.includes(row) ? { ...row, released: true } : row);
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ export function finishRicheseBidLot<T extends RicheseBidPromise>(promises: reado
|
||||
/** The public lot event is intentionally shared by multiple questions. */
|
||||
export function validateRicheseBidPromises(promises: readonly RicheseBidPromise[], players: readonly string[]): void {
|
||||
if (!Array.isArray(promises) || !Array.isArray(players))
|
||||
throw new RicheseBidPromiseError('The Once Around bid commitment record is invalid.');
|
||||
throw new RicheseBidPromiseError('The Once Around or Silent bid commitment record is invalid.');
|
||||
const receipts = new Set<string>();
|
||||
for (const row of promises) {
|
||||
parseRicheseBidClaim(row);
|
||||
@@ -136,7 +136,7 @@ export function validateRicheseBidPromises(promises: readonly RicheseBidPromise[
|
||||
!players.includes(row.asker) || row.player === row.asker || typeof row.answer !== 'boolean' ||
|
||||
(row.fulfilled !== undefined && typeof row.fulfilled !== 'boolean') ||
|
||||
(row.released !== undefined && typeof row.released !== 'boolean') || row.fulfilled && row.released)
|
||||
throw new RicheseBidPromiseError('The Once Around commitment lost its receipt, original lot or participants.');
|
||||
throw new RicheseBidPromiseError('The Once Around or Silent commitment lost its receipt, original lot or participants.');
|
||||
receipts.add(row.id);
|
||||
}
|
||||
}
|
||||
+5
-5
@@ -360,12 +360,12 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
|
||||
}
|
||||
if (v.kind === 'richeseBid') {
|
||||
check(Object.keys(v).sort().join(',') === 'event,kind,minimum,target,turn',
|
||||
'Choose the current Once Around lot and bid minimum.');
|
||||
'Choose the current Richese lot and bid minimum.');
|
||||
const claim = parseRicheseBidClaim(v), lot = g.richeseAuction;
|
||||
check(bidPromiseModeSupported({ ...g, semutaPreview: undefined }) && g.status === 'playing' && g.phase === 3 && lot &&
|
||||
lot.method === 'onceAround' && !lot.outcome && lot.event === claim.event && claim.turn === g.turn &&
|
||||
(lot.method === 'onceAround' || lot.method === 'silent') && !lot.outcome && lot.event === claim.event && claim.turn === g.turn &&
|
||||
!g.response && !g.decision && !g.phaseOpening && !g.pendingKarama && !g.pendingTreacheryDiscard,
|
||||
'Choose a quiet supported Once Around auction and its current public event.');
|
||||
'Choose a quiet supported Once Around or Silent auction and its current public event.');
|
||||
return { kind: 'richeseBid', target: v.target, ...claim };
|
||||
}
|
||||
if (v.kind === 'phaseBid') {
|
||||
@@ -1053,7 +1053,7 @@ export function resolveTruthAction(
|
||||
'Choose a bidder with a remaining ordinary opportunity in this lot.');
|
||||
if (w.question.kind === 'richeseBid')
|
||||
check(effects.richeseBidAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
|
||||
'Choose a bidder with a remaining Once Around opportunity.');
|
||||
'Choose a bidder with a remaining Richese auction opportunity.');
|
||||
if (w.question.kind === 'phaseBid')
|
||||
check(effects.phaseBidAnswers?.(g, g.players.find(other => other.id === w.question!.target)!, w.question).length,
|
||||
'Choose a phase bid question compatible with supported native opportunities and earlier answers.');
|
||||
@@ -1189,7 +1189,7 @@ export function resolveTruthAction(
|
||||
}
|
||||
if (q.kind === 'richeseBid') {
|
||||
check(effects.richeseBidAnswers && effects.bindRicheseBid && effects.richeseBidAnswers(g, p, q).includes(a.answer),
|
||||
'Choose an answer consistent with this Once Around lot and your legal bid.');
|
||||
'Choose an answer consistent with this Richese lot and your legal bid.');
|
||||
if (a.answer !== 'unknown') effects.bindRicheseBid(g, p, q, a.answer === 'yes', current.player);
|
||||
}
|
||||
if (q.kind === 'phaseBid') {
|
||||
|
||||
Reference in new issue
Block a user