veratown-prototype

Roulette Wheel Helper Bot Proposal

Status: Implemented; archived 2026-10-27. This document is the original design record. For current behavior and operator instructions, see the casino guide. Date: 2026-10-27
Scope: Optional, separately deployed character that presents roulette stages and emotes the public result.

Executive decision

Use one dedicated Helper Bots account as the visible roulette-wheel character. Keep CasinoGameService, casino rules, wallet reservations, random draw, durable outcome, settlement, access checks, and private player reports in the existing supervisor process. The helper is a presentation executor only: it does not accept bets, calculate outcomes, access player wallets, or decide when a round is complete.

Extend the existing helper task control plane with a narrowly typed roulette presentation task and a bounded ordered queue. Reuse its configured identity, room/runtime checks, observed-location checks, leases, action budget, journal, BC client, and observation reconciliation. Do not add a second casino engine, general-purpose task-text facility, new socket, or separate task collection.

The helper is a BC character, not a map object. Its avatar is operator-managed to resemble the wheel. The initial visual effect is a staged sequence of reviewed public Emotes; this does not promise a client-side animated wheel or modify the shared room map.

Current implementation to preserve

The existing casino already owns the behavior the proposal needs:

These are behavior boundaries, not reasons to copy the roulette code into the helper deployment. Relevant implementations include src/features/casino/game-service.ts, src/features/casino/result-format.ts, src/features/casino/result-reporter.ts, src/features/helper-bots/plugin.ts, src/features/helper-bots/services/helper-task-executor.ts, and src/features/helper-bots/repository.ts.

Terminology and room binding

“Casino roulette room” must resolve to the existing configured BC room and room-scoped location/rules:

Use cases

ID Actor Use case Required result
UC-01 Player Place, cancel, or inspect a roulette bet Existing supervisor commands, authorization, wallet, and replies are unchanged.
UC-02 Casino supervisor Open a round after an accepted first bet, an authorized manual open, or renewal Queue one idempotent “betting open” event from the existing start callback or successful admin-open result.
UC-03 Casino supervisor Close betting with active wagers Queue a “wheel spinning” event at the existing close boundary; do not accept new bets after the deadline.
UC-04 Casino supervisor Persist and settle a roulette result Use the existing result formatter and queue one public result Emote only after settlement succeeds.
UC-05 Roulette helper Present an authorized event in the configured region Send one room Emote through its own communication API and report task outcome through the existing helper lifecycle.
UC-06 Casino supervisor Report individual wager outcomes Continue the existing private, consolidated whispers to participants; never send these through the helper.
UC-07 Operator Enable or disable the wheel helper Bind an enabled helper identity and reviewed stage templates to the roulette rules; default behavior remains the current whisper announcements.
UC-08 Operator / runtime Helper is missing, out of position, disconnected, or ambiguous Fail closed for helper dispatch; retain existing whisper fallback only when no helper action could have been dispatched. Never duplicate a possibly emitted result.
classDiagram
    direction LR
    class Player {
        <<actor>>
    }
    class Operator {
        <<actor>>
    }
    class CasinoSupervisor {
        <<actor>>
    }
    class RouletteHelper {
        <<actor>>
    }
    class Bet {
        <<use case>>
    }
    class Open {
        <<use case>>
    }
    class Spin {
        <<use case>>
    }
    class Result {
        <<use case>>
    }
    class PrivateReport {
        <<use case>>
    }
    class Configure {
        <<use case>>
    }
    Player --> Bet
    CasinoSupervisor --> Bet
    CasinoSupervisor --> Open
    CasinoSupervisor --> Spin
    CasinoSupervisor --> Result
    CasinoSupervisor --> PrivateReport
    RouletteHelper --> Open
    RouletteHelper --> Spin
    RouletteHelper --> Result
    Operator --> Configure

State and stage flow

Casino session state remains authoritative and unchanged. The wheel state is a presentation projection of that session, not a second game state machine:

  1. Betting starts from the existing round-start callback after the first accepted funded bet or renewal. An authorized manual open enters the same presentation path from its successful command result. The helper emits a reviewed opening Emote.
  2. At the existing betting deadline, the supervisor closes betting. If active wagers remain, it requests one spin-stage Emote before drawing. There is no new betting extension or independent helper timer.
  3. The casino persists the outcome and settles wagers using current recovery rules. Only a settled CasinoRouletteClosedEvent can request a result presentation.
  4. The helper emits the aggregate text produced by formatRouletteRoundResult. The supervisor continues participant whispers.
  5. Result display, cooldown, renewal, and idle behavior continue to follow the persisted casino session. Restart recovery never replays an old public presentation automatically.

An empty or fully cancelled round closes quietly as it does today. A failure before a durable outcome refunds/reconciles the wagers and must not invent a winning number. If a spin-stage Emote was already observed before such a failure, the proposal does not claim that a result exists; a separate operator-approved “round interrupted” message can be considered later.

stateDiagram-v2
    state CasinoSession {
        [*] --> Idle
        Idle --> Betting: accepted funded first bet / admin open
        Betting --> Betting: accepted bet / permitted cancellation
        Betting --> Settling: deadline + active wagers
        Betting --> Idle: deadline + no active wagers
        Betting --> Refunding: interruption before durable outcome
        Settling --> Settled: outcome persisted; wagers settled once
        Settling --> Settling: recovery reuses persisted outcome
        Refunding --> Cancelled: reservations refunded once
        Settled --> Results: configured result window
        Results --> Renewal: configured wait
        Renewal --> Betting: new empty session
        Cancelled --> Idle
    }
    state WheelPresentation {
        [*] --> Ready
        Ready --> OpeningPending: round-start event enqueued
        OpeningPending --> BettingOpen: matching Emote observed
        BettingOpen --> SpinPending: deadline event enqueued
        SpinPending --> Spinning: matching Emote observed
        Spinning --> ResultPending: settled result event enqueued
        ResultPending --> ResultDisplayed: matching result Emote observed
        ResultDisplayed --> Ready: result and wait windows end
        BettingOpen --> Ready: empty / fully cancelled round closes quietly
        Spinning --> Ready: pre-result recovery; no result exists
        OpeningPending --> Fenced: identity, scope, position, or queue check fails
        SpinPending --> Fenced: disconnect or ambiguous dispatch
        ResultPending --> Fenced: disconnect or ambiguous dispatch
        Fenced --> Ready: operator reconciliation / next eligible session
    }
sequenceDiagram
    actor Player
    participant Supervisor as Supervisor / CasinoFeaturePlugin
    participant Game as CasinoGameService
    participant Wallet as CasinoWalletService + Economy
    participant Bridge as RoulettePresentationPort
    participant Queue as Helper task repository
    participant Helper as HelperBotsPlugin / executor
    participant BC as BC server

    Player->>Supervisor: roulette bet / authorized open
    Supervisor->>Game: validate location and invoke existing command
    Game->>Wallet: reserve wager and persist session
    Wallet-->>Game: accepted reservation
    Note over Game,Bridge: Manual admin-open uses its successful command result; first bet and renewal use existing callbacks.
    Game->>Bridge: round started(session)
    Bridge->>Queue: enqueue idempotent open-stage event
    Queue-->>Helper: claim leased event
    Helper->>BC: Emote via CharacterCommunicationApi
    BC-->>Helper: server-observed Emote
    Helper->>Queue: mark observed; release event

    Note over Game,Queue: At the existing deadline, no new bets are accepted.
    Game->>Bridge: spin stage(sessionId)
    Bridge->>Queue: enqueue next ordered event
    Queue-->>Helper: claim spin event
    Helper->>BC: reviewed spin Emote
    BC-->>Helper: server-observed Emote
    Helper->>Queue: mark observed; release event

    Game->>Wallet: persist outcome, settle all active wagers
    Wallet-->>Game: settled event with durable outcome
    Game->>Bridge: closed(settlement)
    Bridge->>Bridge: formatRouletteRoundResult(existing settlement)
    Bridge->>Queue: enqueue idempotent result event
    Queue-->>Helper: claim result event
    Helper->>BC: public result Emote
    BC-->>Helper: server-observed Emote
    Helper->>Queue: mark observed
    Supervisor->>Player: existing private consolidated wager report

The diagram omits transport acknowledgements: BC room-message dispatch alone is not proof of delivery. The task becomes observed only through the existing matching inbound Emote evidence; unresolved dispatch stays reconciliation_required and is not automatically replayed.

Proposed component and contract boundaries

classDiagram
    direction LR
    class CasinoGameService {
        +onRouletteStarted(session)
        +onRouletteSpinning(sessionId)
        +onRouletteClosed(event)
    }
    class CasinoCommandSuite {
        +openRouletteRound()
        +authorizePlayer(member, roulette)
    }
    class RoulettePresentationPort {
        <<interface>>
        +roundStarted(sessionId, safeEmote)
        +spinning(sessionId, safeEmote)
        +result(sessionId, safeAggregateEmote)
    }
    class RoulettePresentationRouter {
        <<composition-root adapter>>
        +enqueue(event)
        +fallbackIfNotDispatched(event)
    }
    class HelperTaskRepository {
        +createPresentationEvent(eventId, agent, location, text)
        +claimNext(agentId)
        +observeTask(eventId)
        +reconcileExpired()
    }
    class HelperBotsPlugin {
        +start()
        +stop()
        +observeMessage(message)
    }
    class HelperTaskExecutor {
        +executePresentation(event)
    }
    class CharacterCommunicationApi {
        +emote(text)
    }
    class ActionCoordinator {
        +executeDispatchOnly(action)
    }
    class MongoDB
    class MongoCasinoRepository
    class MongoHelperTaskRepository

    CasinoGameService --> RoulettePresentationPort : lifecycle callbacks
    CasinoCommandSuite --> RoulettePresentationPort : manual-open result
    RoulettePresentationPort <|.. RoulettePresentationRouter : implemented at composition root
    RoulettePresentationRouter --> HelperTaskRepository : durable ordered event
    HelperTaskRepository <|.. MongoHelperTaskRepository
    MongoCasinoRepository --> MongoDB : casino collections
    MongoHelperTaskRepository --> MongoDB : helper collections
    HelperBotsPlugin --> HelperTaskExecutor : leased task
    HelperTaskExecutor --> CharacterCommunicationApi : Emote only
    CharacterCommunicationApi --> ActionCoordinator : existing journal owner

The dependency direction is intentional:

Event contract and queue

Add a discriminated, strict presentation-event variant to the existing helper task model. Keep existing template tasks unchanged. A roulette event contains only:

Field Meaning
eventId Stable idempotency key, derived from roulette session ID and stage sequence.
sessionId Casino session correlation; safe identifier, not an authority to settle the game.
stage Fixed enum: betting_open, spinning, or result.
roomKey, locationKey, assignedAgentId Exact operator-configured scope and helper identity.
type Literal Emote; no Chat, Whisper, arbitrary target, or map write.
templateId / text Opening/spinning reference reviewed fixed helper templates. Result text is generated by the existing formatRouletteRoundResult and stored only in this typed internal event.
Existing task fields Lease epoch, expiry, status, result code, and observed operation metadata.

Only the internal casino bridge can create this event variant. Do not expose its text field through /bot helpers assign, the helper command surface, or a player-controlled command. Validate the discriminant, stage, scope, length, and agent capability at insertion and again before dispatch. The result contains only aggregate number/color, wager counts, and net total; it excludes member numbers, names, per-player stakes, bet selections, wallet balances, and private text.

The current helper repository permits one active task per agent, including queued work. Extend its task-state/index semantics only as needed to support a short, bounded FIFO of roulette stage events while retaining one leased/dispatching event per helper. Enforce unique eventId, queue capacity, expiry, and session/stage ordering with atomic compare-and-set transitions. The result event has priority over stale queued visual events: discard or expire an undispatched opening/spin event when superseded, but never cancel or replay an event that may already have dispatched. Keep ordinary operator-assigned template tasks on their existing one-active-task behavior.

Do not create a second collection for this first release. Reuse prototypeHelperTasks, its terminal retention policy, prototypeActions, and the current helper observation and notification mechanisms. Add only indexes needed for event idempotency/ordered claims, with names and options documented before implementation.

Presentation timeline and ordering

Casino lifecycle point Helper event Source text Ordering rule
Existing onRouletteStarted after open betting_open Reviewed, fixed opening Emote Must not delay or undo an accepted wager.
Close boundary with active wagers, before draw spinning Reviewed, fixed spin Emote New bets are already closed; skip if helper is not ready.
Durable result settled result Existing formatRouletteRoundResult(event.settlement) Highest priority; only enqueue after settlement succeeds.
Empty/cancelled round or pre-result recovery None None Keep existing quiet close/refund behavior; do not invent a result.
Participant settlement None Existing private result formatter Continues from supervisor to each participant.

Add an optional casino lifecycle callback for the spin stage at the existing deadline boundary. It is presentation-only and best-effort: it must not block the draw or settlement, and failure is reported with the session ID without changing wager acceptance, draw, settlement, refund, or renewal. Event sequence numbers preserve stage ordering if enqueue completes asynchronously. Do not add an extra spin delay in the first release. The staged Emote is a roleplay cue; an actual animated wheel or client-side pose transition is not established by the current API.

Failure, fallback, and recovery policy

Condition Required behavior
Helper disabled or absent before enqueue Use the existing eligible-occupant private-whisper announcement path. Private participant settlement remains unchanged.
Helper is known out of region from fresh shared BC observation, or its task fails a pre-dispatch identity/location check Use the existing whisper fallback only after no-dispatch is certain; position-sharing evidence is optional, never inferred from Mongo or liveness.
Queue insertion is confirmed not to have created a task Report the queue failure; existing whisper fallback is allowed because no helper dispatch is possible.
Helper task is queued, claimed, or dispatch status is ambiguous Do not send a second public announcement automatically. Expire/reconcile the task using existing helper status semantics; an operator may inspect the task and observed room history.
Helper out of region or wrong room/runtime/identity at execution Fail the task before dispatch; no helper Emote. A later phase may proceed only under its own idempotency key and current session state; do not retry the failed stage under a new key.
Helper disconnects before dispatch Lease/recovery policy returns only definitely undispatched work to a safe terminal/retry decision; do not treat heartbeat or Mongo state as BC evidence.
Helper disconnects after possible dispatch Mark reconciliation_required; never blindly retry a non-idempotent Emote.
Supervisor restarts after result settlement Do not announce a prior result on recovery. Preserve the current casino rule that settled sessions are not reannounced; unresolved presentation task remains separately visible for manual reconciliation.
Casino outcome draw or persistence fails before a durable outcome Existing casino recovery refunds or reconciles wagers. No result task may be created.
Helper action budget exhausted Do not bypass the helper budget or BC send queue. Drop stale, undispatched cosmetic stages first; a result may use whisper fallback only after its task is confirmed not dispatched.

Fallback is phase-specific and must not duplicate a possible public Emote. A known pre-dispatch unavailability may use the current whisper behavior. After a task is claimed or a BC action may have been emitted, only observed evidence or operator reconciliation resolves uncertainty. Do not retry the same failed stage with a different idempotency key; a later stage has its own key and must pass current session and helper checks.

Configuration and operator decisions

Use the existing room-scoped casino.rules and helper registry rather than adding environment variables or accepting arbitrary room/location names. The implementation should add a strictly validated, optional roulette-presentation section with:

The result wording stays with the existing casino result formatter, not a new operator-defined settlement formula or free-form result template. The selected helper record must match the configured member number, room key, runtime target, and a new, explicit roulette-wheel presentation capability. Reuse the roulette location binding; do not create a second authorization/location registry. Before dispatch, recheck the active presentation mode so a switch back to whisper fences queued but undispatched helper events.

Before activation, operators must:

  1. Create a dedicated BC account and configure its operator-managed avatar to resemble the wheel; keep credentials only in that helper deployment’s environment.
  2. Register one enabled helper identity in the selected prototype database with the dedicated member number, main room key, target runtime, roulette presentation capability, and a conservative task limit.
  3. Ensure its observed position is inside the existing roulette region. Startup teleport can use a point location inside that region; it cannot use a region record as its target. The optional position-sharing stream may help the supervisor preflight this; without fresh shared evidence, the helper’s own local BC observation remains the dispatch gate.
  4. Approve opening/spinning Emote text and verify that the aggregate result message is appropriate for public room display.
  5. Start with mode: whisper in both isolated targets. Qualify the helper path with test accounts/database, then explicitly enable it for one room.
  6. Confirm the per-helper one-minute action budget can accommodate the three stage Emotes under actual timing. Do not silently raise the default budget; qualify any operator adjustment and preserve the local BC queue.

The literal BC room name, avatar design/pose, stage wording, whether a known pre-dispatch failure should fall back per event, and the budget needed for three-stage choreography are release decisions. No helper is required for casino gameplay; disabling presentation returns the casino to its current announcement behavior.

Security and protocol invariants

Implementation and qualification phases

Phase 0 — Confirm the deployment contract

Phase 1 — Typed presentation events

Phase 2 — Casino stage bridge

Phase 3 — Test and controlled qualification

Phase 4 — Production opt-in

Acceptance criteria

  1. Roulette calculations, wallet state, results, private participant reports, access checks, and timing remain owned by existing casino code.
  2. A valid round produces at most one opening, spin, and result presentation event per session; event ordering is durable and duplicate enqueue is idempotent.
  3. A public result Emote can be emitted only from a successfully settled casino event, and its text comes from the existing aggregate formatter.
  4. The helper emits only while its configured identity is in the correct BC room and observed roulette region.
  5. A task that may have dispatched is never automatically replayed or paired with a fallback that could duplicate its Emote.
  6. Private participant results remain whispers from the supervisor regardless of helper availability or player movement.
  7. Default/off mode is behaviorally identical to the current casino announcements. No new socket, dependency, player-supplied task text, shared-map mutation, or cross-feature import is introduced.
  8. Test and production deployments remain isolated; production activation is separately qualified and explicitly approved.