# Bounce — Level 1 · dev harnesses

These files are **not** part of the game. The shipped game is `index.html`, `style.css`
and `game.js`, which run in a browser with no build step and no network access.
The harnesses here exist only to prove the physics and the level are correct.
They load the real `game.js` (which exports itself when `module` exists) and run on Node.

```
node dev/gen.js             # regenerate the tile map, print counts + a preview
node dev/sim.cjs            # physics + level design checks  (66 checks)
node dev/headless.cjs       # whole screen flow with a DOM/canvas stub and a bot
node dev/render-audit.cjs   # draw-call audit: viewport clamping, objects drawn
```

## dev/gen.js
Generates the 87x16 tile map row by row and prints it. The map text it prints is what
lives in `game.js` as `MAP`. Edit the level by editing `gen.js`, running it, and pasting
the printed rows into `MAP` (or re-run the splice used during development).

## dev/bot.cjs
A model of a competent player: it probes the real physics to measure how far a full
bounce carries, how far a short landing carries, and which speeds clear each hazard,
then plays the level with that knowledge. Used by `sim.cjs` and `headless.cjs`.
If the bot can no longer finish the level, the level has become unfair.

## dev/sim.cjs — what it proves
- map sanity: 6 hoops, 2 checkpoints, 1 crystal, 2-tile door, spawn/checkpoints/hoops
  supported and never on a hazard, exactly one floor gap and it is spike-floored
- the central rule: the bounce apex is exactly 3 tiles from standing and at full roll
  alike; speed buys distance, never height
- frame-rate independence: identical landing at 30 / 60 / 144 / 240 fps
- spikes kill, the pit kills, every hazard is clearable over a wide speed range
- the crystal detour is reachable, the closed door is impassable and cannot be jumped
  from any approach or speed, the open door completes the level
- a 1-tile ledge step still shows a visible rebound
- the bot finishes with 3 lives

## dev/headless.cjs — what it proves
Runs the actual browser code path (rAF loop, key events, DOM HUD, canvas) against a
stub. Title -> play -> death -> burst -> respawn at checkpoint -> Level Complete with
the exact score -> title -> Game Over -> title, with the level state fully reset each
time, and the camera clamped to the level bounds.

## dev/render-audit.cjs — what it proves
Records every draw call and checks the ball is drawn as a 1-tile circle, hoops as rings,
solid tiles as fills, the death burst as fragments, that nothing is drawn more than one
tile outside the 128x128 viewport at any camera position, and that the viewport window
always lies inside the map.
