Groundwork for the demo becoming a splash TEMPLATE. Two things the script
surface could not express, so the demo had to stay in Rust.
**`game.terrain` carried its own single-octave value noise.** That meant the
terrain an AI could reach from splash was strictly worse than the terrain the
engine could make, and the two drifted independently — every fix to
`libs/game/gen/terrain.rs` (fBm, domain warp, world-unit frequency,
slope-aware colour, rim relief) was invisible to any authored game. It now
calls that generator. One generator, one set of bugs.
New params exposed: `feature` (distance between hills, in world units —
answerable, unlike "what is a good freq"), `octaves`, `warp`, `ridged`,
`flatten`, `rim`/`rim_start`.
Two compatibility decisions worth stating:
- `freq` still works. It meant cycles per CELL INDEX, so its wavelength in
world units is span/((cells-1)*freq); translating rather than ignoring it
keeps an existing world looking like itself.
- `step` now defaults to 0, not 1.0. The old default quantised every smooth
slope into one-unit stairs, so a script asking for smooth terrain got a
contour map. Scripts that want terraces still ask for them.
**`Car` now carries a model**, as `Character` already did, and `game.car`
takes `{model}`. Without it a host could only ever draw ONE kind of car —
which is exactly why the arcade fleet had to live in Rust, and why a
splash-authored world could not have more than a single vehicle shape.
The renderer prefers the car block's own model and falls back to walking the
`parked_car` role, so the same code path serves an authored game and the
built-in demo without the script needing to know the fallback exists.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**A guy with a beard.** The player wears the barbarian, pinned BY NAME
rather than by cast index — the cast is assembled from whatever the asset
library contains, so an index silently becomes a different person the
moment a pack is added, and that failure reads as a cosmetic surprise
rather than a bug.
Kenney was the obvious cheaper choice and it does not work: every character
GLB in both Kenney packs has only root/torso/head/arm/leg parts, and their
colormap is flat colour swatches with no painted faces, so there is no
bearded Kenney character to pick. Verified by rendering the heads rather
than by guessing from filenames.
**The sun is fixed.** It swept a full day every 40 seconds, which looked
lively for about ten seconds and then just cost money: AO and cast shadows
are baked, and the baker rebakes whenever the sun crosses
`sun_rebake_angle`, so nothing on screen ever settled. Now one explicit
direction at 38 degrees elevation — the engine default sits at 54, nearly
overhead, where shadows barely clear their own footprint and nothing reads
as standing on anything. At 38 a shadow runs about 1.3x its caster's
height: long enough to describe the shape and show the ground's slope,
short enough that the village does not vanish into its own shade. The day
cycle survives behind ARCADE_DAYCYCLE, where it belongs until rebaking is
incremental.
**The simulated wheels now sit under the drawn ones.** Reported as the car
"not really following the landscape, wheels not really touching". The
config's track and wheelbase are authored for a generic chassis while the
mesh is scaled from whatever the pack shipped: for the stock truck the
drawn wheels sit 1.30x wider and 1.39x further apart than the simulated
ones, with a 1.33x radius. On flat ground that is invisible, which is
exactly why it survived a flat-ground test measuring a 3e-6 residual — the
model's lowest point still lands on the road. On a slope it is not
invisible: the body pitches and rolls about the SIMULATED contact points,
and a drawn wheel further out swings through a bigger arc, so it lifts off
the ground. Adding terrain is what made it visible.
Fitted from measured bounds at model-load time, the same discipline as
sitting the mesh on measured bounds rather than on the collision box.
I first read "floaty" as suspension bounce and stiffened the springs. That
was the wrong problem; reverted.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**The flat plane is gone.** Terrain existed all along — heightfield collision
via box3d, AO and shadow raymarching against it, a mesh path, a
`game.terrain` verb — and arcade simply spawned a flat slab instead. That is
the fourth capability this week that was built and never called.
Turning it on was not enough, because the generator had faults that a
screenshot explains faster than prose:
- **Single-octave value noise.** Detail at exactly one scale reads as melted
blobs. Now fBm with domain warp, which is the single highest-value knob
here: warping the sample point bends contours into ridges and valleys
instead of round lumps on a visible grid.
- **`step` defaulted to 1.0**, quantising every smooth slope into 1-unit
stairs. The old default renders as a literal contour map. Off by default.
- **Noise was indexed by CELL INDEX, not world position**, so asking for a
finer mesh silently generated a different landscape. Resolution should buy
detail, never a new world.
- **Colour came from height alone**, which paints terrain in horizontal
stripes like a contour map. Now height AND slope, so rock lands on cliff
faces and grass on the shelf above them.
Generation moved to `libs/game/gen/terrain.rs` as a pure function. Beyond
testability that was forced: arcade's demo world has no script VM, so the
only generator in the tree was one it could not reach.
`rim_relief` is the load-bearing idea. Terrain interesting everywhere is
terrain you cannot put a town on; terrain flat enough to build on is a green
table. Growing the relief outward gives a playable basin ringed by something
worth looking at, and doubles as a soft boundary. It SCALES the noise rather
than adding a radial ramp — the ramp version has no noise in it and renders
as a smooth machined ring between two flat plains, which I built first and
threw away after looking at it.
Two things the tests taught me rather than confirmed:
- Normalising fBm by the sum of octave amplitudes — the textbook form — makes
five octaves come out FLATTER than one, because summing decorrelated fields
concentrates them about the mean. Normalising against the field's own
extents makes `amp` mean literal relief at any octave count.
- The octave test measures CURVATURE, not slope. At gain 0.5 / lacunarity 2
every octave contributes equally to slope — that is what self-similar
means — so a slope-based test reports no difference while the terrain
visibly gains detail. My first version of that test was wrong, not the code.
Statics get one conform pass after composition; movers already clamp to the
terrain every tick in `step.rs`, so the player, villagers and car find the
ground themselves.
**Cars now touch the road.** Kenney authors vehicles origin-at-the-contact-
patch — tyres exactly on y=0, with each wheel node lifted by its own radius —
and every vehicle in every kit measures min.y == 0, verified across all 4442
GLBs in the library. We were dropping the model by `half.y` instead, a rule
that is right for a walker (a Mover's box bottom really is its feet) and
wrong for a raycast vehicle, whose suspension probes from the chassis origin.
The float was suspension travel plus wheel radius, 0.341 units, predicted in
closed form and matched by simulation to 1e-5. The same line also shifted in
world Y after rotation, so the mesh slid out from under a leaning chassis.
Both now derive from measured bounds along the body's own down axis — no
constant anywhere. Note the convention is real but NOT universal: track and
road pieces go to min.y = -1.0, so it must be read, never assumed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
**The HUD was never drawn.** `libs/game/render::hud` has existed and
gamemaker has called it all along; arcade simply never did. So the
interact prompt was computed every tick, published to `world.hud_slots`,
and thrown away — the get-in-a-car mechanic was discoverable only by
guessing the key. Same shape as the gamepad never being polled: the
capability was there and the app never called it.
Two things found while wiring it:
- The prompt asked for `size: 1.0`, which reads like a scale but is an
absolute point size to the renderer — any value above zero is taken
literally. It would have drawn a one-point speck. Now 17.0, deliberately
above the 12.0 default, since this is the one line a player has to
notice without being told to look.
- Added a standing controls hint. Nobody sits a child down with a manual,
and the affordance prompt only appears once you are already next to
something; this is what tells you how to get there.
**Arcade could not build headless.** The gamepad poll was added without
the `cfg(headless)` split gamemaker uses, and arcade had no `build.rs` to
define the cfg at all, so `MAKEPAD=headless` failed to compile and the
render-to-PNG path went with it.
**The walking camera sat too close** — at 6.5m the character filled a
fifth of the frame and hid the world behind them. Now 9.0m.
That broke a test asserting `car.distance > on_foot * 1.5`, and the fix
is the test, not the number. That ratio read like a decision but was
really the quotient of two values that happened to be current, so
correcting the walking boom on its own merits broke an assertion about
driving while nothing about driving had changed. Restated as a margin:
what has to hold is that getting into a car visibly widens the view, and
several metres does that at any walking distance.
Also logs gamepad connect/disconnect on change, so "the pad does nothing"
is answerable from a release log — either the app never saw the device or
it saw it and the mapping is at fault.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A game gets a walkable character that can get into a car and drive it
in three lines and never mentions a camera:
let id = Character::new(...);
player_rigs.insert(PlayerId(0), PlayerRig::new(id));
Everything crafted is engine-side with defaults nothing has to specify:
coyote time, jump buffering, variable jump height, asymmetric accel and
decel, air control, landing recovery, a boom that snaps in and eases
out, look-ahead, speed pullback, and delayed recentring.
Script surface grows by three verbs (115 -> 118): game.player_character,
game.interactable, game.interact_prompt. Cars and doors-with-interiors
are derived affordances, so a generated game with a car and a house
declares nothing; game.interactable is only for chests and switches.
The prompt and the press share one search so they cannot disagree, and
it picks the nearest candidate in front rather than merely the nearest.
Four bugs found by looking at what ran, not by reading:
- Arcade never polled the gamepad at all. No game_input_states() call
existed anywhere in the app, so the pad's state never entered the
process and every binding downstream read a struct nobody filled.
- LT drove both brake and negative throttle, and brake force opposes
reverse motion. Measured: clean reverse covers 13.8m in 2s against
22.25m forward; with brake held, 0.63m. car.rs is unchanged -- a foot
on the brake winning is a car behaving like a car.
- Mount cleared `hidden`, which means "solid to everything, drawn by
nothing" -- so boarding left the driver as an invisible collider at
the kerb. Now uses attached_to, the sim's seat pin, and saves and
restores hidden rather than asserting a value.
- GameWorld::new() never set gravity; only reset_content() did. All 70
new() call sites floated, and four files had each independently grown
their own `world.gravity = 30.0`. A floating character never reports
on_floor, so the controller silently refused to jump.
The in-vehicle boom goes 9.0 -> 13.0. The boom is a time budget, not a
length: at the car's top speed 9m was 0.37s of road ahead, too little to
plan a turn. The test states it against CarConfig::top_speed, so raising
the car's speed fails the test instead of quietly making the view tight
again. The pivot deliberately does not rise with it -- eye.y is
pivot.y + sin(pitch)*boom, so the longer boom already buys the height,
and driving should sit lower and more planted than walking.
Player rigs now survive Blocks::clear(): where you are sitting and where
you are looking are the player's state, not the game's content, so a
script edit no longer ejects the driver mid-corner.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A game gets walking, jumping, a follow camera, mounting and dismounting without
ever touching a camera itself:
blocks.player_rigs.insert(PlayerId::LOCAL, PlayerRig::new(character_id));
blocks.tick_player_rigs(&mut world, &raw_inputs);
blocks.pre_step(&mut world);
rig.apply_camera(&mut world);
PlayerRig::tick fills BOTH the walking and driving fields of DriveInput every
tick, so changing seat needs no second code path — whichever block is listening
reads its own fields.
The character craft already existed in character.rs and passed; what was missing
was anything consuming it. The camera side is CameraConfig::on_foot (rotates
faster than it translates, so position lag reads as weight while aim stays
crisp; no recentring — the player aims it and it stays) and ::in_vehicle (lower,
35% speed pullback, recentring after a 0.9s delay so it yields to your hand and
only takes over once you let go). Mounting blends with smoothstep.
FOUR REAL BUGS, three of which no existing test could have caught:
- The camera blend never interpolated: `blend / blend.max(0.0001)` is always
1.0, so the rig held its old shape for the whole transition and snapped at the
end — precisely the cut the blend exists to prevent. It needed the blend's
ORIGINAL length, not the remainder
- The blend timer froze when its subject vanished, because tick bailed on the
entity lookup before advancing time. A car despawning mid-blend would have
frozen the camera permanently
- **heading_to_right returned LEFT** — the exact negation of forward x up. Its
doc said "+X" and its test asserted `r.x < -0.99 || r.x > 0.99`, which accepts
BOTH SIGNS and so could never fail. A test that cannot fail is worse than no
test, because it is counted as coverage
- The renderer and the sim use opposite yaw AND pitch signs. Derived by matching
the two eye-position expressions component-wise rather than guessing; the
conversion now lives in heading.rs as heading_to_camera_yaw/pitch — ONE named
boundary, never a negation at a call site. That discipline is why heading.rs
exists, and this is the same bug class that produced the reversed steering
Two changes from peer review: DriveInput.run is f32 so stick deflection gives a
real walk-to-run continuum instead of snapping at a threshold; and pre_step
gained a modality gate, because a player owning both a character and a car was
driving AND walking simultaneously — the stick steering your car was also
walking the body you left in the seat, invisible until you got out somewhere you
had never been.
108 tests across sim and blocks (from 88), including the full walk-in-drive-out
journey, analog deflection landing within 10% of the true midpoint, a reload
keeping you seated with the camera where it was, a vanished car putting you back
on your feet with working controls, and the affordance prompt agreeing with the
button at every distance on the approach.
Known gaps, reported not hidden: dismount picks a side but doesn't check the
GROUND there (spot_clear tests overlap, not floor), recentring uses velocity
heading so slow reversing can hunt, and PlayerRig assumes one local player
because world.cam_yaw is a single device field — split-screen needs a per-player
write path that doesn't exist yet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Character feel went into character.rs rather than a fork — its own doc comment
argues a second controller would drift, and it was right. Acceleration and
deceleration ramps, deliberately ASYMMETRIC (starts have weight, stops are
crisp), partial air control, coyote time, jump buffering, variable jump height,
asymmetric gravity via the sim's own gravity_scale, landing damp.
New controller.rs: FollowCamera with separate position and rotation smoothing,
clamped pitch, a boom that snaps IN but eases OUT, look-ahead, speed pullback,
and delayed recentring that yields to the player's hand. Mount/Seat with camera
blending. movement_intent with deadzone and diagonal normalisation, so
diagonal WASD can't outrun cardinal.
ELEVEN FEEL TESTS, each named for the complaint it prevents: speed ramps
monotonically rather than stepping; stopping is crisper than starting; a late
jump off a ledge still registers; a jump pressed before touchdown fires on
landing; releasing early measurably lowers the apex; falling takes fewer ticks
than rising; air control is neither zero nor total; the camera cannot invert or
bury itself; dismounting puts you BESIDE the car, not inside it; the boom
recovers gradually rather than popping.
Two bugs it found in its own work, both invisible to endpoint-only tests:
- Variable-height jump broke an existing test: callers who set jump_pressed but
never hold `jump` — the older single-flag convention, and what a generated
game will most likely write — had their jump cut on the next tick. Now cutting
requires evidence the button is genuinely held; full height for everyone else
- THE CAMERA BLEND NEVER ADVANCED. `blend / blend.max(eps)` is always 1.0, so t
was pinned at 0: the rig held its old shape and then snapped — precisely the
cut the blend exists to prevent. Tracked against blend_total with smoothstep
Four of its own tests were wrong before the code was: air control limits the
RATE, so enough air time still reaches full speed (the real claim is that the
same input builds speed slower airborne); the buffer window genuinely cannot
survive a long fall; two needed the character settled on the ground first.
NOT DONE, deliberately: the controller is not yet exposed as verbs
(game.player_character({})) and arcade still uses its own camera and WASD path.
The prefab and its defaults exist and are tested; binding is the remaining step,
and stopping beat half-wiring input routing.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
AO — ZERO EXTRA BYTES. It lives in the alpha byte of the packed colour lane,
which was already dead weight: model.rs wrote the glTF baseColorFactor alpha
there, the skinned shader multiplied it into v_tint.w, and the pixel shader
threw it away by returning a hardcoded 1.0. We were paying for the channel and
never reading it. 24 bytes/vertex before and after.
The multiply scales AMBIENT ONLY — `albedo * (ambient * ao + direct)`. Folding
AO into direct as well would darken a sunlit wall twice, and direct light is
already zero where a surface faces away, which is precisely where occlusion is
the ambient term's job.
Static-only holds BY CONSTRUCTION without touching skin.rs: that file writes
pack_unorm8x4(1,1,1,1), so characters get ambient * 1.0 — an exact no-op
through the shared shader.
Cost over the real catalogue (4,442 models, 2.5M verts): 2.64 ms/model average,
102 ms worst case — down from 409 ms. Dense interior kits get a reduced ray
budget, and the hemisphere distributes over the ACTUAL ray count rather than
the nominal one; without that fix a reduced budget samples only near the normal
and reads as uniformly unoccluded. The 4x speedup moved the crevice share
14.7% -> 15.0%, i.e. cost nothing visually. Nothing in the library falls below
0.40 occlusion — the floor clamp is what keeps low-poly art out of the mud.
Contact AO needed one fix found by rendering it: an ellipse inscribed in a
square footprint pulls away from the corners, so a castle piece read as
standing in a spotlight rather than touching the ground. It is a squircle now
(|x|^4+|z|^4=1) with segments landing on the corners and edge midpoints.
STEERING FIXED ONCE, AT THE SOURCE. New libs/game/sim/heading.rs states the
convention in one place — forward is -Z, right is +X, POSITIVE YAW TURNS LEFT —
with heading_to_forward/right, forward_to_heading, steer_to_yaw_rate,
heading_delta. Seven tests read as statements of intent ("steering right
decreases heading") so a future sign flip fails loudly. The car's torque and
its autodrive route-follower both route through it and the inline atan2 calls
are gone. The inversion was exactly the trap the module now documents: positive
steer produced positive yaw, which turns left.
DOUBLE BRAINS, found by wiring: spawn_blocks ran unconditionally after
build_world, so every villager got a SECOND Npc block — two brains steering one
body — plus a second car. 28 NPCs for 14 entities; now 14.
The car is a real mesh (toy-car-kit/vehicle-truck) found by description and
scaled from its own bounds onto the chassis, box hidden. The rigid body stays
the physics.
BIG WORLD RENDERS: ARCADE_WORLD=big, street demo still default. 596 props, 217
colliders, 64 draw items (per-pack atlas batching working), 611 shadow casters,
14 NPCs, 63 of 64 models loaded, 15 ms to plan. 506,962 TRIANGLES — that will
not fit a Quest, and roads are 382 of 596 placements, so road decoration and
distant woods scatter are the first cuts a governor should make.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
blocks/npc.rs + sim/sense.rs. Utility scoring re-run every ~0.45s per NPC,
staggered so a crowd doesn't re-plan on one tick. Candidates scored additively:
visit a POI (tag appeal x distance falloff x novelty x jitter), loiter near
someone (gated on sociability), go home (grows with time away), wander (the
fallback, so a world with no POIs still moves).
Three things do the legibility work. POIs, so NPCs walk to THINGS rather than
coordinates. Per-NPC seeded personality (haste/patience/sociability/curiosity/
homebody) so identical config still yields unlike villagers. And a day clock
with a per-NPC phase offset — benches read as afternoon, doors as evening —
which is what stops ten villagers doing the same thing in unison. Activities
are deliberately only four (Idle/Travel/Dwell/Follow); routines come from
sequencing them, not from twenty verbs.
Sensing reads THE SAME SOLID FILTER the mover sweep uses, so perception and
collision cannot disagree. obstacle_ahead sweeps the NPC's own box rather than
casting a ray, because a ray through a doorway reports "clear" for a body twice
its width. Blocked -> jump if the top is in reach with landing room, else
sidestep toward the side with clearance (blended with the goal so it curves
rather than turning 90 degrees), else a stuck timer abandons the goal.
Reading the existing tests caught a bug in the new logic: "low obstacle -> walk
over it" is wrong, because the 0.55 step-up is a TERRAIN contract and
sweep_axis blocks against static boxes at any height. That branch is gone —
it was exactly the perception/physics disagreement this module exists to avoid.
Two bugs the tests caught:
- MUTUAL SOCIAL LOCK: two sociable NPCs each chose to loiter near the other,
permanently. One moved exactly 0.0 units in 90 seconds. A social cooldown
stops Follow being re-picked immediately
- VILLAGE DRIFT: an unbiased random walk has no centre, and a trace showed a
villager 43 units out with every POI inside 18. Wander steps past a 26-unit
leash now aim home
Cost against a 16.6 ms budget: 50 NPCs 0.007 ms/tick, 200 NPCs 0.046 ms/tick
(full sim step — a pre_step-only figure would be a lie, since without
step_world the NPCs never move and re-decide more often).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
libs/game/audio (77 tests): WAV decoder (8/16/24/32-bit int, f32/f64,
total on malformed input), Ogg container with packet reassembly, sample
bank with on-demand decode + linear resample + LRU eviction and pinning so
a playing voice can't be evicted, and a 24-voice mixer with equal-power
pan, playback-rate pitch, click-free fades, priority+age voice stealing and
a limiter. Generation-tagged voice handles mean a stale handle cannot
retune a reused slot. The limiter test caught a real bug: peak was being
measured per-voice, but what clips is the SUM.
Emission is the point — sounds come from the engine observing gameplay,
not from script calls: Material/MaterialPair (order-independent, the softer
material names the sound), an impact curve mapping closing speed to
gain/pitch, and an AudioDirector with repeat-avoiding variant selection,
per-category volumes, per-pair cooldowns and a per-frame cap. A 200-contact
frame yields <=6 sounds and the cooldown map is proven not to leak.
Blocks now emit their own audio: car engine tracking revs, skid on lateral
slip, suspension thud on landing; character footsteps timed off the WALK
CYCLE rather than a timer, so feet and sound stay together when slowing;
jump/land scaled by fall speed; plane engine by throttle; lap and win
stings. RNG isolation proven: heavy audio work interleaved with world-rng
draws leaves the drawn sequence bit-identical to a silent run.
KNOWN GAP, reported rather than hidden: every Kenney audio pack is Ogg
Vorbis only (471 files, zero WAV), and the from-scratch Vorbis decoder is
NOT correct yet — setup header parses exactly, channels/rate/frame count
and envelope shape are right, but floor magnitudes come out ~75x low and
best correlation against an afconvert reference is 0.67. Two real bugs were
found and fixed en route (type-1 residue filled one codeword instead of the
partition; MDCT post-twiddle carried the pre-twiddle's 1/4 term). The
reference test is committed as #[ignore] with its measurements in the
message so it stays runnable. Ogg stays flagged unplayable in the asset
index; WAV and the --transcode path work today.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A room of devices now plays one game: host simulates, clients send input
and render replicated truth.
- libs/game/sim/player.rs: Players roster on GameWorld, slot 0 is always
this device. Player 0's input stays in the world's original held/pressed/
pad/cam_yaw fields and is mirrored into the roster, which is what keeps
single-player numerics bit-identical (tape gate confirms). Ids are never
reused, so a stale reference resolves to None, never to somebody else
- The camera-movement knot resolved: world.player_move(p) rotates that
player's axes by THEIR cam_yaw, carried in their input packet. Player 0's
branch is the original expression character-for-character (the f32 cos
widened to f64 kept deliberately — tidying it would move the numbers)
- libs/game/session/replication.rs: Shared = pos/vel/size/kind/tag;
Derived = facing/anim/scale/glow/blob shadows, recomputed client-side and
costing zero wire bytes; Local = camera/audio/effects. Statics never enter
the per-tick stream
- Protocol (additive, version unchanged): EntityDesc + Descriptors message
splits rare reliable construction data from volatile unreliable state —
without it a joiner sees poses for entities it cannot build. EntityDesc
carries pos because statics never appear in the state stream (the
late-joiner test caught ground arriving at the origin)
- Script: game.players/player_name/player_entity/player_input/bot/on_join/
on_leave; blocks gained owner: PlayerId so a car reads its own driver
- Arcade: ARCADE_HOST=1 / ARCADE_JOIN=<addr>; clients skip world
construction and don't simulate
- Racing wire volume, 6 players x 60Hz x 200 entities: 2400 pps,
20.9 Mbit/s up (audit projected 74 Mbit for the XR stack). Asserted in
racing_scenario_wire_volume_fits_a_living_room
Two more not-a-playable-default bugs, same class as M1b's rng-at-zero:
Entity::default() leaves gravity_scale 0 (weightless wheels) and ground
without friction gives no traction — only the DSL path filled these in.
255 tests green; tape probe BYTE_IDENTICAL; xr/arcade/gamemaker build.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The thesis proof: examples/gamemaker/resources/fixtures/racing.splash is a
complete playable racing game in 72 lines (12-corner oval from a waypoint
loop, 4 cars, gates, standings, restart) with no physics, no AI and no lap
bookkeeping in script. Blocks run engine-side at 60Hz: Blocks::pre_step
(intent -> motion) before step_world, post_step after; Blocks is Clone and
snapshots beside GameWorld so a failed eval rolls both back together.
- game.car (4 suspension raycasts on a box3d rigid chassis), game.character
(drives the existing mover sweep + owns idle/walk/run blending),
game.plane; game.drive/autodrive/speed
- Brains: game.wander/chase/patrol/caught — the fixture's hand-rolled AI,
absorbed engine-side
- Race kit: spawnpoint, checkpoint, place, race, standings, lap/rank/
finished, score/score_of (Shared-tier data, ready for replication)
raycast_vehicle audit (defects documented in car.rs, still live in xr):
libm sin/cos in steering (unreplicable), a wrong side-impulse denominator
(iaj.dot(iaj) where it should be (I^-1 aJ).aJ), and an unguarded division.
Kept the structure, replaced Bullet's friction solver with an arcade force
model: suspension acts at the contact point, grip and drive through the
centre of mass, steering as yaw torque — no lateral force can generate
roll, so it is stable by construction rather than by roll_influence fudge.
Engine bug fixed at the source: GameWorld::new() left rng at 0 and
xorshift64* is a fixed point at zero, so rand() returned 0 forever for any
world built through the sim API. reset_content seeds on every eval, which
hid it from gamemaker entirely; found by a wander brain that never left home.
Tape probe byte-identical; racing fixture evals clean and drives (AI follow
the line, gates bank in order).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>