# Graph engine
Turn chance draws, player decisions, and prize tables into a TypeScript game graph. The shared engine walks it on-chain, so multi-step casino games need no custom Solidity.
:::note[Current SDK checkout]
- `@chain-protocol/casino-sdk@0.7.0`
This guidance reflects this checkout and may include unreleased changes.
[Casino changelog](/sdk/casino/changelog/)
:::
The graph engine runs multi-step casino games without a contract of your own. You write the game
as a TypeScript state machine: chance draws, player decisions, prize tables and fair ladders. The
compiler turns it into a game graph, and one shared contract plays every session by walking that
graph. The chain derives the RTP, the maximum payout and the risk figures from the graph before
any session opens.
**Prefer an engine to your own contract.** A graph-engine title needs no Solidity and no contract
audit. The chain checks its math when it deploys, so the platform review covers your frontend
only. A title whose highest RTP is at or below the approval ceiling (98% at genesis) is
whitelisted as soon as it deploys and joins the offering right away. Write your own `ICasinoGameV2` contract only for
a game neither engine can express (see [Getting Started](/sdk/casino/getting-started/#choose-how-to-write-your-game)).
| Engine | Fits |
| ------------------------------- | ----------------------------------------------------------------------------------------------------- |
| [Slot engine](/sdk/casino/slot-engine/) | One draw settles the bet: slots, scratch cards, Limbo, dice over or under a target. |
| Graph engine | Several steps or player choices: Hilo, a tower climb, blackjack, double-or-nothing, roulette, Plinko. |
The package is `@chain-protocol/casino-sdk/graph-engine`. It holds the compiler
(`compileGraphGame`), the reference walks your frontend replays sessions with, the quote helpers
and the deploy helpers. `@chain-protocol/casino-sdk/graph-engine/contracts/*` holds the engine
contracts as Solidity source. From `0.6.0` the TypeScript API calls a title a Game, so
the definition type is `GraphGameDefinition`. The Solidity contracts and the `deployment.ts` field
`title` keep their names.
## Build your own title
Run every command below in your game's project. A project created with
`npx @chain-protocol/cli init casino` already has the casino SDK and the CLI (see
[Getting Started](/sdk/casino/getting-started/)). Otherwise install the same version of both:
```sh
npm install @chain-protocol/casino-sdk
npm install --save-dev @chain-protocol/cli
```
With Bun, run `bun add @chain-protocol/casino-sdk` and `bun add --dev @chain-protocol/cli`.
### Step 1: write the title
A title is a `GraphGameDefinition`. It has four fields:
- `prizeDenominator`. Prizes are multiples of the wager in units of `1 / prizeDenominator`. With
`10`, a prize of `19` pays 1.9x.
- `roots`. One starting state per bet configuration, in the order game data selects them.
- `key`. A unique string for each state. The compiler merges states with the same key.
- `expand`. The node a state is. The compiler calls it on every state it reaches.
This title is Climb, a three-rung tower. Each climb survives half the time. The player cashes out
for 1.9x at rung 1, 3.9x at rung 2 or 7.6x at rung 3. It is the tower of the
[Climb example](#the-example-climb), which you can play with a full frontend:
```ts
import type { GraphGameDefinition, NodeSpec } from '@chain-protocol/casino-sdk/graph-engine';
export const LABEL = { cashOut: 0, climb: 1, safe: 2, bust: 3 } as const;
const CASH_OUT_PRIZES = [0, 19, 39, 76];
const TOP_RUNG = 3;
type State =
| { at: 'climb'; rung: number }
| { at: 'rung'; rung: number }
| { at: 'cashOut'; rung: number }
| { at: 'bust' };
const climb: GraphGameDefinition = {
prizeDenominator: 10,
roots: [{ at: 'climb', rung: 0 }],
key: state => JSON.stringify(state),
expand: (state): NodeSpec => {
switch (state.at) {
case 'climb':
return {
kind: 'chance',
outcomes: [
{ state: { at: 'rung', rung: state.rung + 1 }, weight: 1, label: LABEL.safe },
{ state: { at: 'bust' }, weight: 1, label: LABEL.bust },
],
};
case 'rung': {
const cashOut = {
state: { at: 'cashOut', rung: state.rung } as const,
label: LABEL.cashOut,
};
if (state.rung === TOP_RUNG) return { kind: 'decision', actions: [cashOut] };
return {
kind: 'decision',
actions: [cashOut, { state: { at: 'climb', rung: state.rung }, label: LABEL.climb }],
};
}
case 'cashOut':
return { kind: 'terminal', prize: CASH_OUT_PRIZES[state.rung] };
case 'bust':
return { kind: 'terminal', prize: 0 };
}
},
};
export default climb;
```
`expand` returns one of five node kinds:
| Kind | What it is |
| ---------- | --------------------------------------------------------------------------------------------------- |
| `terminal` | Settles the session at `prize`. |
| `chance` | Draws one of its `outcomes` by weight and moves to that outcome's state. |
| `table` | Draws one prize from weighted `tiers` and settles at once. Use it for a one-draw payout table. |
| `decision` | Waits for the player to pick one of its `actions`. Action 0 is the default when a session forfeits. |
| `ladder` | A fair ladder: a whole Hilo-style game in one node. See [Fair ladders](#fair-ladders). |
Every chance outcome, table tier and decision action carries a 32-bit `label`. The engine reports
labels, not states, so your frontend renders what happened from them. Keep one label per meaning,
such as `LABEL.bust`, and share the constant between the title and the frontend.
A `terminal` or `table` may set `stake`, the number of wagers escrowed when it settles (1 to 256).
A player action that reaches a deeper stake, such as a double, escrows the difference. A random
draw cannot escrow, so every outcome of a `chance` must hold the same stake. A `decision` holds the
stake of its action 0.
To offer several bet configurations, list several roots. A dice title, for example, has one root
per target. No root may be reachable from an earlier root or share its state. Choices that only
change what the frontend shows belong in the game data's tail, not in a root.
### Step 2: check the figures
`compileGraphGame(title)` enumerates every reachable state, derives the figures the chain will
derive, and packs the chunks it deploys. A title that breaks a rule throws, naming the node or the
bet configuration at fault. Print each configuration's RTP and maximum payout:
```ts
import { compileGraphGame } from '@chain-protocol/casino-sdk/graph-engine';
import climb from './title.ts';
const { graph, derived } = compileGraphGame(climb);
for (const [index, figures] of derived.configurations.entries()) {
const rtp = (Number(figures.rtpWad) / 1e16).toFixed(2);
const maxPayout = Number(figures.maxPayout) / Number(graph.prizeDenominator);
console.log(`configuration ${index}: RTP ${rtp}%, max payout ${maxPayout}x`);
}
```
For Climb, expect `configuration 0: RTP 97.50%, max payout 7.6x`. The RTP assumes best play at
every decision, so it is the most any player strategy returns. Best play climbs from rung 1 and
cashes out at rung 2.
### Step 3: play it locally
Point `deployment.ts` in the project's root at the title module, and name the title:
```ts
import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default {
kind: 'graph',
name: 'Climb',
title: 'title.ts',
} satisfies DeploymentInput;
```
Serve your frontend, then start the SDK simulator:
```sh
npx chain dev casino --frontend-url http://localhost:5173
```
Expect `Deployed Climb at 0x…` and then `Play your game at …`. Open that URL, pick **Climb** in the
game list and play. Every session runs on the real engine bytecode, with randomness from the local
VRF node. The simulator watches the title and every module it imports. Save a change and it
deploys the title again. The simulator needs Node 22.18 or later, or Bun, to load a TypeScript
title. See [Local Simulator → Graph titles](/sdk/casino/local-simulator/#graph-titles).
### Step 4: submit it
Submit the repository through the developer platform. The build loads `deployment.ts`, runs your
title module through the platform's graph compiler without network, and records the chunks and
the title inputs hash. It compiles no contract, because a graph title has none. The verify step
deploys the title on an in-memory chain and derives it to the end. A title whose derive reverts
fails the build, naming the revert. The verdict reports each bet configuration's RTP and maximum
payout.
The review then covers your frontend only. Every title runs on the same engine contract, and the
chain refuses a title whose math breaks a rule, so nobody reviews your math by hand. When the
title deploys, it registers on the diamond. A title whose highest RTP is at or below the approval
ceiling (98% at genesis) is whitelisted at once. A title above the ceiling waits as a pending game until
the security council approves it.
A title with a [Steering contract](#steering-contracts) builds the same way. The platform also
compiles the Steering contract from `contracts/` and hashes the title with the address where
`SteeringDeployer` will create it. When the title deploys, the Steering contract deploys first.
## Build the frontend
The frontend is a normal casino SDK guest. Read [Building Casino Games](/sdk/casino/building-games/)
for the bridge, the snapshot and the session lifecycle. This section covers what is specific to
graph titles.
The frontend needs the compiled graph to replay sessions. Import your title module and call
`compileGraphGame` once, or compile at build time and ship the result. The `manifest` field of
the compiled title maps every node id back to your own state type, which is often the easiest
thing to render from. The engine helpers have no Node imports, and a guest that walks, encodes
and quotes a title bundles them in about 19 KB minified.
- **Open a session.** The game data names the bet configuration. Empty game data opens
configuration 0. `encodePlainGameData(configuration, tail)` opens another. The engine ignores
the tail, which can carry display-only choices such as a dice side.
- **Read where the session stands.** The session's `gameState` starts with the current node id as
4 bytes. Without a Steering contract, nothing follows it, except at a fair ladder.
- **Act at a decision.** Call `submitAction` with `actionData = abi.encode(uint256 actionIndex)`,
the index into the decision's `actions`. Each action requests a randomness word, and the
randomness callback walks the chance nodes that follow.
- **Show what happened.** Replay each randomness word with `walkRandomness(graph, from, word)`,
where `from` is the node the session stood at. It follows the engine draw for draw and returns
the labels of every edge it took and the node it stopped at. Render each label.
- **Quote a bet without a chain call.** `graphQuoteTable(compiled)` gives the figures the engine
quotes from. Build it at build time and ship it. In the browser,
`quoteConfiguration(table, configuration, wager)` returns what the engine's `quoteCaps`,
`quoteRiskParams` and opening reserve give, bit for bit. Still ask the host for the maximum
wager with `hostApi.getMaxWager({ gameData })`.
The frontend never decides an outcome or a payout. The engine settles every session from the
graph and the randomness.
## The example: Climb
Climb is a complete graph-engine game with a frontend. It lives in the Chain repository at
`packages/casino-sdk/graph-engine/example/`, not in the npm package. It has two bet
configurations: the tower from [Step 1](#step-1-write-the-title), at 97.5% RTP, and a one-draw
scratch card, at 76%.
- `ladder.ts` is the title. The frontend, the deploy script and the end-to-end test import the same
module, so the chain, the replay and the UI share one definition.
- `start.ts` deploys Climb on the SDK simulator's chain and lists it in the game picker.
- `frontend/` is the guest iframe. It compiles the title in the browser with `compileGraphGame`,
opens a tower or scratch-card session with `encodePlainGameData`, and reads the current node from
the first 4 bytes of `gameState`. At a rung it offers **Cash out** (action 0) and **Climb**
(action 1) through `submitAction`. It replays every randomness word with `walkRandomness` to show
each draw, and calls `revealOutcome` when a session settles. `frontend/src/climb.ts` holds the
replay.
To run it from a checkout of the Chain repository, in `packages/casino-sdk`:
```sh
bun install
bun run start:graph
```
Expect `Climb 0x… RTP tower 97.50%, scratch card 76.00%`, then
`open http://localhost:3300/?game=http://localhost:3600 and pick Climb in the game list`. Open that
URL, pick **Climb** in the game list and press **Restart harness**. Then climb the tower or scratch
a card. The frontend shows the rung you stand on, every draw (`safe`, `bust`, or the scratch card's
tier) and the payout.
`bun run graph:e2e` plays 10 sessions against the running stack and expects
`10 sessions opened, played and settled as the reference walk predicts.`
## Fair ladders
A fair ladder is one node that plays a whole climb-and-cash-out game, such as Hilo. It holds its
own rungs and a banked multiplier. Every win scales the multiplier by a drawn ratio, and a cash-out
pays the multiplier times the ladder's base prize, up to its cap. A ladder keeps the graph small:
Hilo is one ladder of 26 rungs instead of thousands of nodes.
- **Write** the ladder as a `LadderNode` that `expand` returns: `basePrize` and `capPrize` over the
title's prize denominator, `ratioDenominator`, the `entry` outcomes and the `rungs`. Each rung
lists its options and whether the player may cash out there. Each option lists its outcomes.
Each outcome has a weight, a ratio (0 loses), the rung it moves to and a label.
- **Fairness.** Derive checks that every option is fair to within one part in 10,000, so best
play returns within 2 basis points of the ladder's value. It also requires
`2 × basePrize <= capPrize`, and that no entry outcome reaches the cap.
- **Play.** At a rung, `actionData` is `abi.encode(uint256 index)`. An option index draws that
option, and the option count cashes out. The engine refuses an option one of whose wins would
bring the cash-out to the cap. `walkLadder(graph, node, word, actions, wager)` replays a session,
and `decodeLadderState(gameState)` reads the rung and the banked multiplier.
- **Auto-bet.** `encodeCommittedLadderGameData({ configuration, options }, tail)` opens a session
that plays every listed option in one randomness callback and settles. The session reserves what
the whole chain can win when it opens. A configuration takes at most `committedRounds` options,
a figure derive sets: 93 for Hilo. `walkCommittedLadder(graph, rounds, word, wager)` replays the
callback.
A rung holds at most 254 options and 256 outcomes over all of them.
## Table bundles
A table bundle is one session over several single-table bet configurations, such as several
roulette chips or several Plinko balls. The wager splits across the entries by units, and the
session settles once.
1. **Declare** which configurations combine, in the `GraphGameDefinition`. `drawGroups` lists
groups that share one draw: every table of a group has the same tier weights and labels, so
only the prizes differ. `maxIndependentDraws` (1 to 16) and `independentlyDrawable` declare
configurations that draw independently. Every bundled configuration's root must be a table of
one wager.
2. **Open** a session with `encodeBundleGameData({ draw, entries }, tail)`, where `draw` is
`'shared'` or `'independent'` and each entry is `{ configuration, units }`. A shared bundle takes
1 to 64 entries in ascending order from one draw group. An independent bundle takes up to
`maxIndependentDraws` entries, repeats allowed. `checkBundle` runs the engine's checks off
chain.
3. **Show** the result with `walkBundle(graph, bundle, word, wager)`, which rebuilds each entry's
label and prize. A bundle keeps an empty game state, so read the entries back from the session's
game data with `decodeBundleGameData`.
4. **Quote** a bundle with `quoteBundle(bundleFigures(graph, derived, bundle), wager)`.
A title with a Steering contract cannot declare bundles.
## Steering contracts
A title may name an immutable Steering contract that implements
`@chain-protocol/casino-sdk/graph-engine/contracts/IGraphSteering.sol`. Use one when most of a
game's state never changes the money, such as positions on a 9×9 board. The graph then holds only
what changes the prize, and the Steering contract holds everything else. `act` turns the player's
`actionData` into an action of the current decision. `respond` may force an action when a
randomness walk stops at a decision. If `respond` fails, the engine takes action 0 and carries
on, so a Steering contract can never block a settlement.
Derive takes the best action at every decision, so no Steering contract can pay more than the
derived RTP. That RTP is a ceiling, not what a typical player earns, so measure the gap by
simulation before you ship. A steered title reserves its whole cap when a session opens, and it
cannot hold a fair ladder or declare bundles.
Name the Steering contract in `deployment.ts` as `steering: { source, name, constructorArgs }`.
`source` is relative to `contracts/`, and `constructorArgs` is optional and takes the
`{{diamond}}` and `{{token}}` placeholders. The SDK simulator deploys it directly. The developer
platform creates it through `SteeringDeployer`, which uses `CREATE2` with a zero salt, so its
address depends only on the init code. Titles with the same Steering init code share one Steering
contract. That is safe because `IGraphSteering` is `view` and the session's `gameState` holds the
steering state. A constructor must not read chain state. If it does, the code at that address can
differ from the reviewed build's code, and the deploy then stops and asks for a rebuild. The
platform refuses a steered title with a fair ladder or a bundle when it builds the title.
## Rules the chain enforces
- **RTP.** Derive computes every configuration's value from the graph and rejects one at or above
100% RTP. Best play at every decision counts. The title then registers on the diamond with the
lowest and highest RTP of its configurations. The diamond refuses a title outside the council's
RTP band.
- **Approval.** A title whose highest RTP is at or below the approval ceiling is whitelisted at
once. The ceiling is 98% at genesis, and the council can change it. A title above it is a
pending game until the security council approves it.
- **Game developer.** The creation call names the game developer, who collects the game dev fee.
Anyone may name any developer, since the name only directs the fee. `deployGraphGame` takes it
as `developer` and names the signing account without one. An authorship is an optional way for
a developer's wallet to authorize a specific creator wallet: that wallet writes an authorship for
the title inputs hash and the creator, the creator passes its id to the creation call as
`authorshipId`, and the named developer must be the authorship's writer.
- **Structure.** Every weight is above zero. A run of chance nodes is at most 16 long, so one
randomness word covers it. Every outcome of a chance holds the chance's stake. A decision has at
most 256 actions. A title has at most 65,535 bet configurations and 1,224 chunks.
- **Forfeit.** A session that forfeits at a decision pays the decision's default value, less any
part of its action 0's stake the session has not escrowed.
- **Risk.** The quotes and the maximum payout come from the derived figures, never from the
frontend or the game data. A session reserves what its next step can win, so a long game ties up
little liquidity at its start. Your fees and your maximum wager follow from those quotes. See
[Contract Constraints → What your quotes decide](/sdk/casino/contract-constraints/#what-your-quotes-decide).
## Deploy cost
A title deploys in one transaction when its derive fits the 2²⁴ per-transaction gas cap, which holds
about 4,400 nodes or about 300 single-table configurations. A larger title derives in batches of
calls under 14M gas each, and anyone may send them. The figures below are measured on the local
casino host.
| Title | Nodes | Configurations | Derive |
| -------------------------------------- | ------ | -------------- | ------------------------------------ |
| Hilo, one fair ladder of 26 rungs | 1 | 1 | 2.10M gas of chunks, 0.60M to derive |
| A graph near the one-transaction limit | 4,331 | 1 | inline, 16.0M gas |
| Dice, one configuration per target | 9,800 | 9,800 | batched, 64 calls, 805.8M gas |
| A large graph | 42,083 | 1 | batched, 89 calls, 941.6M gas |
A Hilo session costs about the same gas as a hand-written Hilo contract: about 426k to open and
103k to 107k for each randomness callback.
## Deploy from a script
The developer platform deploys titles for you. To deploy one yourself, for example on a test
chain, call `deployGraphGame({ publicClient, walletClient, deployer, compiled })` with the
`GraphTitleDeployer` address. It uploads the chunks, creates the title naming `developer` (the
wallet's account without one) as its game developer, derives it inline or in batches, and checks
that the chain derived the compiler's figures. `createBatchedGraphGame` and
`deriveGraphGame` run the two halves of a batched deploy, and `deriveGraphGame` resumes from where
the last call stopped.