# Testing physics and determinism

# Testing physics and determinism

Physics that you cannot test turns into physics you are afraid to touch. Ferret's physics runs without a renderer, without a GPU and without a browser, under a fixed step, so tests are small and fast.

## What it is for

- proving a capability does what its law says (a ball falls √(2h/g) seconds; a rope carries m·g);
- keeping a tuned scene from drifting when code changes (a golden trace);
- checking replication and replays (the same inputs, the same run).

## The headless world

`makeWorld()` in `tests/helpers.ts` builds a world on Babylon's `NullEngine`: a real scene graph, no canvas. The worked examples of these guides, which live in `tests/unit/examples`, use it. In your own tests:

```ts
import { makeWorld } from '../helpers';
const tw = makeWorld();                       // { engine, scene, world, dispose }
```

Havok runs headless as well: `await loadHavok()` (from `tests/havok.ts`) reads the WASM from the package and hands it to `HavokKernel`. One module per test file.

## Driving time

| Call | What it does |
|---|---|
| `world.step()` | exactly one fixed step (60 Hz unless you set `fixedStep`) |
| `world.run(seconds)` | that much simulated time |
| `host.fixedUpdate(dt)` | step the physics host alone, for a test with no entities |
| `world.advance(realDt)` | what the game loop calls: runs as many fixed steps as fit the frame time |

Do not use real time in a test. A test that waits is a test that flakes.

## Assert against a law, not against a number you saw

The strongest tests compute the expected value from the physics the capability claims:

```ts
import { PhysicsHost, LightKernel } from '@ferret/physics';

const host = world.addService(new PhysicsHost({ kernel: new LightKernel({ gravity: [0, -9.81, 0] }) }));
const ball = host.kernel.addBody({ shape: { type: 'sphere', radius: 0.1 }, mass: 1, position: [0, 20, 0], gravityScale: 1 });
const t = Math.sqrt(2 * 20 / 9.81);                 // time to fall 20 m
for (let i = 0; i < Math.round(t * 60) - 1; i++) host.fixedUpdate(1 / 60);
```

Choose tolerances from the integrator: a fixed step of 1/60 s with semi-implicit integration is exact to a few per cent of g·dt for free fall. State the tolerance and why, once, in the test.

## Determinism: what it promises

The same inputs run through the same build give the same result, bit for bit, **on the same machine**: the step is fixed, capabilities run in a fixed order (phase, `order`, name), the light kernel has no randomness and capabilities that need chance take a seed (`seededRng`). That is what makes replays, lockstep checks and golden traces possible.

```ts
import { PhysicsHost, LightKernel } from '@ferret/physics';

function run() {
  const tw2 = makeWorld();
  const host = tw2.world.addService(new PhysicsHost({ kernel: new LightKernel({ gravity: [0, -9.81, 0] }) }));
  host.kernel.addBody({ shape: { type: 'plane', normal: [0, 1, 0], offset: 0 } });
  const bodies = [0, 1, 2].map(i => host.kernel.addBody({ shape: { type: 'sphere', radius: 0.3 }, mass: 1 + i, position: [i * 0.5, 2 + i, 0], velocity: [1, 0, 0.5] }));
  for (let i = 0; i < 300; i++) host.fixedUpdate(1 / 60);
  const out = bodies.map(b => b.position.join(','));
  tw2.dispose();
  return out;
}
const a = run(), b = run();
```

What it does **not** promise: the same bits on a different CPU architecture or between a Node build and a browser build (floating-point and the Havok WASM can differ in the last digits); the same run if you change a capability's code, a value, a material, or the order you call things *in your own code*. Network play therefore replicates **state** (positions, velocities, events), not inputs-only lockstep, unless both sides run the same build; see the net guide (skill `ferret-net-accounts`).

## Golden traces

For a tuned scene (a bridge that must hold exactly this load), record a trace: positions of a few chosen bodies every N steps, as numbers rounded to a stated precision, stored in the repo. The test replays the scene and compares within a tolerance. When a deliberate change moves the numbers, regenerate the trace in the same commit and say why in the message. A golden trace that is regenerated without a reason is not a test.

## Visual checks

Physics that looks wrong but passes numbers (a piece that rotates the wrong way, a mirrored mesh) needs eyes. The Playwright runs in `tests/e2e` load a sample and take screenshots; the physics samples under `examples/physics` are the ones to open in the simulator. Keep one scene per behaviour so a screenshot says one thing.

## Debugging

| Symptom | Cause |
|---|---|
| A test passes alone and fails in the suite | shared state: a world per test, `tw.dispose()` after; Havok is one module per file |
| Two runs differ | an unseeded random, wall-clock time, object iteration order of something you built, or a body created in a different order |
| Havok test: `fetch` of a file URL fails | load through `tests/havok.ts`, which reads the binary |
| A tolerance has to be huge | the test is checking something the law does not say: a bounce, a friction regime, a sleeping threshold; assert what the law does say |
| Output is flooded by Babylon | `tests/setup-babylon.ts` quiets it to warnings and errors |

## Reference

`World`, `PhysicsHost`, `LightKernel`, `HavokKernel`, `seededRng`.
