---
title: "Running the samples on phones and in a browser"
url: https://recursiveai.org/onegame/docs/devices/
description: "Check a build on real Android phones, iPhones and iPads, the Steam Deck and gamepads, and what the emulator, simulator and browser runs showed."
version: "1.2.0"
source: https://github.com/recursive-ai-oss/onegame/blob/25626fb470087a9be70c3b1a3da88e91a7d0af59/docs/devices.md
---

# Running the samples on phones and in a browser

The two samples run on the Android emulator, the iOS Simulator and in Chrome with the same state hashes as on a computer. This page is what to do on real hardware, and which commands repeat the simulator checks. Interfaces: [android](https://github.com/recursive-ai-oss/onegame/blob/25626fb470087a9be70c3b1a3da88e91a7d0af59/docs/interfaces/android.md), [ios](https://github.com/recursive-ai-oss/onegame/blob/25626fb470087a9be70c3b1a3da88e91a7d0af59/docs/interfaces/ios.md), [audio](https://github.com/recursive-ai-oss/onegame/blob/25626fb470087a9be70c3b1a3da88e91a7d0af59/docs/interfaces/audio.md), [exporting](https://recursiveai.org/onegame/docs/exporting/).

## One command per device

`tools/devices/` holds a script per kind of device. Each finds the device, builds what it needs, installs and runs the samples (Bricks too, once it is in the tree), compares every scenario's state hash with a run on the computer, takes a frame, checks sound through the `audio` operation, exercises touch or pad input where a script can, prints one verdict line and cleans up. Details of each step are in the script's header; per-step JSON lands in `$ONEGAME_ARTIFACTS/device-<kind>/`.

| Device | Command | Needs |
|---|---|---|
| Android phone | `tools/devices/android.sh [--serial S]` | USB debugging on, `adb`, the Android SDK and NDK the export uses; the phone on the same network as the computer for Lanterns (`--lan-host` if the guess is wrong) |
| iPhone or iPad | `python3 tools/devices/ios.py` on the Mac the device is plugged into | the device trusted and in Developer Mode, unlocked; an App Store Connect API key in `ASC_KEY_FILE`, `ASC_KEY_ID`, `ASC_ISSUER_ID` (the script registers the device and makes the profile; `asc.py` needs `pip install cryptography`), or ready profiles in `IOS_PROFILE_DIR` |
| Steam Deck | `tools/devices/deck.sh [--host H]` | ssh on, this computer's key in the Deck's `authorized_keys`; the Deck found by `steamdeck.local` or a scan of the network (`find-host.py`); desktop mode logged in for the windowed run |
| Gamepad | `tools/devices/pad.sh [--expect NAME] [--rumble]` | the pad plugged in and `/dev/input/event*` readable; `--virtual xbox\|dualsense` runs the same check against a `/dev/uinput` stand-in |

The community controller mapping database is test data: `tools/devices/fetch-gamecontrollerdb.sh <file>` downloads a pinned copy and `pad.sh` does it on first use. What the pad check reports (device id, database entry, whether the layout was mapped, rumble, which buttons and axes moved) needs a person pressing things to cover the buttons; unattended it reports what the idle device says.

For a cluster whose servers run on the computer and whose client runs on a phone, set `ONEGAME_CLUSTER_BIND=0.0.0.0` ([cluster](https://github.com/recursive-ai-oss/onegame/blob/25626fb470087a9be70c3b1a3da88e91a7d0af59/docs/interfaces/cluster.md)); the scripts do. A game's live control on a device the computer can reach only over the network is `--live tcp:*:PORT` ([control](https://github.com/recursive-ai-oss/onegame/blob/25626fb470087a9be70c3b1a3da88e91a7d0af59/docs/interfaces/control.md)).

## A real Android phone

1. On the phone: Settings → About → tap *Build number* seven times; Developer options → *USB debugging*. Plug it in and accept the computer's key. `adb devices` lists it (`ANDROID_SERIAL` picks one when several are attached).
2. From `samples/solitaire`:
   ```
   onegame export --target android --abi arm64-v8a --dev --json
   onegame android install export/Solitaire-*-android-dev.apk
   onegame android test --scenario scenarios --json      # 14 of 14; state hashes equal the desktop's
   onegame android live                                  # prints tcp:127.0.0.1:N for `onegame mcp --socket`
   ```
   Over MCP: `audio` must report `backend: "aaudio"`, `deviceState: "started"` and a growing `framesPlayed`; draw a card (`ui_tap {id: stock}`), then `peakPlayed` is above 0. Touch with a finger, rotate, press Home and come back: `hash` is the same before and after.
3. Lanterns as a client of servers on the computer: build the servers into `bin/` (README of the sample), start `onegame cluster up --spec` with the servers only, and run the client with the computer's LAN address, not `10.0.2.2`:
   ```
   LANTERNS_HOST=<computer's address> onegame cluster test cluster/play-android.yaml --spec cluster-android.yaml --json
   ```
   The phone and the computer must be on one network; the servers listen on all addresses only when their spec says `listen` with a non-loopback address.

What only a phone shows: a real GPU driver's output (compare a capture with the references at the tolerances in the sample READMEs), real touch and keyboard, notches, sound from the speaker, thermals.

## A real iPhone

`tools/devices/ios.py` does the steps below, including the account side, when an App Store Connect API key is set (see the table above). By hand it needs an Apple Developer account, a development certificate in a keychain the shell can unlock (a locked login keychain fails with `errSecInternalComponent` over ssh; `tools/devices/ios-signing.sh` makes a keychain of its own with a certificate the API issues), a provisioning profile that lists the device, and the device in Developer Mode (Settings → Privacy & Security).

1. `onegame export --target ios --dev --bundle-id <an id the account owns> --json` from the sample. The bundle is unsigned and the command prints `signingNeeds`.
2. Copy the profile to `<Name>.app/embedded.mobileprovision`, put the profile's team id into the `.entitlements` file beside the bundle, then
   `codesign --force --sign "<identity>" --entitlements <Name>.entitlements --timestamp=none <Name>.app`
   `xcrun devicectl device install app --device <device id> <Name>.app`
3. Launch it from the phone's home screen, or `xcrun devicectl device process launch --device <id> <bundle id> --console -- --headless --scenario ...`. The `onegame ios` verbs address simulators only; on a device use `devicectl` and read `Documents/onegame-stdout.txt` with `devicectl device copy from`.
4. The same checks as on the simulator: the 14 scenarios give the desktop's hashes, the `audio` op reports `backend: "audiounit"` and `peakPlayed` above 0 after a draw. Lanterns connects to servers on a computer by that computer's LAN address (the first connection asks for the local-network permission).

What only a phone shows: finger touch, the real soft keyboard, the notch and home-indicator safe areas, the speaker and the silent switch (the audio session category is Ambient, so it follows the switch).

## A browser

```
onegame export --target web                        # a folder to host; any static server works
onegame web test --json --tolerance 2 --max-diff 0.02   # solitaire's 14 scenarios in headless Chrome
```

A page needs a browser with WebGPU (Chrome and Edge 113+; Safari and Firefox are not tested). Sound starts after the first click or key press, as browsers require; until then the `audio` op reports `awaiting_gesture`. To see the page present frames on a real GPU, start headed Chrome on a display with `--enable-unsafe-webgpu --ignore-gpu-blocklist --enable-features=Vulkan --ozone-platform=x11 --no-first-run --disable-extensions` (a virtual X server such as `Xvfb` is enough), open the exported page, and `navigator.gpu.requestAdapter()` names the GPU.

## Repeating the simulator checks

| Target | Check |
|---|---|
| Android emulator | `onegame android test --scenario scenarios --json` in `samples/solitaire`, then `onegame cluster test cluster/play-android.yaml --spec cluster-android.yaml --json` in `samples/lanterns` (README of each sample) |
| iOS Simulator | `onegame ios test --scenario scenarios --bundle dev.onegame.solitaire --json`; in `samples/lanterns`, `onegame ios test --scenario scenarios --json -- --capture-tolerance 4 --capture-max-diff 0.0025` and `onegame cluster test cluster/play-ios.yaml --spec cluster-ios.yaml --json` |
| Browser | `onegame web test --tolerance 2 --max-diff 0.02` in `samples/solitaire`; in `samples/lanterns`, `onegame web test --tolerance 4 --max-diff 0.01`; `tools/web/lanterns-test.mjs` for the WebSocket client |

## What the simulators showed

| | Solitaire (14 scenarios, hashes equal to the computer's) | Lanterns (8 scenarios, hashes equal) | Lanterns as a client of native island servers | Sound |
|---|---|---|---|---|
| Android emulator, arm64, API 35 | 14/14; captures within tolerance 48 | 8/8; 3D captures differ on at most 0.22 % of pixels by more than 4 | UDP: bot agrees with its island at tick 3600, zero drift | `aaudio`, `peakPlayed` 0.077 |
| iOS Simulator, Metal | 14/14; captures within the scenario files' tolerance | 8/8 with `--capture-max-diff 0.0025` (largest frame: 0.20 %) | UDP and WebSocket: agree at tick 3600, zero drift | `audiounit`, `peakPlayed` 0.077 |
| Chrome, WebGPU | 14/14 | 8/8; largest 3D frame differs on 0.4 % of pixels | WebSocket: replica view hash equals the server's at five ticks, zero drift | `webaudio`, silent until the first click |

On iOS and in a browser a capture compares against `name-mobile.png` beside `name.png` when that file exists (Bricks' title has no Quit button there, so `title-720p-mobile.png` and `title-1080p-mobile.png` are the same frames without it).

Frames are compared with references drawn on a software renderer; GPU drivers differ along edges, which the tolerances above cover. Headed Chrome on a desktop GPU (NVIDIA, through a virtual X server) presents both samples' frames to the canvas.
