Playing
Racing
A top-down arcade kart racer with a fixed 60Hz simulation, zero runtime dependencies and no recorded assets. Every sprite is drawn in code and every sound is synthesized when it plays. Drive it at the Pit Wall.
Controls
Three input sources, all merged into the same five-bit button state before the simulation sees them. Whichever source is speaking wins, so picking up a controller mid-race works without a setting.
Keyboard
| Keys | Action |
|---|---|
| ↑ W | Accelerate |
| ↓ S | Reverse |
| ← → A D | Steer |
| Space | Held with steering, drift. The charge glows green then gold, and releasing it gives a mini-boost. Held without steering, brake. |
| R | Restart with a fresh seed |
| M | Mute |
Gamepad
Standard mapping, connected and disconnected live through the Gamepad API. Steering is analog with a 0.25 deadzone, and the trigger is a real analog throttle rather than a boolean.
| Input | Action |
|---|---|
| Left stick X (axis 0) | Steer, analog past the deadzone |
| RT (button 7) or A (button 0) | Throttle |
| LT (button 6) or B (button 1) | Reverse |
| X (2), LB (4) or RB (5) | Drift and brake |
| Start (button 9) | Restart, latched so a held button restarts once |
Touch
On a device reporting touch points, an overlay of five thumb buttons is built over the canvas: left, right, gas, drift and reverse. It stays hidden until a real touch happens, so a touchscreen laptop driven from the keyboard never gets thumb buttons in the way.
The three tracks
| Track | Shape | Par (3 laps) | Record floor |
|---|---|---|---|
| Vault Alley vault-alley | Tight technical city circuit, short straights, boxy corners. | 1:45 | 0:58 |
| The Trenches the-trenches | Medium flowing launchpad-district esses into a long straight. | 1:12 | 0:40 |
| Night-Vision Swamp night-vision-swamp | Long sweeping curves. Off-track is water, and water is brutal. | 1:16 | 0:40 |
Off-track is slow everywhere and brutal in the swamp, where the shoulder is water. Robin is the exception, keeping 85% of top speed off the racing line, which is why shortcut lines exist on that circuit for one faction and nobody else.
Pads, gates and tokens
Green and red candles
Boost pads are green candles, slow zones are red candles, taken straight from MowCat's own lore language. A green pad gives 0.7 seconds of boost at 1.22× base, scaled slightly by the faction's item luck. A red zone costs 0.5 seconds of slow. Each track carries two of each.
The Buy Button gate
Each track has exactly one, and it is the 2021-01-28 callback: the button greys out and a slab drops across the racing line. Hit it while it is closed and you are shoved back along the track with your velocity cut to a fifth. Steer around it. The AI does.
| Track | Cycle | Closed for |
|---|---|---|
| Vault Alley | 10.0s | 4.0s |
| The Trenches | 9.0s | 3.5s |
| Night-Vision Swamp | 11.0s | 4.3s |
The vanishing token
Every seven seconds a token spawns at one of three spots on the circuit, pulses for three and a half to five and a half seconds, then puffs away and is returned to sender if nobody takes it. The joke is the 2026-07-09 honeypot wave. Grabbing it is worth a boost scaled by item luck, which is how HMM's 2.2× luck turns into a real advantage over a race distance without touching its engine numbers.
Modes
| Mode | What happens | Scoring |
|---|---|---|
| grand-prix | Six karts. You and five AI opponents drawn from the seed, running handling-aware corner speeds and per-faction rubber banding. | Motorsport table by finishing position: 25 / 18 / 15 / 12 / 10 / 8. Seventh scores nothing. |
| time-trial | Alone against the clock, with your personal best ghost if you have one. Position is always 1. | Tiers against the track par time, from 25 down to 4. |
Three laps, always. The lap count is fixed server side during validation rather than taken from the submission, so a client cannot shorten its own race and claim the time.
Ghosts
A Time Trial personal best is stored in localStorage as { seed, inputLog } rather than as a recorded path. At mount the engine re-simulates it headlessly into a pose per tick and draws that.
This works because the countdown length is fixed and the Buy Button cycle is a pure function of the tick, so ghost tick N lines up exactly with live tick N. It also means a ghost costs a few hundred bytes instead of a few hundred kilobytes, and the same stored pair is exactly what a submission contains.
import { loadPersonalBest, computeGhost } from "@rhgp/game";
const pb = loadPersonalBest("vault-alley"); // { totalMs, laps, seed, inputLog, ... } | null
const ghost = pb ? computeGhost(pb) : null; // poses per tick, sector crossingsMounting the engine
The engine is framework free and has no runtime dependencies. It creates a focusable canvas inside whatever element you hand it, and cleans up every listener on destroy().
"use client";
import { useEffect, useRef } from "react";
import { mount, type RaceResult } from "@rhgp/game";
export function Racer() {
const holder = useRef<HTMLDivElement>(null);
useEffect(() => {
const handle = mount(holder.current!, {
faction: "cashcat", // default "cashcat"
trackId: "vault-alley", // default "vault-alley"
mode: "grand-prix", // default "time-trial"
laps: 3, // default 3
seed: 12345, // random if omitted, echoed in the result
width: 960, height: 600,
onFinish: (result: RaceResult) => submit(result),
});
return () => handle.destroy();
}, []);
return <div ref={holder} />;
}On finish the engine calls onFinish(result) and also dispatches a bubbling rhgp:finish DOM event on the mounted element with the same payload, so a wrapper that never sees the options object can still react to it. The result carries the seed and the input log, which is everything the server needs to check it. Rewards and seasons.