Skip to content
quest'n'click

CreateHow-toFor authorsApplies from 4.3.0

Checking your game

Cette page en françaisThis page on the GitHub wiki

Four commands tell you most of what is wrong with a game before anyone plays it: validate checks the content, solve finds a way to the ending, lint reads the puzzle graph for what a player would trip on, and solve --prove proves that no player can get stuck. Then the game is played for real: by a scripted walkthrough, in a browser, on a phone, and by a person.

What you get

A game whose references all resolve, that can be finished from "New game", whose softlocks are ruled out by an exhaustive proof, and whose remaining warnings you have read and either fixed or kept on purpose. At the end, the checklist Before you share the link.

Context and versions

  • The engine repository (a clone of quest-n-click, to work on the engine or its sample games): the commands are npm scripts, npm run validate. The game is GAME=<id>, otherwise config.game of package.json.
  • A created project (made by npx quest-n-click create): the commands are quest-n-click subcommands, npx quest-n-click validate, and the project's scripts npm run verify, npm run build, npm run release. The game is game/.
  • This page describes 4.3.0 and later (the lint's hero-looks-default arrived in 4.3.0). The status of a solver run is the same since 3.3.

Prerequisites

Where to run the commands

On your machine, at the root of the folder:

CheckEngine repositoryCreated project
Contentnpm run validatenpx quest-n-click validate
A way to the endingnpm run solvenpx quest-n-click solve
What a player would trip onnpm run lintnpx quest-n-click lint
No softlocknpm run solve -- --provenpx quest-n-click solve --prove
All the game's gatesnpm run verify:gamenpm run verify
The exhaustive gatesnpm run prove:gamepart of npm run release

In the engine repository, options go after -- (npm run solve -- --prove); with npx quest-n-click they follow the subcommand directly.

Steps

  1. Validate the content.

    # in the engine repository
    npm run validate
    # in a created project
    npx quest-n-click validate

    Everything referenced exists, every visible thing has a Look line, texts are neither empty nor too long, flags are set and read. Exit 1 on an error. What it checks is listed in What each check looks at.

  1. Find a way to the ending.

    # in the engine repository
    npm run solve
    # in a created project
    npx quest-n-click solve

    It prints ✔ The game can be finished, the path, and three warnings to read: dead ends, items obtained but never used, items never obtained (the warnings). A winning path is not a proof.

  1. Lint the content.

    # in the engine repository
    npm run lint
    # in a created project
    npx quest-n-click lint

    Conditions nothing can satisfy, rules another rule hides, red herrings, hints that cannot fire, actions the solver never ran. Fix each finding, or keep it on purpose in game.ts (lint.ignore, below).

  1. Prove there is no softlock.

    # in the engine repository: the global proof, the proof by chapters, every Reality world
    npm run prove:game
    # in a created project
    npx quest-n-click solve --prove

    Only a solved from --prove rules out softlocks. A truncated result proves nothing: raise the budget (Solver options).

  1. Play it for real. A scripted walkthrough and the browser e2e (Prove it plays), a real phone in landscape (Testing on a phone over the LAN), then a person who has never seen the game (Playtests).
  1. Go through the checklist Before you share the link.

What each check looks at

  • Validated. Broken references, unreachable rooms and untranslated lines are found before anyone plays. validate checks shapes and references: everything referenced exists, every visible thing has a Look line, texts are not empty or too long, flags are set and read. It warns about rooms nothing leads to and declared exits with no way back (oneWay: true when intended). It checks each minigame's required parameters (required), the images cited in their params, every skin id (manifest images, audio sounds) and ending.scratch, and that a required Reality signal's fallback names a rule.

  • Solved. The solver runs the real engine, with no second model of the content. solve asks for a witness, a path to an ending; solve --prove explores every reachable state and fails on a softlock or a truncated search. These are two different promises, and a release asks for the second (What the solver proves). solve also reports the invariants (GameDef.invariants) that became true, with their path.

  • Linted. lint says what validate cannot (it checks shapes and references) and what solve does not say loudly (it answers "can it be finished"). From the puzzle graph:

    • a condition nothing sets (cond-never-true, an error);
    • a rule another rule matches first (rule-shadowed);
    • an item no rule needs (item-red-herring) or nothing gives (item-never-gained);
    • a hint that waits for something nothing sets (hint-stuck, an error), or that shares its until with an earlier hint (hint-never-fires);
    • a dead topic, choice option or listener; a choice with one option; an exit with a condition and no locked line;
    • an action that only changes what nothing live reads (action-dead, info; a flag a character's variant or a text switch reads is not counted as nothing, so the intro's choice of the hero is not reported);
    • two or more values of the hero's flag that no hero variant matches, so those heroes look like the written one (hero-looks-default, info, 4.3.0; see Heroes);
    • a target behind a walk link a condition closes, whose rule does not check that condition (walk-link-gate: the link stops the walk, never the action).

    After a solver run, also: a required signal's fallback the closed witness never plays (fallback-unplayed, a warning); a live action the witness never ran and a room it never entered (rule-never-run, room-never-reached: information with the witness, warnings with a completed --prove; a truncated proof keeps them as information and says so, it never calls anything unreachable); a live action that ran without ever changing the state (rule-no-effect, info: a topic that only talks, or a set already true). Reachability counts every try the solver made, whether or not it changed the state.

    Each finding names its room and path (the one the Studio's Rooms tab shows), its stable id, and what to do. Exit codes: 0 clean, 1 an error that is not ignored, 2 the search was truncated (--json carries status and truncated). --prove runs the exhaustive search first (slow on a big game), --static runs no solver at all, --max=<n> sets the search's budget (20 000). The Studio's Check tab shows the same list with links into Rooms; the lint MCP tool returns it as Markdown. verify:game runs it, so does CI.

  • Replayed. A session is the unit of reproduction (Playtests); saves load across versions, with one frozen save per release from 3.0.0 on (State and saves).

  • Accessible as far as a machine can tell. axe-core on every screen, keyboard and touch play in CI. That is not a WCAG conformance claim: screen-reader and real-device passes are a person's to make (Accessibility, Field passes).

Keeping a finding on purpose. A red herring you want, or a decorative flag (set, never read), is silenced in game.ts; the same list keeps flag-never-read:<flag> out of validate's warnings:

lint: { ignore: ['item-red-herring:rubber_chicken', 'rule-shadowed:house/on[4]'] },   // a code, `code:<id>` or `code:<room>/<path>`

ui.shareSession names the pause menu's row that sends a playtest from a phone (Playtests).

Commands

Every command below runs in the engine repository with npm run. The second column gives the created project's equivalent when there is one; "—" means the created project has none.

The game's content

Engine repositoryCreated projectWhat it does
npm run validatenpx quest-n-click validateBroken references, empty or too-long text, missing Look lines, flags never set or never read. Exit 1 on an error.
npm run validate -- --reportnpx quest-n-click validate --reportThe content profiler (Markdown): rooms, items, characters, what is thin.
npm run validate -- --releasenpx quest-n-click validate --releaseAlso provenance, placeholders, and a stable id on every line of a game shipped in another language than its own or voiced.
npm run validate -- --commercial --errorsnpx quest-n-click validate --commercial --errorsA release that may be sold: no exception, no placeholder, no non-commercial licence, every source checkable; --errors prints the errors only.
npm run solve …npx quest-n-click solve …The solver: Solver options.
npm run lint [-- --prove | --static | --json]npx quest-n-click lint …The content lint, above. lint:content is the same command (alias since 4.1.0).
npm run i18n -- extract [--lang xx]npx quest-n-click i18n extract …Translation tables (locales/<xx>.json); status for the coverage. Translations.
npm run playtests …npx quest-n-click playtests …Players' sessions replayed and summed up: Playtests.
npm run page:world—The map of the world as a page: exits, gotos, unreachable rooms, DOT source.
npm run page:puzzles—The puzzle graph as a page: what every rule needs and changes, a card per item and flag.
npm run replay -- session.json— (the Studio's Play tab)A session played on the real engine: Sessions.

The game's gates

Engine repositoryCreated projectWhat it does
npm run verify:gamenpm run verifyEngine repository: validate, the witness, the witness by chapters, the variants (verify:variants), the translation status, the lint, the playtests. Created project: validate, solve, i18n status, lint, playtests.
npm run prove:game— (npm run release runs solve --prove and solve-reality)The global proof, the proof by chapters and every Reality world; fails on a softlock or a truncation.
npm run build:gamenpm run buildThe game's gates, the bundle, verify:dist, then (engine repository) the spoiler check and the asset audit. No type-check, no unit suite.
npm run buildnpm run buildEngine repository: check (type-check and Node tests), then build:game.
npm run verify:releasepart of npm run releasevalidate --release, weight --release, i18n status, voices check --release, strict playtests.
npm run verify:commercialnpm run release -- --commercialverify:release, then no exception, no placeholder, no NC or ND licence, every source checkable, then verify:dist.
npm run verify:distpart of buildEvery file of dist/ accounted for: code, locked assets (their reviewed bytes), named data, fonts, icons, licenses/.
npm run verify:field—verify:commercial, then the playtest quotas of a release tested by players (not a step of release-check, D12).
npm run provenance [-- --lock]npx quest-n-click provenance [--lock]The shipped assets by licence, what changed since the reviewed lock; --lock records the files after a review. Asset provenance.
npm run weight [-- --release --json --stems]npx quest-n-click weight …What a phone downloads before the first room, per room and per chapter, against assetBudgets.
npm run voices -- status | export | import | checknpx quest-n-click voices …Recorded lines per language. status [--lang xx]: lines with an id by status (draft, record, recorded, approved), clips, orphans; a line with text switches has one row per value, <id>~form=f. export --lang xx [--json] > t.csv: the actors' table (id, who, text, status, file, actor, note). import t.csv --lang xx: statuses, actors and notes back into voices.json (unknown lines refused). check [--lang xx] [--release]: every clip measured with ffmpeg (codec, rate, length for its text, loudness about −16 LUFS, peak); --release: an approved line needs its clip. Voices.
npm run i18n -- voicesnpx quest-n-click i18n voicesThe lines with an id and no clip, the clips no line claims.
npm run ids [-- --write --map], npm run ids -- --linesnpx quest-n-click ids …Stable ids (schema 3) written into the sources, locales renamed, the save migration step. --lines: an id on every say, toast, guide object, list line, hint and kind reaction (--lines=all: plain strings too, required for a translated or voiced release); translations and voices are keyed by it. State and saves.

The engine repository's own checks

CommandWhat it does
npm testThe Node tests of the engine, the tools and the selected game, without the heavy solver tests.
npm run test:heavyThe CPU-bound solver tests: every night, minutes each (Solver options).
npm run test:coverageThe suite under V8 coverage against the floors of vite.config.ts; then npx tsx tools/coverage-ratchet.ts --strict fails on a floor at least three points behind what the tests reach (The coverage ratchet).
npm run test:assetsThe Python-backed image and asset-pipeline tests.
npm run checkType-check and Node tests.
npm run tsc -- …The TypeScript 7 compiler itself; check and quality call it.
npm run qualityBiome formatting and lint, knip, tsconfig.json and tsconfig.strictest.json, then the content lint.
npm run quality:baseline -- --check [--dist]The behaviour of 4.0.0 kept: witnesses, proofs, golden saves, public surface, first visit (The quality baseline).
npm run doctor [-- --release]Node, Python modules, ffmpeg, Playwright browsers (doctor).
npm run release-checkWhat CI runs, in one go: doctor --release, quality, build, coverage and its strict ratchet, verify:release, prove:game, solve --audit-abstractions, the Rust cross-check, mutation of the core, the packages with npm publish --dry-run, the dependency audits. release-check:ci is the same without the mutation sets (Releasing).
npm run field:init, field:check, field:report, field:bundle, field:verify-bundle, field:carryThe Field Kit: the people's passes as JSON reports bound to a candidate run (Field passes).

In a browser (engine repository; the dev server or a production build already running at <url>)

CommandWhat it does
npm run e2e -- <url>A playthrough in Chromium, phone landscape, along the solver's path (Prove it plays).
npm run e2e -- <url> --renderer canvasThe whole game drawn by the Canvas painter.
npm run e2e -- <url> --lang frThe whole game in that language; fails on any visible English default of the engine.
npm run e2e:visual -- <url> [--update]Every room, still, against tests/visual/<game>/*.png (0.5 % of the pixels at most).
npm run e2e:a11y -- <url> [--only=axe,keys,storage] [--allow-skip]axe on the conversation, map, slots, confirmations and every minigame; every minigame won at the keyboard; an older save upgraded (E2E_BROWSER=chromium|webkit; exit 3: a check the browser cannot automate).
npm run e2e:perf -- <url> [--renderer=canvas|dom --cpu=4 --min=30 --room=<id>]Frames per second while the hero walks, CPU slowed (the phone stand-in).
npm run e2e:music -- <url> [--only=offline|live|game|reference …]The music director: 30 min offline without drift, 100 changes without a click, real-time jitter (--live=chromium|webkit), the theme's stems in the game; --only=reference [--browser=webkit] with GAME=reference: two scores, bridges, a restore and a stop mid-transition, the decoded peak (Music).

Documentation and repository tooling (engine repository)

CommandWhat it does
npm run docs:screenshots [-- --only=game|studio --keep-png]The README images from the production bundle and the Studio, as WebP (needs Python with Pillow).
npm run docs:truthWhat the docs state against the code: the save and .wsrun schemas, every npm script and tools/ or scripts/ file they cite, no release link newer than the version. CI's check job.
npm run docs:links [-- --timeout=10000]Every external link of the docs asked once; a person runs it before a release, CI never does.
npm run changes -- --check | --assembleThe CHANGELOG and LOG fragments of a branch (Conventions).
npm run ci:plan [-- --base=origin/main | --files=a,b | --all]Which of CI's heavier jobs a change needs, one line of JSON; since 4.5.0 also docs and docsPages, the wiki pages whose sources the change touches (CI explained).
npx tsx tools/api-doc.ts [--check], npx tsx tools/dsl-doc.ts [--check]The generated API and DSL references (Public API, DSL reference).

Prove it plays

In the engine repository, npm test runs the engine tests and the game's walkthrough: a scripted full playthrough on the real engine (tests/demo-walkthrough.test.ts for the demo). npm run e2e plays the whole game by touch in a phone-sized Chromium and saves a screenshot of every step: look at them, or have your assistant look, to catch what the solver cannot see (a character standing on a table, a line cut by the screen edge).

# in the engine repository: start the dev server, then in another terminal
npm run dev
npm run e2e -- http://localhost:5173/

npm run e2e takes the solver's path (npm run solve -- --json, its steps) and taps through it. It passes only when the solver's run is solved (its exit code, status and steps are checked) and the engine itself reports the ending (state.done): a truncated or unsolved game is a failure, never "the best path replayed anyway". games/demo/e2e.mjs is the example of a game-specific e2e script: it starts from the title screen, plays the pipes and pick minigames and the sealed ending's scratch ticket for real instead of skipping them, then checks the final card.

In CI (the engine repository's .github/workflows/ci.yml): every push to main, every tag and every pull request runs npm run build:game (the game's gates, the bundle, the spoiler and asset audits) and npm run prove:game on the sample game; the heavier jobs (the production e2e in several browsers, the keyboard and French playthroughs, a second game made from the template…) run when ci:plan says the change needs them. main deploys to Pages; a tag releases. The weekly prove workflow (Mondays, or by hand) runs prove:game, the strict playtests and the bench on a generated 100-room game. Dependabot proposes npm and Actions updates weekly, pip ones monthly (Bot PRs).

Before you share the link

  • Premise, want, obstacle per room and ending fit in four lines.
  • The storyboard is final, and every room text comes from it.
  • Each room has one goal, 3 to 6 steps, one "aha", 8 to 15 named things.
  • Every named thing has a Look line; repeated looks follow normal, normal, normal, absurd.
  • Each character has 2 to 4 topics, a hug, a refuse and a distinct colour.
  • Each verb has 3 to 6 fallbacks in the hero's voice. No "That doesn't work".
  • Hints cover every step of every room, in order, until the ending.
  • Every minigame has a reason in the story, a Skip, and lasts about 30 seconds.
  • The map shows only unlocked places; news markers point to the next step.
  • Every state change has a sound.
  • No line over 90 characters.
  • validate: no errors. solve: finished, no dead ends, no unused items. solve --prove: solved.
  • Engine repository: npm test and npm run e2e pass; screenshots reviewed.
  • Played start to end on a real phone, in landscape.
  • Sealed ending checked with npm run check:spoilers (The sealed ending).
  • Someone else played it without help (Playtests).

Then build and ship it: Build and release a game.

Expected result, and how to check it

  • validate: its last line starts with ✔ [<game>] and counts the rooms, items and characters; exit 0. Warnings (⚠) do not fail it. "(images not checked: run npm run assets)" means the images were not checked.
  • solve: ✔ The game can be finished and ✔ Witness solved: …, exit 0.
  • solve --prove: ✔ Proof solved: the ending stays reachable from every one of the N reachable states, exit 0.
  • lint: exit 0; the findings left are information, or ignored on purpose.

The exit code is the verdict: 0 passed, 1 a problem to fix, 2 the search was truncated and nothing is proved.

Common errors

SymptomCauseFix
solve exits 2, truncated: the search stopped at N states, nothing is proved (raise --max)The budget is too small for the gameRaise --max, --time or --mem, prove by chapters, read the profile (Solver options).
softlocks: N reachable state(s) can no longer reach the endingA consumed item, a closing flag, a hidden clueRead the causes and replay their path (What the solver proves).
Dead ends (n): …A state where no action changes anythingGive the item back, or loosen the condition (the warnings).
broken: N invariant(s) broken on a reachable stateA "never again" bug came backThe path is printed; fix the rule it names.
lint exits 1 with cond-never-true or hint-stuckA condition or hint waits for something nothing setsSet it somewhere, or change the condition; or ignore it in lint.ignore if intended.
fallback-unplayedA required signal's fallback is declared on a rule the closed witness never playsA game with the outside world.
The result says (from the proof cache, key …) and you doubt itThe cache answered because nothing changed--no-cache or PROOF_CACHE=0 runs it again (the proof cache).

Files or data changed

The checks only read your game. What they write: .cache/proofs/ (the proof cache), and the folders you name with --out. npm run e2e writes its screenshots to E2E_OUT (default /tmp/e2e). validate --report prints to the terminal; page:world and page:puzzles write their page under dist-pages/ (or --out <dir|file.html>). ids --write and provenance --lock are the exceptions that change files you commit: they are not checks, and they say so.

Recovery or going back

Nothing to undo: a check does not change the game. To start from a clean slate, delete .cache/proofs/.

References