# Local simulator — test PvP games without the platform

The SDK ships a fully standalone local test environment in `simulator/`: a Vite + React harness
that mounts any game iframe by URL and drives it through the exact same `@chain/pvp-sdk` bridge as
the production Chain.wtf host, plus a one-command local backend — its own chain, a minimal PvP
deployment, and a real Verify Network VRF node. Everything runs on your machine; no Chain.wtf
account or backend access is needed.

Because PvP games are multiplayer, the harness is built to be opened in several browser tabs at
once, each tab playing as a different wallet. If you can make your game fun and robust with two
tabs racing each other in the simulator, it will behave the same way in production — the harness
deliberately replicates the host's quirks (strict manifest validation, the flashblock push layer
over a lagging lobby feed, per-lobby routing) so you hit them on day one instead of after
shipping.

## Quick start

No chain tooling is required — the local backend spawns an in-memory Hardhat node from its own
dependencies (already running your own chain? Point `RPC_URL` at it and it is used instead).

From the SDK package root (an npm workspace covering the simulator, the VRF node and the example
game):

```sh
npm install   # installs every workspace package in one go
npm start     # local chain + VRF node + deployment + harness (:3400) + Chain Bones (:3200) + Rock Paper Scissors (:3500)
```

Open **http://localhost:3400** — the harness polls for the local deployment, fills the setup
panel and starts itself as soon as the chain is up. The pieces also run individually from
`simulator/`: `npm run local-node` (chain + VRF + deployment) and `npm run dev` (harness only).

The game URL defaults to `http://localhost:3200` (the Chain Bones example); point it at
`http://localhost:3500` and pick `RockPaperScissorsGame` in the setup panel to run Rock Paper
Scissors instead.
The game contract defaults to
the deployed `BonesGame`. Point it at your own game in the collapsible setup panel — an
unregistered game contract is registered on the local host automatically under the configured
name. Query params override the saved setup:

```
http://localhost:3400/?game=http://localhost:5173&gameAddress=0x…&rpc=…
```

## Playing against yourself

Every tab is its own player. The setup panel picks the tab's wallet from the Hardhat/Anvil
default-mnemonic accounts (all of them hold 1,000,000 test chUSD; account #3 is reserved for the
VRF node) and **Open a new tab as account #N** opens the next one. The choice lives in the tab
(`?player=<index>` in the URL) while the game URL, contract addresses and lag setting are shared
by every tab, so a duel is:

1. Tab A: create a table from the game. Once it is entered and indexed, the tab opens it
   (`?lobby=<id>`), exactly like production navigating to the lobby route.
2. Tab B (another account): join it from the game — the join enters the lobby and starts the
   duel, and the VRF node answers the coin toss for who rolls first.
3. Roll and place from each tab in turn. Every tab reads the same chain, so all of them converge
   on each move: first through the flashblock push, then durably once the indexed feed catches
   up; let a turn timer run out to try the permissionless skip, and leave it idle for two more
   turn lengths to claim the win by forfeit.

Rock Paper Scissors runs the same way on `:3500` against `RockPaperScissorsGame`: both tabs seal a
pick, the guests reveal themselves, and the first to `winsNeeded` rounds takes the pot. It has no
skip path — nobody can play another player's secret move — so let a window lapse to try the two
liveness endings: leave ONE tab idle and the other is offered **Claim the match** (a forfeit, the
whole pot), leave BOTH idle and either is offered **Settle as a no-contest** (each seat claims
half). Either tab can resign at any time.

## What the local backend runs

`npm run local-node` starts and wires up:

1. **A chain** — attaches to `RPC_URL` (default `http://127.0.0.1:8545`) or spawns the bundled
   in-memory Hardhat node if nothing is listening.
2. **Verify Network VRF** — deploys the real router (vendored bytecode), 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 PvP host**:
   * `LocalTestToken` — freely mintable 18-decimals chUSD stand-in.
   * `LocalPvpHost` — a minimal stand-in for the production `PvpGameFacet` that runs the full
     `IPvpGameV2` lobby lifecycle (open → entries → start → actions / randomness → resolve or
     cancel, escrow, contribution ledger, 2.5% protocol fee, immediate and claimable settlement,
     refunds) and emits **byte-identical events**. No diamond, no whitelist governance.
   * `BonesGame`, `JackpotGame` and `RockPaperScissorsGame` — the SDK's reference games, deployed
     and registered so the harness works out of the box with the Chain Bones and Rock Paper
     Scissors example iframes.
4. **Funding** — every dev account gets test tokens, the VRF router client balance is topped up,
   and the deployment is written to `local-node/deployed.json` (served by the harness at
   `/__local-contracts.json`).

To test your own game, drop its `.sol` file into `simulator/contracts/` (it is compiled, deployed
and registered live, as long as it has no constructor arguments) or deploy it with your own
toolchain against `simulator/contracts/IPvpGameV2.sol` and enter its address in the setup panel.

## What the harness replicates from production

* **Strict frame validation** — the manifest is required, fetched from the game origin, validated,
  and its `gameId` must match the registered game name; a game served from the host's own origin
  is rejected. Failures show the exact reason instead of production's generic screen.
* **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 — and overlays them on the lobby list, the participant roster and the
  contribution ledger through a forward-only patch layer) and after a configurable lag on the
  indexed lobby feed the host reads lobbies from. Every participant's events ride the fast
  channel, so an opponent's join, start, game state and result show up in this tab well before
  the indexed feed delivers them. A call's promise still resolves with the receipt, and the
  snapshot may reflect the lobby before or after that, exactly like production: a game must
  track the lobby it just created until it appears in `lobbies.items`.
* **The same host methods** — input validation, lobby/game ownership checks, approval batching
  on `enterLobby`, receipt parsing for lobby / contribution / claim results, and the
  feature-detected `claimPayout` (only when the manifest declares it).
* **Spectator access** — `snapshot.access` on every push, the read-only rejection of writes, the
  feature-detected `requestPlayAccess`, and the sign-in placeholder for games without
  `capabilities.spectate`.
* **The lobby route** — `metadata.room.roomId` follows the lobby the tab has open, and the lobby
  and match panels under the frame show what production renders around the game. **Join**,
  **Watch** and **Replay** in the match panel set the room the way production's lobby route does;
  the extra **Past games** tab lists every finished lobby of the game (production's history shows
  only your own), so any tab can replay a match it never sat at. Clicking the lobby the tab
  already has open reloads the frame, since the pushed room id cannot change — the same as
  reopening the lobby route.
* **The room browser** — the **Room browser** tab above the frame is production's PvP landing
  page: every lobby of every game on the local host, railed as **Open lobbies**, **Matches in
  progress** and **Recently archived** with the same cards chain.wtf renders (game cover or
  initials, lobby id, game title, player count, the featured stake or pot, and the info popover
  with phase, stake, pot, players and chain). It reads the same lagged indexed feed as the game,
  so it shows exactly what a visitor browsing rooms sees while your lobby fills up, starts and
  settles — open it in a second tab next to a playing tab to watch a room move between rails.
  Clicking a card is production's navigation to that lobby: the tab switches to the game page
  with the lobby open (`?view=rooms` keeps the browser open across reloads). A card the tab's own
  account already sits at says **you are seated** — a table you opened yourself waits for another
  account, so join it from a tab opened as a different player. A card of another
  game switches the harness to that game contract; the game URL is shared, so point it at that
  game for the frame to mount. Titles come from the game's manifest (and its `assets.coverUrl`
  when it ships one) for the running game, and from the registered name for every other game.
* **Randomness verification** — `getRandomnessVerification` runs the production checks against the
  local Verify Network router (real ECVRF proofs and enclave signatures read from the fulfillment
  transactions), so a provably-fair view works offline.

## The setup panel

The collapsible sidebar is your chaos-engineering console:

* **Player** — the tab's wallet, plus a one-click way to open the next account in a new tab.
* **Wallet status override** — force `ready` / `disconnected` / `setup-required` /
  `session-key-mismatch` per tab to exercise your game's non-ready screens; clicking the game
  while `setup-required` flips it back to `ready`, the way production opens the wallet setup.
  Any non-ready status also puts the tab in `spectate` access (see the spectator standard in
  `CHAIN_WTF_PVP_GAMES.md`): writes reject with `PVP_SPECTATOR_READ_ONLY`, and the game's
  `requestPlayAccess()` hand-off flips the status back to `ready`.
* **Spectate by host policy** — keep the wallet as configured but mark the viewer read-only with
  `reason: 'host-policy'` and no sign-in hand-off, the production behaviour for a view-only page.
  A game whose manifest lacks `capabilities.spectate` gets the production sign-in placeholder
  instead of the iframe in both spectating cases.
* **Open lobby id** — the room the tab has open, pushed as `metadata.room.roomId`.
* **Flashblock / indexer lag sliders** — tune both data layers live. Crank the indexer lag with a
  small flashblock lag to watch your game ride the flashblock layer (a second tab sees the
  opponent's join, game state and result long before the indexed feed updates); raise the
  flashblock lag to watch it bridge the gap between a resolved call and the snapshot that
  reflects it. A well-built game shows no flicker and no duplicate rows either way.
* **Game URL + contract address** — swap between the example and your own game without
  restarting anything.

Every snapshot pushed to the iframe is logged (`[host-push #n] …`, debug level) and checked
against the previous one; transitions a game could visually trip on (a lobby vanishing, a resolved
lobby mutating, a pot or count going backwards, the newest lobby regressing) log as console
warnings. The full history sits on `window.__hostSnapshotTrace`.

## Intentional differences from production

* Lobby calls are plain EOA transactions instead of the host's gasless smart-vault signing flow.
  The game-facing API and timing are unchanged; `wallet.address` is the tab's local EOA and
  `assets[0].balance` its token balance.
* The host is its own VRF router client instead of going through a randomness-provider contract.

## Editing the harness contracts

The simulator's Solidity sources, the shared interfaces and the reference games (`examples/`)
live in `simulator/contracts/` and compile via `npm run compile-contracts` (solc, `viaIR`), which regenerates the checked-in
`src/local-node/artifacts.ts`. Only needed if you change the `.sol` sources.
