- Status
- PrototypeAn early working build.
- Started
- Updated
- Role
- Product Design, Architecture, Development Orchestration
- Built with
Overview
RowGhost is a rowing game built on the one thing an erg gives you that a treadmill does not: a stroke-by-stroke trace that can be replayed exactly. Save a row and it becomes a ghost. Race the ghost. Beat it, save the better one, and race that.
The first playable milestone runs entirely from a simulator. There is no account, no Concept2 PM5 and no hardware of any kind in the loop — you choose a distance and an opponent in a browser and go. That was deliberate: the whole game had to be playable and tested before a single Bluetooth packet was involved.
It is built on the Expo SDK 57 / React Native / TypeScript foundation from my own AppStarter, which is why a project this young already has routing, theming, storage, auth providers and a validation toolchain that were never written for it specifically.
Stillwater
The course is a real-time 3D lake, rendered with Three.js through the SDK-compatible expo-gl surface. It is entirely procedural — a custom water shader with a 512-pixel planar reflection, terrain ridges, instanced wooded banks and shoreline stones, a boathouse, buoys, and detailed sculls with rowers, oars and wakes. Nothing was downloaded.
Two cameras look at the same world. Overview goes overhead; Expand opens the course fullscreen with the race telemetry over it. Both boats share one world-space meter scale, so what you see on the water and the numeric gap in the readout are the same fact told twice.
Rendering targets 30fps with reflections on alternate frames. Scene resources and render targets are disposed on teardown, unfocused views stop drawing entirely, and an initialization or render exception shows a retry state without ever touching the recorder — the race data survives the graphics failing.
A Ghost Is Not Always a Row
Four things can be an opponent, and the app keeps them distinct rather than flattening them into one leaderboard.
A simulation is a repeatable pacing profile with a start, surges, fatigue and a final sprint. A recorded PM5 row is a real trace. A goal is a target you set. A historical finish time — a 2k you did years ago on a machine that is long gone — becomes a labelled constant-pace estimate, which is explicitly not a reconstruction of how that row actually went.
That distinction is enforced where it matters: simulation bests require complete, gap-free simulation recordings, and real PM5 personal-record selection excludes goals, historical times and anything simulated. A ghost you can race is not the same thing as a record you can claim.
Built Around Data That Arrives Late
The PM5 decoder handles a bounded, non-multiplexed subset of packets — status, pace, stroke power and the final summary — and the simulator encodes each of those separately rather than handing the app a tidy object. Simulation settings can deliberately delay or drop data, because that is what a radio does.
Everything downstream assumes it. The recorder holds monitor time and distance samples, ignores readings older than ones it already has, flags status gaps over 1.5 seconds, and waits for a summary packet before it will finalize. Ghost replay interpolates cumulative distance from elapsed time behind a 500ms presentation buffer, smoothing only confirmed samples — it never predicts your position, and a stale live gap is withheld rather than guessed.
Saving is explicit and happens at the finish. Library writes are serialized, failed saves retry, and a corrupt index blocks saving rather than being quietly erased.
Current State
This is a private prototype. It runs in a browser on a Windows laptop, has been raced end to end at both desktop and 390×844, and has the test evidence to match — packet fixtures, finishes at 500m, 2k and 10k, interpolation, pacing shape, gap handling, summary gating, PR exclusions, concurrent saves, persistence across remounts, write failures and corrupt-index preservation.
What it has not had is a device. There is no actual Bluetooth connection and no physical PM5 or iPhone runtime testing; the iOS export checks the JavaScript bundle, not signing, native storage, safe areas, GL compatibility, frame rate, battery or thermals. expo-gl needs a new native development build before any of that can be answered.
So the next three milestones are in order and the order is not negotiable: a native iPhone build with real on-device visual and storage checks; then the live PM5 adapter compared against the simulator traces it was written from; then accounts, immutable online ghost challenges, notifications and cross-device rematches. Leaderboards and matchmaking come after that, and detailed workout-file imports, Concept2 Logbook sync, crash-resumable recording, commissioned environments and public multiplayer come after that.
Milestones
First playable milestone
Five distances on Stillwater, a real-time 3D lake course, with ghost racing, private saves and rematches — all driven by the simulator rather than hardware.
Validation suite green
12 suites and 221 tests passing, Expo Doctor at 21 of 21 after an SDK 57 patch alignment, and a successful iOS JavaScript bundle export.
Raced end to end in the browser
A fullscreen 2k finished in 7:57.4, one second behind the starter ghost, saved privately and came back as a selectable opponent after a reload.






