veratown-prototype

Dilemma

Dilemma is an independent, room-scoped prediction game. “Blackmail” is fictional framing only: prompts must not ask for real secrets, identifying information, or non-consensual conduct. Players opt in separately for each round, and the decision maker may pass without penalty. The feature does not infer age, build player profiles, or retain prediction history beyond the round record.

The feature is disabled unless the selected runtime database contains a valid, enabled room configuration. Startup inserts the disabled baseline below and ten active English fictional A/B prompts only when absent. There are no in-game configuration commands. Operators provision and review configuration and prompt records in the isolated runtime database; never edit the source database or enable the game outside an approved adults-only room policy. The runtime records the operator’s room-policy confirmation but does not independently verify player age.

The starter catalog uses non-explicit, playful fictional choices. Startup preserves existing config values, enabled state, prompt content, and moderation decisions. Use the party-game provisioning workflow to populate and explicitly enable an approved room.

Commands

All command responses are private whispers. Native /bot and ! command forms are supported by the shared parser.

Command Behavior
/bot dilemma join Opt in for the next round only.
/bot dilemma withdraw Withdraw from the next round’s opt-in roster.
/bot dilemma start Room-admin only; starts a round from present, opted-in members.
/bot dilemma predict A or B Submit or replace one private prediction before predictions close.
/bot dilemma choose A or B Decision maker privately locks the choice early or during the choice window.
/bot dilemma pass Decision maker voids the round without penalty or rewards.

The room receives the fictional scenario and its two options, but not individual predictions or the decision maker’s identity. The selected decision maker is notified privately, but BC does not confirm whisper delivery. Once a choice is durably locked, the room receives only the aggregate result and total credits awarded. A timeout, pass, departure before choice lock, or missed prediction quorum voids the round. Incorrect predictions never lose credits.

Opt-ins are held in process memory until a round starts, a member withdraws, leaves, or the connection becomes ready again. They are not durable preferences; a restart does not carry consent into another round.

Configuration and prompt catalog

The repository owns prototypePartyGameConfigs, keyed by (roomKey, gameId) with _id: "<roomKey>:dilemma", plus prototypeDilemmaPrompts and prototypeDilemmaSessions. Configuration and prompt documents are strict schemas; malformed records fail closed. Operators must increment revision when changing a configuration and contentRevision when revising a prompt.

Required configuration fields:

Field Bounds and meaning
enabled Explicit opt-in; missing or false disables the game.
adultsOnlyRoomConfirmed Must be true to enable; an operator policy attestation, not age verification.
locale Exact locale used to select reviewed prompts.
minPlayers, maxPlayers 2–50 opted-in members, including the decision maker.
predictionWindowMs, choiceWindowMs Each 10 seconds–1 hour; persisted deadlines govern recovery.
minPredictions At least one, no more than the predictor roster.
correctPredictionRewardCredits 1–100 credits when enabled.
maxRewardsPerRound At most 49; the maximum roster must fit the recipient cap.
timeoutPolicy Currently only void; no timeout reward path exists.

The product of the per-prediction reward and recipient cap cannot exceed 1,000 credits per round. An enabled configuration that cannot meet its minimum roster, quorum, or recipient limits is rejected.

Prompt records require _id, locale, category, scenario, optionA, optionB, moderationStatus, contentRevision, createdAt, and updatedAt. Only records marked moderationStatus: "active" are selectable; drafts and archived prompts are not. Scenario text is capped at 1,000 characters and each option at 300. Human review remains necessary: the runtime does not attempt to infer whether content requests secrets or is otherwise appropriate. The active catalog is bounded to 1,000 prompts per locale.

Example disabled configuration shape (timestamps are BSON dates):

{
    "_id": "main:dilemma",
    "schemaVersion": 1,
    "gameId": "dilemma",
    "roomKey": "main",
    "revision": 1,
    "enabled": false,
    "adultsOnlyRoomConfirmed": false,
    "locale": "en",
    "minPlayers": 3,
    "maxPlayers": 10,
    "predictionWindowMs": 60000,
    "choiceWindowMs": 60000,
    "minPredictions": 2,
    "correctPredictionRewardCredits": 5,
    "maxRewardsPerRound": 9,
    "timeoutPolicy": "void",
    "createdAt": "<BSON Date>",
    "updatedAt": "<BSON Date>"
}

Persistence, recovery, and retention

Only one active session per room is allowed. MongoDB enforces the active-room unique key; predictions, phase transitions, outcomes, and reward progress use revision compare-and-set updates. Session snapshots pin the exact config and reviewed prompt revisions and bounded text used for that round, so later catalog edits do not alter an active game.

The roster is frozen when a round starts. Commands from absent members are rejected; a decision maker who leaves before locking a choice voids the round. New arrivals can only join a later round. Timers are wakeups, not state authorities: persisted deadlines and conditional writes determine transitions. Timers are unref’ed, canceled on plug-in stop, and active sessions are recovered on startup. The plug-in detaches room listeners before draining event work. Its service stop may emit a final resolution, so it drains again after releasing the resolution subscription; cleanup failures cannot skip either drain or service shutdown.

The final choice and complete reward plan are persisted before economy writes. Each correct predictor is credited through the shared EconomyPort with a stable dilemma:<sessionId>:<memberNumber> operation ID. If recovery repeats a grant, grantOnce returns the prior result instead of crediting twice. Voided rounds never award credits. Aggregate room announcements use the existing communication action and are dispatched without a BC delivery acknowledgement.

prototypePartyGameConfigs and prototypeDilemmaPrompts are operator-managed configuration/content collections and have no TTL. Active session records have no expiry. Terminal sessions receive an explicit 60-day expiresAt and a TTL index; TTL cleanup is asynchronous. The session stores only bounded member numbers, A/B choices, prompt/config snapshots, deadlines, and reward state—no raw player text or inferred traits.

Runtime diagrams

flowchart LR
    Root["start.ts"] --> Plugin["DilemmaPlugin"]
    Plugin --> Commands["DilemmaCommandSuite"]
    Plugin --> Service["DilemmaService"]
    Commands --> Service
    Service --> Repo["MongoDilemmaRepository"]
    Service --> Economy["EconomyPort"]
    Service --> Comms["DilemmaCommunicationPort"]
    Repo --> Config[("prototypePartyGameConfigs")]
    Repo --> Prompts[("prototypeDilemmaPrompts")]
    Repo --> Sessions[("prototypeDilemmaSessions")]
stateDiagram-v2
    [*] --> Predicting: enabled config + frozen opt-in roster
    Predicting --> Predicting: accept or replace private prediction
    Predicting --> ChoicePending: prediction deadline / early choice
    ChoicePending --> Voided: pass / departure / timeout
    ChoicePending --> Voided: prediction quorum not met
    ChoicePending --> Awarding: choice locked + quorum met
    Awarding --> Awarding: retry pending grant with same operation ID
    Awarding --> Complete: all grants resolved
    Voided --> Complete
    Complete --> [*]

The repository compare-and-set transition is authoritative; deadlines only wake the resolver. Each correct prediction is granted once after the durable outcome and reward plan are saved. A void round never calls the economy.

Design history