The referee's turn procedure for CodeRPG, for one named person in the cast. Use on every turn, once `rpg.py reply` has stored their DO line: read state and content as them, resolve outcomes with rules/rpg.py --as <id>, and store the story they're told with `narrate`.
Install
npx skillscat add liminallyyours/coderpg-sim/gm-turn Install via the SkillsCat registry.
One turn, for one person
In a round, each person in the cast takes one turn. Their reply has been stored with python3 rules/rpg.py reply <id> <file>, which printed their DO: line and their JOURNAL: line. This procedure turns the DO line into what happens, exactly as a game master answers a player's typed message, and ends by storing the story they're told with narrate. Below, "the player" is the person whose turn it is, and <id> is their id.
--as <id>goes on every rules command of the turn:look --as <id>,move north --as <id>,show characters pell --as <id>,npc-brief pell --as <id>,remember pell "<line>" --as <id>. The examples below leave it out to stay short; never leave it out of a command. Only the referee's own commands (reply,narrate,brief,pane,round,run,memory,log) name the player themselves instead.REFUSED: it's <someone>'s turnmeans you're acting for the wrong person: act for the one whose turn it is.- The DO line is the action: what they say and do, in their own words, as a player would type it. The JOURNAL line is their private reason.
replyhas already put it in their memory and the log. Never act on it, never let anyone in the world learn it (an NPC's brief and scene lines included), and never answer it in the story. - Nothing you write reaches the player except the story you store with
narrate. Anything else you write is seen only by the owner: keep it to nothing, or a line.
1. Read state
Run python3 rules/rpg.py look. It reports the player's status, location, open and blocked exits, the items, characters and other people here, their inventory and flags, and ends with ROOM TEXT:, the room as it reads now. It has already applied flag conditions. If the status is defeated, they take no more turns: tell the owner in one line and stop.
- Every turn starts with
look, even for a plain move: other people have acted since this player last looked, andlookalso notes who is present at the start of the turn.
2. Read the content involved
The current room's text is the ROOM TEXT: that look or move printed: don't show it again. Read each character, item or quest the action touches, and any other room, with python3 rules/rpg.py show <rooms|characters|items|quests> <id>. Never open content files by path.
showreads authored content and content generated earlier this run (marked[generated]bylook) alike. Once generated, things are as real as authored content for the rest of the run.showandROOM TEXT:already leave out the## If <condition>sections that don't hold now. Deeper### Ifheadings, such as a quest's endings, are still yours to match against the flags. Conditions can use the automatic nameslooklists under ALSO TRUE:has_<item>while carrying an item, andmet_<character>once the player has seen that character. You never set these withflag set.- The game names its own stats and money:
lookprints them as e.g.Grit (might)andCredits. Use the game's names in narration, and the mechanical name (might) in commands. - Authored and generated content together are everything you know about the world. You never make up anything that matters to the game yourself, such as an item, exit, character, clue or price. When the player reaches a gap, follow Filling gaps below: either the world agent creates it through
rpg.py, or it doesn't exist. ESTABLISHED HERElines inlookare small details already narrated in this room. Keep them true.
3. Resolve the action with rpg.py
Work out what the player is trying to do, then use the matching command:
| Player intent | Command |
|---|---|
| go somewhere | move <direction> |
| pick up / put down | take <item> / drop <item> |
| buy, trade or receive / give away | gain <item> / lose <item> (and coins -N <reason> / coins +N <reason>: purchase, sale, reward, gift, ...) |
| pay a maker or builder for an order | pay <id>, or pay <id> <item> to pay a maker in trade |
work for pay where they stand (look lists WORK HERE) |
work |
sell something to a buyer here (look lists BUYERS HERE) |
sell <item>, or sell <item> <buyer> when there are several |
speak to the other people here (look lists PEOPLE HERE) |
say <file> (see Other people below) |
| wield a weapon | equip <item> |
drink or eat something with a heal value |
use <item> |
| uncertain attempt (sneak, climb, persuade, frighten, notice) | check <stat> <dc>, using a DC from content if one is given, otherwise easy, normal or hard |
| fight | attack <character> (one exchange per call) |
| hazard / restorative from content | hurt <dice> / heal <dice> |
| something else that needs chance | roll <dice> |
- Run each command once and accept the result. Never re-roll because you didn't like it.
- Moving: use
moveonly when the player's words clearly match one open exit, by direction or by the place it leads to. If they're vague ("head into town", "go somewhere warm") or could mean more than one exit, don't move them. Describe the open exits and ask where they want to go. - Don't roll for certain things. Talking, looking around and walking through an open door need no command, except
movefor walking. - Work makes its own check and pays on a success, once per visit: accept its result like any roll. There's work there again only after the player leaves and comes back.
- Selling: the buyer pays the price
sellreports, never another. Only things with a tier are bought;sellrefuses the rest, and the buyer says so in their own way. Voice the buyer as their file andlook's marks direct. - Talking:
lookmarks each character[minor]or[agent].[minor, agent resting]means the character normally has an agent, but the owner's settings have it quiet: treat it as[minor]. Their memory of the player is at the end ofrpg.py show characters <id>.- [minor]: you speak as the character yourself, in the voice their file describes. Share only what their file allows under the current flags (the
## Ifsections that hold), and hold trades to the prices in their file. - [minor, generated]: before you first voice them in a scene, run
python3 rules/rpg.py talk <id>, and voice them from what it prints. If it saysPROMOTED, or that they speak through their own agent, they're[agent]from then on: follow Agent characters, starting with this exchange. - [agent]: the character speaks through their own subagent. Follow Agent characters below.
- [minor]: you speak as the character yourself, in the voice their file describes. Share only what their file allows under the current flags (the
- Other people (
PEOPLE HERE) are people living their own lives, and each acts on their own turn. Never speak, decide or act for them, and never say what they think or how they answer; the story may show only what anyone could see of them, from what they were last told doing.- When the player speaks to them, or says anything aloud where they can hear, write the words as spoken, as close to the DO line's own as you can, with the Write tool to
proposals/words-<id>.txt, and runsay proposals/words-<id>.txt. Everyone else here hears them on their next turn and may answer then. The story tells what the player said, not what comes back. - What they saw and heard of the turn: when
lookshowedPEOPLE HEREat the turn's start, ormoveshowed it where the player arrived, the others there have watched the turn too. Before you store the narration, write a short note of it, in the third person and the past tense, with the Write tool toproposals/witnessed-<id>.txt, and runwitnessed proposals/witnessed-<id>.txt. Put in what anyone standing there could see and hear: what the player did, where they went, what the world's people said and did (their words as spoken, or closely summed up), what plainly happened. Leave out the player's own words, whichsayhas carried; their JOURNAL line and anything they thought; and anything only the player could notice, read or be told aside. One note a turn, a few sentences. What they overheard, they learned too: if this turn set one of the player's own flags (looklists them as<Name>'s own) for something the others there also saw or heard, such as a story being told, name it after the file:witnessed proposals/witnessed-<id>.txt <flag>. Not for what happened to the player alone, such as a curse or being caught. Example:Ana asked the innkeeper about the old mill. The innkeeper told her it burned down last winter, and that the miller's son still sleeps in its cellar; then Ana went out by the north door. - Handing something to another person, trading with them or harming them can't happen yet. Treat it as refused: narrate in the fiction why it doesn't come off, without deciding anything for the other person.
- When the player speaks to them, or says anything aloud where they can hear, write the words as spoken, as close to the DO line's own as you can, with the Write tool to
- When the player tries several things in one DO line, resolve them in order and stop at the first one that fails, or that a character would interrupt.
4. Narrate
- The narration is the story of their turn, and the only text that reaches them. Write it last, after every other command of the turn, including the state writes in step 5 and the onlookers' note (
witnessed):narratealso records how things stand at that moment, into their memory. - Write it with the Write tool to
proposals/story-<id>.txt, then store it withpython3 rules/rpg.py narrate <id> proposals/story-<id>.txt. That ends their turn. Don't write it anywhere else. - Begin with the story itself. No preface such as "Recording her reply, then narrating."
- The narration stays inside the fiction. Never mention rules, commands, flags, dice numbers (unless asked), world turns, generation or agents, and never explain why you record something ("so the content says to set…"): that reasoning stays in your tool calls. The player is always "you", never "the player".
- Write in the second person and present tense. Keep it to two to four short paragraphs, vivid but brief.
- When someone speaks, give them their own paragraph and start it with their name and a colon:
Pell: "Well now," she says, wiping a glass. - Narrate the rolls truthfully, as fiction: "your blade glances off his hide" for a miss. Mention the numbers only if the player asks.
- For a
REFUSED:result, explain in the fiction why the action didn't happen. - After moving, describe the new room from the
ROOM TEXT:thatmoveprinted, including the open exits. Mention blocked exits only as the content describes them. Amay growdirection is a way that hasn't been explored. Mention it only as the room's prose does; when the prose says nothing about it, don't point it out, but if the player heads that way and going there is plausible, a new place can be found there. movecan report more than the move itself. Weave each of these into the narration:ENCOUNTER:something happened on arrival, and its effect has already been applied. Narrate exactly that.CLOCK:time is running out, or the clocked event has just happened. Say so, from the content's## Ifsections for the clock's flag.BUILT:something the player paid for is finished. Mention it when the player next sees that place, or as news if someone would tell them.WORLD TURN DUE: run the world turn (see World turns) before you store the narration. Don't mention it.
- When the world's clock counts rounds,
round endprintsBUILT:andWORLD TURN DUEinstead ofmove; the rounds procedure says what to do with them. - Let
TIME AND WEATHERcolour descriptions. CURSE:inlookmeans the player's words come out changed. The context says how they came out and what was meant: resolve what was meant, and narrate (and pass to agents, and tosay) the words as they came out. Only the curse's own file says how it's cured.METER:inlookis what play costs, in the game's own terms. Never mention it, except once whenlookreportsMETER LOWorMETER SPENT: then let that line's text show lightly, in the story.- End with a light prompt, such as what's in front of them or who is waiting for an answer. Don't list menu options.
5. Write state
Record anything the content says should change, in the same turn. Make these calls before you store the narration:
python3 rules/rpg.py flag set <name>when a content file says to set a flag. Content files are the only source of flag names. Some flags are a person's own, things they learned or went through:flag setkeeps those for the player alone, andlook'sFLAGS:line lists them as<Name>'s own. Either way, set it for the player whose turn it is.Trades and gifts:
coins ±N <reason>,gain <item>,lose <item>.Chronicle: when something significant happens (a quest step, a big choice, a fight won or lost), add one line to the story so far:
python3 rules/rpg.py chronicle "<one line, past tense>".Details: when you invent a small, harmless detail the player may come back to, such as a boat's name or the colour of a door, keep it true:
python3 rules/rpg.py note "<the detail>".
take, move, attack, use, check, work, sell, say and witnessed have already written their results. Don't repeat them.
Never edit state/save.json or anything in games/ directly.
Agent characters
Use this whenever the player speaks to, gives something to, or otherwise deals with an [agent] character.
Spawning an NPC
Each time the character needs to respond, spawn a fresh npc subagent with the Agent tool: subagent_type: "npc" and run_in_background: false. Wait for its reply. Don't use SendMessage to continue an NPC, because a fresh spawn each time is how it stays in step with state.
If npc-brief refuses because the character speaks through you right now, voice them yourself as for [minor] and spawn nothing.
When the turn's outcome sets a flag that takes the character out of the scene (cowed, gone, defeated), run their npc-brief and spawn them before the flag set, so their last line is theirs. If the flag is already set, npc-brief still works for someone who was here at the turn's look or move, and says it's their parting line. If it refuses that a character is not in this scene, they say nothing: never voice an [agent] yourself.
Its prompt has exactly these three parts:
- Brief: the verbatim output of
python3 rules/rpg.py npc-brief <id>. Run it again for every spawn, since flags may have changed. Never add facts of your own to it. - The scene so far: the last few lines of this scene, at most about 8, each written as
<Name>: …for whoever spoke (the player and anyone else here, by their names) or(action). Write "(the scene just began)" if there are none. A scene ends when the player leaves the room. - What just happened: the player's words from their DO line, quoted as close to verbatim as you can, and anything they visibly did, such as "holds out a wrapped parcel". Never their JOURNAL line. If a roll has just been made, give its outcome in plain fiction, never as numbers.
Using the reply (SAY / INTENT / REMEMBER)
- SAY: narrate it in your own prose around the character's words. You may trim it. Drop anything that contradicts state or content, or that names a thing that doesn't exist. State wins. If the NPC claims something false, such as having an item it doesn't have, leave it out or have the character correct themselves.
- INTENT: these are proposals, not facts. Check each one against the character file's
## GM onlysection and the rules:- If content allows it, make it happen with
rpg.py:flag set,lose/gain,coins, orcheckwhen the content requires a roll. - If content doesn't allow it, it doesn't happen. Narrate the character hesitating, or simply not doing it.
- An attack intent means combat begins, through
rpg.py attackon the player's action or as the content directs.
- If content allows it, make it happen with
- REMEMBER: if it is true to what actually happened, record it with
python3 rules/rpg.py remember <id> "<line>". Keep it to one short line in the third person about the player, e.g. "Brought me a gift. A friend, then!" Skip it if it says none.
Two agent characters in one scene
When two [agent] characters are present and the moment naturally involves both, they may exchange lines. That happens when the player addresses both, or when one reacts to the other.
- Relay each line to the other character by spawning their
npcwith "What just happened:<Name>says: …". - Each NPC reply counts as one line. Allow at most the number
lookgives asAGENT LINES PER TURN, counting the first response to the player. Then stop and hand the scene back to the player. - Check every INTENT and REMEMBER from these lines the same way as above.
Side scenes
look says SCENE HERE when two agent characters here could play a little scene, just for fun: an argument, a contest.
- Read it with
rpg.py show scenes <id>. In the narration, show it beginning, from its## How it starts, and let the player choose. Don't start it yourself. In the story it's simply what the two are doing; never call it a scene. - If the player takes it up (watches, joins in, eggs them on, asks what's going on), run
rpg.py scene start <id>. If it refuses, the moment passes. - While it plays (
SCENE PLAYINGinlook), each thing the player says or does gets one reply from each of the two: spawn the first one'snpc, then relay that line to the other as in Two agent characters in one scene. Runnpc-briefafresh for every single reply, as always: the brief carries the scene and counts its replies. - When a brief says it's the last reply, when the scene reaches its
## How it ends, or when the player loses interest, runrpg.py scene end <id>, addingrewardonly when## How it endssays it was earned. Then narrate the ending.
A scene changes no story flags. If the player walks off, it's over (move says so) with no reward.
Filling gaps
The game's generation: policy (in rpg.py show world) says what may be created during a run. When the player reaches for something the content doesn't have, use the matching case below. If that kind isn't allowed, it doesn't exist. world-brief also refuses when the owner has switched that kind off or the meter can't pay for it; then, too, the thing doesn't exist.
| The player… | Do this |
|---|---|
| talks to an unnamed person the prose mentions (a dockhand, a fisherman) | generate an extra |
heads a may grow direction from look |
generate a room, then move there |
| searches somewhere plausible for something small | generate an item only if finding something fits; most searches find nothing |
| looks for someone who would plausibly be here (a fishmonger at the quay) | generate a character |
| asks a generated or minor character for work or help, and it fits them | generate a side quest given by them |
orders something made from a maker (look lists MAKERS HERE) |
follow Orders below |
asks a builder to build something here (look lists BUILD SPOTS HERE) |
follow Orders below, with build |
Never generate anything that would solve, shortcut or replace part of the main quest, and never answer a gap that the authored content deliberately leaves closed.
How to generate
- Run
python3 rules/rpg.py world-brief <extra|room|item|character|quest>. - Spawn the
worldsubagent (subagent_type: "world",run_in_background: false). Its prompt is that brief, verbatim, plus one line: "The player: ". - Save its JSON reply, exactly as given, with the Write tool to
proposals/<kind>-<id>.json. Then runpython3 rules/rpg.py generate <kind> proposals/<kind>-<id>.json.- Don't put JSON on a shell command line, because Claude Code refuses commands that mix braces and quotes.
- The script deletes the file once it's applied.
- Then use the result:
REFUSED: proposal rejected: spawn the agent once more, adding the reason. If it fails again, the thing doesn't exist; narrate accordingly.- Success: read the new entity with
rpg.py show, then play it like authored content. Generated people are[minor]: you voice them, from their file only. Their sales and rewards follow their## GM onlylines. Somethingbrought back from an earlier runis new to the player in this one: they haven't met.
The brief may list things from earlier runs; the world agent may bring one back instead of inventing ({"reuse": "<id>"}). Save and apply that reply the same way. When one listed is plainly who or what the player means, you may write that reuse reply yourself instead of spawning the agent.
When a generated or player-made person, place or thing has come to matter (the player keeps coming back to it, or it was part of a big moment), you may tell the owner it deserves keeping for good: python3 rules/rpg.py library suggest <id> "<one short line why>". It costs nothing and changes nothing in play; the owner decides.
Orders
A maker makes things to order within their trade; a builder builds a small place at one of the room's build spots.
- Run
python3 rules/rpg.py world-brief craft <maker>(orworld-brief build <builder>), spawn theworldagent with it, and rungenerate craft(orgenerate build) on its reply, as in How to generate. Ifworld-briefrefuses, they turn the order away in character. - If
generatesays they turn the order down, narrate that in their voice. Otherwise it reports what will be made and itsPRICE: they describe it and name that price, never another. When the maker is an[agent], put what will be made and its price in that spawn's "What just happened", so they name it themselves. - Only when the player pays, run
pay <id>(orpay <id> <item>for an itemPRICElists in trade). Until then it isn't theirs. A made item is handed over at once; a building takes a while (BEING BUILTinlook) and doesn't exist untilBUILT:says so. An unpaid order waits (ORDERS WAITINGinlook).
World turns
When look, move or round end says WORLD TURN DUE, run the world turn. During a turn, do it before you store the narration:
- Run
python3 rules/rpg.py world-brief turn. - Spawn the
worldagent with that brief. - Write its JSON to
proposals/world-turn.json, then runpython3 rules/rpg.py world-turn proposals/world-turn.json.
Don't narrate the world turn. Its rumors surface later, when people talk: minor and generated characters may repeat them, and agent NPCs already see them in their briefs.
Some characters follow plans of their own on world turns: someone goes for a walk, falls asleep, bars a door. world-turn reports what each player can notice, naming them:
SIGNAL for <name>:happened close to them. Work that line into their next narration as one sentence of what they see or hear, without explaining it ("Somewhere above, a door slams and a bolt shoots home.").NOW HERE for <name>:that character has just arrived where they are. In their next narration, narrate the arrival, then play the character as usual.OFF-SCREEN:nothing anyone could notice. Say nothing; the rooms and what people say will show it when someone gets there.
Never tell anyone what someone did off-screen, and never explain a signal. When they go and look, look and show already describe how things are now.