Skip to content

Crowd Simulator

Firing one test event tells you a handler works. It doesn’t tell you what your Pack feels like when thirty people find the gravity button at the same time. The Crowd Simulator plays a fake audience against your local bridge: fake viewers with plausible names redeem your events at a steady rate, type viewer text where an event asks for it, and pile up in queues exactly like a real crowd would. You watch the game, the overlay, and the Dashboard audit react.

You get the same loop as a live stream, minus the stream:

Terminal window
mobrule dev crowd

That’s the whole zero-config run. It reads your active manifest, picks events with even weights, invents parameter values that pass your schemas, and fires at 10 redeems a minute until you press Ctrl-C. At the end it prints what it fired and how each invocation settled.

You need a running bridge with your Pack active, and your adapter connected. The sim is a loopback client like the rest of mobrule dev: it talks to the bridge’s local API and nothing else, so no Twitch account or cloud setup is involved anywhere.

Every fire is a real invocation. It rides the normal queues, dispatches to your adapter over MPP, credits a fake viewer by name (your adapter’s “X used Y” attribution works), and shows up in the Dashboard audit marked with the sim source, so you can tell a rehearsal from an operator test-fire.

The sim paces itself like a crowd, too. It honors each Reward’s cooldown on its own clock, and it caps itself at 120 fires a minute no matter what rate you ask for.

It skips the platform’s Twitch half: nothing is charged, matched, paused, or refunded, because no Redemption exists. It also can’t vote in Chat-Plays windows. Votes are chat messages, and the sim has no chat. If your Pack declares a vote-window event, give it a weight of 0 in your scenario so rehearsals don’t open windows nobody can vote in.

Events that take viewer input get generated text. Where your schema constrains the value (an enum, a number with a range), the sim derives something valid. For free text it draws from your scenario’s corpus, or falls back to a small built-in word list.

The --chaos flag mixes in hostile input on purpose: a ratio of 0.2 makes roughly one fire in five carry text designed to fail your schema or stress your handler, the way real chat eventually will. Bounded fields get values that fail validation (and you get to watch the failure path work). Unbounded text fields can’t fail validation, so chaos sends adversarial strings instead: injection attempts, control characters, right-to-left marks. Your adapter should survive all of them.

The zero-config run is uniform and generic. A scenario file makes it feel like your crowd. It’s a small TOML file, and a Pack can ship one next to its pack.toml:

[weights]
say = 4.0
add_bot = 2.0
gravity = 1.0
fast_monsters = 1.0
[cadence]
rate_per_min = 12
[corpus]
"say.message" = [
"get behind the barrel lol",
"MOON GRAVITY MOON GRAVITY",
"imagine dodging",
]

Run it with:

Terminal window
mobrule dev crowd --scenario crowd.toml

Everything in a scenario is optional, and it only reshapes the run. Weights skew the event mix (a weight of 0 removes an event, unnamed events keep 1.0). rate_per_min overrides --rate. Corpus entries feed viewer text, keyed by event name or by event.param when one field needs its own pool. A usernames list adds extra names to the built-in pool. A scenario never invents events: a weight naming something absent from the manifest is ignored.

Pass --seed and the whole run is reproducible: same seed, same scenario, same sequence of fires.

Terminal window
mobrule dev crowd --seed 42 --duration 60 --record run.jsonl
mobrule dev crowd --replay run.jsonl

--record writes the run as a timeline: one JSON line per fire, with a version header up front. --replay executes a timeline exactly as written. That pair turns a good rehearsal into a regression fixture. Record a run that exercises the paths you care about, replay it in CI, and assert on the invocation audit or your pack state afterward. The timeline format is versioned, and a replay refuses a version it doesn’t know rather than guessing.

FlagWhat it does
--rate <n>Mean fires per minute (default 10, hard cap 120)
--seed <n>Fixes the random sequence for a reproducible run
--chaos <0..1>Ratio of fires carrying hostile viewer text
--duration <secs>Stop after this long instead of on Ctrl-C
--scenario <path>Load a scenario TOML
--record <path>Write the run as a replayable timeline
--replay <path>Execute a recorded timeline