Allow late Mentat bribes and retain turn-end promises

This commit is contained in:
Quintine committed 2026-10-26 15:41:10 +11:00
1 parent f5ae61f382
commit c81e00e6dd
19 files changed
+590 -51

No files matched your search

+9 -2
View File
@@ -7,10 +7,10 @@ export interface BribeOptions {
targets: { id: string; maximum: number; limitation: string | null }[];
}
/** Mirror only the existing bribe timing gates; bribes are allowed out of turn. */
/** Ordinary bribes remain out of turn, including after Mentat's opening
* collection. Late receipts wait for the next opening, not another collection. */
export function bribeTimingBlock(g: Game): string | null {
if (g.status !== 'playing') return 'Bribes require a game in progress.';
if (g.phase === 8) return 'Bribes cannot be made during Mentat Pause.';
if (g.inflation?.side === 'double') return 'Bribes are prohibited while Inflation shows Double.';
if (g.pendingTreacheryDiscard || g.pendingNullentropy || g.bureaucratPayments?.pending ||
['name', 'reveal'].includes(g.battle?.mentatQuestion?.stage ?? '') ||
@@ -21,6 +21,13 @@ export function bribeTimingBlock(g: Game): string | null {
return null;
}
/** The existing automatic token lifecycle will turn an unflipped Cancel into
* Double at this opening. An owed payment must precede that known closure. */
export function bribesCloseBeforeNextMentat(g: Pick<Game, 'turn' | 'inflation'>): boolean {
const token = g.inflation;
return !!token && token.updatedTurn < g.turn && !token.flipped && token.side === 'cancel';
}
/** Payment only removes spendable spice, so a preserved completion is monotonic. */
export function maximumBribe(available: number, preservesCommitments: (amount: number) => boolean): number {
let low = 0, high = Math.max(0, Math.floor(available));
+9 -7
View File
@@ -34,7 +34,7 @@ import { BidPromiseError, bidPromiseModeSupported, bidPromiseRange, bidRequired,
recordPromisedBid, validateBidPromises, type BidClaim, type BidOpportunity, type BidPromise } from './bid-promises';
import { classicDiscoveryLeaderSkillsProfile, classicDiscoveryNexusProfile, homeworldDiscoveryProfile, discoveryModeSupported, discoveryModuleProfile, mixedE1E2DiscoveryNexusProfile, mixedE3DiscoveryNexusProfile, pairedDiscoveryNexusProfile, pairedE3DiscoveryNexusProfile, singleE1E2DiscoveryNexusProfile, standaloneE3DiscoveryNexusProfile } from './discovery-module-profile';
import { advancedMoritaniLeaderSkillsProfile, selectedDeckLeaderSkillsProfile, homeworldLeaderSkillsProfile, independentTreacheryLeaderSkillsProfile, classicNexusLeaderSkillsProfile, classicTechLeaderSkillsProfile, guildBetrayalNexusLeaderSkillsProfile, mixedE1E2NexusLeaderSkillsProfile, mixedE3NexusLeaderSkillsProfile, nativeDiscoveryLeaderSkillsProfile, nativeEcazLeaderSkillsProfile, nativeExpansionLeaderSkillsProfile, nativeTechLeaderSkillsProfile, ordinaryLeaderSkillModeSupported, pairedE3LeaderSkillsProfile, pairedE3NexusLeaderSkillsProfile, pairedNexusLeaderSkillsProfile, singleE1E2NexusLeaderSkillsProfile, standaloneE3NexusLeaderSkillsProfile, strongholdLeaderSkillsProfile } from './leader-skill-profile';
import { bribeTimingBlock, maximumBribe, type BribeOptions } from './bribe-options';
import { bribeTimingBlock, bribesCloseBeforeNextMentat, maximumBribe, type BribeOptions } from './bribe-options';
import { quoteAtreidesFullPlan } from './atreides-full-plan';
import { quoteTleilaxuSpecialKarama } from './tleilaxu-special-karama';
import { quoteAutomaticAdvisorFlips, validateAutomaticAdvisorFlip, AutomaticAdvisorFlipError, type AdvisorStance, type AutomaticAdvisorFlipSource, type AutomaticAdvisorTransport, type AutomaticAdvisorGamont, type AutomaticAdvisorBattle, type AutomaticAdvisorHomeworldTransport, type AutomaticAdvisorFaceDance, type AutomaticAdvisorStorm, type AutomaticAdvisorRelocation } from './advisor-automatic-flip';
@@ -15067,7 +15067,7 @@ function bribePromiseIntegrity(g: Game) {
bribePromiseRule(() => validateBribePromises(g.bribePromises ?? [], g.players.map(player => player.id)));
}
function bribePromiseTurn(g: Game): boolean {
return g.status === 'playing' && g.phase >= 0 && g.phase < 8 && bribePromiseModeSupported(g);
return g.status === 'playing' && g.phase >= 0 && g.phase <= 8 && bribePromiseModeSupported(g);
}
function bribeQuestionWindow(g: Game): boolean {
// A current Double side can fix No without authorizing any payment. Every
@@ -15125,9 +15125,9 @@ function bindBribeTruth(g: Game, p: Player, claim: BribeClaim, answer: boolean,
requireRule(bribeTruthAnswers(g, p, claim).includes(answer ? 'yes' : 'no'),
'Choose a bribe answer compatible with the current funds and earlier answers.');
(g.bribePromises ??= []).push({ ...claim, event: crypto.randomUUID(), player: p.id, asker, answer, progress: 0 });
if (answer && g.phase === 7 && g.ready.includes(p.id)) {
if (answer && (g.phase === 8 || g.phase === 7 && bribesCloseBeforeNextMentat(g)) && g.ready.includes(p.id)) {
g.ready = g.ready.filter(player => player !== p.id);
log(g, `${p.name} has a new Truthtrance bribe commitment and must finish Collection again after paying it.`);
log(g, `${p.name} has a new Truthtrance bribe commitment and must finish ${g.phase === 8 ? 'Mentat Pause' : 'Collection'} again after paying it.`);
}
}
function reconcileBribePromises(g: Game, voluntary?: { actor: string; action: Action }) {
@@ -38581,7 +38581,7 @@ function applyActionInner(
p.spice -= n;
log(
g,
`${p.name} paid a ${n}-spice bribe to ${target.name}; its recipient share becomes spendable at the next Mentat Pause.`,
`${p.name} paid a ${n}-spice bribe to ${target.name}; its recipient share becomes spendable ${g.phase === 8 ? 'at the start of next turn’s Mentat Pause' : 'at the next Mentat Pause opening'}.`,
);
const source = stampBureaucratPayment(g,'bribe',p.id,target.id,n).bureaucratPayment;
if (!offerBureaucratPayment(g,source,{kind:'bribe'})) target.bribes += n;
@@ -39900,8 +39900,10 @@ function applyActionInner(
}
if (t === 'ready') {
if (allianceQuestionWindow(g)) guardAlliancePromiseReady(g, p);
if (g.phase === 7) requireRule(!liveBribePromises(g.bribePromises ?? [], p.id, g.turn).some(promise => promise.answer),
'Complete your Truthtrance bribe payments before finishing Collection.');
const bribeWindowCloses = g.phase === 8 || g.phase === 7 && bribesCloseBeforeNextMentat(g);
if (bribeWindowCloses) requireRule(!liveBribePromises(g.bribePromises ?? [], p.id, g.turn).some(promise => promise.answer),
g.phase === 8 ? 'Complete your Truthtrance bribe payments before finishing Mentat Pause.'
: 'Pay your Truthtrance bribes before the known Inflation Double opening prevents them.');
if (g.phase === 4) { guardPromisedRevival(g, p, action); guardPromisedLeaderRevival(g, p, action); guardRevivalExpressionReady(g, p); }
requireRule(
[1, 2, 4, 7, 8].includes(g.phase) ||
+11 -11
View File
@@ -290,22 +290,22 @@ export const RULE_TOPICS: RuleTopic[] = [
category: 'Getting started',
coverage: 'Partial',
developmentStage: 'Prototyped',
summary: 'Pay a non-allied faction now; its recipient share becomes spendable at the next Mentat Pause.',
summary: 'Pay a non-allied faction now; its recipient share becomes spendable at the next Mentat opening, including next turn for a late Mentat payment.',
steps: [
'Open Bribes beneath the table. Choose a non-allied faction and a positive whole amount, then use the named Pay button. The amount is public and leaves your supply immediately. A message or promise alone does not pay spice.',
'The recipient keeps paid spice separately until the next Mentat Pause. Your incoming bribes are shown separately from your spendable supply; they cannot fund bids, shipments or another bribe yet.',
'You may pay out of turn when no decision or response is pending. You cannot bribe yourself or your ally, during Mentat Pause, or while Inflation shows Double. Emperor gifts to allies use their own power controls.',
'The recipient keeps paid spice separately until the next Mentat opening. A payment after the current opening waits for next turn’s opening. Your incoming bribes are shown separately from your spendable supply; they cannot fund bids, shipments or another bribe yet.',
'You may pay out of turn when no decision or response is pending, including during Mentat after opening collection. You cannot bribe yourself or your ally, or while Inflation shows Double. Emperor gifts to allies use their own power controls.',
'Your payment limit excludes reserved spending and preserves binding Truthtrance answers. If an alliance, pending decision or available supply changes, recheck the recipient and amount. A rejected payment does not change the game.',
'A qualifying Bureaucrat may divert two spice to the Bank without reducing the payer’s cost. The remainder stays deferred. Unsupported combinations remain unavailable and do not imply an official rule prohibition.',
'Minimal AI follows supported Truthtrance ordinary-payment answers and their legal guides. Strategic bribe initiation and negotiation remain deferred. Discuss other agreements with human players; broader prose deals are not automatically enforced.',
],
related: ['table-discussion', 'alliance-funding', 'card-truthtrance', 'privacy'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Existing payment and escrow are connected to private server-derived limits and supported Bureaucrat continuation. Complete negotiation and all module combinations remain open.', evidence: ['game/bribe-options.ts', 'game/engine.ts'] },
{ area: 'Implementation', status: 'Partial', detail: 'Existing payment and escrow include quiet Mentat payments deferred to next turn’s opening, with private server-derived limits and supported Bureaucrat continuation. Complete negotiation and all module combinations remain open.', evidence: ['game/bribe-options.ts', 'game/engine.ts'] },
{ area: 'Player controls', status: 'Implemented', detail: 'Named recipient, amount, explicit payment, deferred incoming supply and current availability guidance.', evidence: ['components/bribes.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Passive receipt/collection and existing Bureaucrat responses work. Strategic bribe initiation and negotiation wait for feature completion.' },
{ area: 'Documentation', status: 'Implemented', detail: 'Payment, escrow, restrictions, saved continuation and remaining boundaries are recorded.', evidence: ['docs/BRIBE_CONTROLS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Projection/JSON, committed budgets, privacy, target validation, Bureaucrat decisions, rendered controls and concurrent HTTP payment are covered. Full integrated acceptance remains open.', evidence: ['tests/bribe-controls.test.tsx', 'tests/bribe-controls-http.test.ts', 'tests/battle-promises.test.ts', 'tests/shipment-promises.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'Payment, escrow, restrictions, saved continuation and remaining boundaries are recorded.', evidence: ['docs/BRIBE_CONTROLS.md', 'docs/MENTAT_BRIBES.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Original late Mentat/next-turn receipt journeys supplement existing projection/JSON, committed budgets, privacy, target validation, Bureaucrat, controls and concurrent HTTP evidence. Full integrated acceptance remains open.', evidence: ['tests/mentat-bribes-runtime.test.ts', 'tests/bribe-controls.test.tsx', 'tests/bribe-controls-http.test.ts', 'tests/battle-promises.test.ts', 'tests/shipment-promises.test.ts'] },
],
},
{
@@ -3243,19 +3243,19 @@ export const RULE_TOPICS: RuleTopic[] = [
steps: [
'Choose Bind ordinary bribe payments this turn, another respondent, a named non-allied recipient and a positive whole minimum. The current turn is fixed. Registered native Basic and Advanced tables without optional modules support this funded-payment prototype.',
'Yes requires cumulative matching ordinary payer payments after the answer while possible. No keeps later matching payments below the minimum, even across smaller payments. Earlier payments, gifts and allied aid do not count. Several Yes questions for one recipient share the largest deficit; different recipients reserve separate spice.',
'The payer spends spice when the actual native bribe is accepted. The recipient keeps that payment deferred until the original Mentat collection. The question creates no immediate recipient cash, discount or refund. Current auction and sealed-battle liabilities stay reserved.',
'The payer spends spice when the actual native bribe is accepted. The recipient keeps that payment deferred until the next Mentat opening; a late Mentat payment waits for next turn. The question creates no immediate recipient cash, discount or refund. Current auction and sealed-battle liabilities stay reserved.',
'Only answers supported by current spendable spice and earlier battle, bid, revival, shipment and movement commitments are offered. Future income, card preparations, pledge withdrawal and gifts are not forecast. A later supported question must preserve an earlier bribe Yes too.',
'Your persistent panel shows named cumulative progress and fulfilled/released status. Its legal ordinary-payment guide works out of turn in a clear native window. Current payment controls also apply the recipient cap and retain funding for other Yes answers. Stale or modified guides reject.',
'Your own voluntary spending, alliance offer to the recipient or finishing Collection cannot evade an unpaid Yes. An already-ready Collection respondent must finish again after a new Yes. Completed opposing consequences may release impossible answers in accepted order. No finishes at the original turn boundary.',
'The current software bribe window excludes Mentat and Inflation Double. No can be fixed under Double, while Yes requires a currently legal funded payment. This cumulative funded-current-turn protocol is a provisional composition of general Truthtrance authority; optional modules, future preparations, broader deals and complete-mode acceptance remain due.',
'Your own voluntary spending, alliance offer to the recipient or finishing Mentat cannot evade an unpaid Yes. A new Yes reopens only an already-ready Mentat respondent; Collection readiness stays. A known automatic Inflation Double closure requires payment before Collection Ready ends that legal window. Completed opposing consequences may release impossible answers in accepted order. No finishes at the original turn boundary.',
'Quiet Mentat remains available after opening collection; Inflation Double still prohibits actual payments. No can be fixed under Double, while Yes requires a currently legal funded payment. This cumulative funded-current-turn protocol is a provisional composition of general Truthtrance authority; optional modules, future preparations, broader deals and complete-mode acceptance remain due.',
],
related: ['card-truthtrance', 'bribes', 'truthtrance-bid', 'truthtrance-revival', 'implementation-checklist'],
checklist: [
{ area: 'Implementation', status: 'Partial', detail: 'Recipient-specific cumulative ordinary payments, joint funding/caps, real payer completion, deferred native receipt and accepted-order release retain original mechanics.', evidence: ['game/bribe-promises.ts', 'game/truthtrance.ts', 'game/engine.ts', 'tests/bribe-promises.test.ts', 'tests/truth-bribe-runtime.test.ts'] },
{ area: 'Player controls', status: 'Partial', detail: 'Typed current-turn named-recipient fields, public preview, target-only answers and persistent guarded out-of-turn payment guide; ordinary recipient limits retain other commitments.', evidence: ['components/bribe-claim-fields.tsx', 'components/bribe-promises.tsx', 'components/truthtrance.tsx', 'components/game-table.tsx', 'tests/truth-bribe-controls.test.tsx'] },
{ area: 'AI', status: 'Partial', detail: 'Existing four minimal profiles answer and execute legal ordinary-payment guides. Strategic initiation/negotiation and calibration remain deferred.', evidence: ['game/bots.ts', 'tests/truth-bribe-runtime.test.ts'] },
{ area: 'Documentation', status: 'Implemented', detail: 'General Truthtrance/physical bribery sources, provisional cumulative meaning, funded scope, original timing and remaining gates are explicit.', evidence: ['docs/TRUTHTRANCE_BRIBE_PROMISES.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused pure, original deal/payment/Mentat runtime and controlled/native UI evidence. Native alliance campaigns, optional compositions, future preparations and final browser/deployed acceptance remain open.', evidence: ['tests/fixture-truth-bribe.ts', 'tests/truth-bribe-runtime.test.ts', 'tests/truth-bribe-controls.test.tsx'] },
{ area: 'Documentation', status: 'Implemented', detail: 'General Truthtrance/physical bribery sources, provisional cumulative meaning, funded scope, original timing and remaining gates are explicit.', evidence: ['docs/TRUTHTRANCE_BRIBE_PROMISES.md', 'docs/MENTAT_BRIBES.md', 'docs/RULE_DECISIONS.md'] },
{ area: 'Verification', status: 'Partial', detail: 'Focused pure, original deal/payment/Mentat/next-turn runtime and controlled/native UI evidence. Native alliance campaigns, optional compositions, future preparations and final browser/deployed acceptance remain open.', evidence: ['tests/fixture-mentat-bribes.ts', 'tests/mentat-bribes-runtime.test.ts', 'tests/fixture-truth-bribe.ts', 'tests/truth-bribe-runtime.test.ts', 'tests/truth-bribe-controls.test.tsx'] },
],
},
{
+1 -1
View File
@@ -256,7 +256,7 @@ function parseQuestion(g: Game, asker: string, value: unknown): TruthQuestion {
check(Object.keys(v).sort().join(',') === 'kind,minimum,recipient,target,turn',
'Supply the current turn, named bribe recipient, minimum and respondent.');
const claim = parseBribeClaim(v), respondent = g.players.find(player => player.id === v.target)!;
check(bribePromiseModeSupported(g) && g.status === 'playing' && g.phase >= 0 && g.phase < 8 &&
check(bribePromiseModeSupported(g) && g.status === 'playing' && g.phase >= 0 && g.phase <= 8 &&
claim.turn === g.turn && claim.recipient !== respondent.id && claim.recipient !== respondent.ally &&
g.players.some(player => player.id === claim.recipient) && !g.response && !g.decision && !g.phaseOpening &&
!g.pendingTreacheryDiscard && !g.pendingKarama,