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.
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.
The existing casino already owns the behavior the proposal needs:
CasinoGameService opens a round after the first valid funded bet, supports an
optional administrator open, closes at the configured deadline, persists the
random outcome before settlement, and renews a played round after its result
and wait windows.onRouletteStarted and onRouletteClosed expose the existing stage
boundaries. formatRouletteRoundStarted and
formatRouletteRoundResult provide the current public announcements.reportRouletteClosed sends the public aggregate before consolidated private
participant reports. The current public announcement path privately whispers
eligible roulette-region occupants; the private reports are sent even if a
participant has left the region.HelperBotsPlugin. A helper uses its own BcClient,
sends reviewed Chat/Emote tasks through CharacterCommunicationApi, and
executes only when its configured identity and BC-observed location match.
Existing helper tasks use fixed templates, one active task per helper, a
three-action-per-minute default budget, and no automatic replay after an
ambiguous dispatch.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.
“Casino roulette room” must resolve to the existing configured BC room and room-scoped location/rules:
BC_ROOM / PrototypeConfig.room is the exact BC room name joined by both
processes. If the intended BC room is literally casino.roulette, configure
that exact value for both; do not infer it from a location key.main. Their roulette
binding names an enabled type: "game" location with explicit region
geometry. That binding, not a hard-coded string, is the authority for player
access and the helper’s presentation location.RUNTIME_TARGET, room key, BC room, and
selected prototype database. Production and test remain isolated.| 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
Casino session state remains authoritative and unchanged. The wheel state is a presentation projection of that session, not a second game state machine:
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.CasinoRouletteClosedEvent can request a result
presentation.formatRouletteRoundResult. The supervisor continues participant whispers.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.
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:
features/helper-bots.features/casino and never starts the casino
plugin. It receives a validated presentation event and dispatches its bounded
Emote.src/app/start.ts is the sole composition root. It adapts the casino callback
to the existing helper task repository and selects the configured helper.CharacterCommunicationApi, action coordinator, journal, and rate-limited
BcClient; no second socket or raw packet path is introduced.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.
| 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.
| 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.
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:
mode: whisper (default/current behavior) or wheel_helper;agentId: required only for wheel_helper, identifying one enabled,
dedicated helper;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:
main room key, target runtime, roulette
presentation capability, and a conservative task limit.mode: whisper in both isolated targets. Qualify the helper path
with test accounts/database, then explicitly enable it for one room.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.
CasinoGameService, game access checks,
random draw, durable outcome, wallet/economy writes, and private participant
settlement.CharacterCommunicationApi.emote() and its existing
ActionCoordinator; one action journal owns the Emote. Never call
socket.emit, BcApiPlane.dispatch() from an already-journaled action, or
edit a shared map object to simulate the wheel.RUNTIME_TARGET; never read the production/source database for configuration.casino.roulette means the exact BC room name or an existing
map location/region.casino.rules presentation settings, defaulting to
current whisper behavior.src/app/start.ts; keep the casino
feature and helper feature independent.formatRouletteRoundResult for public aggregate content. Do not alter
roulette odds, wagers, deadlines, payout rules, or recovery semantics.whisper mode and helper mode in the isolated test database and
dedicated BC room. Verify the Emote’s observed sender/member and location;
verify the helper never receives private wager details.whisper; do not change casino
sessions, wallet records, or game outcomes.