Enable Basic Semuta and Truthtrance bribe questions

This commit is contained in:
Quintine committed 2026-10-27 22:18:00 +11:00
1 parent ac4302a8ed
commit c8889e94e2
9 files changed
+306 -1

No files matched your search

+7 -1
View File
@@ -47,8 +47,14 @@ export function bribePromiseModeSupported(game: NativePromiseScope & {
nexusHarkonnenBetrayalPreview?: unknown; moritaniAssassinatePreview?: unknown;
mobileStronghold?: unknown;
}): boolean {
if (!nativePromiseScopeSupported(game) || game.semutaPreview || game.nexusKullPreview ||
if (!nativePromiseScopeSupported(game) || game.nexusKullPreview ||
game.richeseBetrayalPreview) return false;
if (game.semutaPreview) return game.semutaPreview === true && !game.advanced &&
!game.kullPreview && game.expansions.includes('choam') &&
game.players.some(player => player.faction === 'richese') &&
!game.guildBetrayalPreview && !game.nexusIxianReplacementPreview &&
!game.nexusIxianBetrayalPreview && !game.nexusHarkonnenBetrayalPreview &&
!game.moritaniAssassinatePreview;
if (!game.kullPreview) return true;
return game.kullPreview === true && game.advanced === false && (game.expansions.length === 2 ||
game.expansions.length === 3 && game.expansions.includes('ecaz')) &&
+57
View File
@@ -1853,6 +1853,11 @@ export type Game = {
| { kind: 'battle'; index: number; value: BattlePromise }
| { kind: 'traitor'; index: number; value: TraitorDecisionPromise }
| null;
/** New Basic Semuta bribe answers retain their exact native binding. */
bribeBinding?:
| { kind: 'bribe'; index: number; value: BribePromise }
| { kind: 'bribeExpression'; index: number; value: BribeExpressionPromise }
| { kind: 'mixedBribe'; index: number; value: NonNullable<Game['mixedBribeQuestions']>[number] };
parentSignature: string;
}
| {
@@ -7141,6 +7146,9 @@ function truthtranceDiscardSignature(g: Game, next: TruthDiscardContinuation) {
record: next.record,
remaining: next.remaining,
promise: next.promise,
...(next.bribeBinding ? { bribeBinding: next.bribeBinding,
bribePromises: g.bribePromises, bribeExpressionPromises: g.bribeExpressionPromises,
mixedBribeQuestions: g.mixedBribeQuestions, bribePromiseSequence: g.bribePromiseSequence } : {}),
controls: nullentropyParentSignature(
g,
next.resume,
@@ -7251,6 +7259,17 @@ function finishTruthtranceAnswer(g: Game) {
promise,
parentSignature: '',
};
// Older saved Truth discard receipts keep their original signature. These
// bindings are only created for the newly admitted Basic Semuta questions.
if (g.semutaPreview && record.question.kind === 'bribe') continuation.bribeBinding = {
kind: 'bribe', index: g.bribePromises!.length - 1, value: structuredClone(g.bribePromises!.at(-1)!),
};
else if (g.semutaPreview && record.question.kind === 'bribeExpression') continuation.bribeBinding = {
kind: 'bribeExpression', index: g.bribeExpressionPromises!.length - 1, value: structuredClone(g.bribeExpressionPromises!.at(-1)!),
};
else if (g.semutaPreview && record.question.kind === 'mixedBribe') continuation.bribeBinding = {
kind: 'mixedBribe', index: g.mixedBribeQuestions!.length - 1, value: structuredClone(g.mixedBribeQuestions!.at(-1)!),
};
continuation.parentSignature = truthtranceDiscardSignature(g, continuation);
stageTreacheryDiscard(
g,
@@ -8220,6 +8239,41 @@ function treacheryDiscardIntegrity(g: Game) {
!g.battle.plans[q.target];
const expectedMovement = q.kind === 'movement';
const expectedTraitor = q.kind === 'traitorCall';
const expectedBribe = g.semutaPreview &&
(q.kind === 'bribe' || q.kind === 'bribeExpression' || q.kind === 'mixedBribe');
if (expectedBribe) {
const binding = c.bribeBinding;
const actual = binding?.kind === 'bribe' ? g.bribePromises :
binding?.kind === 'bribeExpression' ? g.bribeExpressionPromises : g.mixedBribeQuestions;
requireRule(binding && binding.kind === q.kind && Array.isArray(actual) &&
Number.isSafeInteger(binding.index) && binding.index >= 0 &&
binding.index === actual.length - 1 &&
JSON.stringify(binding.value) === JSON.stringify(actual[binding.index]) &&
binding.value.player === q.target && binding.value.asker === record.asker &&
binding.value.answer === (record.answer === 'yes') && binding.value.turn === g.turn,
'The completed Truthtrance lost its original bribe answer.');
if (binding.kind === 'bribe' && q.kind === 'bribe')
requireRule(binding.value.recipient === q.recipient && binding.value.minimum === q.minimum &&
binding.value.progress === 0 && !binding.value.fulfilled && !binding.value.released &&
g.bribePromiseSequence?.at(-1) === binding.value.event,
'The completed Truthtrance bribe differs from its original payment.');
else if (binding.kind === 'bribeExpression' && q.kind === 'bribeExpression')
requireRule(JSON.stringify(binding.value.claim) === JSON.stringify(q.claim) &&
binding.value.progress.length === 0 && !binding.value.fulfilled && !binding.value.released &&
g.bribePromiseSequence?.at(-1) === binding.value.event,
'The completed Truthtrance bribe expression differs from its original question.');
else if (binding.kind === 'mixedBribe' && q.kind === 'mixedBribe') {
const residual = g.bribeExpressionPromises?.at(-1);
requireRule(JSON.stringify(binding.value.mixed) === JSON.stringify(q.mixed) &&
(binding.value.binding ? !!residual && residual.event === binding.value.event &&
residual.player === q.target && residual.asker === record.asker &&
residual.answer === binding.value.answer && residual.turn === g.turn &&
residual.progress.length === 0 && !residual.fulfilled && !residual.released &&
g.bribePromiseSequence?.at(-1) === binding.value.event :
!g.bribeExpressionPromises?.some(item => item.event === binding.value.event)),
'The completed mixed bribe answer no longer matches its payment binding.');
}
} else requireRule(!c.bribeBinding, 'This Truthtrance answer has no bribe binding.');
if (expectedShipment || expectedBattle || expectedMovement || expectedTraitor) {
const actual = expectedTraitor ? g.battle?.traitorDecisionPromises : expectedMovement ? g.movementPromises : expectedShipment
? g.shipmentPromises
@@ -8835,6 +8889,9 @@ function semutaOfferSupported(
const richeseTruthDiscard = supportedRicheseTruthLot(g) &&
(continuation.kind === 'truthtranceDiscard' || continuation.kind === 'saphoTruthOrderDiscard') &&
!!continuation.richeseLotEvent && continuation.richeseLotEvent === g.richeseAuction?.event;
if (continuation.kind === 'truthtranceDiscard' &&
['bribe', 'bribeExpression', 'mixedBribe'].includes(continuation.record.question.kind) &&
(!continuation.bribeBinding || !bareBasicSemutaProfile(g) || !!g.kullPreview)) return false;
const richeseGiftDiscard = continuation.kind === 'karamaRicheseGiftDiscard';
const richeseDiscard = richeseGiftDiscard || continuation.kind === 'karamaRicheseAuctionDiscard';
const auctionAdvantageDiscard = continuation.kind === 'karamaAuctionAdvantageDiscard';
+1
View File
@@ -3470,6 +3470,7 @@ export const RULE_TOPICS: RuleTopic[] = [
'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.',
'Ordinary payment witnesses use current spendable spice, CHOAM’s own automatic opening receipt once, and one remaining owned Charity claim after prepayments. The automatic receipt and ordinary claim have separate markers; guides spend only actual cash. Later installments share the final purse with battle, bid, revival, shipment and movement commitments. Actual counters or opposing claims can change funding. Other future income, pledge withdrawal, gifts and wider card preparation remain bounded. A later question must preserve earlier answers 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.',
'In supported bare Basic Richese games, an answered bribe question retains its payment commitment while the used Truthtrance passes through the Semuta claim-or-pass opportunity. The same continuation applies to combined and mixed bribe questions. Kull and optional module combinations remain outside this preview.',
'Your own voluntary spending, alliance offer to the recipient or finishing Mentat cannot evade an unpaid Yes. A new Yes reopens an already-ready Charity or 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.',
],