veratown-prototype

Dilemma & Blackmail (The Ultimatum) — Feature Proposal

Status: Implemented as a disabled-by-default runtime feature. See the Dilemma feature guide for commands, room configuration, persistence, privacy, and recovery behavior.

Archived: 2026-10-16 (UTC). The feature guide is the current runtime reference.

Purpose and scope

Propose an independent, room-scoped prediction game. The bot selects one opted-in decision maker and posts an approved two-option scenario from MongoDB. Other opted-in players privately predict option A or B before the decision maker privately locks a choice. Correct predictions earn shared economy credits.

“Blackmail” is fictional game framing only. The feature must not use real secrets, threats, leverage, or pressure to extract a personal disclosure. Prompts describe fictional choices; the decision maker may pass without penalty. The game records a choice for this round, not a psychological profile.

Product rules

The game measures predictions about one fictionalized choice, not a player’s personality, intent, or actual private behavior. Do not accumulate prediction histories into a profile or expose individual predictions before the outcome.

Room configuration and content

Store one strict, typed config document per (roomKey, gameId) in prototypePartyGameConfigs, using <roomKey>:dilemma as this game’s ID. Missing or invalid config means disabled. Every setting is loaded from the DB; there is no hidden runtime default that silently enables the feature.

Setting Proposed validation / intent
enabled Explicit room-level opt-in; false when absent.
minPlayers, maxPlayers Bounded roster including one decision maker.
predictionWindowMs, choiceWindowMs Positive bounded durations, persisted as deadlines on a session.
minPredictions Bounded number of valid predictions required to settle.
correctPredictionRewardCredits Bounded non-negative integer; checked against economy ceilings.
maxRewardsPerRound Hard cap on recipient count / total award exposure.
timeoutPolicy Typed choice; proposed default is void and no reward.

Each prompt record has an ID, locale, category, scenario, option A, option B, moderation status, content revision, and timestamps. Only reviewed active records are selectable. Keep adult content in the DB, not source code. Prompt text must not request identifying information, non-consensual conduct, or disclosure of real secrets. Config and prompt revisions are pinned to the session so a live round does not change when an operator edits the catalog.

Persistence, concurrency, and recovery

prototypeDilemmaSessions holds roomKey, bounded joined member numbers, decision maker, prompt ID/revision, config revision, phase/revision, one bounded prediction per eligible member, locked choice, deadlines, outcome, and settlement IDs. Do not store free-form player messages or inferred traits. Active records have no TTL; terminal records receive an explicit expiresAt under the approved retention policy.

Permit only one active Dilemma session per room. Enforce this with a unique active key and conditional revision updates. A timer is only a wakeup: persisted deadlines and a compare-and-set phase transition determine the winner. Timers are unref’ed, tracked, canceled on plugin stop, and recovered from the repository at startup. A duplicate prediction, timeout callback, or process instance must not cause duplicate rewards.

Persist the final choice and outcome before settlement. Credit each correct prediction through the shared economy using a session- and member-specific idempotency key. A crash between outcome persistence and wallet settlement leaves the round pending; recovery retries the same keys and cannot pay twice. If a player leaves after joining, keep the frozen roster but reject commands from a member no longer present. A decision maker who leaves before locking a choice voids the round. New arrivals join only a later round.

UML

Component diagram

flowchart LR
    Root["start.ts"] --> Plugin["DilemmaPlugin"]
    Plugin --> Commands["DilemmaCommandSuite"]
    Plugin --> Service["DilemmaService"]
    Commands --> Parser["BotCommandParser"]
    Commands --> Service
    Service --> Rules["DilemmaRules"]
    Service --> Repo["DilemmaRepository"]
    Service --> Economy["Injected EconomyPort"]
    Service --> Comms["Injected CharacterCommunicationApi"]
    Repo --> Config[("Room-scoped game config")]
    Repo --> Prompts[("prototypeDilemmaPrompts")]
    Repo --> Sessions[("prototypeDilemmaSessions")]
    Comms --> BC["Existing BcClient actions"]

Class diagram

classDiagram
    class DilemmaPlugin {
        +start()
        +stop()
    }
    class DilemmaCommandSuite {
        +register()
        -predict(A|B)
        -choose(A|B)
        -pass()
    }
    class DilemmaService {
        +startRound(roomKey)
        +predict(sessionId, member, option)
        +choose(sessionId, member, option)
        +pass(sessionId, member)
        +recover(roomKey)
        +stop()
    }
    class DilemmaRules {
        +validatePrompt(prompt)
        +resolve(predictions, choice, minPredictions)
    }
    class DilemmaRepository {
        <<port>>
        +createActiveSession()
        +recordPrediction(expectedRevision)
        +lockChoice(expectedRevision)
        +persistOutcome(expectedRevision)
        +listRecoverable(roomKey)
    }
    class EconomyPort {
        <<injected shared contract>>
        +grantOnce(operationId, member, amount, reason)
    }
    DilemmaPlugin --> DilemmaCommandSuite
    DilemmaPlugin --> DilemmaService
    DilemmaCommandSuite --> DilemmaService
    DilemmaService --> DilemmaRules
    DilemmaService --> DilemmaRepository
    DilemmaService --> EconomyPort

Use-case diagram

flowchart LR
    Admin((Room admin)) --> Configure([Enable and tune this room])
    Player((Opted-in player)) --> Join([Join next round])
    DecisionMaker((Selected player)) --> Decide([Choose A, B, or pass])
    Predictor((Eligible predictor)) --> Predict([Predict A or B])
    Timer((Deadline worker)) --> Resolve([Lock and resolve predictions])
    Configure --> Config[(Room config / reviewed catalog)]
    Decide --> Session[(Round state)]
    Predict --> Session
    Resolve --> Reward([Credit correct predictions once])

Activity diagram: prediction round

flowchart TD
    Start([Start requested]) --> Gate{Enabled config and enough joined?}
    Gate -->|No| Deny[Private disabled / insufficient-player reply]
    Gate -->|Yes| Select[Select decision maker and reviewed prompt]
    Select --> Create[Persist session and prediction deadline]
    Create --> Post[Post scenario and options]
    Post --> Predict[Accept/revise private A/B predictions]
    Predict --> ClosePred{Prediction deadline or choice submitted?}
    ClosePred -->|Predictions closed| Lock[Hide further predictions]
    ClosePred -->|Choice arrives| Lock
    Lock --> Choice{Decision maker chooses or passes?}
    Choice -->|Pass / leave / timeout| Void[Persist void outcome]
    Choice -->|A or B| Save[Persist locked choice and outcome]
    Save --> Quorum{Minimum predictions met?}
    Quorum -->|No| Void
    Quorum -->|Yes| Award[Grant each correct predictor idempotently]
    Award --> Finish([Announce aggregate result and close])
    Void --> Finish

State machine

stateDiagram-v2
    [*] --> Lobby
    Lobby --> Predicting: enabled + joined roster + prompt
    Predicting --> Predicting: valid prediction / allowed replacement
    Predicting --> ChoicePending: prediction deadline closes
    Predicting --> ChoicePending: decision maker chooses early
    ChoicePending --> Resolving: valid choice locked
    ChoicePending --> Voided: pass / departure / timeout
    Resolving --> Awarding: outcome persisted and quorum met
    Resolving --> Voided: insufficient predictions
    Awarding --> Complete: every idempotent reward resolved
    Awarding --> Awarding: recovery retries same operation IDs
    Voided --> Complete
    Complete --> [*]

Sequence / communication diagram

sequenceDiagram
    participant Player
    participant Cmd as DilemmaCommandSuite
    participant Service as DilemmaService
    participant DB as DilemmaRepository
    participant Economy as EconomyPort
    participant BC as CharacterCommunicationApi
    Service->>DB: load enabled room config and active prompt
    Service->>DB: create session with revision/deadline
    Service->>BC: publish fictional scenario and options
    loop Before prediction lock
        Player->>Cmd: private predict A|B
        Cmd->>Service: predict(member, enum)
        Service->>DB: CAS bounded prediction
    end
    Player->>Cmd: private choose A|B
    Cmd->>Service: choose(decisionMaker, enum)
    Service->>DB: CAS lock choice and persist outcome
    loop Correct predictors
        Service->>Economy: grantOnce(session/member operation ID)
        Economy-->>Service: credited / prior result / explicit failure
    end
    Service->>BC: announce aggregate result (dispatch is unconfirmed)

Adopted decisions and acceptance criteria

The feature is not a mechanism for storing or learning psychological profiles. Tests cover strict options/configuration, duplicate and replaced predictions, ineligible members, early choice, quorum, pass, deadlines, departure, concurrent close, restart recovery, and idempotent awards. Disabled or void sessions cannot credit an account.

References