# SheevChat Declarative Chat Game v2

`sheevchat-chat-game-v2` is the genre-neutral Marketplace runtime for command-driven games. The desktop owns safety, persistence, identity, transactions, and output delivery. The package owns all game names, rules, resources, commands, settings, tables, prompts, messages, and optional overlay declarations.

This is the active creator contract in released SheevChat **2.8.0**. New packages use `schemaVersion: 2`; schema-v1 packages are historical validation inputs and are not templates for the active runtime. Minimum beta versions below record when additive capabilities first became available; they do not mean the contract is still prerelease-only.

## Package envelope

A source release lives under the Marketplace `chat-games/<package-id>/<version>/` directory and contains a root `manifest.json`, root icon, and `marketplace.json`. The downloadable `.sheevpack` excludes `marketplace.json`, cannot exceed 25 MB, and cannot contain SQL, executables, command scripts, or unsafe paths. The declarative game lane cannot execute JavaScript. A package that declares the bounded `overlay_widget` capability may include HTML/CSS/JavaScript files referenced only by its sandboxed optional `widgets[]` companions.

The package may depend on `action_library`, `widget_pack`, `theme_pack`, `community_pack`, `chat_game`, or `builtin_widget` entries. Catalog trust values are `official_sheevchat`, `verified_partner`, `verified_creator`, `marketplace_verified`, `community`, `local_import`, and `unverified`; trust never grants additional runtime capabilities.

## Design boundary

A chat game's authoritative behavior is JSON data. It cannot execute JavaScript, SQL, shell commands, filesystem access, arbitrary network requests, Electron APIs, or unrestricted expressions. A package may only compose operations registered by the installed runtime. Optional companion JavaScript runs only in the isolated `sheevchat-widget` iframe and cannot enter the declarative game runtime. Remote game content can use a named host-controlled provider adapter; packages never provide request URLs. Unsupported capabilities require a newer SheevChat version.

No resource is mandatory. A game may use SheevChat loyalty, package counters, inventory-style resources, all three, or none. Loyalty is an explicit resource with `kind: "loyalty"`; it is never inferred from the game genre.

## Supported capability families

- `managed_commands` — protected package commands with normal authority, platform, and cooldown controls.
- `loyalty_resource` — optional atomic reads and adjustments of the channel's configured loyalty balance.
- `package_resources` and `inventory` — package-isolated numeric balances and collections.
- `weighted_random` — bounded package tables with cryptographically sourced selection.
- `prompt_banks` — bounded prompt/content pools with package-scoped anti-repeat history.
- `remote_prompt_sources` — cached prompt pools from a host-approved provider with bounded requests, field mapping, and safe text transforms.
- `scoped_state` — typed viewer, pair, channel, round, and stream values.
- `sessions` and `multiplayer` — bounded open/join/leave/close lifecycles and participant identity locks.
- `community_state` — channel-scoped contributions and goals.
- `voting` — one ballot entry per linked viewer with bounded tally output.
- `overlay_events` — package-declared snapshots and animation events; overlays never settle outcomes or mutate authoritative state.
- `overlay_widget` — optional `widgets[]` companion surfaces that run in the isolated SheevChat widget iframe and are placed explicitly by the streamer.
- `card_decks` — package-owned, scoped, persistent shoes containing a developer-selected number of standard 52-card decks; draws never replace cards automatically.
- `scheduled_flows` — persistent, bounded package-flow deadlines, executed by the host independently of chat messages or overlays.
- `session_controls` — participant reads/locking, replaceable participant votes, resource tiebreaks, and atomic fixed participant rewards.

## Generic operations

Commands declare at most 64 top-level operations. Nested `when` and `repeat` blocks remain subject to validation and runtime budgets.

| Operation | Purpose |
| --- | --- |
| `require` | Compare two bounded values and emit a package-authored instructional error. |
| `resource` | Atomically add or remove an optional loyalty or package resource. |
| `set`, `increment`, `contribute` | Mutate declared scoped state. |
| `choose` | Select from a declared weighted table and apply its declared effects. |
| `prompt`, `history` | Select content and maintain anti-repeat history. |
| `session_open`, `session_join`, `session_leave`, `session_close` | Manage bounded multiplayer/community sessions. |
| `vote`, `vote_tally` | Record a unique viewer vote and read the generic tally. |
| `leaderboard` | Read a bounded package-resource leaderboard. |
| `when`, `repeat` | Run bounded declarative branches or repetitions. |
| `emit_overlay` | Publish a bounded package event after the authoritative transaction succeeds. |
| `deck_reset` | Create and shuffle a full shoe using the declared deck count; explicitly replaces the old shoe. |
| `deck_shuffle` | Shuffle only the remaining cards, preserving drawn-card exclusion. |
| `deck_draw` | Draw 1-52 physical cards without replacement. |
| `deck_status` | Read initialized status and counts without exposing undealt cards or their order. |
| `call` | Run a declared reusable flow within the current transaction. |
| `schedule`, `cancel_schedule` | Replace or cancel one named channel deadline targeting a declared flow. |
| `session_read`, `session_lock` | Read participant membership/count or lock admission while allowing participant voting. |
| `session_reward` | Grant a nonnegative fixed viewer-resource amount to every locked participant once per settlement ID. |
| `vote_reset` | Clear the named ballot for the next decision. |

## Reusable timed session flows

Starting in **`2.8.0-beta.26`**, packages can declare up to 40 `flows[]` entries, each with a stable `id`, up to 64 top-level `steps`, and an optional `response`. Flow steps use the same declarative operations as commands. `{ "op": "call", "flow": "deal" }` runs that flow in the caller's transaction. Calls, branches, weighted effects, and repetitions share a 4096-operation budget; flow call depth is capped at 16. No flow contains executable game code. `concat` calculations combine arrays (maximum 1000 entries), and `length` reads an array/string length, allowing packages to maintain their own card-hand rules.

`schedule` requires `scheduled_flows` and a literal timer `id`, a declared `flow`, and `delaySeconds` resolving to an integer from 1 to 3600. It returns `{ id, dueAt }` under optional `result` (default `schedule`). Scheduling replaces the same package/channel/timer deadline; a superseded job cannot execute later. At most 64 pending timers exist per package. `cancel_schedule` removes that named timer for the current channel.

Timer `scope` defaults to `channel`; `installation` shares one timer namespace across all platforms in this app profile. The same new `installation` scope is available for state and shoes in beta.26, isolated by package ID and independent of platform/channel fallback IDs. It leaves existing channel-scoped package data untouched. Use the same scope for scheduling and cancellation. Chat Blackjack uses installation-wide state, shoe, and timers so every connected platform participates in one table.

Jobs capture the initiating identity, settings, and channel context inside the authoritative command transaction. The host polls due jobs every 500 ms, recreates the package runtime, and executes its flow once in a new transaction, consuming the job atomically with its state/resource changes. Jobs survive app restart. Current package enable/compatibility, loyalty enablement, effective command names, and point labels are rechecked at execution. Game-level deadlines must still be checked by the package before accepting chat input; the polling interval never extends a participation or voting window.

Uninstalled, incompatible, or paused packages cannot run due jobs; unavailable-package jobs are discarded when encountered. A failing job rolls back all its state and resource changes, is removed to avoid repeated payout attempts, and logs a diagnostic. Packages should expose moderator recovery that cancels their timers and closes an interrupted session. A chat-delivery failure after commit cannot undo or duplicate settlement. Overlays never schedule or execute flows.

Session-control operations require `session_controls`:

- `session_read` takes `sessionId` and sets `session` plus optional `result` to `{ id, type, status, ownerId, state, count, isParticipant, expiresAt }` for the current identity.
- `session_lock` takes `sessionId`, requires an open session, changes it to `locked`, and clears its admission expiry. `session_join` accepts only open, unexpired sessions; `maxParticipants` may resolve to 1-1000 (default 1000). Already joined viewers do not consume another slot. Use a fresh session ID for every independently rewarded round.
- Enhanced `vote` takes `sessionId`, `id` (ballot name) or a resolved `ballotId`, `choice`, optional literal `choices`, `replace: true`, and `requireParticipant: true`. Replacement changes the existing viewer's choice instead of adding a second vote. Open/locked and unexpired session checks apply. Legacy votes retain their existing duplicate-rejection behavior.
- Enhanced `vote_tally` takes `sessionId`, ballot `id` or `ballotId`, 1-20 unique `choices`, optional viewer `resource`, and `defaultChoice`. It returns `{ choices: [{ choice, total, points }], winner, by, total }`. Only participants count. Highest vote count wins; only tied leaders compare combined current resource balances; exact ties use `defaultChoice`. `by` is `votes`, `points`, or `default`. This reads balances without spending them. Resource sums outside safe integer range reject the transaction.
- `vote_reset` takes `sessionId` and ballot `id` or `ballotId`; it clears only that ballot.
- `session_reward` takes `sessionId`, a stable settlement `id`, declared viewer `resource`, and fixed `amount` resolving to an integer from 0 to 1,000,000. The session must be locked. Without a filter every participant receives the same amount. A duplicate settlement ID is rejected even with a fresh command key. All resource adjustments and point logs share one transaction; if any adjustment fails, nobody is paid. Zero records the settlement without changing balances. The package chooses reward eligibility/precedence and closes the session afterward.
- Optional capability `filtered_session_rewards` (minimum SheevChat `2.8.0-beta.69`) enables `session_reward.filter: { "ballotId": "prediction", "choice": { "ref": "state.winner" } }`. `ballotId` is a static identifier; `choice` is a nonempty string of at most 80 characters or a reference resolving to one. Only locked participants whose final recorded choice in that ballot matches receive the fixed reward; unregistered votes and other choices are excluded. The returned `participants` and stored settlement count describe matching recipients, including zero matches. The same settlement ID cannot pay a different group on retry. Malformed filters or missing capability fail validation, and the publication pipeline enforces the minimum app version. Existing unfiltered rewards retain their behavior.

Commands may declare `defaultAuthority: "moderator"` or `"broadcaster"` for initial managed-command configuration (otherwise `anyone`). Streamer-saved command authority overrides this default. Scheduled flows are internal continuations of the authorized initiating command, not public managed commands.

## Public overlay snapshot hydration

`emit_overlay` with `snapshot: true` requires its `id` in `overlay.snapshots`. The transaction retains only the latest public payload for that package/event; each payload is capped at 16 KiB. The normal committed event still displays live. New WebSocket clients receive bounded `chatGameSnapshots`, filtered to installed, enabled compatible packages. The SheevChat widget bridge includes only the selected package's entries in `snapshot.games`, with `{ packageId, type, at, payload, replay: true }`.

A companion reads these public snapshots to redraw without animation or reward replay. It must never receive hidden cards, future shoe order, private settings, or participant balances individually. Browser reload cannot rerun a game operation. Current snapshots are data hydration rather than animation events; existing non-snapshot game events are not replayed.

## Standard card decks

Available starting in **SheevChat `2.8.0-beta.25`**. Packages using this capability must declare that minimum compatible app version in `marketplace.json`. The existing v2 runtime ID is retained; older hosts reject the unknown capability instead of executing a partial game.

Declare `card_decks` and a `decks[]` list (at most 16 independently named shoes). Each definition has a stable `id`, a `scope` (defaults to `channel`), and a `count` (defaults to 1). Count is either an integer from **1 to 16**, or a single `{ "ref": "setting.<id>" }` reference to an integer management setting whose minimum, maximum, and default stay within that range. Developers can fix the deck count or expose it as a streamer setting. Each shoe contains `count × 52` cards with no jokers.

```json
{
  "capabilities": ["managed_commands", "card_decks"],
  "decks": [
    { "id": "shoe", "scope": "channel", "count": { "ref": "setting.deck-count" } }
  ],
  "management": {
    "settings": [
      { "id": "deck-count", "type": "integer", "label": "Number of decks", "default": 6, "minimum": 1, "maximum": 16 }
    ]
  },
  "commands": [
    {
      "id": "reset-cards", "trigger": "!resetcards",
      "description": "Reset the shoe using the configured number of decks.",
      "steps": [{ "op": "deck_reset", "deck": "shoe", "result": "shoe" }],
      "response": "Shuffled {result.shoe.decks} decks: {result.shoe.remaining} cards ready."
    },
    {
      "id": "draw-card", "trigger": "!drawcard",
      "description": "Draw one card without replacement.",
      "steps": [{ "op": "deck_draw", "deck": "shoe", "count": 1, "result": "draw" }],
      "response": "Drew {result.draw.cards.0.label}. {result.draw.remaining} cards remain."
    }
  ]
}
```

This fragment illustrates the deck fields within a normal v2 package envelope. All four operations require `deck` to name a declared shoe; optional `result` defaults to that deck ID. Draw `count` defaults to 1 and can use the existing bounded value/calculation references; its resolved value must be an integer from 1 to 52.

Each operation returns `{ id, initialized, decks, total, drawn, remaining }`. Only a draw additionally returns `cards[]`, in draw order. A physical card looks like:

```json
{ "id": "1:clubs:A", "deck": 1, "suit": "clubs", "rank": "A", "rankNumber": 1, "label": "A♣" }
```

Suits are `clubs`, `diamonds`, `hearts`, and `spades`. Ranks are `A`, `2`-`10`, `J`, `Q`, and `K`; `rankNumber` is the ordinal 1-13, **not a game score**. The one-based `deck` number distinguishes identical faces in a multi-deck shoe. IDs are unique within the shoe and may recur after an explicit reset; they are not global event IDs.

Scopes use the existing viewer, viewer-stream, pair, channel, round, and stream identities (`viewer`, `viewer_stream`, `pair`, `channel`, `round`, `stream`). Pair shoes require a target viewer. Round shoes use the current command's opened/joined session or `context.roundId`; stream scopes require an active stream identity. Missing scope identities fail rather than sharing an empty scope. Package IDs and shoe IDs always isolate storage. Shoes survive runtime recreation and app restart; uninstall retains their database state under the normal game-state policy.

`deck_status` on an uninitialized shoe returns `initialized: false` and zero counts. Drawing or shuffling requires a preceding explicit reset in that scope. Drawing more cards than remain fails without consuming any cards. An empty shoe remains empty until reset. Shuffling never restores dealt cards. Changing the configured deck count takes effect on the **next reset**, leaving the current shoe intact. Reset and shuffle use the host's cryptographic random source.

Deck writes participate in the command transaction: a later failure rolls them back, and an idempotent replay does not draw or shuffle again. Status/reset/shuffle results never expose future card order. Packages can store selected cards in declared JSON state and include them in committed overlay payloads through normal value references. Nested object and array references resolve recursively. The package must omit any hidden cards from public payloads until its rules reveal them.

The handler supplies card identities and shoe lifecycle only. Hand scoring, ace values, dealer rules, votes, timers, participation, and rewards belong to package rules or separately approved generic capabilities. Companion widgets remain visual clients and cannot draw cards or settle outcomes.

## Remote prompt sources

`promptSources` may supply a bank used by the normal `prompt` operation. The first registered provider is `random-word-api`. Its host and route are fixed by SheevChat; the package may declare only bounded language, count, difficulty, length, cache, and field-mapping options.

Mapped fields support `identity`, `lowercase`, `uppercase`, and `shuffle_characters` transforms. SheevChat accepts HTTPS JSON only, blocks redirects, limits request time and response size, filters malformed words, and persists a package/source cache. An expired cache remains available during a provider outage; if no cached content exists, the command reports that its remote prompt source is temporarily unavailable.

## Values and templates

Operations may use literals, `{ "ref": "..." }` references, or bounded `calc` objects. Registered calculations are `add`, `subtract`, `multiply`, `divide`, `floor`, `ceil`, `negate`, `minimum`, `maximum`, and `basis_points`.

Responses and errors use safe placeholders such as:

```text
{identity.username}
{args.quantity}
{setting.price}
{context.pointsName}
{context.commands.play}
{resource.tokens.balance}
{state.streak}
{result.reward.label}
{error.required}
{error.short}
```

Unknown placeholders become empty text. Output is capped by the host. Templates cannot call functions or evaluate code.

`context.commands.<command-id>` resolves to that installed command's effective local trigger. Use it whenever one command teaches or references another. This keeps help text correct after the streamer renames a Marketplace command or SheevChat assigns a collision-safe alternative.

## Command ownership and instructions

The host owns the shared command namespace in this order: SheevChat built-in commands and aliases, custom commands and aliases, then Marketplace commands and aliases. A Marketplace install never replaces a higher-priority command. When a primary trigger collision appears, SheevChat tries the command's declared `alternativeTriggers` in order and finally assigns a deterministic package-scoped fallback. The selected trigger is persisted so removing the higher-priority command does not unexpectedly rename the game again.

`aliases` are active at the same time as the effective primary trigger and are appropriate for common spelling, singular/plural, or familiar-name variants. `alternativeTriggers` are not aliases: they are ordered candidates for replacing the primary trigger only when its preferred name is unavailable. Package aliases that collide with a higher-priority owner are skipped. Streamers can customize or reset aliases from the installed command card without modifying the package.

Streamers may rename an installed Marketplace command. Explicit renames cannot claim a built-in, custom, or other Marketplace command. Resetting requests the package default, but the host still selects a safe alternative when that default is occupied.

Every game should provide:

- a short `management.summary`;
- concrete rules rather than generic contract language;
- a `How to play` management section listing exact syntax and what each play command does;
- one dedicated `!howto...` command that explains the full game in chat; and
- specific command descriptions that remain useful in the Commands drawer and `!cmdsearch`.

Choose a canonical trigger that says what the command does and avoids known built-in names. Declare a small `aliases` list for genuinely useful variants and an ordered `alternativeTriggers` list with equally understandable collision fallbacks. Do not generate broad or arbitrary aliases inside the package.

## Persistence and transactions

- Package state is keyed by package ID and cannot read another package's data.
- Existing `chat_game_counter_balances` rows remain the authoritative store for package numeric resources, allowing package upgrades to preserve balances when IDs remain stable.
- Command mutations run inside a SQLite transaction and use the incoming message ID as an idempotency key when available.
- Replaying the same key with the same payload returns the stored result; changing its payload is rejected.
- Failed requirements or mutations roll back the entire command.
- Viewer, session, vote, history, and output sizes are bounded by the host contract.
- Uninstall removes the installed package and unregisters its commands, but current package-owned database state is retained for a later reinstall. A package cannot execute uninstall code or request arbitrary host-data deletion.

## Optional overlays

The package may declare supported event and snapshot names in `overlay`. Placement is always explicit. The desktop remains authoritative and sends only bounded snapshots/events through the Marketplace widget bridge. An iframe cannot grant rewards, change balances, select outcomes, or bypass command permissions.

For a same-package companion, declare `overlay_widget` in `capabilities`, set package-level `compatibility.provider` to `sheevchat-widget` and `compatibility.api` to `sheevchat-widget-v1`, then define one or more surfaces in `widgets[]`. Each surface has a stable `id`, label, dimensions, file refs, fields/defaults, and exact package-scoped subscriptions. Installation makes the surfaces independently selectable under Overlay Builder Addons and never places them automatically. Pausing or uninstalling the package disables the game and every companion surface together.

`overlay.companionPackageId` remains supported when a creator intentionally wants the visual companion to be a separately versioned `widget_pack`. That companion depends on the `chat_game` and uses the same sandbox and package-scoped listener form `game.<game-package-id>.<event-id>`.

The parent delivers a committed event to an exact subscription such as `game.sheevchat-party-wheel.wheel-spin`. Same-package companions receive only events emitted by their own package even if they declare a wildcard listener. Idempotent command replays do not resend overlay animations, and browser-source reloads do not replay game events.

## Creator checklist

1. Use `schemaVersion: 2`, `type: "chat_game"`, and `runtime: "sheevchat-chat-game-v2"`.
2. Declare only the capabilities the package actually uses.
3. Keep IDs stable across versions when balances or settings must migrate.
4. Give every managed command a sensible trigger, ordered fallback triggers where useful, a specific description, bounded arguments, steps, and a response.
5. Add package-authored How to Play content and one discoverable `!howto...` command that references effective triggers through `context.commands`.
6. Use package-authored errors to explain balances, shortages, and recovery commands.
7. Declare the minimum compatible SheevChat version in `marketplace.json`.
8. Run Marketplace validation and desktop contract validation before publishing.

