Build a slot
Eight steps from an empty grid to a running server. Every piece is a pure function of a stream of floats, so the whole game replays from one recorded seed.
Pick a shape
import { rect } from "@open-rgs/grid";
const SHAPE = rect(5, 3); Draw the reels
import { stackyFill } from "@open-rgs/markov";
const BASE = { LOW: 47, MID: 24, HI: 12, PREM: 7, WILD: 6, SC: 0 };
const reels = stackyFill(SHAPE, BASE, 0.35); Spawn the scatter
import { withScatters, oneInFor } from "@open-rgs/scatters";
const scatters = {
symbol: "SC",
count: { 0: 9000, 1: 700, 2: 250, 3: 45, 4: 5 },
reels: [1, 2, 3],
protects: ["WILD"],
};
oneInFor(scatters, 3); Name the boards
import { recipes, place } from "@open-rgs/recipes";
import { randomN, cols } from "@open-rgs/selectors";
const board = recipes([
{ name: "base", p: 0.97, draw: withScatters(reels, scatters) },
{ name: "hi-tease", p: 0.03, draw: place(3, "HI", randomN(3, cols([0, 1])), reels) },
]); Price the wins
import { paytable, totalMultiplier } from "@open-rgs/paytable";
import { evalLines, rowLines } from "@open-rgs/pay-lines";
const roles = { wilds: ["WILD"], scatters: ["SC"] };
const LINES = rowLines(5, 3);
const PAY = paytable({
LOW: { 3: 0.2, 4: 0.8, 5: 3 },
MID: { 3: 0.4, 4: 1.5, 5: 6 },
HI: { 3: 0.8, 4: 3, 5: 12 },
PREM: { 3: 1.5, 4: 5, 5: 22 },
WILD: { 3: 2.5, 4: 10, 5: 45 },
}); Add the feature
import { triggersOn } from "@open-rgs/pay-anywhere";
import { beginRespins, stepRespins, isCycleOver, settleRespins } from "@open-rgs/holdwin";
const JACKPOTS = { MINI: 8, MINOR: 20, MAJOR: 60, GRAND: 400 };
const RESPINS = { respins: 3, fullBoardAward: "GRAND" }; Write the math
The module default-exports a factory taking the host.
That is the RNG seam: the math has no other source of randomness, and
loadTsMath rejects a bare-object export for exactly that
reason.
import type { MathHost, SimpleMath } from "@open-rgs/contract";
const MAX_WIN = 5000;
export default function createMath(host: MathHost): SimpleMath {
return {
kind: "simple", name: "demo", version: "1.0.0", rtp: 0.965,
play() {
const { grid, recipe } = board(host.rng_next);
const wins = evalLines(grid, LINES, PAY, { roles });
let mult = totalMultiplier(wins);
if (triggersOn(grid, "SC", 3)) {
let s = beginRespins(toCoinGrid(grid, host.rng_next), RESPINS);
while (!isCycleOver(s)) s = stepRespins(s, landCoins(s.grid, host.rng_next), RESPINS);
mult += settleRespins(s, RESPINS, JACKPOTS);
}
const capped = Math.min(mult, MAX_WIN);
return {
multiplier: capped,
ops: [{ kind: "spin", grid: grid.cells, recipe }],
type: capped > 0 ? "win" : "loss",
};
},
};
} Run it
import { createServer, binaryTransport, loadTsMath, cryptoRng } from "@open-rgs/core";
import { defineGame } from "@open-rgs/contract";
import { MockPlatform } from "@open-rgs/platform-mock";
await createServer({
manifest: defineGame({
id: "demo", declaredRtp: 0.965, defaultMode: "default",
modes: {
default: {
math: await loadTsMath("./maths/demo.ts", { rng: cryptoRng }),
stakeMultiplier: 1,
},
},
}),
platform: new MockPlatform({ startingBalance: 100_000 }),
transport: binaryTransport({ port: 80 }),
}); Where the rest of it lives
The walkthrough above builds one base game with a hold-and-win. The pieces for everything else it might grow are separate packages, each with its own page, and each an ordinary import:
- freespins: the trigger table, the counter, retriggers, a global multiplier, cells held across spins.
- picks: pick N of M, reveal until a stop, a weighted wheel.
- gamble: double-or-nothing and collect-or-risk, priced by their edge.
- symbols: mystery reveals, transforms, walking wilds, multiplier wilds, splits.
- meters: collect five of these and something happens.
- strips: the classic reel, when a port or a lab asks for one.
A feature is a state machine your math steps through between the board and the win, so adding one changes the math file and nothing else: the manifest, the transport and the adapter never learn about it.
Then measure
Nothing above tells you the game is any good. Simulate it, and check the confidence interval before believing the number: a game with a rare feature paying hundreds of times bet needs tens of millions of spins to bound RTP usefully, and a point estimate from 400,000 will happily "confirm" anything within several points.