LRGP
LRGP — the Lightweight Reticulum Gaming Protocol — is what Ratspeak uses to play turn-based games over the mesh. Today it ships two games, Chess and Tic-Tac-Toe. Each move is small enough to fit inside LXMF's single-packet budget, while still leaving the app free to use Direct or Offline Inbox delivery when reliability or offline pickup matters.
If you want to play games in the Ratspeak app rather than build on the protocol, start with Games.
What's in the box
Tic-Tac-Toe. The reference game. A 3×3 grid, nine cells, and compact move envelopes that fit under LRGP's 200-byte budget. It's the simplest thing you can build on top of LRGP, and that's the point: the implementation is short enough to read in one sitting, and it exercises every part of the protocol — challenge, accept, move, win/draw detection, resign, draw offers. If you want to learn how a game is wired up, start here. Both peers replay the move list and validate independently, so a tampered-with board on one side gets caught on the other.
Chess. A full game of chess, validated on both sides with cozy-chess for legal-move generation. Moves go on the wire as UCI strings (e2e4, e7e8q for promotions); FEN reconstruction and rule enforcement happen locally on each peer. Terminal reasons (checkmate, stalemate, threefold repetition, fifty-move, insufficient material, resign, agreement) are coded as 2–3 character tags so a complete envelope still fits the single-packet budget. The responder currently chooses colour with a random coin flip and includes the chosen White hash in the accept payload.
How it works
A move is a small MessagePack envelope tucked into an LXMF custom field. The envelope has five keys — what game (a), what command (c, e.g. move or resign), which session (s, an 8-byte ID generated by the challenger), a game-specific payload (p), and a fresh 8-byte CSPRNG nonce (n) used for replay deduplication. The whole thing is capped at 200 packed bytes, well under LXMF's 295-byte single-packet ceiling. That headroom is the entire reason the protocol looks the way it does.
Because each envelope is tiny, all three LXMF delivery modes stay available. Standalone LRGP can send opportunistically when that is appropriate. Ratspeak defaults game actions to proof-backed Direct delivery for recently seen peers, and can use Offline Inbox delivery when a propagation node is reachable for offline pickup. Sessions are stateless on the wire; both sides hold their own copy of the game state and replay history independently. The receiver re-validates each move against its local state. Invalid moves are rejected and surfaced as game errors; the protocol defines an error action for implementations that send explicit error envelopes.
The n nonce gives receivers a way to drop opportunistic retransmits without re-applying them. Each peer keeps a per-session bounded LRU of recently-seen (session_id, nonce) pairs (default 512 entries, 10-minute TTL) and silently drops duplicates. The cache is dropped when a session reaches a terminal state.
A session goes through a small set of states: the challenger sends challenge (status pending), the responder replies with accept (active) or decline (declined), and from there both sides exchange move envelopes until somebody resigns, accepts a draw, or the game reaches a terminal position (completed). Either side can also send draw_offer / draw_accept / draw_decline along the way.
Sessions live in SQLite. An unanswered challenge expires after 24 hours. Default active sessions expire after 7 days of inactivity, with an hour of grace built in for clock skew between peers, and individual games can override that policy; Tic-Tac-Toe currently uses a shorter active TTL. Expiry is local — no message is sent, each peer just stops accepting moves on that session ID. Completed games are kept indefinitely.
A non-LRGP LXMF client receiving one of these messages still sees something useful: the standard content field carries fallback text like [LRGP TTT] Move 3 or [LRGP Chess] e2e4, which renders as an ordinary message. Game state is lost but the conversation isn't.
Implementations
Three implementations share one wire format. The Rust spec (SPEC.md in lrgp-rs) is the source of truth — if any two disagree, the spec wins.
| Implementation | What it's for |
|---|---|
| lrgp-rs | Standalone Rust crate. Embed in your own Reticulum app. |
| lrgp-py | Standalone Python package. Quick prototyping, also the spec's reference implementation. |
| Ratspeak built-in | What ships in the Ratspeak desktop and mobile app. Renders the full Chess and Tic-Tac-Toe UI. |
The standalone packages have parallel module layouts and a shared SQLite schema, so a session created by one can in principle be read by another. The protocol marker on the wire is always lrgp.v1. Cross-language wire fixtures (tests/chess_*.bin, tests/ttt_*.bin) are byte-identical between the two repos to enforce that the Rust and Python encoders agree.
The Ratspeak app is the only "full" client today — it renders an interactive UI. Other LXMF readers see the fallback text path.
Build a new game
Each game is a single file in apps/. To add one, implement the GameApp trait (Rust) or subclass GameBase (Python) and register it with the router at startup. A game declares a manifest — id, version, display name, session type (turn_based, real_time, round_based, single_round), validation model (sender, receiver, or both), the list of commands it accepts, preferred delivery per command, and TTLs — and provides a handful of behaviors: validate an incoming action, produce an outgoing action, and render fallback text. The router takes care of envelope packing, session bookkeeping, persistence, and dispatch.
Tic-Tac-Toe is the simplest example to read — it's about as much code as you'd expect for nine cells and a turn check. The Chess implementation shows what a more involved game looks like: same shape, same trait, with cozy-chess (Rust) or python-chess (Python) doing the legal-move heavy lifting. The standalone lrgp-rs examples (basic_envelope, session_lifecycle, tictactoe_game, chess_game) and the lrgp-py examples (ttt_local, chess_local) walk through the protocol piece by piece without the rest of an app in the way.
A few constraints to keep in mind. Envelopes must stay under 200 packed bytes — single-character payload keys, short string codes, no JSON-ish verbosity. Actions must be idempotent under retransmission, since LXMF can redeliver. Validation should be deterministic and replay-safe: if both peers run the same move sequence they should agree on the result. If you genuinely need to send a binary blob, LXMF will auto-escalate to a Resource transfer over an established link (good for up to a few megabytes), or you can attach it explicitly via the standard file-attachments field — but the move envelope itself should still be small enough to keep opportunistic and low-cost delivery possible.
Build & versions
# Standalone Rust crate
git clone https://github.com/ratspeak/lrgp-rs
cd lrgp-rs && cargo test
cargo run --features test-helpers --example chess_game
# Standalone Python package
git clone https://github.com/ratspeak/lrgp-py
cd lrgp-py && pip install -e ".[chess,dev]" && pytest
pip install -e ".[rns]" # add Reticulum + LXMF for the network bridge
python examples/chess_local.py
The LRGP wire spec is v0.3; lrgp-rs is v0.3.1 and lrgp-py is v0.3.0. The standalone LRGP packages are MIT-licensed. The version shipped inside Ratspeak is distributed as part of the AGPL-licensed Ratspeak app. The [chess] extra on the Python package pulls in python-chess (the Rust crate uses cozy-chess directly).