# 3drace: research and implementation plan

Research date: 18 September 2026. Current implementation: **Stage 03, a shared 16-car room race**, at `/3drace/`, with up to eight human drivers. The detailed design below still includes future features and stronger guarantees than this prototype implements.

Stage 03 runs a continuous 16-car room race, with eight human driver slots, same-room spectators, authoritative 60 Hz simulation and 10 Hz complete checkpoints. Late joiners can take an available AI car; disconnects use AI cover; reconnects reclaim the same car. Foreground peers take over a missing or hidden host. Results include a shared 15-second countdown and AI victory lap before the next race.

The networking correction gives the host one authoritative simulation for both driving and watching. Non-host driving replays the entire collision world at a shared tick; spectators interpolate complete host snapshots. Rendering does not run collision resolution or mix future local cars with past opponents. Small free-flight errors retain a finite 100 ms visual fade; contacts and larger errors correct immediately. Non-host input scheduling uses six ticks plus measured round-trip time; host controls use the next actual tick. Exact spatial track queries reduce replay cost without changing physics. See [NETWORKING.md](NETWORKING.md) for source research, concrete implementation, regression tests and latency limitations. The more ambitious design below is planning material, not a claim that every feature is implemented.

Tests cover ten simulated devices with 150 ms one-way delay and two real browser sessions with added latency. A complete checkpoint is about 16 KiB (roughly 160 KiB/s per recipient at 10 Hz, before transport overhead). Recovery remains best-effort browser hosting, not quorum-backed consensus or durable results; complete room shutdown loses the race.

Stage 02 supplied the grid, countdown, lap races, AI passing lanes, contacts, results, six cameras and instance-batched cars. The 1.56 km circuit, steering, brake-to-reverse, tilted overview and shaped shadows carry forward from Stage 01. The car study remains at `/3drace/car.html`. Polished reflective paint and distinct two-colour liveries sit against flat-coloured, untextured scenery. Sound, gears, additional tracks and elevation remain pending.

## 1. Agreed direction

Stage 02 refinement: AI now plans braking against bend distance and completes a clean solo lap in about 36 seconds. Collision snapshots carry directional impact severity and persistent cosmetic damage. The renderer adds damped body/camera reactions, a bounded spark/chip pool, compressed noses, bent wings and dented sidepods. These effects respect pause and reduced-motion settings and do not impose damage-related handling penalties.

Build one RCWeb application, **`3drace`**, with a **Race / Watch** choice. Each racer drives and renders the game on their own phone, tablet, laptop, or desktop. A watching device can show the entire circuit or follow the action through trackside and onboard cameras. Devices share one race through the existing RCWeb WebSocket relay.

The user confirmed **up to eight human racers, with AI filling a 16-car grid**, and **current browsers with newer three.js**. Target WebGL 2. Use the screenshots for the visual direction: crisp, flat-coloured polygons, exposed wheels, strong silhouettes, saturated scenery, and readable arcade instruments.

The central recommendation is a **browser referee with local prediction and authoritative correction**. One capable, foreground browser simulates the official race. Every racing browser immediately predicts its own inputs; spectators interpolate a slightly delayed, coherent view of the official race. The referee role is independent of Race / Watch and can migrate to another device.

Success means responsive driving at the device's available display rate, one agreed lap count and finishing order, useful watching on any device, and reconnecting to the same car without restarting everybody's race. It does not mean eliminating the consequences of network delay: sudden remote steering and collisions cannot be known before their messages arrive.

### Planning assumptions

- Interpret the requested 150 ms delay conservatively as **one-way browser-to-browser delivery through RCWeb**, including both relay legs. That is approximately 300 ms for a request/reply. Also test 150 ms RTT and a harsher case of 150 ms per relay leg, or 300 ms end to end.
- One human driver per device/tab. Other tabs may watch. Local split-screen and a separate controller-only app are outside the requested scope.
- Eight humans is a limit; 16 total cars is the default field. A lobby can reduce AI numbers for practice. Additional watchers do not consume racing slots. Validate eight racers plus eight watchers initially; larger audiences require load measurements.
- New participants can join an active race by taking over the lowest-placed available AI car, preserving position, laps and damage. Reserved human cars and finished cars are excluded. Returning participants reclaim their existing cars.
- A disconnected driver's car continues under AI. A missing referee causes a short, visible recovery pause.
- First release is for cooperative/trusted RCWeb rooms. The existing relay executes room-supplied JavaScript and cannot provide competitive anti-cheat or durable server authority.
- No Java, Maven, server configuration, or shared communications changes are needed for this design. Any later finding that requires them is a separate scope decision.

## 2. What to carry forward from Virtua Racing

### Arcade foundations, using Wikipedia as requested

Wikipedia is the main gameplay/history reference for this plan. Its account identifies the 1992 Sega AM2 Formula One racer and three original circuits: **Big Forest** (beginner), **Bay Bridge** (intermediate), and **Acropolis** (expert), distinguished by an amusement park, bridge, and tight hairpin. It describes transmission selection, four selectable driving views, eight-player cabinet linking, and the optional **Live Monitor**, which covered players through a virtual sportscast featuring Virt McPolygon. These directly inform the proposed track progression and Watch mode. [Wikipedia: Virtua Racing](https://en.wikipedia.org/wiki/Virtua_Racing)

Keep later additions separate: Wikipedia describes extra cars/tracks in home ports and online play, split-screen, and downloadable replays in the Switch version. Those are optional modernization ideas, not requirements inherited from the original arcade game. [Wikipedia: home console versions](https://en.wikipedia.org/wiki/Virtua_Racing#Home_console_versions)

For details the article does not establish, avoid inventing precision. The **16-car grid** is the user's confirmed scope and is also visible in the supplied references. Four laps by default, a configurable five-lap race, timer generosity, gear ratios, and AI settings are `3drace` design choices. Exact cabinet configuration is not a research dependency.

Two useful supplementary checks already made: an arcade record describes checkpoint timing and automatic/manual gears, and Sega's own history describes tyre wear, pit work, and a 20-lap option. Keep these as optional depth rather than expanding the first release around operator settings. [Arcade record](https://adb.arcadeitalia.net/dettaglio_mame.php?game_name=vr&search_id=), [Sega arcade history](https://www.sega.jp/history/arcade/product/7792/)

### Feature decisions

| Feature | Proposed treatment in 3drace |
| --- | --- |
| Formula-style open-wheel car | One equal-performance car type; colour, number, and driver name distinguish entrants. |
| Three difficulty circuits | Original circuits inspired by the beginner forest, intermediate bridge/coast, and expert mountain themes. Build one complete circuit first. |
| Short arcade races | Four laps by default; position, lap, best lap, speed, gear, revs, and course map. |
| Checkpoint countdown | An **Arcade Challenge** preset with per-car time extensions. The default multiplayer **Circuit Race** lets everyone finish without a checkpoint elimination timer. |
| Automatic/manual gears | Automatic default; seven forward gears with sequential up/down controls as a proposed tuning choice. |
| Four instant driving views | Cockpit, close chase, distant chase, and high follow. Full-circuit Watch is an additional, distinct camera. |
| Linked racing | Eight humans on independent devices, AI filling vacant grid slots. |
| Live Monitor | Full Watch experience with overview, selected-car cameras, trackside positions, and automatic race coverage. |
| Pit crew, tyre wear | Include in the later 20-lap endurance milestone; avoid making pit strategy mandatory for the first short race. |
| Attract sequence | AI demonstration using the same simulation and Watch cameras while the lobby is empty. |
| Lap records/results | Show best lap, race time, finishing order, and whether AI assisted a participant. Room results and local personal records initially. |
| Replays | A later, explicitly modern extension; live camera selection is required in the first complete release. |

The three screenshots establish the desired **style**, not asset or code requirements. Recreate the faceted car shapes, bright green verges, blue water/sky, polygonal trees, geometric grandstands, and orange bridge steelwork using newly authored geometry and original circuit layouts. The second reference's broadcast overlay is useful composition inspiration; its commentator character is not a required asset.

## 3. Player experience and race rules

### Entry and lobby

At `/3drace/?r=<room>`, show two large choices: **Race** and **Watch**. The URL may preselect a mode with `&mode=race` or `&mode=watch`; mode changes remain inside the same app and connection. A shared display launched from the catalogue initially shows the overview/lobby and the same two choices.

Race opens a driver card, input check, car colour/number, automatic/manual choice, Ready button, and grid. Use RCWeb's `rcwebName`, normalized to 20 characters. Driver identity is separate from the name and transport client ID. Watch opens the full-track view immediately and a selectable list of human and AI cars.

The lobby organizer selects the race preset and AI difficulty. Racers vote on tracks; use a visible deterministic tie-break, first the easier course and then its stable ID. Watchers do not vote. The organizer is a user-facing role separate from the technical referee and transfers to a connected racer when necessary.

Freeze track, physics version, race settings, roster, and random seed before the start. Each racing client acknowledges matching content and readiness. Schedule the start at least three seconds ahead on the synchronized race clock. A device that misses readiness becomes a watcher; it must not hold everyone else indefinitely. It can explicitly choose Race to take over an eligible AI car through the referee.

### Racing

- Proper acceleration, braking, steering, racing lines, run-off areas, speed loss on grass, and recoverable wall contact. No compulsory auto-steer or nitro.
- Grid positions assigned before countdown. Practice/first race uses a seeded shuffle; rematches may use previous finishing order if selected in the lobby.
- Position comes from completed laps, ordered track gates, and distance along the current route segment. Euclidean proximity to the finish line is insufficient.
- Gates are direction-sensitive. Crossings use the swept motion between ticks. Wrong-way driving, shortcuts, reverse finish crossings, and bridge overlaps cannot award laps or time extensions.
- The referee resolves a finish at the fractional crossing time within its physics tick, not message arrival time. Equal crossings within the chosen timing precision share a place; display ordering may use car number without inventing a win.
- After the winner finishes, give the remaining field a visible 45-second finish window. Cars still racing then receive DNF with progress shown. Finished cars coast safely under AI; spectators can keep following them.
- Reset-to-track is an authoritative action: require a stuck/off-track condition, place the car behind its last valid progress point, apply a time/speed penalty, and use a brief, visible collision-protection period. Reset never advances a gate.
- Opening a menu does not pause a multiplayer race. Let the driver explicitly hand over to AI. An empty solo session can pause locally; referee recovery is the only automatic room-wide pause.
- In Arcade Challenge, each car's timer is authoritative. Time expiry retires only that entrant and offers Watch; it does not stop the other racers.

Race phases: `LOBBY -> LOADING -> COUNTDOWN -> RACING -> FINISHING -> RESULTS`. `RECOVERING` temporarily suspends an active phase and resumes it from a shared checkpoint. Every transition carries an epoch, revision, and effective tick.

## 4. Cameras and Watch mode

Camera choices are local UI state. Choosing a car or viewpoint never changes another user's camera, their inputs, or the referee.

| Camera | Required behavior |
| --- | --- |
| **Entire track** | Default Watch view: a fixed orthographic overhead camera fits the whole circuit, pit lane, and run-off bounds, including safe UI margins. All 16 cars have readable numbered markers. Never auto-zoom onto the leader. |
| Overview with detail | Optional small selected-car panel on capable devices. The full-track overview remains intact. Turn the second render pass off first when reducing quality. |
| Selected car | Cockpit, nose, close chase, distant chase, high rear, and left/right track-relative views. Select any human, AI, or temporarily AI-controlled car. |
| Trackside | Authored locations at the start/finish gantry, first corner, hairpin, bridge, grandstand, and pit entrance. Fixed position with a restrained pan/zoom towards the selected car. |
| Broadcast | A deterministic local director chooses nearby battles, overtakes, pit activity, and finishes. Minimum shot length around 5 seconds; manual selection disables automatic cuts. |
| Inspection | Watch-only orbit/pan with Reset to entire track. Keep this separate from the guaranteed full-track overview. |

For an orthographic overview with available aspect ratio `a`, track width `w`, and depth `d`, choose `halfHeight = 0.5 * max(d, w / a) * margin` and `halfWidth = halfHeight * a`. Compute this from the viewport left after panels, not the entire browser window. Include track elevation in clipping planes. Recalculate on resize/orientation change without changing the selected camera.

Trees, tunnel roofs, bridge trusses, and grandstands must not hide racing positions in the overview. Use simplified overview scenery, roof visibility rules, and always-visible markers. Keep upper/lower route segments distinct at crossings. Side/onboard cameras need obstruction checks and a safe fallback angle; do not let them pass through walls or terrain.

Watch HUD: standings, selected driver, human/AI status, lap, speed, best lap, sector gaps, connection state, and camera label. Update all of these from the **same delayed playback tick** as the cars. Schedule lap/finish captions at their event tick so the screen does not announce an overtake before showing it. Switching camera does not clear the playback buffer.

## 5. Rendering, track representation, and driving physics

### three.js and compatibility

Pin **three.js r186**, verified as the published latest release during this research, and vendor its matching `three.module.min.js`, `three.core.min.js`, and license inside the app's immutable assets. Verify the complete import chain during implementation; do not mix revisions or depend on an unversioned CDN. The repository already contains r146/r160 classic builds and r183 WebGPU/core files, but those are not the proposed renderer. [three.js r186 release](https://github.com/mrdoob/three.js/releases/tag/r186)

Use **WebGLRenderer / WebGL 2**, which is sufficient for this visual style. Current WebGLRenderer no longer supports WebGL 1. Show a clear capability failure rather than an empty canvas on an unsupported device. Modern-browser support was explicitly selected by the user; old signage devices are not an acceptance target. [three.js WebGLRenderer documentation](https://threejs.org/docs/pages/WebGLRenderer.html)

Keep first-party simulation/network/input code in the repository's ES5 style and conservative CSS. A minimal module bootstrap imports the modern three.js library and calls the existing global app initializer; the module-loading exception is limited to the chosen modern renderer. Expose all RCWeb callbacks explicitly as `window.race3d` (a JavaScript identifier cannot begin with `3`). Register connection hooks before `rc.connect()`, and do not let renderer loading race initialization.

### Flat polygon art

- Flat normals and solid material/vertex colours; `MeshLambertMaterial` with `flatShading` for faceted lighting and `MeshBasicMaterial` where lighting is unnecessary. No texture maps on world surfaces. [Material documentation](https://threejs.org/docs/pages/MeshLambertMaterial.html)
- Geometry for lane markings, curbs, tyre skid ribbons, car liveries, sponsor-like original signage, and low-sided tyres. Use ordinary DOM text for instruments and names.
- One directional light and restrained ambient fill. Simple dark ground polygons under cars replace dynamic shadow maps. No bloom, screen-space effects, physical reflections, or translucent layered foliage in the baseline.
- Flat sky colour; optional distant polygon clouds and mountains. Flat coloured water, not reflections. Camera motion, scale, roadside rhythm, and sound should communicate speed.
- Instance repeated scenery and merge static track pieces by material/region. Bound temporary debris and skid geometry. [InstancedMesh documentation](https://threejs.org/docs/pages/InstancedMesh.html)
- Begin with budgets of approximately 150,000 visible triangles and 120 draw calls in the mobile overview, including 16 cars. These are prototype budgets to measure, not claims of achieved performance.

### One track model for everything

Author each circuit as an ordered road graph with centreline samples, cumulative arc distance, width, elevation, banking, surface type, wall edges, ordered race gates, pit branches, and camera anchors. Generate the visible mesh, collision surfaces, overview map, progress calculation, and AI path from this shared data.

Store a baked sampled representation with the app, versioned and hashed. This avoids deriving different road geometry from runtime spline approximations on different devices. Track height/segment identity prevents cars on a bridge colliding with cars underneath. Use nearby segment lookup and a spatial grid rather than raycasting the full decorative scene every physics step.

Proposed circuits:

1. **Pine Circuit:** wide beginner track, long straight, flowing corners, amusement-park landmark, short orange truss bridge. First complete implementation and latency test circuit.
2. **Harbour Link:** coast, elevated road, tighter turns, bridge, and lower route crossing. Tests layered collision and camera visibility.
3. **Ridge Run:** elevation changes, mountain hairpin, narrow technical section, rocky verges, and demanding braking zones.

Names and dimensions are design proposals. Tune the first track to roughly 45-65 seconds per lap before committing the other layouts.

### Physics

Use a small custom arcade vehicle model, not a new general-purpose physics dependency. Cars move freely across a continuous 3D road surface; the simulation uses longitudinal/lateral velocity, heading, yaw rate, steering response, grip, and road height/banking. This is more economical and predictable than full suspension and tyre simulation while retaining real cornering and overtaking.

The fixed simulation step is **1/60 second**, independent of rendering. Interpolate between adjacent predicted poses for higher-refresh presentation; account for this roughly one-tick presentation offset in input-latency measurements. Acceleration curves, braking, lateral grip limits, off-road friction, and gear changes use that timestep. All variants share identical physics parameters. Steering response depends on speed; keyboard input ramps towards its target instead of instantly applying maximum lock. Internal units are metres, seconds, and radians; km/h is a display conversion.

Maintain complete replayable state: position, route segment, heading, velocities, yaw rate, steering filter, gear/shift timer, grip/tyres when enabled, contact/reset state, race gates/laps, timers, and AI memory. Equal car IDs and a stable contact-pair order prevent avoidable simulation differences. A seeded PRNG supplies all race-relevant randomness; decoration never consumes its sequence.

Use swept car capsules or simplified oriented rectangles with elevation filtering. At 300 km/h, a car travels about 1.39 metres per 60 Hz tick, so checking overlap only at tick endpoints is unsafe. Resolve modest arcade impulses and speed loss, not uncontrolled rigid-body pileups. Cosmetic debris has no racing collision.

Do not claim perfect cross-browser floating-point determinism. Regular authoritative state replaces drift. Physics tests run the same recorded inputs across engines and examine bounded differences. [Fixed timestep rationale](https://gafferongames.com/post/fix_your_timestep/)

## 6. Network architecture

### What RCWeb supplies, and what the app must supply

Repository inspection confirms that `comms.js` opens one WebSocket, serializes `rc.sendFunctionCall`, evaluates received JavaScript, and retries disconnections. It queues up to 200 sends while disconnected, then flushes after `onConnected`. `rc.onUpdateClients` receives a list of transport client IDs; it is not an app roster or a race authority service. The server relays messages; it does not validate race physics or retain a recoverable race snapshot.

Use a small global API, for example `race3d.receive(envelope)`, and route all racing messages through an app adapter. Call `rc.sendFunctionCall(refereeClientId, 'race3d.receive', message)` for racer intents. Use a comma-separated subscriber list for race snapshots, or the `3drace` app selector when all clients share a rate. Handle the referee's local input directly through the same validation path; do not depend on receiving a self-echo.

```mermaid
flowchart LR
  A["Racing device\ninputs + immediate prediction"] -->|"timestamped inputs"| R["RCWeb room relay"]
  R --> H["Referee browser\n60 Hz official physics + AI"]
  H -->|"20 Hz authoritative snapshots"| R
  R --> B["Racers\nrestore + replay + visual correction"]
  R --> C["Watchers\nbuffer + interpolate + local cameras"]
  R --> D["Backup peers\ncomplete recovery checkpoints"]
```

The referee is one of these browsers, not another application or a backend service. Prefer a stable foreground Watch device with adequate simulation performance, but never require a spectator to start a race. A racing referee uses the same prediction timeline and input deadline as everyone else.

### Choose delayed authoritative simulation, not lockstep or unrestricted rollback

Pure lockstep would wait for the slowest player. Fully independent client-owned cars would make contact and finishing order ambiguous. Rewinding the entire race every time an old input arrives introduces substantial collision/event complexity.

Instead, keep the referee's official simulation **slightly behind the synchronized live race clock**, giving most inputs time to arrive. Racing clients predict ahead to the live tick. Once an official tick closes, it is not rewritten for a late input. This makes race events stable while retaining local response. Prediction plus acknowledged state correction follows established client/server techniques; the shared delayed tick schedule below is a proposed adaptation for RCWeb. [Prediction and reconciliation](https://gabrielgambetta.com/client-side-prediction-server-reconciliation.html)

| Component | Initial setting |
| --- | --- |
| Official and predicted physics | 60 Hz |
| Rendering | `requestAnimationFrame`, at available display rate; no network wait |
| Input samples | One per simulation tick |
| Input messages | 30 Hz, normally batching two samples; recent bounded history for gaps |
| Official motion snapshots | 20 Hz, all 16 cars |
| Presence/heartbeat | 2 Hz; race snapshots also prove referee liveness |
| Complete recovery checkpoint | 2 Hz, plus race transitions |
| Race input history | At least 2 seconds in a bounded ring buffer |
| Authority delay B | Start near 250 ms for the conservative baseline; tune before each race |
| Spectator interpolation cushion J | Initially 100 ms; adapt within approximately 75-200 ms |
| Maximum prediction from a state | Initially 600 ms; beyond it show degraded/recovery state |

All numbers are **engineering starting points**, not measured results.

### Clock and input scheduling

Use `performance.now()` for elapsed time. Exchange four timestamps with the referee to estimate round trip and clock offset, reject outliers, and slowly slew the offset. Wall-clock timestamps and `Date.now()` must not decide physics progression or elections. One-way estimates inferred from RTT assume some symmetry; include a margin and test asymmetric paths.

Choose B from the worst admitted racer's observed upper-tail delivery estimate, plus input batching and clock/jitter margin; round up to physics ticks. For example, 150 ms one-way + 30 ms jitter + 33 ms batch wait + 17 ms margin gives approximately 230 ms, rounded conservatively to 250 ms. Use the same B for every racer and lock it for that race. A severely delayed participant enters degraded/AI state rather than continuously stretching everybody's timeline.

Let `L` be the synchronized live tick. The referee finalizes through `H = L - B_ticks`. Each input sample names its intended tick, connection generation, and monotonically increasing sequence. Hold early input until its tick; reject absurdly future input. The referee processes each tick once using the appropriate sample or its documented missing-input policy.

Late inputs for closed ticks are explicitly rejected and counted. Do not replay elapsed acceleration again or secretly backdate it. The newest still-valid held control state may be scheduled for the next open tick, with that effective tick returned to the sender. Edge actions such as shift/reset have IDs and are either scheduled once or explicitly rejected. Repeated copies cannot create extra shifts.

The referee returns `finalizedThroughTick`, per-driver processed sequence/dispositions, and effective held controls. A received sequence is not automatically proof that the corresponding input was simulated. In particular, replay is tick-based: restore at H and replay only samples/actions effective after H; do not blindly append every unacknowledged packet.

### Worked latency example

At live race time 10.000 seconds, a driver turns. Their own simulation uses it at the next 60 Hz step. A batched input reaches the referee around 10.150-10.183. With B = 250 ms, the referee simulates that original 10.000 tick at wall time 10.250. Its confirming snapshot arrives around 10.400, plus up to one 50 ms snapshot interval.

The client has already been driving locally. It restores the snapshot's state for 10.000 and replays its saved inputs to its current tick, roughly 10.400-10.450. This is approximately **400-450 ms of speculative state**, despite only 150 ms of one-way transport delay. A spectator adds J and normally watches about **500-550 ms behind the live clock**. These delays must not be mislabeled as merely a 100 ms buffer.

At 300 km/h, 150 ms covers 12.5 metres. Unpredictable remote braking or contact therefore needs correction; increasing FPS cannot eliminate that uncertainty.

### Racer prediction and correction

Every racer maintains a lightweight speculative copy of the race, using its known inputs, the latest official remote controls, track constraints, and shared vehicle rules. Replay all interacting vehicles to the **same target tick**, including AI state, so a current local car is not colliding with remote cars displayed hundreds of milliseconds in the past. The first implementation sends inputs only to the referee; additional peer input broadcasts are unnecessary until measurements justify them.

On a valid snapshot:

1. Validate race/content version, referee epoch, state tick, sequence, and bounded numeric fields.
2. Restore authoritative physics and race state at the snapshot tick. Include velocities and hidden control/AI state, not just position and heading.
3. Remove processed/rejected history through that tick, respecting action dispositions. Replay newer local inputs and predicted remote inputs up to the current live tick.
4. Correct simulation immediately. Apply smoothing only to the rendered car/camera offset, never gradually blend the actual collision world.
5. Reduce small visual offsets over roughly 100-150 ms. Larger offsets require quicker convergence; do not animate a car through a wall. Reset, teleport, large error (initially >3 m or >20 degrees), or changed road level uses a clear short camera transition and immediate valid pose.

These thresholds need driving tests. Expose correction count, size, and age in diagnostics. Smoothness is a target, not a reason to hide diverging physics. State synchronization literature specifically separates physical correction from presentation smoothing. [State synchronization and visual correction](https://gafferongames.com/post/state_synchronization/)

Car-to-car contact is the main risk. Predict modest contact for immediate feel, but the referee alone commits collision outcomes, penalties, resets, lap completion, and finishes. Deduplicate contact effects by event ID so replay does not repeat bangs or particles. Keep impulses bounded. Offer an explicit lobby **Non-contact race** option for very poor connections; never silently switch collision rules mid-race.

All racing cars use the same temporal policy, including distant cars. Save visual work with lower-detail meshes rather than switching remote vehicles between incompatible time offsets. Under sustained stale state, stop competitive prediction beyond the bound, label the connection problem, and let the referee's AI substitute continue; do not pretend a several-second guess is live.

### Spectator interpolation

Watch clients need not run predictive physics. Buffer snapshots and render a playback tick for which two surrounding states normally exist. Interpolate position using velocities where useful, and interpolate heading along the shortest rotation. Fall back to linear interpolation around collisions, resets, or pit transitions to avoid spline overshoot. Never interpolate across a teleport or authority epoch.

Measure snapshot age relative to the referee clock to adapt the cushion, including WebSocket stall bursts. If the buffer runs dry, allow only a short bounded continuation, then show a stalled feed. Resume from coherent buffered state. [Snapshot interpolation](https://gafferongames.com/post/snapshot_interpolation/)

This source discusses UDP. RCWeb uses WebSockets over TCP, so its packet-loss recommendations are not directly transferable: a lost transport segment normally stalls subsequent delivery rather than simply dropping a snapshot.

### WebSocket congestion and messages

The browser WebSocket API offers no automatic application backpressure. `bufferedAmount` reports this browser's queued outgoing bytes, not the RCWeb server's queues or another device's reception quality. Combine it with receiver acknowledgements, snapshot age, RTT, and skipped-send counters. [WebSocket API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket), [bufferedAmount](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/bufferedAmount)

- Send motion only when `rc.connected` and the socket is OPEN. Keep a bounded app-owned pending state. Do not deliberately fill RCWeb's reconnect queue with old steering or snapshots.
- If a close races a send, envelope generations and age checks still reject its eventual stale delivery. `onConnected()` starts a fresh handshake; it does not mean the driver can immediately control a car.
- Coalesce unsent snapshots to the newest one. Keep a bounded recent input window; do not resend seconds of obsolete steering after reconnect.
- Begin throttling around 32 KiB local queued output and treat a queue that stays above 128 KiB or excessively old snapshots as degraded. Tune from measured serialized sizes and delay, not these numbers alone.
- Already-sent TCP data cannot be recalled. If a stream remains badly backed up, visibly resynchronize/reconnect under a cooldown rather than feeding an endless queue. Race state survives via its normal reconnect protocol.
- Slow watchers receive a lower motion rate or are unsubscribed until they resync. Verify that a slow receiver does not stall delivery to other room members: the server's direct-send path makes this a necessary feasibility test.
- Separate replaceable motion from critical events. Events have IDs, acknowledgements, bounded retries, and snapshot representation. Protocol retries are for disconnect/recovery semantics, not an attempt to duplicate TCP reliability.

Start with complete compact JSON motion snapshots; do not introduce binary transport or fragile delta baselines before measurement. A planning estimate of **3 KiB x 20/s = 60 KiB/s** is the referee's snapshot upload and each recipient's download. Sixteen recipients would mean about **960 KiB/s relay egress**, before protocol overhead and other messages. Budget up to **20 KiB x 2/s = 40 KiB/s** extra for complete recovery checkpoints to eligible backup peers. Input batches should initially fit around 0.3-0.6 KiB x 30/s per racer, or 9-18 KiB/s. Verify real UTF-8 sizes; JSON numbers are not packed binary floats. No unlimited-spectator claim is justified by this estimate.

### Message contract

Every envelope includes `protocolVersion`, `roomRaceId`, `authorityEpoch`, `senderClientId`, `connectionGeneration`, message type, and message sequence. Race state additionally identifies track/physics content hashes and tick. Epoch is an ordered term plus elected referee ID, not a client wall-clock time.

| Message | Purpose |
| --- | --- |
| `HELLO / WELCOME` | Capabilities, role, stable participant ID, reclaim token, selected referee, roster, and full state. |
| `PING / PONG` | Clock estimate, RTT, and liveness. |
| `READY / CONFIG / START` | Version agreement, committed settings, and future start tick. |
| `INPUT_BATCH` | Driver session, sample ticks/sequences, steer/throttle/brake, and deduplicated edge actions. |
| `SNAPSHOT` | Full motion state, authoritative tick, applied controls, input dispositions, lap/position status, and event watermark. |
| `EVENT / ACK` | Joining, ownership handover, reset, retirement, finish, and configuration events. |
| `CHECKPOINT / CHECKPOINT_ACK` | Complete restorable world, input cursors, AI memory, PRNG, roster, event ledger, and authority metadata. |
| `RECOVER / PROPOSE / ADOPT / RESUME` | Referee failure, candidate evidence, chosen checkpoint/epoch, and synchronized restart. |
| `RESYNC / REJECT` | Missing history, invalid input, incompatible version, or explicit stale-generation rejection. |

Validate lengths, enumerations, finite numeric values, array counts, and tick windows. Record every rejection in bounded counters/logs with a surfaced reason where relevant; do not flood the player with one toast per stale packet. Render player text with `textContent`. Claimed sender IDs and tokens protect ordinary session handling, not against a hostile participant able to execute room JavaScript.

## 7. Disconnects, returning drivers, and referee migration

### Driver ownership

Separate a stable participant ID, the race car ID, the current `rc.client`, and a connection-generation number. Store the participant/reclaim identity in session storage for reloads; maintain car ownership at the referee. Shared local storage is appropriate for the name, not an unchecked single driver ID used by every tab.

A duplicated tab may copy session storage. The referee must grant only one active control lease: if the existing lease is live, the duplicate initially watches; it cannot steal or clone the car. A cross-device continuation needs an explicit transfer mechanism later and is not implied by reload recovery.

| Condition | Planned behavior |
| --- | --- |
| Brief input gap | At the official deadline, hold the previous controls briefly, then decay steering and release throttle. Initial maximum hold: 150 ms of missing simulation samples. Report late-input counts. |
| Driver absent/stale for about 500 ms | Referee schedules AI takeover at a specified tick. Mark the car AI-assisted. Other racers continue. |
| Browser blur, hidden tab, sleep, gamepad loss | Clear local held inputs immediately. Request AI handover if communication is available; absence detection handles suspended browsers. |
| Driver reconnects | HELLO with reclaim identity; receive full state and a new generation; synchronize clock; send neutral input/Ready; reclaim only at an acknowledged future tick. |
| Rejoin handshake incomplete | Remain watching the car under AI. Never accept old input from a previous generation. |
| Driver chooses Watch | Schedule the same AI handover. Returning to Race reclaims that car, not a fresh car with reset progress. |
| Entire app session lost | Race progress survives only in retained peer state or a saved recovery checkpoint; show a recovery choice instead of inventing continuity. |

Reserve an absent participant's car for the rest of that race. AI covers the absence; new entrants can take over only unreserved AI cars, subject to the eight-driver limit. A returning driver keeps the car's current position, including gains or losses made under AI. Flag the result as AI-assisted and exclude AI-driven laps from personal unassisted lap records. Never roll back the rest of the field to the returning driver's old local position.

### Referee selection and migration

Use a sticky referee, selected from healthy eligible peers by deterministic priority: foreground availability, demonstrated ability to run the simulation, a preference for a Watch device, then stable ID. Do not choose by raw wall-clock creation time or constantly replace the host when FPS fluctuates. Keep at least one acknowledged backup when another eligible device exists.

The browser's visibility state matters: background tabs commonly throttle timers and stop animation callbacks. A worker is not a guarantee that a suspended phone can keep hosting. Request voluntary handoff before hiding where possible; otherwise timeout recovery applies. [Page Visibility API](https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API)

Migration sequence:

1. The referee sends complete checkpoints every 500 ms and at key transitions. Backups acknowledge `(epoch, checkpointSeq, tick, hash)` and keep a short ring. Critical finish/phase events are mirrored before they are presented as committed results when a backup is available.
2. When official updates are stale for approximately two seconds, reachable peers enter `RECOVERING`. Freeze competitive progress and display why. Predicted laps/finishes are not commits.
3. Peers exchange the last referee-issued checkpoints and their retained event/ack watermarks. Prefer the designated healthy backup; fallback candidates use the same deterministic ordering. Choose the newest complete checkpoint with matching integrity/version evidence. Never merge independent car coordinates or trust the farthest-ahead speculative tick.
4. Agree a new authority epoch and adopted checkpoint among the reachable session membership before resuming. Reset clock mapping and input generations. Broadcast a full baseline and a future resume time. The old referee, if it returns, joins as a follower.
5. Restore timers, AI seed/state, car ownership, laps, and event deduplication along with physics. Replay only preserved official events after the checkpoint when their complete ordered prerequisites are present. Otherwise report the amount of rollback and any affected provisional event.

**Guarantee boundary:** deterministic tie-breaking alone is not a consensus protocol. RCWeb's membership list has no versioned authority lease or durable commit service. Normal tab closure and reconnect can be handled; arbitrary partitions or inconsistent membership cannot safely promise both uninterrupted progress and one irreversible result. If competing authorities/committed results appear, remain paused, converge on agreed evidence, or mark the race interrupted and offer a restart. Do not silently pick a winner by the newest timestamp. A dedicated authoritative backend would be the upgrade path if partition-safe competitive results become a requirement.

A browser checkpoint can be saved periodically in bounded browser storage, with visible feedback on failure. On total room shutdown, the race stops. A later saved-state restore is an explicit paused continuation, not a claim the race kept running while every device was offline.

## 8. Computer opponents

Only the referee decides official AI behavior. Racing clients may predict AI using the same saved state; watchers display snapshots. Avoid separate unseeded AI decision-making on each device.

Use a precomputed racing line and curvature-based speed profile. Lookahead grows with speed. Compute a safe corner speed from curvature/grip, then make a backward braking pass so the AI brakes before the bend rather than when it reaches it. Feed AI steering, throttle, brake, and shifts into the same vehicle model as human controls.

Run higher-level decisions around 10 Hz and physics at 60 Hz. Inspect nearby cars and occupied route space; choose a passing side, retain it with hysteresis, leave room at merges, and abandon an unsafe move. Seed modest variation in preferred speed, braking margin, and reaction delay. Difficulty changes those parameters, not hidden teleportation or extra top speed.

AI takeover should first stabilize the current car and follow a safe line, then race at the agreed substitution difficulty. Replicate its chosen passing side, target speed, recovery timers, and PRNG state so takeover and host migration do not cause an abrupt new driving policy. For endurance, add pit-entry intent, lane occupancy, service timer, tyre state, and safe pit exit.

## 9. Keyboard, touch, and game controllers

All devices produce the same normalized input: `steer [-1,1]`, `throttle [0,1]`, `brake [0,1]`, plus edge actions. Use explicit input ownership/last active device and neutral handoff; a held keyboard key and a gamepad must not fight each other.

| Action | Keyboard default | Touch default | Standard gamepad default |
| --- | --- | --- | --- |
| Steer | A/D or left/right | Left-thumb analogue steering pad | Left stick X |
| Throttle | W or up | Large right-side accelerator | Right trigger |
| Brake | S or down | Separate large brake target | Left trigger |
| Gear up/down | E/Q | + / - when manual is selected | Right/left shoulder |
| Change view | C; 1-4 direct views | View button | Y / upper face button |
| Look behind | Hold R | Hold rear-view button | B / right face button |
| Menu | Escape | Menu button | Menu/Start |
| Watch target | Left/right when watching | Car list / previous-next | D-pad left/right |

Use at least 48 CSS-pixel touch targets, independent pointer IDs, pointer capture, and release on `pointercancel`, lost capture, blur, and visibility changes. Support simultaneous throttle/brake/steering touches. Brake wins if both pedals are fully pressed. Prevent scrolling only over the driving controls. Landscape is preferred, with a usable portrait layout; do not require orientation lock. Optional auto-accelerate is a clearly selected assist, not the only control mode.

Poll `navigator.getGamepads()` every rendered frame after user interaction. Use standard mappings when reported, configurable deadzone around 0.1, analogue trigger values, and a small remapping/calibration UI for nonstandard controllers. Re-read the current gamepad object rather than retaining the connect-event object. Do not promise every wheel or haptic feature. [Gamepad API guide](https://developer.mozilla.org/en-US/docs/Web/API/Gamepad_API/Using_the_Gamepad_API)

Gamepad access requires a secure context in current browsers. `http://localhost:8080` on the same computer may qualify; a phone opening `http://192.168.x.x:8080` generally does not. Test through the existing HTTPS RCWeb deployment for multi-device controller support, and show a specific explanation if the API is unavailable. This plan does not change server/TLS configuration. [getGamepads requirements](https://developer.mozilla.org/en-US/docs/Web/API/Navigator/getGamepads)

Audio starts after a local gesture. Use locally generated engine/rev, tyre, impact, countdown, and checkpoint cues with a mute control. Remote cameras change the mix/panning, not race state. Limit simultaneous engines to the nearest few. Touch/keyboard support remains usable when gamepad/audio permission or hardware is absent.

## 10. RCWeb integration and proposed files

Keep all runtime callbacks in the single `race3d` namespace and all UI modes at `/3drace/`. There is no `3drace-c` app.

Proposed implementation layout, to create only when implementation begins:

```text
src/main/apps/app/3drace/
  PLAN.md                 this research/design record
  index.html              comms + QR library + app scripts + renderer bootstrap
  style.css               responsive racing/watch/lobby controls
  script.js               lifecycle, lobby, public race3d API, RCWeb hooks
  engine.js               pure fixed-step vehicle/race/contact logic
  tracks.js               baked road/collision/gate/camera data
  network.js              timing, prediction, snapshots, ownership, recovery
  ai.js                   racing-line and traffic decisions
  graphics.js             three.js scene, cameras, flat meshes and HUD binding
  input.js                keyboard, multitouch, gamepad normalization
  audio.js                bounded local racing sound
  appinfo.md              user instructions, limitations, controls and images
  *.test.cjs              behavioral simulation/protocol tests
  *.browser.test.cjs      RCWeb, input, layout and recovery tests
  [verified icon files and screenshots]

src/main/immutable/app/3drace/vendor/three-r186/
  three.module.min.js
  three.core.min.js
  LICENSE
```

Split files only at these concrete responsibilities; this is not a request for an engine framework. Retain the repository's existing Node test and browser-test conventions without adding runtime dependencies.

For catalogue launch, use the existing viewer-launch convention so shared displays open `3drace`. Once the referee is established, it brings `"*-c,c"` controllers into `/3drace/?r=<room>&mode=race`, initially at the lobby/choice rather than automatically claiming a car. Repeat at race start for late controllers. Guard the redirect so any device already in `3drace` keeps its selected Race/Watch mode and session. Never use a blanket `*` redirect from the app.

After `rc.onConnected()`, build the standard QR panel with `rc.buildQRCodePanel` linking to the same room's `3drace` entry, copy the canonical four panel classes from `v/style.css`, and preserve its host/room labels and 150x150 QR content. Race/Watch chooses the role after arrival. Provide **Back to apps** at `/v-c/?r=<room>#apps` in the current tab. Leaving a racing tab schedules AI handover; leaving the referee schedules migration when possible.

At implementation completion, register one app in `app-intro.html`, `app-list.html`, and `v-c/index.html`, preserving unrelated edits and correct category counts. Add actual icons before referencing them. `appinfo.md` must use renderer-supported headings/lists, not Markdown tables. Include the supported player limit, modern-browser requirement, secure-context gamepad note, AI substitution, and recovery behavior.

Relevant inspected repository references:

- `agents.md`, `src/main/apps/app/AGENTS.md`, and `src/main/resources/docs/llm-info.md`: authoritative project conventions.
- `src/main/resources/assets/core/comms.js`: socket lifecycle, queueing, serialization, and hooks.
- `src/main/java/net/targetr/rc/Room.java` and `client/ClientWebSocketHandler.java`: read-only verification of membership and relay behavior.
- `src/main/apps/app/racer/script.js`, `appinfo.md`, and tests: names, QR/return links, sessions, and test structure. Its single-driver model and wall-clock election are not sufficient for this multiplayer design.
- `src/main/apps/app/spacewar3d/index.html` and `script.js`: existing three.js/RCWeb integration.
- `src/main/apps/app/astra-craft/index.html` and `script.js`: same-app multi-device precedent; its modern syntax does not override the root rules for unrelated first-party code.
- `src/main/apps/app/v-c/index.html`, `v-c/script.js`, and `v/style.css`: launch behavior and canonical QR appearance.

## 11. Implementation milestones and acceptance gates

Do the network proof early, before investing in three detailed circuits. The most expensive uncertainty is enjoyable car-to-car interaction over delayed WebSockets.

| Milestone | Deliverable | Gate to proceed |
| --- | --- | --- |
| 1. Timing and relay proof | Headless 16-car model, two real RCWeb clients, input schedule, snapshots, and latency instrumentation. | Correct tick acknowledgements; bounded queues; slow watcher cannot stall racers; no duplicate laps under delay. |
| 2. One-track driving | Pine Circuit, faceted car, keyboard/touch/gamepad, four driving views, complete lap validation. | Same distance/timing across render rates; enjoyable braking/cornering; swept wall/contact checks. |
| 3. Multiplayer race | Eight human slots, AI grid fill, countdown/results, prediction/reconciliation, contact/non-contact presets. | Eight-client delayed-race tests; stable shared results and acceptable correction measurements. |
| 4. Complete Watch mode | Whole-track camera, per-car views, trackside shots, director, synchronized HUD. | Every car remains discoverable; complete circuit fits all target sizes; no camera changes sent to others. |
| 5. Recovery | Driver AI substitution, reclaim protocol, duplicate-tab handling, referee migration, saved paused restore. | Disconnect scenarios below pass without duplicate ownership or invented progress. |
| 6. Complete first release | Other two tracks, performance pass, audio, documentation, catalogue and real-device checks. | Full requirement matrix passes, including real `/v-c/` launch and HTTPS gamepads. |
| 7. Optional arcade depth | 20-lap endurance, tyre wear, pit strategy, later replay recording. | Adds no instability to the completed short-race mode; each feature gets its own race-rule tests. |

Milestones 1-6 together satisfy the requested first release. Milestone 7 is additional gameplay identified by the research, not a dependency for the requested live cameras or multiplayer.

### Network and recovery test matrix

Build a deterministic test transport with a virtual clock for app-level behavior. Separately run actual browser/RCWeb tests. Simulated independent message drops do not faithfully reproduce TCP loss: include FIFO stalls, delayed release bursts, reconnection, and asymmetric relay legs. Record the random seed and input trace for reproducibility.

- Baselines: no added delay; 75 ms each way (150 ms RTT); **150 ms each way**; 300 ms each way; asymmetry of 75/225 ms.
- Jitter: bounded +/-30 and +/-75 ms; occasional 300-1000 ms TCP-style stalls. Keep the single-connection ordering that WebSockets guarantee while allowing different senders to interleave.
- Recovery: racer disconnect for 0.5, 5, and 30 seconds; reload; duplicate tab; suspend phone; gamepad unplug while accelerating; visibility change while steering.
- Authority: close the referee while braking, during contact, at a checkpoint, in countdown, and at a photo finish. Disconnect referee and preferred backup together; return the old referee with stale queued messages.
- Membership: watcher becomes racer next heat; racer becomes watcher mid-race; new participant arrives during racing; incompatible track/physics version; storage unavailable.
- Race correctness: reverse crossing, skipped gate, shortcut, bridge overlap, delayed shift duplication, reset near finish, AI-assisted fastest lap, and simultaneous finish.
- Transport load: eight humans plus eight watchers; one artificially slow watcher; oversized/invalid messages; sustained output backlog; stale critical-event ACK; bounded ring buffers over a 20-minute soak.
- Total outage: RCWeb unavailable and all browsers disconnected. No fake continued official progress; restore only an agreed checkpoint as a visibly paused race.

### Measurable acceptance targets

These are targets to validate, not results obtained during planning:

- Local input affects the next simulation step and becomes visible within **two display frames** under normal load, including at the 150 ms one-way baseline.
- Physics remains at 60 Hz and lap results match at 30, 60, 90, 120, and 144 Hz rendering. No frame-delta-dependent acceleration or AI advantage.
- Designated midrange phone sustains approximately 60 fps at the selected adaptive resolution with 16 cars. On higher-refresh capable hardware, rendering follows available refresh rate. A 30 fps low-quality mode is explicit and does not alter physics.
- Referee simulation p95 stays below 4 ms/tick on the selected host device; rendering quality drops before tick correctness is sacrificed. Bound catch-up work; a long scheduling stall triggers recovery rather than a seconds-long catch-up loop or silent lost time.
- Under the baseline delay/jitter profile, a no-contact lap aims for p95 correction under 0.5 m and no routine >3 m snaps. Measure collision cases separately. If these fail, tune the input window and model before adding artwork or claiming smooth multiplayer.
- All devices converge to identical **official** laps, event ledger, and finish results. Compare state at equal authoritative ticks, not local wall-clock screenshots of different playback times.
- A returning driver owns exactly one car; obsolete generations never regain control. A normal referee closure aims to resume within **five seconds**, with any restored interval disclosed. Partition conflicts remain paused rather than violating this rule to hit the target.
- Watch overview contains the entire drivable circuit and all car markers at 1920x1080, 960x540, 1080x1920, and 450x960. Also check common phone widths around 360-430 CSS pixels and landscape touch layout.
- Real launch from `/v-c/` moves existing controllers to the same app/room, preserves already joined `3drace` sessions, and leaves other viewers in place during controller handoff. QR and Back to apps links are tested by navigation.
- Real Chrome/Edge, Firefox, Android Chrome, and iOS/iPadOS Safari checks cover WebGL 2, touch cancellation, context loss/restore, and controller behavior through HTTPS where required.
- No silent late-input loss, rollback, invalid state, or storage failure: diagnostics count every rejection/skip and the UI explains consequential interruptions.

### Research and planning limits

This document verifies source material and repository contracts; it does not prove performance, collision quality, device support, or recovery correctness. Those require the milestones and tests above. Exact game feel, numerical vehicle parameters, circuit lengths, and final network tuning remain prototype work. Saved replay, durable global leaderboards, and a dedicated backend are not part of the initial implementation scope.
