Skip to content

Stages

A stage is a single puzzle within a campaign: one Markdown file with frontmatter for its answer, hints, difficulty, and position in the chain.

The stage chain

Stages in a campaign must form a single linear chain: exactly one stage marked start: true, each stage linking to the next via next, with no cycles and no orphans.

graph LR
    A["01-welcome<br/>start: true"] -->|next| B[02-answering]
    B -->|next| C[03-...]
    C -->|next| D["0N-final<br/>(no next)"]

Answers

The answer is never shipped in clear text, only its trimmed, lowercased SHA-256 hash reaches the client for in-browser checking.

To publish a campaign's source without revealing solutions, drop answer and supply answer_sha256 instead: the SHA-256 hex digest of the trimmed, lowercased answer (the same value answer would produce). A stage may carry either field, both, or neither. Neither is valid for informational stages that only link next. When both are present, the plaintext answer wins and a stale answer_sha256 is logged as a build warning. A malformed answer_sha256 (not 64 lowercase hex characters) fails the build so a stage never ships silently unsolvable.

Hints

Hints are ordered and timed: each one becomes click-to-reveal after wait_seconds of the player being on the stage. See Frontmatter below for the exact shape.

Difficulty

Stage difficulty is required and must be an integer from 1 to 5, the build fails otherwise. (Campaign-level difficulty is an optional, unconstrained indicator.)

Frontmatter

campaigns/<dir>/stages/<file>.md:

---
title: Welcome                  # required
author: alice                   # required
difficulty: 1                   # required, 1–5
slug: welcome                   # optional, derived from filename
tags: [intro, warmup]           # optional
eta_minutes: 5                  # optional time estimate
theme: dark                     # optional override (stage > campaign > global)
start: true                     # marks the campaign entry point (exactly one)
next: answering                 # slug of the next stage (omit on last)
answer: 42                      # expected solution; shipped as a SHA-256 hash
answer_sha256: 9c56...          # precomputed answer hash (ship without plaintext)
completion_message: Solved!     # shown on stage completion
inputs: [data.json]             # from inputs/; download chips + ref as inputs/<file>
assets: [diagram.png]           # from assets/; not listed; ref as assets/<file>
hints:                          # ordered, timed click-to-reveal hints
  - wait_seconds: 30
    text: Look at the headers.
  - wait_seconds: 120
    text: It's base64.
published_at: 2026-01-01T00:00:00Z
last_update_at: 2026-02-01T00:00:00Z
---
Markdown body of the puzzle…
Field Required Notes
title yes
author yes
difficulty yes Integer 1–5; the build fails outside that range.
slug no Derived from the filename if omitted.
tags no
eta_minutes no Estimated time to solve.
theme no Overrides campaign/global theme for this stage.
start no Marks the campaign's entry point, exactly one stage per campaign.
next no Slug of the next stage; omit on the last stage in the chain.
answer no* Plaintext expected answer; shipped to the client only as a SHA-256 hash.
answer_sha256 no* Precomputed hash, for publishing source without the plaintext answer.
completion_message no Shown when this stage is solved.
inputs no Files from inputs/; rendered as download chips, referenced as inputs/<file>.
assets no Files from assets/; not listed in UI, referenced as assets/<file>.
hints no Ordered list of {wait_seconds, text}, click-to-reveal after the wait.
published_at no RFC 3339 timestamp.
last_update_at no RFC 3339 timestamp.

* A stage may carry answer, answer_sha256, both, or neither. Neither is valid for an informational stage that only links next. If both are present, the plaintext answer wins and a stale answer_sha256 is logged as a build warning. A malformed answer_sha256 (not 64 lowercase hex characters) fails the build.