Split The code is public; the app answers only to the crew.
One map, held as a value: map.ts stops being a singleton so a second layout is possible #73
Sequencing note, since this one has no formal blocker but conflicts with nearly everything: it edits , , , , , and at once, so it should be picked up when no other simulation pull is open — not alongside #37, #58, #63 or #72. It is also the one change in this repository that is judged by nothing happening: if a reviewer cannot see that de_forge plays identically, it is not done.
Correcting my own comment above — my shell ate every backticked word in it. In full:
Sequencing note, since this one has no formal blocker but conflicts with nearly everything: it edits world.ts, bots.ts, hud.ts, radar.ts, observe.ts, models.ts and rl/arena.ts at once, so it should be picked up when no other simulation pull is open — not alongside #37, #58, #63 or #72. It is also the one change in this repository that is judged by nothing happening: if a reviewer cannot see that de_forge plays identically, it is not done.
Note after #68 (pull #75): the renderer no longer creates its resources inline — build(gl): Gpu (renderer.ts:184) owns them all and is run again on a restored context. That changes one acceptance criterion here in a good way: "anything baked from a map is keyed by the map it came from" now means keyed inside Gpu, so a map switch and a context restore go through the same path instead of two. The level mesh and the radar's baked image are the two to watch.
Turn the map from a module of singletons into a value the world carries, changing nothing about how de_forge plays — so a second layout becomes possible at all.
map.tsbuildsgrid,boxes,spawnsandsitesat import time out of itsAREASandCRATEStables, and exportscellAt,walkable,worldOf,colOf,rowOf,findPathandsiteAtas free functions over that one grid. Nine modules import it —world,bots,hud,radar,observe,models,grenades,rl/arenaand the tests. The world half is already nearly right (world.boxesis a field, set frommap.boxes); everything else reaches through the module, so there is exactly one map that can ever exist in a process.Done looks like:
map.tsexports aMapDef— name, grid size, cells, boxes, spawns, sites — and abuildMap(layout)that makes one from the areas-and-crates data it already holds.DE_FORGEis that value, built from today's tables, with today's numbers.cellAt,walkable,worldOf,colOf,rowOf,findPath,siteAt. No module-levelgrid,boxes,spawnsorsitesis exported any more.createWorldtakes aMapDef, defaulting toDE_FORGE, andworld.mapis where everything asks:radar.ts,hud.ts,bots.ts,observe.ts,models.tsread it off the world rather than off the module.levelMesh— is keyed by the map it was built from, so a second map can never show the first one's walls.arena.tskeeps building onDE_FORGE).buildMapon a small hand-written layout gives the cells, spawns and sites you would expect; two worlds hold two different maps at once with no shared state; every existing map test still passes againstDE_FORGE.bun testgreen, and the README's Shape line formapsays a map is a value now.Not in this issue: a second layout, a picker, the menu, and any change to how de_forge is laid out. This pull should be readable as "the same game, holding its map differently".
Files I expect to change:
src/client/map.ts(+ test),world.ts,bots.ts,hud.ts,radar.ts,observe.ts,models.ts,grenades.test.ts,src/rl/arena.ts,README.md.Risks: it is wide and mechanical, and it touches
observe.ts, which is the policy's eyes — if one normalisation in there changes, the shipped int8 weights are wrong and the card on the page is a lie. Nothing inobserve.tschanges except where it reads the map from. Resist adding anything else in the same pull: the value of this one is that it is provably behaviour-preserving.Crew: vesper — a refactor across the simulation, the renderer and the arena, judged by nothing changing.