# Local simulator: test games without the platform
Put your casino game through its paces with a local chain, VRF node, and SDK host bridge. Catch balance guards and timing quirks before players do.
:::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/)
:::
`chain dev casino` runs the casino SDK simulator in your game's repository. It mounts your game
frontend in the simulator page, which drives it through the same `@chain-protocol/casino-sdk`
bridge as the production Chain.wtf host. Behind the page it runs its own chain, a minimal casino deployment,
a real Verify Network VRF node and the contract your `deployment.ts` names. Everything runs on
your machine; no Chain.wtf account or backend access is needed.
If you can make your game fun and robust in the simulator, it will behave the same way in
production. The simulator page replicates the host's quirks (optimistic updates, lagging data feeds,
balance guarding) so you hit them on day one instead of after shipping.
## Quick start
The simulator ships inside the SDK your project installed, and the `chain` command from
`@chain-protocol/cli` starts it. A game created with `chain init casino` has both, plus a
`simulator` script (see [Getting Started](/sdk/casino/getting-started/)). To add them to an existing
project, install the same version of each:
```sh
npm install @chain-protocol/casino-sdk
npm install --save-dev @chain-protocol/cli
```
With Bun:
```sh
bun add @chain-protocol/casino-sdk
bun add --dev @chain-protocol/cli
```
Serve your frontend, then start the simulator in the project's root, next to `deployment.ts`:
```sh
npx chain dev casino --frontend-url http://localhost:5173
```
With Bun, run `bunx chain dev casino --frontend-url http://localhost:5173`. Expect
`Play your game at http://127.0.0.1:3300/?game=…` when it is ready. Open that URL. Press Ctrl-C
to stop the chain, the VRF node and the simulator page. The simulator page listens on port 3300
and the dev chain on 8545; [`chain dev` options](https://www.npmjs.com/package/@chain-protocol/cli#chain-dev-options) lists the flags that change them.
No chain tooling is required. The dev chain is an in-memory EDR chain inside the simulator's own
process. If a chain already answers on the RPC port, the simulator deploys to it instead.
The player defaults to dev-mnemonic account #0, which holds 1,000,000 test chUSD. Query params
override the saved setup:
```
http://localhost:3300/?game=http://localhost:5173&gameAddress=0x…&rpc=…
```
The simulator is the SDK's internal `./simulator` subpath. It sits outside semver; do not import
it from a game. Because `chain dev` runs the simulator of the SDK your project installed, the
simulator always matches the SDK version your game builds against.
## What `chain dev casino` runs
1. **A chain**: the in-memory dev chain (EDR) on the RPC port, or the chain already answering
there.
2. **Verify Network VRF**: deploys the real router from `@verify-network/local`, registers a
local fulfilling node (dev account #3) and keeps answering randomness requests with real
ECVRF proofs. Your contract's randomness path runs for real, not mocked.
3. **A minimal casino**:
- `LocalTestToken`: a freely mintable six-decimal chUSD stand-in, matching USDC.
- `LocalCasinoHost` and `LocalCasinoVault`: a minimal stand-in for the production
`CasinoGameFacet` that runs the full `ICasinoGameV2` session lifecycle (open,
randomness or player actions, settle, escrow deltas, payout caps, forfeit and
cancel-stuck-randomness) and emits **byte-identical events**. No diamond and no portfolio
risk accounting. It keeps the diamond's game registry: the slot title deployer, the graph
title deployer and a casino bytecode deployer are registered on it, and dev account #0 is its
security council. It checks no RTP band.
- `CoinflipGame`: the reference coinflip contract, listed after your game in the game picker.
When your game registers under the name `CoinflipGame` too, the picker lists only yours.
- Each game in `/__local-contracts.json` carries the Game ID it took when it registered
(ADR 0025), numbered as a fresh deployment of the diamond numbers them.
4. **Your game**: the contract, slot title or graph title `deployment.ts` names, compiled and
created through the registered deployer of its kind, from dev account #0, which the creation
call names as its game developer. A title at or below the 98% approval ceiling registers whitelisted. A title above it,
and every raw-SDK casino game, registers as a pending game: the terminal says so, and the
simulator's council account approves it so you can play it. On a live chain it takes no sessions
until the security council approves it. The game is listed first in the game picker.
5. **Funding**: player tokens and vault liquidity. The host's router balance starts empty: every
call that may request randomness pays for it, as on chain. The simulator page serves the
deployment at `/__local-contracts.json`.
## Your game contract
`deployment.ts` in the project's root names the contract:
```ts
import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default {
kind: 'casino',
contract: { source: 'src/MyGame.sol', name: 'MyGame' },
} satisfies DeploymentInput;
```
- `contract.source` is relative to `contracts/`. Import the interface as
`@chain-protocol/casino-sdk/contracts/ICasinoGameV2.sol`; it resolves to the Solidity of the
SDK your project installed. Other imports resolve relative to the file or from `node_modules`.
- `constructorArgs` lists constructor arguments. `{{diamond}}` and `{{token}}` stand for the addresses of
`LocalCasinoHost` and `LocalTestToken`.
- For a slot title, write `{ kind: 'slot', title: 'my-slot/title.json' }` instead. See
[Slot Engine](/sdk/casino/slot-engine/).
- For a graph-engine title, write `{ kind: 'graph', name: 'My Title', title: 'title.ts' }`. See
[Graph titles](#graph-titles).
- For a slot threshold Game written in TypeScript, write
`{ kind: 'threshold', name: 'DiceGame', title: 'title.ts' }`. The module's default export lists
its `ThresholdConfiguration`s by index. See [Slot Engine](/sdk/casino/slot-engine/#threshold-titles).
- Keep `deployment.ts` self-contained: import only types.
### Graph titles
The `graph` kind deploys a [graph-engine](/sdk/casino/graph-engine/) title:
```ts
import type { DeploymentInput } from '@chain-protocol/casino-sdk';
export default {
kind: 'graph',
name: 'My Title',
title: 'title.ts',
steering: { source: 'MySteering.sol', name: 'MySteering' },
} satisfies DeploymentInput;
```
- `title` names a module whose default export is the `GraphGameDefinition`. It may import other
modules. The simulator compiles it in a fresh Node or Bun process (Node 22.18 or later, which
strips TypeScript types by default).
- `steering` is optional. It names the title's Steering contract, with `source` relative to
`contracts/`. Its constructor takes no arguments. Import the interface as
`@chain-protocol/casino-sdk/graph-engine/contracts/IGraphSteering.sol`.
- The simulator deploys the Steering contract, uploads the chunks, creates the title, derives it to
the end (in batches when one 14M-gas call cannot hold it) and registers it under `name`. Every
bet configuration's RTP must be below 100%.
- A session picks its bet configuration with its game data. Empty game data opens configuration
0, and `0x00 ‖ uint16 configuration ‖ tail` opens that configuration, with the 16-bit index
big-endian. The engine ignores the tail, which can carry display-only choices such as a dice
side. `encodePlainGameData(configuration, tail)` from `@chain-protocol/casino-sdk/graph-engine`
writes it. Other leading bytes revert `GraphEngine__InvalidGameData`.
- A change to the title, any module it imports (under Node), or the Steering contract redeploys
the title. The simulator watches the title's whole import graph, workspace packages outside the
project included, and skips only installed packages under `node_modules`. A large title takes many transactions. See [Graph Engine → Deploy cost](/sdk/casino/graph-engine/#deploy-cost).
The simulator compiles with solc and watches `deployment.ts` and every file the build read. Save a
change and it redeploys the game at a fresh address within a couple of seconds, with no restart. A
compile error prints in the terminal, and the previous deployment keeps running.
To test a contract built some other way, create it with your own toolchain against
`http://127.0.0.1:8545` through the `casinoBytecodeDeployer` that `/__local-contracts.json` names,
naming its game developer, and paste its address into the setup panel. The page approves the pending game as the host's
council and names it under the configured name. A contract no registered deployer created takes no
sessions, as on chain.
## Host-path tests
`@chain-protocol/casino-sdk/local-host` runs the simulator's host in your own tests, on Hardhat's
EDR or any dev chain, so a test can open a session, deliver randomness and check the escrow, the
reserve and the payout transfer:
```ts
import {
deployLocalCasinoHost,
localCasinoHostAbi,
testRandomnessRouterAbi,
whitelistOnLocalHost,
} from '@chain-protocol/casino-sdk/local-host';
const { host, vault, token, router } = await deployLocalCasinoHost({
publicClient,
walletClient,
vaultLiquidity: 10n ** 12n,
});
await whitelistOnLocalHost({ publicClient, walletClient, host, games: [game] });
```
- `walletClient`'s account deploys the contracts and governs the host as its security council.
- Mint the test token to the player and approve the host, then call `openSession` on the host.
- `TestRandomnessRouter.fulfill(requestId, word)` delivers the word your test picks, under the
router's 1M callback gas. `requestId` is in the host's `CasinoSessionAdvanced` event, and
`decodeCasinoSession` reads the session it carries.
- The subpath ships ABIs and bytecode, not Solidity. The host follows the production facet's
session lifecycle and changes with it.
## What the simulator page replicates from production
The host side behaves like the real Chain.wtf host, so your game faces the real integration
quirks before it ever ships:
- **Optimistic sessions** — a `pending:` row at phase `WAITING_RANDOMNESS` is pushed the
moment `openSession` is called, then confirmed to the real session id via the broadcast tx
hash (both arrival orders handled, exactly like production).
- **Two live data layers** — every chain event reaches the host twice: near-instantly on a
flashblock push channel (in production the host observes flashblocks — sub-block
preconfirmations — merged through a forward-only patch layer) and after a configurable lag on
the indexed session feed (including the intermediate phase-`NONE` update right after
`CasinoSessionOpened`).
- **Monotonic settled results** — the settled flip is atomic (payout + gameState together), and
a published result never regresses.
- **Game-steered balance** — the displayed balance follows `betPlaced`/`revealOutcome` with a
delayed chain reconcile, never a raw poll.
- **Randomness payments** — `openSession` and `submitAction` attach the ETH a randomness request
owes, the host's `requiredPayment` plus a 10% buffer, starting from an empty router balance.
What a request does not use stays in the balance for the next one. A call whose step
requests no randomness gets its ETH back in the same transaction.
- **One request per player call** — a step reached from `onRandomness` may not request randomness
again. The host refuses it with `LocalCasinoHost__RandomnessRequestedFromCallback`, the router
logs the failed delivery as `RandomnessCallbackFailed`, and the session waits for
`cancelStuckRandomness`, which refunds the whole escrow. Derive every further random value from
the one `onRandomness` received.
- **No escrow change from a callback** — a step reached from `onRandomness` may not return a
nonzero `escrowDelta`. The host refuses it with `LocalCasinoHost__EscrowChangedFromCallback`,
and the session waits for `cancelStuckRandomness` as above.
- **Stuck-randomness path** — an expired randomness deadline surfaces the permissionless
`cancelStuckRandomness` banner. The VRF node in `chain dev` answers every request within a
second, so a session reaches this path only if the node falls behind.
## The setup panel
The collapsible sidebar is your chaos-engineering console:
- **Flashblock / indexer lag sliders** — tune both data layers live. Crank the indexer lag to
watch your game ride the optimistic layer; a well-built game shows no flicker and no duplicate
results.
- **Wallet status override** — force `ready` / `disconnected` / `setup-required` to exercise
your game's non-ready screens.
- **Max-wager answer** — choose what `hostApi.getMaxWager` answers, live: a `limit` at a cap you
set in whole tokens (1000 by default), `no-limit`, or `unknown`. The game sees a change at its
next request. Use it to exercise your bet clamp and your fallback for an unknown answer.
- **Game URL + contract address** — swap between the example and your own game without
restarting anything.
The simulator advertises `capabilities.playAccess: true`. A guest `requestPlayAccess()` records a **simulated play-access intent** in the frame and Bridge calls log. Resolution accepts the intent; it does not make the wallet ready, authenticate, sign, fund, or bet. Change readiness explicitly with **Wallet status override** and wait for the resulting snapshot. An already-ready request is a no-op.
## Intentional differences from production
- Bets are plain EOA transactions instead of the host's gasless smart-vault signing flow. The
game-facing API and timing are unchanged; `wallet.address` and `wallet.smartVaultAddress` are
both the local EOA, which also sends the randomness payment. On chain.wtf a relayer sends it,
and a covered call charges it to the player in the token.
- `getMaxWager` answers the cap from the setup panel, whatever game data you pass. The production
host reads the vault's maximum wager for your game and that game data. The local host does not
enforce the cap: it admits any wager the game's quotes allow.
- `getRandomnessVerification` runs the production checks against the local Verify Network
router (real ECVRF proofs and enclave signatures), so the
provably-fair view is testable locally. It is optional and feature-detected, so your game must
still handle hosts without it.