Puzzle circuits

Bounded signal graphs for puzzles: switches, pressure plates, and/or/xor/not gates, delays, pulses and latches that open doors.

Source: module-kits:kits/puzzle_circuits/usage/en.mdView Markdown source

Also read offline — CLI: grip-forge docs puzzle-circuitsMCP tool: gripforge_docs { "topic": "puzzle-circuits" }

Switches, plates and logic gates (puzzle.circuits) #

What it is for #

puzzle.circuits is a small, deterministic logic engine for puzzles: switches and pressure plates feed boolean gates, on-delays, pulses and latches, and the outputs of the graph tell your game which doors or mechanisms to open or close. It holds the rules only. Your game owns the meshes, the colliders, the weight of what stands on a plate, the animation and the sounds. For the sibling kit that traces light beams and for how the two compose, read the puzzles guide.

Turn it on #

gripforge_gamekit_install { project_id: "gkp_…", id: "puzzle.circuits" }

It needs no other kit. The kit is a service with up to 128 independent instances (one per room or mechanism); the instance default exists from the start, built from the kit configuration. {} gives one input node named switch and one output named door fed by it.

First example #

<!-- test: kits/game_engine/host/tests/puzzle_circuits.rs::door_driver_handles_blockage_and_applies_output_once --> The test sends step with the input switch set to true, then sync, which answers an action with a token. The game tries to open the door; here the door is blocked, so it answers resolve with success: false. The signal stays committed (signals.switch is true) but applied.door stays false. A later sync proposes the change again; this time the game applies it and answers resolve with success: true, and applied.door becomes true. A second resolve with the same token is an error, and a sync with nothing left to do answers a null action.

Settings and defaults #

SettingTypeDefaultMeaning
nodeslist, 1 to 128one input named switchthe signal graph, acyclic; declaration order does not matter
outputslist, 0 to 64one output door sourced from switch{ id, source }: actuators fed by a node

Node kinds (every node has an id of 1 to 128 bytes and a kind; unknown fields are rejected):

KindFieldsMeaning
inputinitial (false)a boolean the game sets
platethreshold (1)true when the observed load in kg reaches it
and, or, xorinputs (2 to 16)gates; xor is odd parity
notinputs (1)inverts a signal
delayinputs (1), seconds (1)true after its input has been high for that long
pulseinputs (1), seconds (1)a rising edge starts a pulse of that length
latchinputs [set, reset]holds its state; reset wins

Durations are 0.001 to 3600 seconds. Cycles are refused; a latch supplies memory. A configuration can also be { "preset": "<name>" }, which takes a document of the presets registry of the project; the kit ships switchDoor, twoPlates, timedDoor and latchedDoor. Other keys given next to preset win over the preset.

Services, events, variables #

The service puzzle.circuits:

  • create(id, config): a new instance; answers its id. Refused beyond 128 instances, for a duplicate id, or for an invalid graph.
  • request(id, command): runs a command (below) and answers its result.
  • remove(id): answers true if it existed.
  • ids(): the instance ids.

Commands (op): step (dt 0.000001 to 0.1 s, inputs as absolute observations: { id, value }, a boolean for input, kg for plate), sync (answers { state, action }; force: true sends every output), resolve (token, success), status, config, reset, preview, save and load. state holds inputs, memory, signals and applied.

Events, each named with the prefix of the kit id (for example puzzle.circuits followed by a dot and the word below): created (instance, preset), removed (instance), changed (instance, op, result) after a command that changes the instance, and refused (instance, op, by, reason) when a tap refused a command. The kit has no variables.

For scripts and rules, the action and the condition of the kit send a command to an instance and ask whether it exists (instance defaults to default).

Hooks and modding #

  • puzzle.circuits.create: { instance, preset, config }, run before an instance is created. A tap may change config, or cancel: create then fails with "creation refused".
  • puzzle.circuits.request: { instance, op, command }, run before a command that changes the instance (not status, config, save, preview, load, step, sync). A tap may change command, or cancel: the call answers { ok: false, cancelled: true, by, reason } and the event refused is emitted.

Engines #

One Rust core is used on the web, the native player and any engine through the C library; the same graph gives the same signals everywhere with the same fixed dt and the same observations. The engine side reads the plates, applies the output batch and animates.

When it does not do what you expect #

  • The door never moves: you did not call sync after step, or you did not resolve the token.
  • A plate stays lit: the game must send 0 when it empties, and must count a body once.
  • A configuration is refused: a cycle, an unknown reference, a duplicate edge, too many inputs, or an id longer than 128 bytes.
  • A command fails while an action is pending: resolve it first; changes, save and load are refused until then.
  • Timing changes between runs: the tick size changed; use one fixed dt.

Limits #

  • There is no hidden clock: nothing advances unless you send step.
  • The kit never moves a door itself and does not query the physics.
  • Signal graphs are bounded: 128 nodes, 64 outputs, 16 inputs per gate, 128 instances.

Versions #

Kit version 1.1.0 (service contract 1.1.0). 1.1.0 added the hooks, the presets, the outcome events and the script verbs.