From 91dcca46fb38ca9e5a1e65a1e730af93b451d538 Mon Sep 17 00:00:00 2001 From: Quintine <15965533+Quintine@users.noreply.github.com> Date: Mon, 21 Sep 2026 10:22:18 +1000 Subject: [PATCH] Connect ordinary bribe payments to player controls Players can select a non-allied recipient and explicitly pay a whole-spice bribe outside Mentat and open decision/response windows. The server derives private payment limits from uncommitted spice and existing joint Truthtrance completion checks on isolated copies. Target-specific Bureaucrat guards preserve supported redirect/allow continuations. Reserved bids, sealed spending and incoming escrow cannot be spent twice. No strategic AI negotiation or mode/publication changes. The panel separates deferred incoming bribes from spendable supply, disables stale/invalid selections and uses existing request/version fencing. The chronicle now describes actual payment and next-Mentat spendability rather than a promise. Internal guidance and the implementation checklist record this boundary. The separate Emperor gift/Bureaucrat source ambiguity remains explicitly pending. Independent review found no material issue. Focused tests cover out-of-turn payment, JSON escrow/collection, private budgets, invalid targets/amounts, phase/response gates, reserved bids, joint promises, supported redirection and rendered controls. Existing Bureaucrat SQLite/all-four legal response tests pass. The new HTTP test races duplicate submissions and verifies one debit, refreshed private balances and stale/over-budget rejection. Types, lint, all 4968 offline tests, production build and 49 HTTP tests pass on stable source. Preliminary checks caught a typed fixture opener and semantic-status/internal-link/template lint requirements and the new SSR test needed the existing Vinext Link alias. All were corrected before the final4 run. Browser QA used genuine fresh Basic setup with a human payer and Easy AI recipient: 11 rejected by controls, 4 paid from10 leaving6, one public payment entry and unchanged recipient spendable10 with4 in escrow after refresh. Keyboard selection and amount access passed. At390px the panel fits with16px text and44px-or-larger controls; linked rules and back navigation work. Screenshot capture timed out; full visual/assistive and all-configuration acceptance remain open. Browser transport loss was not injected; HTTP concurrency is separately covered. This is playability evidence, not difficulty calibration. All 1418 original rooms, 2991 seats and auxiliary rows are unchanged. SQLite integrity and foreign keys pass. The healthy server was reused without restart, migration, reset or recurring maintenance. Private evidence: 20260921-bribe-controls. Verified source tree: b9a8594c5dcd85f78974c9647f24f5f6eba60a38c9dcd2badcf4529d1e825ae1 --- README.md | 4 + app/globals.css | 7 ++ components/bribes.tsx | 53 ++++++++++++ components/game-table.tsx | 2 + docs/BRIBE_CONTROLS.md | 51 ++++++++++++ docs/BUREAUCRAT_PAYMENTS.md | 17 ++++ docs/CURRENT_STATUS.md | 33 ++++---- docs/IMPLEMENTATION_STATUS.md | 12 +++ docs/RULE_DECISIONS.md | 1 + game/bribe-options.ts | 41 +++++++++ game/engine.ts | 54 ++++++++++-- game/reference.ts | 24 ++++++ tests/battle-promises.test.ts | 6 ++ tests/bribe-controls-http.test.ts | 61 ++++++++++++++ tests/bribe-controls.test.tsx | 134 ++++++++++++++++++++++++++++++ tests/shipment-promises.test.ts | 8 ++ 16 files changed, 482 insertions(+), 26 deletions(-) create mode 100644 components/bribes.tsx create mode 100644 docs/BRIBE_CONTROLS.md create mode 100644 game/bribe-options.ts create mode 100644 tests/bribe-controls-http.test.ts create mode 100644 tests/bribe-controls.test.tsx diff --git a/README.md b/README.md index fba6b02b..4a7c6a6d 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ duplicates. Private history follows the seat through recovery or handover. Messages do not execute game actions, and AI opponents do not answer them yet. See [table discussion](docs/TABLE_DISCUSSION.md). +Use **Bribes** beneath the table to pay a non-allied faction. The recipient's +share becomes spendable at the next Mentat Pause; the panel shows your own +incoming bribes and the current payment limit. See [bribe controls](docs/BRIBE_CONTROLS.md). + The table header offers optional **Sound effects**, with mute, volume and a test sample. Short cues mark phase changes and automatic action notices. Sound starts off; preferences stay in your browser. See [table sounds](docs/TABLE_SOUNDS.md) diff --git a/app/globals.css b/app/globals.css index 474ec036..dda7e4db 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1942,3 +1942,10 @@ footer { .table-talk-messages p { white-space: pre-wrap; unicode-bidi: plaintext; margin: 0.5rem 0 0; font-size: 1rem; } .table-talk-author { display: flex; flex-wrap: wrap; justify-content: space-between; gap: 0.25rem 1rem; } .table-talk-author time { font-size: 0.875rem; } +.bribe-panel { font-size: 1rem; } +.bribe-panel > summary { cursor: pointer; min-height: 44px; padding-block: .65rem; font-weight: 600; } +.bribe-panel p, .bribe-status { display: block; margin-block: .7rem; } +.bribe-fields { display: flex; flex-wrap: wrap; align-items: end; gap: .8rem; } +.bribe-fields label { display: grid; gap: .35rem; flex: 1 1 12rem; min-width: 0; } +.bribe-fields select, .bribe-fields input { min-height: 44px; width: 100%; min-width: 0; font-size: 1rem; } +.bribe-fields button { font-size: 1rem; min-height: 44px; max-width: 100%; height: auto; white-space: normal; overflow-wrap: anywhere; } diff --git a/components/bribes.tsx b/components/bribes.tsx new file mode 100644 index 00000000..1b692972 --- /dev/null +++ b/components/bribes.tsx @@ -0,0 +1,53 @@ +'use client'; +import { useId, useState } from 'react'; +import Link from 'next/link'; +import type { Action, GameView } from '@/game/engine'; +import { bribeAction } from '@/game/bribe-options'; +import { faction } from '@/game/catalog'; +import { Button } from '@/components/ui/button'; + +export function Bribes({ game: g, act, busy }: { + game: GameView; act: (action: Action) => void; busy: boolean; +}) { + const [target, setTarget] = useState(''); + const [amount, setAmount] = useState('1'); + const description = useId(); + if (g.status !== 'playing') return null; + const options = g.bribeOptions; + const recipient = options.targets.find(candidate => candidate.id === target); + const player = recipient && g.players.find(candidate => candidate.id === recipient.id); + const action = bribeAction(options, target, Number(amount)); + return ( +
+ Bribes{options.incoming > 0 ? ` · ${options.incoming} spice awaiting Mentat` : ''} +

Pay another non-allied faction now. The amount is public; the recipient can spend their share only from the next Mentat Pause. A message or promise alone does not pay spice.

+

Your incoming bribes: {options.incoming} spice, separate from your spendable supply.

+ {options.blocked ? {options.blocked} : null} + {options.targets.length === 0 ?

No non-allied recipient is available.

: ( +
+ + + +
+ )} +

{recipient ? `You may pay up to ${recipient.maximum} spice to this recipient.` : `Uncommitted spice: ${options.available}. Choose a recipient to see the payment limit.`}

+ {recipient?.limitation ?

{recipient.limitation}

: null} + {recipient && !options.blocked && !action ? {recipient.maximum < 1 ? 'No spice is available for this payment.' : `Enter a whole number from 1 to ${recipient.maximum}.`} : null} +

Eligible Bureaucrats may divert two spice to the Bank. AI opponents can receive spice, but do not negotiate deals yet. Bribe rules

+
+ ); +} diff --git a/components/game-table.tsx b/components/game-table.tsx index c650d5f0..f5de4015 100644 --- a/components/game-table.tsx +++ b/components/game-table.tsx @@ -58,6 +58,7 @@ import { smugglerBattleModeSupported, smugglerBattlePlanBlock } from '@/game/smu import { RihaniChoice, RihaniHistory } from './rihani-decipherer'; import { MentatQuestion, MentatHistory } from './mentat-question'; import { BureaucratPayment } from './bureaucrat-payment'; +import { Bribes } from './bribes'; import { SukGraduatePanel } from './suk-graduate'; import { NullentropyBox, NullentropySearch } from './nullentropy-box'; import { OrnithopterMovement } from './ornithopter-movement'; @@ -4823,6 +4824,7 @@ export function GameTable({ : ' · Flips at the next Mentat Pause'}

)} +