Demo players
Demo players are engine-driven, simulated players that occupy player slots, follow a game’s objectives, and trigger its real rules — so a designer can watch a freshly authored ruleset play out, and a prospective player can preview what gameplay looks like, without real phones in real geography.
They are deliberately distinct from bots. A
bot is a first-class game entity the design depends on; a demo player is a
debugging/preview tool that lives outside the game design, works for any
ruleset that opts in, and is visibly labeled as a simulation. A ruleset opts in
by declaring an optional top-level demo block.
Three player kinds
Section titled “Three player kinds”The rules engine treats every player the same — proximity, state, and zone conditions evaluate identically regardless of kind. The kind only changes how positions update each tick and how the GM console labels the marker.
| Kind | Position source | Part of game design? | Counts for minPlayers? | Declared where |
|---|---|---|---|---|
human | Phone GPS | n/a | yes | joins via the four-letter code |
bot | Engine (behavior tree) | yes | no | Ruleset.bots: BotClass[] |
demo | Engine (behavior tree) | no | no | Ruleset.demo?: DemoPolicy |
interface DemoPolicy { count: number; zoneId: string; // home zone: spawn inside, teleport home initialState?: Record<string, unknown>; balanceTeams?: boolean; // default true behavior: BehaviorNode;}count— default roster size when the GM hits Fill to N in the Demo players card.zoneId— the demo roster’s home zone, a zone id from the game’s map (mirrorsBotClass.zoneId). Demo players spawn at a random point inside it. Because they simulate humans, their movement roams the union of all placed zones — the home zone is the spawn point and the teleport-home target if one ends up outside every zone. Game start refuses when this zone is missing or unplaced.initialState— startingstatefor each demo player. Optional. When omitted, demo players seed fromplayerJoinStatesin join order (see Seeding below); supplyinitialStateonly when you want every demo player to start identically regardless of join order.balanceTeams— alternate demo joiners between teams in team games. Defaulttrue. Ignored for games with no teams.behavior— the behavior tree driving every demo player each tick. It uses the sameBehaviorNodegrammar asBotClass.behavior, so there is one vocabulary to learn — see Bots & behavior trees for the full node vocabulary.
Example
Section titled “Example”From Pirate’s Booty (apps/wage-engine/src/games/pirates_booty/game.yaml):
# Demo players for previewing a 2v2 raid without four phones. Three branches:# carrying a chest → run it home to my crew's ship; empty-handed → go grab the# nearest chest on Treasure Island; otherwise wander. balanceTeams (default)# splits demo joiners across the two crews so deposits happen on both ships.demo: count: 4 zoneId: treasure_island behavior: kind: selector children: - kind: sequence children: - { kind: state_equals, key: carrying, value: true } - { kind: goto_zone, zoneRef: { ofTeam: same } } - { kind: collect_nearest_item, itemKind: treasure_chest } - { kind: wander }Read top to bottom: a carrier runs the chest home to its own crew’s ship; an
empty-handed pirate steers toward the nearest treasure chest; everyone else
wanders. balanceTeams is left at its default, so demo joiners alternate
between Pearl and Revenge Crew and deposits happen on both ships.
Behavior nodes for demo players
Section titled “Behavior nodes for demo players”Demo players reuse the shared behavior-tree grammar, but three nodes were added with simulated objective-following in mind. They work for bots too — see Bots & behavior trees for the authoritative definitions:
goto_zone— steer toward a placed zone’s center.zoneRefis either a literal zone id or{ ofTeam: "same" | "other" }, resolved against the acting player’s team at evaluation time. Pirate’s Booty uses{ ofTeam: same }so a carrier heads for its own ship.collect_nearest_item— steer toward the nearest un-held world item, optionally filtered byitemKindand confined to a zone (where). It only steers; the engine’s normal pickup handling claims the item on contact. Fails when no matching item exists, so a siblingwandercan take over.chase_target— head toward one specific player named by the actor’s own state (default keytargetId). Useful where a game assigns each player an individual mark to pursue.
How demo seeding works {#seeding}
Section titled “How demo seeding works {#seeding}”Demo players draw from playerJoinStates in join order, the same way humans
do as they arrive. This reproduces a game’s asymmetric roles across a demo-only
roster without any extra rule: the first “it”, the patient-zero zombie, the lone
shark, or the single bomb-holder all fall out of the join order naturally. When
you set demo.initialState, it overrides this and every demo player starts from
that bag instead.
When balanceTeams is true (the default) and the ruleset has teams, each new
demo joiner alternates onto the smaller team so a team game previews with both
sides populated.
Runtime and the GM console
Section titled “Runtime and the GM console”minPlayers. Demo players don’t count towardminPlayers. Their mere presence puts the session in preview mode, which waives the floor — so a demo-only roster can start a game that would otherwise require, say, four humans.maxPlayers. Still bounds humans only. Adding demo players never eats into the human cap.- Demo players card. When a ruleset declares a
demoblock, the GM console shows a Demo players card in the Players panel with Add, Fill to N (fills the roster up todemo.count), and Clear. The card is hidden for rulesets with nodemoblock. - Dashed markers. On the GM map, demo players render with a dashed marker outline and a “Demo” label, so they are never mistaken for humans or game bots.
- Dashboards. GM dashboard metrics aggregate over all players — human, bot, and demo — so a demo-only preview reads correctly.
Opting in
Section titled “Opting in”Demo mode is entirely opt-in and game-agnostic: any ruleset can support it by
adding a demo block whose behavior expresses how a simulated player should
pursue that game’s objectives. Rulesets without a demo block simply don’t show
the Demo players card and reject demo-roster API calls.