Give the game a catalogue of maps with one map in it, chosen in the menu and remembered like difficulty, so a second layout is a table rather than a refactor.
map.ts:211 is export const DE_FORGE: MapDef = buildMap(DE_FORGE_LAYOUT) — the map is already a value (#73), but there is no list, no name-to-map lookup, and settings.ts (28, 121) validates difficulty and knows nothing about a map. createWorld takes the map it is handed, so everything downstream is ready; what is missing is the seam a player chooses through. Build it against de_forge alone: nothing new to play, one new thing to choose.
Done looks like:
[ ] map.ts exports a catalogue — MAPS (name → MapDef, built once), MAP_NAMES, DEFAULT_MAP and mapFor(name) returning the default for anything unrecognised. Nothing builds a MapDef at module level twice.
[ ] settings.ts gains map, guarded exactly as difficulty is: an unknown string in localStorage falls back to the default and never reaches createWorld. Tested with junk, a number, and a name that was valid last week.
[ ] A Map select beside the Difficulty select, rendered server-side in pages/game.ts, its id in the contract main.test.ts checks (#99), and the pause overlay saying which map you are on.
[ ] A saved match records its map: one column in db.ts with a migration that leaves old rows readable and defaults them to de_forge, shown in the recent-matches list on the page and in the terminal's matches tab.
[ ] /api/match accepts a map name only from the fixed list — an unknown name is a 422 from parseResult, never a row.
[ ] Tests that fail without it: mapFor("nonsense") is the default; a result naming a map that does not exist is refused; the select's id is in the contract test.
Files:src/client/map.ts, src/client/settings.ts, src/pages/game.ts, src/client/main.ts, src/result.ts, src/db.ts, src/tty.ts and their tests.
Risks: the migration is the only irreversible part — add the column, default it, never rewrite an existing row. A select with one option looks odd; it stops looking odd the day the second layout lands.
Crew: scout — page, settings, schema and validation, which is the shape of everything scout has landed here.
Give the game a catalogue of maps with one map in it, chosen in the menu and remembered like difficulty, so a second layout is a table rather than a refactor.
map.ts:211isexport const DE_FORGE: MapDef = buildMap(DE_FORGE_LAYOUT)— the map is already a value (#73), but there is no list, no name-to-map lookup, andsettings.ts(28, 121) validatesdifficultyand knows nothing about a map.createWorldtakes the map it is handed, so everything downstream is ready; what is missing is the seam a player chooses through. Build it against de_forge alone: nothing new to play, one new thing to choose.Done looks like:
map.tsexports a catalogue —MAPS(name →MapDef, built once),MAP_NAMES,DEFAULT_MAPandmapFor(name)returning the default for anything unrecognised. Nothing builds aMapDefat module level twice.settings.tsgainsmap, guarded exactly asdifficultyis: an unknown string in localStorage falls back to the default and never reachescreateWorld. Tested with junk, a number, and a name that was valid last week.pages/game.ts, its id in the contractmain.test.tschecks (#99), and the pause overlay saying which map you are on.db.tswith a migration that leaves old rows readable and defaults them tode_forge, shown in the recent-matches list on the page and in the terminal's matches tab./api/matchaccepts a map name only from the fixed list — an unknown name is a 422 fromparseResult, never a row.mapFor("nonsense")is the default; a result naming a map that does not exist is refused; the select's id is in the contract test.Files:
src/client/map.ts,src/client/settings.ts,src/pages/game.ts,src/client/main.ts,src/result.ts,src/db.ts,src/tty.tsand their tests.Risks: the migration is the only irreversible part — add the column, default it, never rewrite an existing row. A select with one option looks odd; it stops looking odd the day the second layout lands.
Crew: scout — page, settings, schema and validation, which is the shape of everything scout has landed here.