# Puzzles: beams and circuits (puzzle.* kits) Two reusable puzzle engines written in Rust: light beams that bounce between mirrors, and signal graphs that open doors. They hold the rules only; your game owns the meshes, the colliders, the animation and the sounds. Readable offline: `gripforge docs puzzles`, or the MCP tool `gripforge_docs` (topic `puzzles`). ## What it is for | Kit | What it adds | | --- | --- | | `puzzle.beams` | sources, finite mirrors, colour splitters (prisms), absorbing receivers and blockers; one `trace` answers every beam segment and which receivers are lit | | `puzzle.circuits` | switches, pressure plates, boolean gates (and, or, xor, not), on-delays, pulses and latches in a bounded graph whose outputs open or close actuators | They compose: feed the `active` value of a receiver into an `input` node of a circuit, and let the circuit drive a door. There is no second physics world and no hidden tick: the game decides when to trace and when to step. ## Turn it on ```text gripforge_gamekit_install { project_id: "gkp_…", id: "puzzle.circuits" } gripforge_gamekit_install { project_id: "gkp_…", id: "puzzle.beams" } ``` Both need no other kit. Each one is a service with up to 128 independent instances (one per room or mechanism): `create(id, config)`, `request(id, command)`, `remove(id)`, `ids()`. A command is a small JSON object with an `op`. Rust is the only implementation: the same core runs as a native C library, as a WebAssembly module and inside the kit host, and JavaScript only carries the JSON. ## puzzle.beams Configuration (all optional, unknown fields and out-of-range values are rejected): | Field | Default | Bounds | Meaning | | --- | --- | --- | --- | | `maxDistance` | 100 | 0.01 to 10000 | total path length in metres, shared across reflections along each branch | | `maxBounces` | 16 | 0 to 64 | mirror and prism interactions a child ray follows | | `maxSegments` | 256 | 1 to 2048 | segment budget of one complete trace | | `minPower` | 0.001 | | rays whose brightest channel is below this are pruned | Send the complete current optical scene every time something moves: ```json { "op": "trace", "sources": [{ "id": "laser", "position": [-4,1,0], "direction": [1,0,0], "power": [1,1,1] }], "surfaces": [ { "id": "mirror", "kind": "mirror", "position": [0,1,0], "normal": [1,0,1], "radius": 1 }, { "id": "sensor", "kind": "receiver", "position": [0,1,-4], "normal": [0,0,1], "radius": 0.5, "threshold": [0.5,0.5,0.5] } ] } ``` - 0 to 16 sources and 0 to 128 surfaces; ids are unique per category. Units are metres in one right-handed frame. `power` is an RGB triplet of a unitless gameplay quantity. - Surface kinds: `mirror` (`tint`), `prism` (`tint` and `reflect`: a configurable splitter, for instance `reflect: [1,0.5,0]`, not a physical refraction), `receiver` (`threshold` per channel; active when every positive threshold is met; it absorbs, it does not pass the beam on), `blocker` (absorbs; may be a `sphere`). Surfaces are finite disks by default (`radius`, `normal`, `twoSided`). Tint and splitting never amplify power. - The answer holds `segments` (`source`, `from`, `to`, `power`, `hit`, `depth`), `receivers` (`id`, `power`, `threshold`, `active`), `truncated` and `limits`. If a budget is exhausted, `truncated` is true and EVERY receiver is reported inactive, so an incomplete trace never powers a door. - Walls that must stop a beam have to be supplied as blockers: the kit does not query engine meshes. Use threshold margins, never equality of floating-point values. - Other commands: `status`, `config`, `reset`, `preview` (runs on a clone), `save` (`gripforge.beams/1`) and `load` (the derived results are recomputed, never trusted). Damage from a beam is not applied by the kit: it is a game decision. ## puzzle.circuits `{}` creates one `input` named `switch` and one output named `door` sourced from it. A configuration: ```json { "nodes": [ { "id": "left", "kind": "plate", "threshold": 20 }, { "id": "right", "kind": "plate", "threshold": 20 }, { "id": "both", "kind": "and", "inputs": ["left", "right"] }, { "id": "held", "kind": "delay", "inputs": ["both"], "seconds": 1 } ], "outputs": [{ "id": "door", "source": "held" }] } ``` 1 to 128 nodes, 0 to 64 outputs, at most 16 inputs per gate, no cycles (a `latch` supplies memory). Node kinds: `input` (`initial`), `plate` (`threshold` in kg, true when the observed load reaches it), `and` / `or` / `xor` (2 to 16 inputs, xor is odd parity), `not`, `delay` (true after the input has been high for `seconds`, 1 by default), `pulse` (a rising edge starts a pulse of `seconds`), `latch` (`inputs: [set, reset]`, reset wins). Durations are 0.001 to 3600 s. The loop of the game, once per fixed tick: 1. `{ "op": "step", "dt": 0.016, "inputs": [{ "id": "left", "value": 25 }] }`. `dt` is 0.000001 to 0.1 s; observations are absolute (a plate with nobody on it must be sent as 0) and `input` nodes take booleans. 2. `{ "op": "sync" }` answers `{ state, action }`. `action` is null or `{ token, effect }` where the effect lists absolute boolean commands, for instance `{ "kind": "outputs", "changes": [{ "id": "door", "value": true }] }`. 3. Apply the whole batch to the world once, then `{ "op": "resolve", "token": 1, "success": true }`. If a door is blocked or an actuator is missing, undo any partial work and resolve with `success: false`; the signals stay committed and a later `sync` retries. `sync` with `force: true` sends every output, false ones included (use it right after creation). While an action is pending, other changes and save/load are refused. Use the instance lifetime plus the token as your receipt key so a duplicate resolve cannot apply an effect twice. Other commands: `status`, `config`, `reset`, `preview`, `save` (`gripforge.actions/1`), `load`. Replay is exact with the same fixed tick and the same observations. ## Hooks and modding These two kits are pure drivers: they declare no hooks, collections or moddable keys. The decisions are yours, in the code that calls them: when to trace, which engine objects become sources or blockers, how a receiver or an output is shown or sounded. Everything else of the game (rewards, doors, interactions) reacts to the answers through the ordinary events and scripts of the game. ## Engines The Rust core is delivered as a native C library, a WebAssembly module and a kit-host adapter, so the same puzzle runs in the web player and the native player, and any engine can call the C library. The engine adapter converts positions and normals, owns the colliders and actuators, and renders the beams (lines, particles, sounds) from the returned segments. ## When it does not do what you expect | Symptom | Cause and fix | | --- | --- | | A receiver is never active | a budget was exhausted (`truncated`), a blocker is missing or in the way, or the threshold needs more power than arrives | | A beam passes through a wall | the wall was not sent as a blocker | | A plate stays lit | the engine did not send 0 when it emptied, or one body was counted twice | | The door never moves | no `sync` after `step`, or the effect was not resolved (`resolve` with the token) | | The graph is refused | a cycle, an unknown reference, a duplicate edge or an id over 128 bytes | | Timing differs between runs | the tick size changed; keep one fixed `dt` |