Puzzle circuits
Bounded signal graphs for puzzles: switches, pressure plates, and/or/xor/not gates, delays, pulses and latches that open doors.
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 #
| Setting | Type | Default | Meaning |
|---|---|---|---|
nodes | list, 1 to 128 | one input named switch | the signal graph, acyclic; declaration order does not matter |
outputs | list, 0 to 64 | one 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):
| Kind | Fields | Meaning |
|---|---|---|
input | initial (false) | a boolean the game sets |
plate | threshold (1) | true when the observed load in kg reaches it |
and, or, xor | inputs (2 to 16) | gates; xor is odd parity |
not | inputs (1) | inverts a signal |
delay | inputs (1), seconds (1) | true after its input has been high for that long |
pulse | inputs (1), seconds (1) | a rising edge starts a pulse of that length |
latch | inputs [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): answerstrueif 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 changeconfig, or cancel:createthen fails with "creation refused".puzzle.circuits.request:{ instance, op, command }, run before a command that changes the instance (notstatus,config,save,preview,load,step,sync). A tap may changecommand, or cancel: the call answers{ ok: false, cancelled: true, by, reason }and the eventrefusedis 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
syncafterstep, or you did notresolvethe token. - A plate stays lit: the game must send
0when 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,
saveandloadare 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.