Developer docs

Build on FORGE

Everything you need to take one TypeScript module from an idea to staked, settled matches on Robinhood Chain.

01 — The model

One module, everything else is engine

A FORGE game is a single pure-TypeScript object — a GameModule. It owns rules, state and scoring. It does not know about sockets, wallets, rendering or the chain. The engine supplies all of that: matchmaking and custom invite lobbies, deterministic room execution, per-player state redaction, scripted-bot fill, N-player stake escrow on Robinhood Chain, and split settlement with a hard-capped rake. You bring any renderer — three.js, canvas, plain DOM — and paint the frames the relay streams.

Because rooms are deterministic (fixed steps, seeded RNG, ordered inputs), the same module runs authoritatively on the relay and instantly in a browser tab. The playground literally runs the shipped Tap Duel module client-side.

02 — Quickstart

Relay up in three commands

# 1. run the relay (rh-game-engine-backend)
npm install && npm run dev        # relay on :4500

# 2. verify everything
npm run smoke                     # determinism + full duel + 3p lobby
npm run test:escrow               # 32-check EVM audit of ForgeEscrow

# 3. write your game
#    src/templates/my-game.ts — export a GameModule
#    register it in src/templates/index.ts — done:
#    matchmaking, lobbies, bots, stakes and settlement now apply to it.

03 — The module API

Rules in, winner table out

export const myGame: GameModule<State, Input> = {
  id: "my-game", name: "MY GAME", blurb: "…",
  minPlayers: 2, maxPlayers: 4,
  tickRate: 20,        // fixed sim steps per second
  snapshotRate: 12,    // frames streamed to clients
  maxDurationMs: 120_000,

  setup(ctx)  { /* initial state — ctx.random is the seeded stream */ },
  onInput(state, player, input, ctx) {
    /* validate + apply. This IS your rules engine — reject anything
       illegal here and cheating is impossible by construction. */
  },
  tick(state, ctx)  { /* advance one fixed step (ctx.dt seconds) */ },
  view(state, player) { /* optional: hide what this player can't see */ },
  result(state)     { /* null while live; else winners + splitBps
                         summing 10000 — the engine pays this table */ },
};

Three shipped templates cover the genre space: Tap Duel (real-time reactions), Grid Battle (turn-based tactics) and Score Rush (2–4 player contested scramble). Copy the closest one.

04 — Client SDK

Any renderer, five events

import { ForgeClient } from "rh-game-engine-backend/src/client";

const forge = new ForgeClient("https://your-relay.example");
forge.on("found",  (m) => showLobby(m));
forge.on("frame",  (f) => render(f.view, f.events));   // any renderer
forge.on("finish", (p) => showResult(p.result));

forge.queue("my-game", "free");            // or a staked tier + address
button.onclick = () => forge.sendInput({ kind: "tap" });

// custom games: forge.createLobby(...) → share code → forge.joinLobby(code)

05 — Money protocol

Stakes in, proof out

Staked matches run through ForgeEscrow.sol — a single non-upgradeable contract built on OpenZeppelin v5, generalizing the audited two-player escrow that powers RICOCHET:

referee openMatch(id, players[2..8], stake)
players deposit(id)            // one tx each, exact stake
→ Funded → room goes live
module reports result → referee settle(id, winners[], splitsBps[])
→ pot − rake paid by the split table (dust-exact), rake accrues

draws        → cancelMatch → every deposit refunded
relay dead?  → players reclaim() after timeout windows;
               the FIRST funded reclaim permanently blocks settlement —
               a pot can never be double-spent.

Owner powers stop at: rotate referee, set rake for future matches (hard cap 10%), pause new activity, sweep accrued rake. The owner can never touch player stakes; pausing never blocks exits; hostile winners fall back to pull-payments. All of it is executed against the compiled bytecode in npm run test:escrow — 32 checks including solvency.

06 — Determinism

Three rules make a module relay-grade

1. no Math.random()   → use ctx.random (seeded per room)
2. no Date.now()      → use ctx.now (simulated milliseconds)
3. mutate state only inside setup / onInput / tick

same seed + same inputs = same result, everywhere.
That property is what makes replays, desync detection and
browser-identical local play possible — the smoke test asserts it.

Fastest way to feel the engine: play the module it ships.

Play the engine