Repository navigation
EN Contribute Architecture
Français · Type: explanation · Audience: contributor · Context: engine repository · Status: ready · Applies from: 4.1.15
How quest'n'click is put together, for someone reading its code for the first time. The Code tour
is the same map walked in order, half an hour with the code open; docs/dev/adr/
explains the decisions that look surprising without their context (Decisions and ADRs).
The engine is one code path that a phone plays, the solver explores, the replay re-runs and the Studio edits. A game is data the engine reads; everything that comes from outside is checked before it is trusted; every effect on screen goes through one port, so the same engine runs silent in Node. The sections below say where each of these lives. The description holds for the code of 4.4.0; the architecture dates from 4.1.0 "Clarity" (the engine split into modules behind facades) and 4.1.1 (signals from outside).
games/<id>/ content: data only (rooms, rules, lines, layouts, assets)
│ defineGame / compileGame
▼
src/engine/core/ the engine: state, rules, commands, scripts, sessions, saves — no DOM, runs anywhere
▲ ▲
│ └──── src/engine/tools/ validator, solver, replay, lint, weight, provenance (Node, workers, tests)
src/engine/dom/ the player: App, the Presenter, the room view, audio, input, menus, offline
src/studio/, tools/ the Studio (browser UI, its server and MCP), the command line
What each folder may import is written in src/engine/BOUNDARIES.md and checked by
tests/boundaries.test.ts: the core imports neither the DOM nor the tools; the player never imports the solver, the
validator or the Studio statically; the tools never import the DOM; no cycle of static imports anywhere. The full table
is Import boundaries.
src/engine/
api/ the public entries: content, player, minigames, testing, reality
core/ pure logic (no DOM): state, conditions, reactions, commands, scripts, conversations, hints, saves
types.ts the content format, commented (the reference); split by subject in core/types/
engine.ts the Engine facade: player actions, resolution, lifecycle
ports.ts the Presenter interface (what the core asks of the display) and FakePresenter for Node
cmds.ts the catalogue of commands; command-handlers.ts runs each one
cond.ts condition evaluation
define.ts defineGame / defineRoom, compileGame, stable keys for once/nth/cycle/random blocks
scene/ the scene frame (pure): SceneFrame, the hit test, the Renderer and Intent contracts (4.1.11)
dom/ the browser display
app.ts landscape layout, verbs, inventory, menus, title screen; composes the engine, the presenter, the renderer
presenter.ts the Presenter the engine talks to (lines, overlays, map, minigames, sealed ending) and intent()
fonts.ts default fonts (Galmuri11, DotGothic16 behind it, Press Start 2P), overridden by skin.fonts
room.ts the scene: background, props, characters, depth sorting, walking, poses
walk.ts walkable area (earcut triangulation and navmesh paths), scale by depth
audio.ts music (fade, stack, one-off track) and sound effects, through Howler
assets.ts images and sounds prepared by npm run assets
minigames/ minigames (DOM plugins): pipes, stroke, pick, hide, runner, scratch, cables, code-wheel; their images come from params
ending/ the sealed ending (optional): seal.ts (AES-GCM), card.ts (the final card), index.ts (decryption, ticket, confetti)
reality/ signals from outside (4.1.1): protocol, client, HTTP port
tools/ validator, solver, replay, lint, i18n and the other tools (Node)
dev/ debug overlay and placement editor (development only)
boot.ts bootGame: wires a game module, the engine and the player
The engine names no game. src/engine/ never imports from games/ and never names a single image, sound or
interface text:
- texts come from
GameDef.ui; - icons, sounds, fonts and default heights come from
GameDef.skin(Skin and fonts); - minigame images come from their
params; - the sealed ending comes from
GameDef.ending.
A game is a folder games/<id>/ whose index.ts exports game, layouts and manifest, and optionally
minigames, extraImages, commands (custom commands) and locales (the GameModule of tools/game.ts).
src/main.ts imports @game, an alias for games/<GAME>/index.ts, and knows nothing else about the game. The
current game is GAME=<id>, else the .cache/game link, else package.json "config": { "game" }, else demo
(Developer setup).
Geometry constants. The logical scene measures 640 × 400; the floor goes no lower than Layout.floor (default 395,
FLOOR in core/define.ts); an approach point recorded more than 150 units from its target (NEAR) is considered
stale and recomputed.
Verbs. Their ids are free (the game's verbs). Four have a meaning for the engine: look (Look texts), talk
(conversations, hints), give and use (two terms: an inventory item then a target; tapping an inventory item with no
verb selected picks use).
A game is a GameDef (src/engine/core/types.ts, split by subject in
src/engine/core/types/): rooms, verbs, items, characters, rules ({ verb, a, b, if, do }), scripts, listeners,
dialogues, a map, an ending. There is no function in it, so it can be validated (src/engine/tools/validate.ts),
saved, diffed and solved. Code a game needs (a custom command, a minigame) is trusted code declared beside the content
and reached by name (core/custom.ts, src/engine/minigames/). compileGame (src/engine/core/define.ts) freezes
the content and gives every rule, choice, topic, listener and script step a stable id: saves, sessions and the solver
speak in those ids. How the engine runs that data is How the engine works.
-
Input. A tap on the scene or the inventory (
src/engine/dom/input.ts) becomes anAction{ verb, a, b }; a double tap picks the verb a player means (src/engine/core/default-verb.ts). -
Engine.
Engine.act(src/engine/core/engine.ts) walks the hero there, opens a session entry (core/session-runtime.ts) and asksresolve(core/interactions.ts) for the answer: a written rule, else a kind, else a refusal or a fallback line. -
Commands. The rule's
dolist runs throughexec(core/command-runtime.ts), oneCmdat a time, each by its handler (core/command-handlers.ts): set a flag, give an item, say a line, walk, play a sound, emit an event (core/event-runtime.ts), start a script (core/script-runtime.ts). Every effect on screen is a call to thePresenter(src/engine/core/ports.ts). -
State. Commands only change
engine.state, aGameState(core/types/state.ts) of plain JSON: room, inventory, flags, props, counters, what was seen. -
Autosave. When the engine is idle again,
Engine.savehands the state to theSaveStore: in the browserIndexedDbSaveStore(src/engine/dom/save-store.ts), which writes a versioned envelope (core/save.ts) and reads it back before saying it is saved. -
Render.
App(src/engine/dom/app.ts) composes the engine, thePresenter(dom/presenter.ts, since 4.1.11) and the room view's renderer: the room view (dom/room.ts, painted by the DOM or, on demand, by Canvas), speech (dom/speech.ts), the inventory, music (dom/director.ts,dom/audio.ts).
The order in which reactions are evaluated, and what each command does to time and randomness, is Execution model.
Content and engine code are trusted; everything that comes from outside is not, and starts as unknown: a save
(parseSave, a zod schema), a session file (parseSessionFile), a storyboard (storyboardProblems), a layout import,
an assistant provider's response, a Studio request (tools/studio/security.ts). The same invalid input gives the same
diagnostic in the command line, the Studio and the MCP (tests/diagnostics-parity.test.ts).
Limit ·
no-sandboxTrusted means not sandboxed: a game's TypeScript modules and custom commands run with the engine's rights. quest'n'click does not sandbox an untrustedgames/<id>/directory (Security policy).
Saves are envelopes { format, schema: 4, gameId, gameSaveVersion, variant, state } (4.1.15: variant is the world
the game was played in; a schema 3 envelope is read as the story world). An old save is migrated by the game's
migrations (core/migrate.ts), and one save per release since 3.0 is loaded by tests/save-v3.test.ts. Every input
is also an entry of the engine's session, with the answers given while it ran (choices, random draws) and the
digest of the state after it: replay (src/engine/tools/replay.ts) plays a session again and names the first entry
whose state differs. A bug report is a session file. The formats are in File formats.
solve (src/engine/tools/solve/) explores the game from "New game" with the same Engine and a silent presenter
(FakePresenter): whatever it finds, a player can do. A witness search finds one path to the ending; a proof explores
every reachable state and names the softlocks. The search keys a state only on what can still matter
(solve/abstractions.ts); each abstraction is audited against the explicit search (--audit-abstractions, and the
corpus of npm run audit:corpus). The path from solve() to a verdict is in src/engine/BOUNDARIES.md, "Inside the
solver" (Import boundaries). What a result lets an author conclude is
What the solver proves.
A game may declare signals (reality; Reality Bridge). A separate Reality Bridge
(bridge/src/) authorises connectors with Biscuit and signs each accepted fact; the player verifies the signature
before reading anything (src/engine/reality/protocol.ts), and the engine applies the signal at most once, as a session
entry, saved before it is acknowledged. The engine never touches the network: the transport is a port
(WorldSignalPort), and a game without reality loads none of this code.
Public: quest-n-click/content (the types and defineGame), quest-n-click/player (boot), quest-n-click/minigames,
quest-n-click/testing (Engine, the fakes, the save envelope, solve), quest-n-click/reality (since 4.1.1), the
content format, the save envelope and the MCP tools, held by tests/api-surface.json and the support policy
(Stability and support, Public API). Internal: the modules behind the Engine
and App facades (their members marked @internal), the solver's modules, the tools' code. Internal code may move
between minor versions.
- ADR 0002 declarative content; ADR 0003 the solver runs the engine; ADR 0004 the session is the unit of reproduction; ADR 0005 IndexedDB read back; ADR 0007 Biscuit and signed events; ADR 0011 scene frame, intents, journal.
- The full index: Decisions and ADRs.
Generated publication · publication générée — current documentation · documentation courante: quest'n'click 4.4.0 r1, documentation revision · révision de la documentation 8dc717c69acb62c4742b69c54a7e7dfb620f75d8.
Earlier versions and permalinks · versions précédentes et liens permanents: https://wanoo.github.io/quest-n-click/docs/
An edit made in this wiki is overwritten at the next publication · une modification faite dans ce wiki est écrasée à la publication suivante.
Edit the source · Modifier la source · Report an error · Signaler une erreur
Index (6)
Start (6)
Play (9)
Install (17)
- Requirements
- Install from the release archive
- Install from npm, once published
- Install from the repository to work on the engine
- doctor: checking your machine
- Image toolchain
- Audio toolchain: ffmpeg, fluidsynth, Furnace, uv
- Upgrade
- Upgrade notes: 4.4
- Upgrade notes: 4.3
- Upgrade notes: 4.2
- Upgrade notes: 4.1
- Upgrade notes: v3
- Upgrade notes: v2 to v3
- Coming from web-scumm
- Package reference
- Install troubleshooting
Create a game (55)
- Your first game
- Layout of a created project and of the engine repository
- The working loop
- Content principles and the identity contract
- Rooms
- Geometry: placement, walk zones, masks, layers, camera
- Something is visible but unreachable
- Reactions
- Game commands
- Conditions
- Flags, variables and events: which to use
- Custom code
- Characters
- Items and inventory
- World map
- Dialogues
- Scripts and events
- Moving characters: patrols and walks
- Exits
- Chapters and objectives
- State and saves: persistent or temporary, renaming an id, migrations
- Staging: wide rooms and animations
- The stage: lights, particles, floors
- Skin and fonts
- Title and credits
- Heroes: several characters and hero choice
- Built-in minigames
- A custom minigame
- Translations
- Music in stems
- Sound effects
- Voices: per language and per hero
- Images: prompts, cutting sheets, transparency, scale
- Asset provenance
- The storyboard, source of truth
- Puzzle design
- Classic mechanics
- Review pages
- DSL reference
- Checking your game
- What the solver proves
- Solver options
- Playtests
- Remix
- Speedrun for authors
- The sealed ending
- Production template
- Example games
- Working with an AI
- Build and release a game
- Publish on GitHub Pages
- The static game on Clever Cloud
- Other static hosts
- Sub-path, cache and service worker, incomplete deploys, going back
- Testing on a phone over the LAN
Local Studio (24)
- The local Studio: overview and launch
- Edit a room
- Placement and stage
- Preview, apply, undo
- Storyboard
- Assets and cutting
- Check: validator, solver, critical path, heat, health, coverage
- Before and after captures
- Semantic diff
- Play and replay a session
- Rule explainer
- Language and objectives
- Voices and the heroes × languages matrix
- Map
- Notes
- Remix in the Studio
- Music
- The Assistant: bring your own key
- Demo mode and studio-apply
- Local and LAN security
- Studio API
- Local MCP: setting up each client
- MCP tools
- Studio troubleshooting
Team Studio (21)
- What runs where
- Concepts: revision, transaction, span, lease, preview, build
- Starting as an invited dev
- Working together day to day
- Roles and actions: the full matrix
- Invitations
- Managing members
- Write refused on the hosted Studio: stale base revision (409)
- Write refused on the hosted Studio: span locked (423)
- Recovering after a lost lease, a disconnection or a stale preview
- Comments
- Map and semantic diff
- Publishing builds
- Remote MCP
- The game in Git
- Upgrading the engine of a hosted game
- Hosted assets
- The server's command line
- Health and metrics routes
- Trying it locally with compose.yml
- Known limits
Clever Cloud (23)
- Clever Cloud: overview, cost and prerequisites
- Apps and add-ons
- Domains and TLS
- Keycloak
- Generic OIDC and GitLab
- Configuration
- First deployment
- Backups
- RPO and RTO
- Restore the Studio on a fresh installation
- Redeploys
- Upgrading the server and checking state after a migration
- Limits of going back
- Monitoring
- Security
- Rotating keys
- Recovering admin access
- Changing domain or OIDC provider
- Worker, PostgreSQL or Cellar down
- Exporting the game and leaving the installation
- Retention
- Clever Cloud troubleshooting
- Costs and teardown
Reality Bridge and connectors (16)
- The Reality Bridge: what it is
- Quick start
- Guarantees
- Install and run the Bridge
- Backup and restore
- Several tenants
- Signals and the manifest
- The Bridge's command line
- Connectors overview
- Email connector
- Telnet connector
- SSH connector
- Open Badges connector
- The connector SDK
- Security: Biscuit and signatures
- Bridge and connector troubleshooting
Contribute (25)
- Welcome
- Developer setup
- The dev loop
- Before a pull request
- Conventions
- Writing documentation
- Architecture
- Code tour
- Import boundaries
- Which tests run when
- Mutation testing
- The coverage ratchet
- The quality baseline
- CI explained
- Field passes
- Benchmarks
- Releasing
- npm packages
- Decisions and ADRs
- Governance
- Bot pull requests
- Contributing with AI agents
- Security policy
- Code of conduct
- Licences and credits