Felt World — tech stack and physics tests
How the game is built, how its physics was tested in each world, and what was not tested. Play the game · How the felt look is made · Source on GitHub
1. Is a game engine used?
No. There is no game engine (no Unity, Unreal, Godot, Babylon.js or PlayCanvas) and no physics engine (no Rapier, cannon.js or Ammo.js).
The game uses three.js 0.170.0. three.js is a rendering library: it gives a scene graph, cameras, materials, and WebGL drawing. It does not give a game loop, collision, physics, audio, input, or world streaming. The game code writes all of those itself, in one JavaScript module inside feltworld/index.html (3,231 lines, 178,562 bytes).
2. Tech stack
| Part | What is used |
|---|---|
| Delivery | One HTML file. No build step, no bundler, no package manager. Any static web server serves it. GitHub Pages hosts the play link. |
| Rendering | three.js 0.170.0 from the jsDelivr CDN through an import map (three and three/addons/). WebGL. |
| Look | Toon materials with a soft gradient ramp. Running-stitch outlines (inverted hull, fragments discarded in a dash pattern). A post-process ShaderMaterial: wool-fibre fuzz, felt grain, soft seams at depth edges, a stitched fabric border. A sky ShaderMaterial. Details: How the felt look is made. |
| Textures | Drawn at start on 2D <canvas> elements: felt noise, knitted roads with a stitched centre line, quilted fields. No image files. |
| World | Pure functions of (x, z): height, biome masks (town, fields, meadow, forest, mountain, ocean), and a warped road grid. Chunks are built one per frame, nearest first, from a seeded random generator per chunk, and are removed past a radius. |
| Props and crowds | InstancedMesh batches of scaled primitives with a colour per instance. Traffic, pedestrians, sheep, rabbits, fish, jellyfish, rays, sea turtles, crabs and whales are crowds on instanced meshes that respawn round the player. |
| Physics | Hand-written. See section 3. |
| Audio | Web Audio API only: a look-ahead note scheduler, a generative radio with several songs, note-phrase chimes, engine hum, wind (band-pass noise), and a low-pass filter on the master bus under water. No audio files. |
| UI | HTML and CSS over the canvas: title card, clock and time-of-day buttons, speed, mode, depth readout, collected counts, minimap, help, Pause and Music buttons, touch joystick and buttons. |
| Fonts | Fredoka and Patrick Hand from Google Fonts. |
| Quality | Three tiers. Low: pixel ratio 0.75, no shadows, 40 cars, 30 pedestrians. Medium: 1.0, 1024 px shadows, 70 cars, 50 pedestrians. High: 1.5, 2048 px shadows, 110 cars, 80 pedestrians, a larger chunk radius. The tier is saved in localStorage. |
| Testing | feltworld/physics-check.js, an ES module that runs inside the live game. Claude drove it in Chrome through the Claude in Chrome extension. node --check on the extracted module did syntax checks after each edit. |
| Author | Claude Code with the Opus 5.5 model, from a sequence of requests. See the README. |
3. How the physics works
The physics is arcade physics. It does not simulate forces between bodies. It keeps every body on or above the correct surface and out of every solid thing.
Ground
groundAt(x, z) returns the height of the ground as it is drawn: it interpolates the triangles of the chunk's terrain mesh, then takes the highest road deck at that point. Everything on land uses it: the player car, the walker, traffic, pedestrians, sheep, rabbits. Crabs, fish, jellyfish and rays use it for the sea floor. A drawn road flattens the ground under it fully, so a road deck cannot float above the terrain.
Solid things
Each chunk has a list of colliders: circles (trees, rocks, lamps) and rotated boxes (houses, fences, the shipwreck, the chest). Every collider has a height range: a top, and for tree crowns and windmill sails also a bottom (bot). collide(p, rad, down, tall) pushes a round body of radius rad and height tall out of every collider whose height range it meets, and out of every traffic vehicle (a moving rotated box). The bird can fly under a tree crown but not through it.
Moving bodies against each other
- Pedestrians walking one way keep 0.6 m nearer the road than pedestrians walking the other way, so they pass. A pedestrian never steps further into another pedestrian or into a car; if two block each other for 1.5 s, one turns round.
- Sheep and rabbits never step further into another sheep or rabbit.
- Jellyfish, rays, sea turtles, whales and crabs push each other apart (and out of the submarine and the player turtle) before each one moves, so each animal's own floor, surface and rock rules still apply after the push. Each body is an upright cylinder (a jellyfish's includes its tentacles, 2.8 × size under the bell). The game and the test read the same table of animal cylinders (the submarine's cylinder, 2.2 m wide and ±1.4 m high, is written in both files). The push is sideways only, at most 0.3 m per frame (no limit for an animal that just appeared), and it starts 1 m before two bodies touch vertically (2 m for a ray, which climbs and drops with the floor at up to 10 m/s). A new animal only appears at a spot clear of the others. Rays glide at least 2 m above the floor and lift over crabs. Fish are not in this rule: a school keeps its formation.
- A whale keeps its nose and tail, not only its middle, above the sea floor.
These bodies collide: the player car, the submarine, the walker, the player turtle, the bird, pedestrians, sheep, rabbits, sea turtles, rays, whales and crabs. Kelp, coral, flowers and small bushes are passable on purpose. Sheep are passable for the player on purpose.
Traffic
- Cars follow the car ahead in their lane.
- At a crossing of two main roads, only one road may use the crossing at a time. A car waits at a stop line until no car of the other road is inside, and the car that arrived first goes first. Before this rule, four cars could each wait for the next one round the crossing and stop for ever (a 90 s traffic run on the old code found 2 such circles).
- Where two cars' paths meet away from a main crossing, the car with the lower index goes first; the other car yields.
- A car stops for a pedestrian in front of it.
- A car never moves into another vehicle: if a step would cause an overlap, the step is undone.
- At a road end, a car does a U-turn as a 2.5 s arc. No car spawns within 30 m of a road end.
- A gridlock breaker respawns a car that has stopped for 8 s, if the camera does not see it or it is more than 120 m away.
4. How the physics was tested in all worlds
The reference (the "oracle")
The test does not trust the game's own height code. For each sample it casts a ray straight down against the drawn terrain and road meshes (drawnGround). A body passes only if it is within a tolerance of what the player sees.
How it runs
The test imports the running game's debug handle (window.__game), teleports the player to each world, moves the game with step(1/60, n), and samples every visible agent. It also sends real KeyboardEvents (W, A, D, E, F, T, Q) through the same input path a player uses.
What each world checks
| World | Checks | Tolerance |
|---|---|---|
| Town | Traffic on the road; pedestrians on the ground; no two traffic vehicles inside each other (oriented-box separating-axis test); the player car on the ground; nothing inside a house, tree, lamp, fence or vehicle. Real input: drive with W + A for 8 s; press E to get out; walk with W, then W + D, for 8 s (the walker must cover more than 30 m); walk into a house and into a tree / rock / lamp; drive into a house. | On ground: −0.15 m to +0.35 m. Player car: −0.15 m to +0.45 m (it counts as airborne above 0.4 m). |
| Forest | Drive into a tree; land agents on the ground and out of solids for 5 s. | As town. |
| Mountain road | The test searches for the road lane with the largest gap between the road deck and the plain terrain, parks the car there, then holds W for 10 s. The car must stay on the road and move more than 50 m. | As town. |
| Fields | Sheep and rabbits on the ground and out of trees and houses, for 20 s. | Sheep −0.3 m to +0.35 m; rabbits −0.3 m to +0.95 m (they hop). |
| Deep sea (−70 m) and shallows (−14 m) | Fish, jellyfish, rays, sea turtles and whales stay above the sea floor and below the surface; a whale's nose and tail (±9 m) stay above the floor; crabs stand on the floor; no animal inside a seabed rock, the shipwreck or the chest; the submarine stays between the floor and the surface. 10 s each. | Crabs ±0.35 m. Inside a solid: more than 0.05 m fails. |
| Sea, real input | Drive the submarine (W) into a seabed rock; press T and swim the turtle into the same rock; press T, then hold Q + W for 4 s to dive. | The body must stop at the rock surface. |
| Air | Press F on a meadow road, dive with W for 3 s, glide for 2 s: the bird stays above the ground and the water. The bird glides into a house at mid-wall height and into a tree crown. | The bird must stop at the solid. |
Proof that the test can fail: fault injection
m.faults() breaks the physics on purpose in 9 ways and checks that the matching check goes red. This shows that no check passes by construction.
- A traffic car 2 m under the road.
- A traffic car moved into another car.
- The player car put inside a house.
- The player car 3 m under the ground.
- The bird put inside a house.
- A sheep put inside a tree.
- A fish lifted out of the sea.
- A crab 2 m under the sea floor.
- A whale dropped onto the sea floor.
Traffic soak
m.soak('town') runs 90 s of traffic (5,400 steps) and counts vehicles inside each other, cars stopped for more than 20 s (with the reason each car gives), U-turns started, finished, and longer than 10 s, and deadlocks: it follows each stopped car's reason ("follow 12", "cross 7", "junction 3") to the car it waits for, and a path that comes back to a car already on it is a deadlock. The old count of "cars stopped for more than 20 s" missed the crossing deadlock, because the gridlock breaker removes stuck cars that the camera does not see.
Body against body
m.overlaps(biome) runs 20 s in a town, a field or the sea and counts pairs more than 0.3 m inside each other: traffic (the whole body, as circles along its length) against trees, lamps and houses; traffic against pedestrians and sheep; pedestrian against pedestrian; sheep and rabbits against each other; every pair of whales, sea turtles, rays, jellyfish and crabs; and the submarine against each of them.
5. Results
| Test | Low | Medium | High |
|---|---|---|---|
run() | 150 checks, 0 fails, 59,349 samples | 150 checks, 0 fails, 88,729 samples | 150 checks, 0 fails, 130,905 samples |
faults() | 9 of 9 caught | 9 of 9 caught | 9 of 9 caught |
overlaps() town / field / sea | 0 bad in 1,683 / 1,698 / 6,352 | 0 bad in 3,107 / 3,295 / 6,872 | 0 bad in 5,416 / 5,975 / 10,869 |
soak('town'), 90 s | 0 overlaps, 0 deadlocks, 0 stopped > 20 s; 5,029 close pairs; 52 of 52 U-turns finished | 0 overlaps, 0 deadlocks, 0 stopped > 20 s; 27,187 close pairs; 40 of 41 U-turns finished | 0 overlaps, 0 deadlocks, 0 stopped > 20 s; 64,862 close pairs; 56 of 57 U-turns finished |
| Test time (all five) | 58 s | 74 s | 102 s |
An unfinished U-turn is one still turning when the 90 s ended. No U-turn took more than 10 s.
Sea animals, every frame. overlaps() samples every 0.5 s. A separate run measured every sea pair on every frame: 3 runs of 40 s (2,400 frames) each on Medium, 12,430 to 18,732 close pairs per run. The worst overlap was 0.18 m, under the 0.3 m limit.
What the upright-cylinder check found first. The sea push used to treat each animal as a sphere, while the check treats it as an upright cylinder. The check failed on Low and on Medium. On Low, one 20 s run had 346 bad jellyfish-against-jellyfish samples, 3.51 m deep at worst. On Medium, the worst was a whale against a jellyfish, 4.27 m deep. The fixes: the game uses the check's cylinders; the push starts before two bodies meet vertically; a new animal appears only where it is clear; a ray lifts over a crab and over the top of a rock instead of a sideways jump of about 5 m.
Before the physics pass, an earlier version of the same test found these failures: 700 of 700 traffic samples failed "on road", and the worst car was 10.8 m below the road. The overlap check found 68 to 104 traffic overlaps.
Cost of the physics pass
Median time per frame (step + render) on High quality, measured in a hidden Chrome tab. These times are from before the crossing, pedestrian and sea-animal changes of 27 September 2026, and were not measured again:
| Place | Before | After |
|---|---|---|
| Town | 6.2 ms | 7.5 ms |
| Fields | 5.8 ms | 7.3 ms |
| Deep sea | 6.7 ms | 8.0 ms |
6. What was not tested
- Real frame rate on screen at High quality. Only the hidden-tab frame times above were measured.
- Phones. Touch controls were tested only in a 667 × 375 frame in desktop Chrome. The frame rate on a real phone and the size of the buttons under a real thumb were not measured.
- Other browsers. All tests ran in Chrome. Firefox and Safari were not tested.
- Audio. No automatic test covers sound. It is not known if audio starts again after Pause → Play when the game was started with a real Start click.
- Some sea objects. Starfish, urchins, the shipwreck and the treasure chest were never seen in a screenshot. The physics test covers the shipwreck and chest as colliders only.
- A human play-test of the final physics build. The automatic tests passed, but a person has not yet played this build to confirm it.
- Everything between samples. The test samples every 5 to 60 frames. A fault shorter than the sample interval can pass unseen.
Known limits (not overlaps)
- A U-turn can wait more than 10 s when a crossing car stops in its arc.
- A traffic jam that the camera sees stays until you look away, because the gridlock breaker only respawns cars that the camera does not see.
- Fish check the sea floor but not rocks when they move. The tests found 0 fish inside rocks.