Skip to content

Repository files navigation

Wyrd

Crates.io Version Crates.io Downloads CI CodSpeed License Discord Buymecoffee

Wyrd is a Rust library for composing game behavior as validated signal graphs. You author a Weave, bind it once into dense runtime state, sample host inputs, settle the graph, and apply its outputs back to your game.

Status: Wyrd 0.4 is a pre-1.0 release line. CI gates the engine-neutral crate, Bevy adapter, feature matrix, public Rustdoc, and package manifest; the publish workflow creates and cross-checks the exact package artifacts. Minor 0.x releases may make breaking public-API changes; patch releases preserve the public Rust API unless the changelog calls out a required correction.

Engine-neutral · no_std + alloc · f32 or Q16 i32 signals · Bevy 0.19 adapter

When Wyrd fits

Use Wyrd when you want game logic that is:

  • authored as a closed catalog of typed knots and ports;
  • validated before execution for cycles, fan-in, required inputs, numeric compatibility, and budgets;
  • independent from engine entities, components, and world access;
  • executed through dense handles without string lookup on the tick path; and
  • portable between desktop f32 builds and Q16 i32 hosts.

Wyrd does not own doors, cameras, entities, or other game state. Your host samples that state into the runtime and applies the resulting signal levels and commands.

From a knot to a puzzle world

Wyrd is for more than a single switch. Its small unit is a typed knot and its useful unit is a readable piece of game behaviour: a latched gate, a timed bridge, a multi-object puzzle, or a one-shot transition request. Compose those machines into rooms; let the host compose rooms into a game.

Host queries world state
        │
        ▼
active room Weave(s) ── SignalOut / EmitCommand ──► host moves, opens, persists, or transitions
        ▲                                                        │
        └──────────── host samples saved / next-room state ─────┘

This division is deliberate. Wyrd has no Door, Room, Warp, Entity, or physics knot. Instead, a host turns "crate is on the sun pad" into a SignalIn, and interprets "shrine.gate.open", "bridge.target", or "world.request_transition" as its own effects. That keeps a puzzle rule portable between engines and makes world ownership explicit.

Quickstart

Prerequisites

  • Rust 1.75 or later
  • Rust 1.95 or later when using the Bevy adapter

Add the engine-neutral package under the wyrd crate name:

cargo add wyrd-for-games --rename wyrd

This writes the following dependency entry:

[dependencies]
wyrd = { package = "wyrd-for-games", version = "0.4.0" }

To verify a checkout with a small end-to-end test:

cargo test -p wyrd-for-games --test hello_and_door

Expected result excerpt:

test result: ok. 2 passed; 0 failed; ...

Core API example

The test above exercises the same author → bind → sample → loom → outbox flow shown here. In an application, the core API looks like this:

use std::error::Error;

use wyrd::{
    is_truthy, weave, BindOpts, HostTime, KnotKind, Runtime, SignalDomain, ONE,
};

fn main() -> Result<(), Box<dyn Error>> {
    let weave = weave! {
        id: "hello";
        knots {
            source = KnotKind::signal_in(SignalDomain::Bool);
            invert = KnotKind::not();
            sink = KnotKind::signal_out("debug.inverted", SignalDomain::Bool);
        }
        threads {
            source.out -> invert.in;
            invert.out -> sink.in;
        }
    }?;

    let mut runtime = Runtime::bind(weave, BindOpts::default())?;
    let source = runtime.sense_id("source").expect("validated SignalIn");
    let output = runtime
        .path_id("debug.inverted")
        .expect("validated SignalOut");

    runtime.begin_frame(HostTime { tick: 0 });
    runtime.port_writer().set_sense(source, ONE)?;
    runtime.loom();

    let outbox = runtime.outbox();
    let sample = outbox
        .signals()
        .iter()
        .find(|sample| sample.path == output)
        .expect("SignalOut sample");
    assert!(!is_truthy(sample.value));

    Ok(())
}

Runtime::bind consumes the validated Weave. The resulting Runtime is the sole executable artifact, and loom() is infallible after a successful bind.

How a host tick works

Host world
   │ sample_into(PortWriter) using SenseId
   ▼
Runtime::begin_frame → Runtime::loom
   │
   ├─ SignalOutSample { path: HostPathId, value }
   └─ Emit { cmd: CmdId, payload }
   ▼
Host applies effects to its own world

Resolve SenseId, HostPathId, and CmdId once during setup. These handles are owned by the runtime that created them; using one with another runtime returns HandleError::ForeignRuntime.

You can implement Host and call tick_once, use NullHost or ScriptedHost for headless execution, or schedule sample and apply systems around the Bevy adapter.

Choose a package

Package Use it for
wyrd-for-games The engine-neutral wyrd crate: signals, authoring, validation, binding, runtime, and headless hosts
wyrd-for-games-bevy The wyrd_bevy crate: Bevy 0.19 scheduling and host-integration helpers for the f32 path

The dependency direction stays one-way:

wyrd-for-games-bevy → wyrd-for-games

Author graphs

Choose the authoring surface by how much topology is known up front:

  • Use weave! for a fixed, readable graph. It supports explicit author IDs, numeric-path selection, pattern instances, and knot-to-pattern connections while lowering through the same checked builder API.
  • Use pattern! when that fixed topology is a reusable fragment with named inputs and outputs. It lowers to the same validated Pattern model, so a pattern can be included by a weave or builder without a second runtime representation.
  • Implement Recipe when a graph is reusable by a host. Its associated Ports type resolves the recipe's SenseId, HostPathId, and CmdId handles once after bind; RecipeInstance keeps them tied to the runtime that created them.
  • Use Scenario::<MyRecipe>::run for deterministic, closure-scoped recipe frames and typed assertions. It is a test and example helper; applications can still drive Runtime directly.
  • Use Weave::compose for generated topology. Composer has Bool, Level, and Count wire helpers for common operations, while knot, input, output, and thread retain the complete WeaveBuilder catalog as an escape hatch.

Recipe::manifest derives a deterministic endpoint summary from the validated weave. With the optional schema feature, the serializable graph and recipe manifest types also implement schemars::JsonSchema; this feature enables std and serde and is intentionally absent from the default and no_std dependency sets.

Use WeaveDef and PatternDef for editable or serialized data. Converting a definition into an immutable Weave or Pattern performs structural validation. The optional RON and JSON codecs also validate while loading.

See the wyrd package guide for the public API and authoring overview, then follow the tiered examples for complete, compile-checked lessons.

Integrate with Bevy

wyrd-for-games-bevy configures three ordered system sets:

WyrdSet::Sample → WyrdSet::Loom → WyrdSet::Apply

Add both published packages under their library target names:

[dependencies]
wyrd = { package = "wyrd-for-games", version = "0.4.0" }
wyrd_bevy = { package = "wyrd-for-games-bevy", version = "0.4.0" }

Use the adapter through wyrd_bevy alongside the engine-neutral wyrd API.

The plugin owns only the loom step. Your systems read components during Sample and mutate components during Apply. Bevy messages such as WyrdSignalConfirm confirm applied host effects; they are not graph threads.

Run the headless two-plate door example:

cargo run -p wyrd-for-games-bevy --example and_door

The example samples two plate states, settles an And knot, applies SignalOut("door.open") to a host-owned Door component, and emits confirmations when the component changes. See the wyrd_bevy guide for the exact ownership boundary.

Learn through tiered examples

wyrd::examples presents 22 ordered, human-readable lessons. Each lesson is a complete Rustdoc example checked as a doctest; the core API example in this README remains the intentionally duplicated quickstart.

Tier Focus Lessons
A Foundations Not, two-input And, bind/sample/loom, tick_once, validation failure
B Reusable Weaves Monostable Pattern, two-plate door, Flag, Counter threshold, Delay
C Game-logic patterns Latches, timers, cooldowns, Threshold, Map, Digitize, OnStart, Emit, Or, typed Composer
D Chamber-scale composition Multi-object latch, moving-host target, and one-shot room-transition request

Check the complete ladder with the same features used by docs.rs:

cargo test -p wyrd-for-games --doc --no-default-features \
  --features "std,signal-f32,serde-ron,serde-json,schema" --locked

Start at Tier A and work forward, or choose the smallest lesson matching the rule you need. The Tier D capstone is deliberately engine-neutral: it proves the rule circuit while leaving spatial queries, movement, persistence, and room loading to the host.

Features and numeric paths

Enable exactly one of signal-f32 and signal-i32.

Feature Crates Behavior
std (default) wyrd-for-games Desktop/test support through no-std-compat
alloc wyrd-for-games Heap-backed graph/runtime storage without std
signal-f32 (default) wyrd-for-games Floating-point signal path; uses libm for no_std square root
signal-i32 wyrd-for-games Q16 integer signal path for constrained hosts
serde wyrd-for-games Serde derives for author definitions
serde-ron wyrd-for-games RON load/save with validation on load
serde-json wyrd-for-games JSON load/save with validation on load
schema wyrd-for-games Opt-in std + serde JSON Schema support for graph and recipe manifests
bevy_log wyrd-for-games-bevy Forwards Bevy's bevy_log feature

wyrd-for-games-bevy always uses signal-f32. Use wyrd-for-games directly for signal-i32 hosts.

CI verifies both numeric paths, codecs, runtime no_std builds, and Bevy.

Stability and saved state

The public Rust API follows Cargo's SemVer compatibility rules. Before a release, CI compares the current API with the latest published packages and builds the configured docs.rs feature surfaces. The publish workflow creates and verifies both package artifacts before publishing in dependency order.

WeaveDef and PatternDef are validated authoring formats, but their serialized representation is not yet a cross-minor compatibility promise. RuntimeState is opaque in Rust but has a versioned optional-Serde checkpoint format. Capture it only after graph evaluation and host apply, before the next frame; outbox effects are never restored. A restore checks its format version, executable fingerprint, and buffer shape before mutating a runtime. Wrap game progress in a host-owned save format and use RuntimeState only when both runtimes use the same bound executable contract.

Playdate / constrained hosts

Use wyrd-for-games directly rather than the Bevy adapter, selecting the integer signal path and the allocator supplied by the host application:

[dependencies]
wyrd = { package = "wyrd-for-games", version = "0.4.0", default-features = false, features = ["alloc", "signal-i32"] }

Bind a Weave when loading a room or scene, resolve its dense sense/path handles once, then call begin_frame → write senses → loom once per host tick. Keep buttons and counts as integers; quantize continuous host input at the boundary.

Use the Playdate Rust toolchain's device build (for example, cargo-playdate's cargo playdate run --device) to validate a consuming game. The simulator is useful for iteration, but profile representative Map/Sqrt-heavy Weaves on physical hardware before making a frame-time claim.

Validate changes

Run the same primary checks used during development:

cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo test --workspace

Run the full local performance suite:

cargo bench --workspace

CodSpeed runs the same runtime and Bevy benchmark targets on pushes to main and pull requests.

CI enforces the numeric/codec matrix, Bevy builds, runtime no_std checks, warnings as errors, and line-coverage gates.

Reference and next steps

License

MIT