Skip to content

Troupe authoring guide

This guide is informative. It shows how to build common stories from Troupe’s primitives. The authoring brief is a one-page summary for people and for LLMs asked to write Troupe files. Troupe Format is the normative document; “Format 7.2” means its section 7.2.

Most needs compose from entries, variables, events and conditions.

Players’ lines and veils go in the session’s limits, which hold for the whole chat. An author’s note is a session entry on the root, placed in system:

{ "id": "note", "position": "system", "text": "Keep the pace slow; let silences land." }

Replacing it later is another session entry with the same ID. A reminder every few rounds is "when": "turn % 4 == 0" with a numeric position.

{ "id": "goal_setup", "knownBy": ["elena"], "when": "state.phase == 'setup'", "text": "Elena wants to prove the plan works without Kai." },
{ "id": "goal_escape", "knownBy": ["elena"], "when": "state.phase == 'escape'", "text": "Elena will protect Kai at any cost." }

A one-way enum phase only moves forward through its values.

A character’s feelings toward everyone else are its own keyed variable:

{ "id": "feelings", "type": "int", "of": "others", "min": -100, "max": 100, "by": ["model"], "maxStep": 15,
"description": "How {{char}} feels about each person." }

An entry on the character shows its feelings toward the player’s character: {"id": "toward", "knownBy": ["kai"], "text": "{{char}} toward {{user}}: {{own.feelings.user}}."}.

{ "id": "alarm_tick", "when": "turn > 0 && ago('alarm_tick') >= 3", "compute": { "alarm": "state.alarm + 1" } },
{ "id": "alarm_full", "when": "state.alarm >= 4", "once": true, "direct": "The alarm bells start ringing. Guards flood the halls." }

alarm is a one-way int. Time of day is a one-way int clock in hours with derived views: {"id": "day", "type": "int", "derive": "state.clock / 24 + 1", "description": "The day."}.

An enum phase, entries gated on it, and events that advance it when a beat is done. A beat written in plain words is a model-set one-way bool.

ago() covers change keys, so a status can expire without bookkeeping: "when": "own.mood == 'upset' && ago('kai.mood') < 3". String literals like 'kai.mood' need rewriting if Kai is renamed.

A roll decides whether a risky attempt succeeds, and the reply narrates the result. A model records the attempt on the player’s message, and an event on: "player" rolls for it. attempt is a scenario enum with values ["none", "lock"] and by: ["model"]; picked is a scenario bool labeled “opens” and “holds”:

{ "id": "pick_lock", "on": "player", "when": "state.attempt == 'lock'",
"set": { "attempt": "none" },
"compute": { "picked": "roll(20) >= 12" },
"direct": "The lock {{state.picked}}. Narrate it." }

The label makes the direction read “The lock opens.” or “The lock holds.”, and the roll is recorded, so the model can’t change it. Random encounters are an event with weighted outcomes.

A mystery’s culprit is chosen once by an event with one outcome per suspect, "when": "state.culprit == ''", and culprit is a spoiler. Clues are scenario entries with knownBy: ["narrator"] gated on state.culprit, so the narrator plants them and the suspects never see them. Each suspect carries {"knownBy": ["<id>", "narrator"], "when": "state.culprit == self"} for their own guilt. A reveal pairs a private entry with a public one gated on a model-set bool, as Format Appendix B.2 does with master_key and key_known.

A set without values holds free-form items: ["rope", "a silver chalice"]. For quantities, use an int with of: "*" and min: 0: {"inventory.torch": 2}. Setting a key back to 0 removes it, and keys render with underscores as spaces.

Make the people who matter characters, with a summary for when they are offstage, and the rest world entries keyed on their names. An opening’s set can list who starts present. Someone new who appears during a chat joins the session’s characters, with a name, description and summary.

A scenario variable records where each character waits, and one event brings whoever is there into the scene when the location changes. Characters who go along with the players have "":

{ "id": "where", "type": "string", "of": "cast", "initial": { "*": "", "ghost": "cellar" }, "by": ["model"],
"description": "Where each character stays when not with {{user}}; empty for those who go along." }
{ "id": "arrive", "when": "ago('scene.location') == 0 && scene.location != ''",
"compute": { "scene.present": "scene.present.filter(c, state.where[c] == '') + state.where.filter(c, state.where[c] == scene.location)" } }

Mark the ghost’s cast member spoiler, a secret until the players walk into the cellar. Until then it is offstage. Openings should set scene.location, so the event can sort presence from the start. Like any event, it fires at most once per round, so presence follows the first move in a round.

Give a companion its own variables for the relationship, such as {"id": "closeness", "type": "int", "min": 0, "max": 100, "by": ["model"], "description": "How close {{char}} feels to {{user}}."}, and let a model add memories as session entries with pull and a title (Format 12.3), so each comes back only when it matters.

elapsed, hour and weekday let a companion notice time: {"id": "late_night", "when": "hour >= 0 && hour < 5", "text": "It is the middle of the night where {{user}} is."}.

A one-way enum chapter, with entries gated on it, keeps a canon character from knowing events after their moment in the story.

Copy the characters into a new bundle, credit their authors in credits, add a scenario with a cast, and rename clashing IDs. In a bundle, a renamed ID appears in cast members, knownBy, speakers, example speakers, scenario.worlds, location parent, of lists, keyed variable keys, the character keys of initial objects, keys and scene values in openings and events, {{@id}}, state and own macro paths at any segment, with or without @ ({{state.trust.kai}}, {{own.feelings.kai}}), and CEL member paths such as state.kai.nerve; string literals in CEL that name it need a look too. Each character brings its own entries, variables and events, and keeps using them through own and self.

License