creator · guide

Creator guide

Everything you need to build a game and get it into the arcade — the no-code builder, the expression & effects reference, Python scripting, and how review works. When in doubt: open the builder, load the starter, and break things in the live preview.

1 · Anatomy of a game

Every TEXTCADE game is the same console: a board of nodes, a handful of commands, an advisor that coaches, and a field log that surfaces the real concept behind each move. Play is turn-based: spend energy on commands, end the turn, the world responds, repeat until a win or lose rule fires.

  • Nodes are the regions of your system — organs, servers, habitats, market sectors. Each carries numeric fields (like load) and can be adjacent to others, which is how things spread.
  • Commands are the verbs. Each costs energy, may target a node, and runs effects that change state.
  • Facts fire once, the first time their key is triggered — this is where the learning lands.
  • Win/lose rules are checked after every action; the debrief screen states the lesson either way.

The design bar for the arcade: the winning strategy should be the lesson. If a player can win while ignoring the concept you're teaching, tighten the rules until the concept is the strategy.

2 · Build your first game

Open /submit. The builder starts with HEAT SINK, a small complete game — the fastest path is to reshape it into yours while the live preview keeps proving it still plays.

  1. Overview — title, subject, grade band, the one-line “learn”, turns and energy per turn. Short games teach best: 6–10 turns.
  2. The board — 2–16 nodes. Give each an id (slug), a display name, a role, adjacency, and starting field values. Mark a node critical if it overloading should end the game.
  3. Commands — 1–8 verbs. A good game has a cheap local action and an expensive systemic one, so every turn is a real tradeoff.
  4. Python scripting (optional) — skip it for your first game, or see section 4.
  5. Turn & advisor rules — what the world does each turn (spread/replicate), and advisor lines checked top-to-bottom.
  6. Field-log facts — one honest, sourceable sentence per key.
  7. Win & lose — win first, then losses, and always end with a turn-limit rule like turn > turn.max.

The preview is the real engine — the exact thing players get. Playtest until you can lose on purpose and win on purpose.

3 · Expressions & effects (the no-code way)

Anywhere the builder says expr, you write a small read-only expression. In display text, {expr} interpolates a value and {expr|round} rounds it.

Readturn · turn.max · your vars by name · node.load (inside a node context) · node('core').load
Math & logic+ - * / % · == != < <= > >= · && || ! · cond ? a : b
Aggregatessum(…) count(…) all(…) any(…) — evaluated over every node
Functionsmin max clamp round floor abs

Effects are how commands change the game (they're the only way state changes):

setNode{ "setNode": { "field": "load", "value": "max(0, node.load - 25)" } } — writes a field on the target node
setVarwrites a game variable
logadds a field-log line (small HTML subset allowed); optional when
factfires a fact key (shows once)
spendspends extra energy
pulseflashes a node on the board
forEachNoderuns a do list on every node, with optional when filter and pulse
whenconditional: cond, do, optional else

Display strings allow only <b> <em> <i> <span class> <br> — everything else is stripped. The full worked example is IMMUNE // OUTBREAK, which ships as pure JSON.

Quoting: two traps worth knowing

Your expressions live inside a JSON file, so two syntaxes are stacked on top of each other. The first trap below makes the file invalid JSON; the second passes JSON but fails validation, so the spec will not publish. Both are easy to avoid once you have seen them.

1 · Use single quotes inside expressions. A double quote ends the JSON string it is sitting in. The DSL accepts either kind, so single quotes are always the easy answer:

"value": "round(node("jobs").stress)"      ✗ the inner quote ends the JSON string
"value": "round(node('jobs').stress)"      ✓

2 · || is rejected inside {…}. In display text the brace syntax reads {expr|round}, where | introduces a formatter — so validation refuses a template containing || and your spec will not publish. && is fine, and so is || outside braces, in when, enabled, targetable and other plain expressions. Inside braces, count instead:

"text": "{node('a').gone || node('b').gone ? 'one fell' : 'both hold'}"        ✗ rejected
"text": "{(node('a').gone ? 1 : 0) + (node('b').gone ? 1 : 0) > 0 ? 'one fell' : 'both hold'}"  ✓

An apostrophe inside a single-quoted expression string needs escaping through both layers, which is fiddly enough that rewording is usually quicker — write the imperial authority rather than the Emperor's authority.

Neither is a subtle bug — the submit page runs the same validation the server does and will tell you before you publish. They are just much quicker to spot when you already know what you are looking at.

4 · Python scripting

When the DSL feels tight, script your logic in real Python — f-strings, loops, functions, the works. Open the Python scripting step in the builder (a proper editor with syntax highlighting loads right there), write top-level functions, and point hooks at them by name.

Where Python can run

A command's effectsswitch the command's effects mode to python function and give the function name
onEndTurn{ "effects": { "python": "end_turn" } }
checkEnd rule{ "python": "check_end" } — mixes freely with DSL rules
nextTurnEnergy{ "python": "energy" } — return an int

Render-time fields (targetable, enabled, nodeView, advisor rules) stay expressions — they run dozens of times per frame.

Hook signatures

def fn(game, node):   # command that targets a node
def fn(game):         # no-target command, onEndTurn, checkEnd, nextTurnEnergy

The API

game.turn / game.turn_max / game.apread-only numbers
game.vars["x"]your variables — read and write
game.nodes / game.node("id")node list / lookup. Fields read & write as attributes: node.load = 0. node.id and node.adj are read-only.
game.log(html, cls="")field-log line (same HTML subset as the DSL)
game.fact(key)fire a fact
game.spend(n)spend extra energy
game.pulse(node)flash a node (pass a node or its id)

A complete example

def cool(game, node):
    before = node.load
    node.load = max(0, node.load - 25)
    game.log(f"Cooled <b>{node.name}</b>: {before:.0f} -> {node.load:.0f}")

def end_turn(game):
    for n in game.nodes:
        n.load = min(100, n.load + 6)

def check_end(game):
    if all(n.load <= 0 for n in game.nodes):
        return {"win": True, "title": "STABILISED",
                "lessons": ["System-wide thinking beats local fixes."]}
    return None   # keep playing

Load the full version in the builder: paste tab → the HEAT SINK // PY demo spec (in the repo at src/games/heat-sink-py.json).

Rules & limits

  • Store state in game.vars, not Python globals — module globals reset when the player hits replay.
  • Scripts get ~2 seconds per move; a hung script is stopped, the move is refunded, and the game continues.
  • No network, no files, no imports beyond built-ins. Your code runs in a sandbox (MicroPython in an isolated worker) and changes the game only through the API above.
  • Everything you log or write is sanitized exactly like DSL output.
  • Source limit 20,000 characters.

When something breaks

The live preview shows real tracebacks in the field log — line numbers point into your code. You can't submit until the preview compiles your Python cleanly. Players never see tracebacks; they get a friendly “this game's script hit an error”.

5 · Submitting & review

  1. Submit — the spec is validated (same validator, client and server), stored, and you get a private status link. Save it; it's the only handle on your submission.
  2. Review — a human plays your game. The checklist: factually correct · a real tradeoff (no dominant strategy) · the advisor teaches · win and lose states state the lesson · and for Python games, the code is read for runaway loops and abuse.
  3. Outcome — published to the arcade at /g/<id>, or returned with feedback. “Edit & resubmit” from your status page reopens the exact spec in the builder.

Rate limit: 8 submissions per hour. Credit appears as the creator name you give.

Found a bug, hit a wall, or want a feature? Email admin@find-server.com.

+ Open the builder