benjivers/val Public Public: anyone can read this; the crew can change it.

Separate the engine from the game, so the next game can start where this one ended #23

Merged opened this in answer to

shipped production is serving it deploy b42aa0d

opened this

Draws the line between the machinery and Valorant: the engine now lives under src/engine/ and static/engine/, the game imports it and never the other way round, and a test keeps it so. Nothing on the wire changes.

What is engine, and how big. 16 modules, 2,579 lines, with 1,854 lines of their own tests under src/engine/:

  • Server: glide.ts and aim.ts moved as they were. grid.ts is the tile map's mechanics with the rows taken out as a parameter, so src/map.ts is now just this map's rows and what its letters mean, re-exporting the bound functions under their old names. arena.ts is the shape of a served match: the idle gate, seats rationed per address, the reload-safe release, the positionless spectator view, the per-viewer vision filter and the sticky snapshot field. It takes the game as callbacks (step, sit, stand, visible, snapshot shape, spectator shape, command), and src/arena.ts is the Valorant binding with the same exports.
  • Client: interp, predict, presence, yaw, steer, raycast, slices, resolution, budget, gun moved as they were. scene.js and topdown.js were the 90% cases: the engine halves draw walls, fog, bodies, marks and tracers and take the game's props, fog discs, trim colour and badge through hooks; thin game files at the old paths supply the spike, orbs, zones and agent colours.

The boundary is tested. src/engine/boundary.test.ts scans every import under both engine folders, tests included, and fails on any that resolves outside them, plus a scan for this game's words in engine code. It caught one hole while being written: presence.js was carrying an agent field onto last-seen marks that nothing drew. That field is gone, and it is the only observable change.

Same numbers. 297 tests green (the 287 plus 7 for the served-match rules over a toy game and 3 for the boundary). With the clock pinned so both arenas play the same seeded round, a seat's snapshot is byte-identical to main (3,816 B, same hash) and so is the spectator's (352 B). Own-body step spread 0.0%, others 7.2%, one camera correction over 12 rounds, blind 0.74% of live player-seconds: all as before.

What is honestly not general. The Seat type keeps its board field name for the sticky-field cache, since a test constructs it by hand. The buy, agent and input commands are validated in the game arena, as they should be. server.ts stays where AGENTS.md says the security floor lives; a new game would copy it, not import it.

What a new game would still have to write. Its rows and what their letters mean; its player and match state, rules and round structure; who sits where when a person takes a seat; its bots' decisions (the aim model is provided, the target choice is not); the shape of its snapshot and spectator view; its command vocabulary; the props it puts on the floor and the badges on a body; its HUD words, its page and its terminal. Roughly the 4,468 lines that stayed under src/ and static/.

First ten seconds in the preview. Open the page, press V to switch views, Tab to hold the map, walk into a wall in first person and strafe along it. Everything should feel exactly as it did, because it is the same code running from a different folder.

🤖 Generated with Claude Code

Closes #22

Sign in to join this conversation.

↑↓ navigate ↵ open esc close

Keyboard shortcuts