Repository navigation
Stock a floor with safe rooms and healing things when it is laid out - #99
Merged
Merged
Conversation
A floor's row now names its supplies: floors.safe_rooms, and floors.healing_count with healing_name and healing_description. Laying the floor out draws them last, off the place's own generator, so a floor with none is laid out exactly as before: first the safe rooms, from every room but the entry, the way-down room and the guards' room, then one room each for the healing things, from every room but the entry. A safe room is written safe with no hazard, population nobody and locations.sanctuary set, and nobody is born in a sanctuary whatever its population says. Each healing thing is a world row lying in its room (use_kind healing, bulk handy, placed as any thing is), marked with items.supply = 'floor' and no kit_key. It is written once, with the floor: no later turn of any game stocks the floor again, and a game takes its own copy, which keeps the marker. realization::bespoke leaves supply rows out, so they count against neither a room's three things nor the story's thing limit, and the room is still furnished from its sort. Already Here lists them among the loose things, as it lists the kit's. The glance's here gains safe: for a sanctuary, the engine's line "A safe room: nothing hostile is born here, and resting here heals." No request carries it, and nothing tells the room writer a room is safe. Migration 20261031150000 adds the four floors columns, locations.sanctuary and items.supply, applied to every world dump as Rails' SQLite adapter applies it. store::OPTIONAL_COLUMNS holds all six, so the game at its current pin still opens and plays; SCHEMA_VERSION and shape.txt do not move. 33 records files gain only "supply": null on the item rows and "sanctuary": 0 on the location rows they write; no golden moved, and the fight simulator's reference fights are unchanged. New world a-stocked-floor (the game's loader at server 383a67d, then the migration and the supplies set) and script a-floor-stocked-at-layout, in PASSING with a golden and records.
… b696798 The game's pin PR (renderedstep/server#375) reads safe_rooms: and healing:, so the dump is now the one its own loader writes from a schema load: the supplies are written by the loader rather than by hand. The rows are the same; only timestamps, the schema digest and the order the dump lists them in moved. No golden or records file moves.
calebl
added a commit
to renderedstep/server
that referenced
this pull request
Oct 30, 2026
calebl
added a commit
to renderedstep/server
that referenced
this pull request
Oct 30, 2026
Pin renderedstep/engine#99's head (e5c28ab): stock a floor with safe rooms and healing things when it is laid out
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stage 7 of the dungeon plan, engine half: floor supplies, healing items and safe rooms.
floors.safe_rooms, andfloors.healing_countwithhealing_nameandhealing_description.safe, with no hazard,population: nobodyandlocations.sanctuaryset. Nobody is born in a sanctuary, whatever its population says.use_kind: healing,bulk: handy, the supply markeritems.supply = 'floor', and nokit_key. It counts against neither the room's three things nor the story's thing limit, and its room is still furnished from its sort.A world without the new keys lays out, plays and prompts byte for byte as before. No model was called; every command ran with
OPENROUTER_API_KEYandTYPESAFE_API_KEYunset.The words
The plan's §4.3 draft, verbatim (
glance::SAFE_ROOM):here.safe, while the player stands in a room whoselocations.sanctuaryis set. It isNoneeverywhere else.tests/floor_supplies.rswalks every room of the floor and checks that no request containssafe room,sanctuaryorresting here heals. The sweep's safe-room reply assertsprompt_excludes: [safe room, sanctuary].Requests that change because a supply row is now in the world
No existing request changes: no world here, and no world the game seeds, has a supply row yet. Every stored records file keeps its requests byte for byte.
In a world that has supplies, three existing lines carry the healing thing's name. No template changed. These are the exact before/after lines, from the new sweep's walk replayed on the same world with its supplies set to zero (
before) and as stocked (after).kit_key, so it is among the names this line lists, as any thing a world file seeds is today (realization.rsitems_instructions, thetakenlist). It is listed once per row, so two draughts are named twice:taken, or their names de-duplicated), and it should go to the owner with the room-writer line. Either way the room writer cannot reuse the name: the registry already refuses a thing whose name matches a world thing with nokit_key.Fixed in place: rock shelf, rubble. In the rubble: shard of pottery. +Loose, and could be picked up: red draught. Do not list any of them again as a thing lying here.sparseroom can), the whole "Already Here, Decided By The Game" block now appears for the draught alone., besides the ones Already Here above: those are written already, and a thing named again is not a new one.data/location/generator.ymlnobody_here), because nobody is born there:Nothing else in any request moved. In particular, the allowance line still reads
List AT MOST 3 portable thingsin a room holding a draught.What changed
floor.rs.Floorgainssafe_roomsandhealing.Laidgainssafeandhealing: room indices, in the order drawn.safewith no hazard. Then one room each for the healing things is drawn, from every room but the entry.floorsrow, lays out exactly as before.turn/told/realize.rs.lay_outreads the floor row's supplies, then writes each safe room withpopulation: nobodyandsanctuary: true.leave_supplywrites one world item per healing room, placed byspot::placeas any thing is. It runs after the doorways and the guards.healing_nameleaves nothing.slotsdraws nobody for a sanctuary, whatever its population says, so asanctuary: truethe world file sets on any location holds too.furnishis unchanged: it skips a room only when the room holds a kit row, and a supply row has nokit_key, so a stocked room is still furnished.realization.rs.FLOOR_SUPPLY("floor").bespokeleaves out a row withsupply. That one predicate is whatitems_instructions(the allowance it offers) andadmit_items(what it admits) count, so the room's 3 and the story's limit both exempt supplies.already_herelists supply rows among the loose things.turn.rs. A game's copy of a thing carriessupply. The copy names every column it copies, and would otherwise panic on the new one.glance.rs.Here::safeandSAFE_ROOM.store.rs.OPTIONAL_COLUMNS, so a database with or without them opens.SCHEMA_VERSIONandshape.txtdo not move.AddSuppliesToFloors), applied to all 35 world dumps inparity/worlds/. I used the same method as stages 5 and 6:bin/rails db:migrateran on a copy of it in a scratch checkout of the game atSERVER_COMMIT(383a67d), holding only this migration.20261029154235, its version row was added before the run and deleted after.floors:"safe_rooms" integer DEFAULT 0 NOT NULL,"healing_count" integer DEFAULT 0 NOT NULL,"healing_name" varcharand"healing_description" text;"sanctuary" boolean DEFAULT FALSE NOT NULLonlocations;"supply" varcharonitems;20261031120000, the latest migration atSERVER_COMMIT. Check it against a freshly pulled main before the pin PR.floorandglancerows of the module table.Things a reviewer should know
safe_roomsis capped at the rooms other than the entry, the way down and the guards' room.healingis capped at one per non-entry room.safe_rooms ≤ rooms − 2) is the server's to enforce.admit_known). That is the doctor finding below.supply. A copy has aplaythrough_id, so no cap counts it either way. The marker says where the thing came from.What did not move
parity --check parity: AGREED 109 of 113. The other four are runner-only, as before."supply": nullon the item rows and"sanctuary": 0on the location rows they write. I checked every one: with those two keys removed, each file is identical tomain's.fight --check sim: AGREED, 10 fight goldens, unchanged. Nothing was re-blessed. The simulator's worlds have no floors, so nothing is stocked in them.Sweep
New world
a-stocked-floorand scripta-floor-stocked-at-layout, inPASSINGwith a golden and a records file.How the world was made. I loaded the world file below with the game's loader at
SERVER_COMMIT(383a67d), withoutsafe_rooms:andhealing:, which that loader does not read. Then the migration ran, and oneUPDATE floorssetsafe_rooms = 1, healing_count = 2, healing_name = 'red draught', healing_description = …on The Stocked Floor, which is what the new loader will write. As with stages 2–6, rebuild the dump from the pin branch's own loader once it reads the keys.The world. Odile (level 3, d8) stands on a landing above The Stocked Floor: four rooms, uneasy, no hazard, kind
warren(so its rooms are caves and furnished from the cave kit),safe_rooms: 1, two red draughts. Its layout makes room 4 the safe room and leaves a draught in room 2 and room 4.a-floor-stocked-at-layout(8 steps):coin, rock shelf, rubble, stalagmite). Its request has noLoose, and could be picked up: red draught.Loose, and could be picked up: red draught.andList AT MOST 3 portable things.hereis the draught, the kit (rock shelf, rubble, shard of pottery) and the three./take red draught:carrying: [red draught]. This is the game's copy.Loose, and could be picked up: red draughtandWrite NOBODY into this place., and excludessafe roomandsanctuary.present: [].hereis the draught and the kit.The records file holds the rows the layout wrote: exactly one location with
sanctuary: 1, and exactly two items withsupply: "floor",kit_key: null,use_kind: "healing",bulk: "handy", each placed. The glance's safe line does not show in the sweep (the dump has no key for it);tests/floor_supplies.rsasserts it. I checked everyexpect:against the engine's dumps: 15 expectations, 0 mismatches.Tests
floor.rs.a_floor_is_laid_out_to_its_count_and_holds_every_rulenow stocks every floor at the plan's rates (1 safe room per 20 rooms, 1 draught per 16), over every count from 1 to 200 and some larger, over many places. It checks:safewith no hazard.supplies_are_drawn_last_and_move_no_other_draw: a stocked floor's doorways, way down, guards' room and rooms are the bare floor's, but for each safe room's danger and hazard; a floor with none draws nothing. A two-room floor with a way down has no room for a safe room and one for a draught.tests/floor_supplies.rs(new; live path with replayed replies):safe, no hazard,nobody).here.safeisSAFE_ROOMin room 4 andNonein rooms 1–3. No request of the walk containssafe room,sanctuaryorresting here heals. The safe room's request carries the nobody-here instruction.things_limitis set to 1. Without the exemption, the two draughts would make the room writer's allowance 0.AT MOST 1 portable thing, and lists the draught as loose.a crowd; the request still says nobody, and a person in the reply is not admitted.tests/engine.rs).floors.safe_roomsdeclared nullable, alocations.sanctuarydeclaredvarchar, or anitems.supplydeclaredbooleanis refused.every_world_has_the_shape_the_engine_is_written_againstholds for all 36 dumps.Local runs, all with both provider keys unset:
cargo fmt --check,cargo clippy --workspace --all-targets --locked -- -D warningsandcargo test --workspace --locked: all pass.parity --check parity: 109 of 113 agree (the four runner-only, as before).fight --check sim: AGREED, 10 fight goldens.SERVER_COMMIT(383a67d), on its own schema (no new column), with the extension built from this checkout:parity/runner.sh: 112 of 113 agree; the new script is "not in the checkout yet";engine:rust_gates: PASSED for 112 scripts (goldens, invariants, doctor and audit).SERVER_COMMITdoes not move. The game's extension builds against this checkout unchanged:glance::Hereonly gains a field;glance_jsonbuilds the room's JSON by name, soPlaythrough::Glance::Room(Data.define(:id, :name, :within)) is not handed a key it does not know.What the server pin PR must change
Line numbers are at
383a67d.Migration
db/migrate/20261031150000_add_supplies_to_floors.rb. Use exactly this version and these declarations, so the dumps here stay byte-true;store::OPTIONAL_COLUMNSholds all six.db/schema.rbmoves to2026_10_31_150000.Models, each with a model test:
Floorvalidatessafe_roomsandhealing_countas integers ≥ 0, and requireshealing_nameandhealing_descriptionwhenhealing_count > 0.ItemgainsSUPPLIES = %w[floor], validatessupplyin it allowing nil, and#supply?.Loader (
lib/world_seed/loader.rb). Thefloor:keys gainsafe_roomsand ahealing:block (count,name,description), written in both directions on every load; absent writes0/0, nil, nil. Refuse:safe_rooms;safe_roomsaboverooms − 2(the plan's rule; the engine clamps whatever it is given);healing:that is not a mapping;count;nameordescriptionwhencount > 0.Every location gains a generic
sanctuary: true(boolean; absent writes false), for any world, not only floors.Re-seed rule: as with
guards, a re-seed never writes, moves or removes a supply row, or unsetssanctuaryon a room, of a floor already laid out. Those are layout rows, not file rows (Item::TemplateRefreshmust leavesupplyrows alone, as it leaves kit rows).Tests:
Exporter (
lib/world_seed/exporter.rb:598). Emitsafe_roomswhen it is non-zero,healing: {count, name, description}when the count is non-zero, andsanctuary: trueon any location that has it, except a floor's own laid-out rooms, which the exporter does not write. Round-trip test.Registries' exemption.
Item.bespoke(app/models/item.rb:376) becomesportable.where(kit_key: nil, supply: nil), soItem::Registry#room_for(registry.rb:138) and#story_item_count(:176) leave supplies out, as the engine'srealization::bespokedoes.Item::NOT_COPIED(item.rb:587) already copies every other column, so a game's copy carriessupply. Check thatStory::Fork,Story::SnapshotandPlaythrough::SqlitePackagecopylocations.sanctuaryanditems.supply; they do if they copy columns generically.things_limit: 1, with two supply rows lying in the room.Doctor findings (report, never repair), with tests:
sanctuary_with_a_hostile_resident(warning): a location withsanctuarywhere a hostile character stands, world or game (a livinghostilecharacter there, or one a game's NPC state makes hostile).supply_with_a_kit_key(error): a supply row with akit_key, whichfurnishwould read as the room's kit.floor_stocked_to_another_count(warning): a laid-out floor whose rooms hold a different number of its supply rows or sanctuaries than its row says. A re-seed changed it after layout.Protocol and glance (the front end's rendering is stage 9, out of scope here):
glance_json(ext/renderedstep/src/lib.rs:1349) adds"safe": here.safeto theroomobject.Playthrough::Glance::Room(glance.rb:30) gains:safe, and its consoleto_s(:136) prints it on its own line.Protocol::V1#glance(protocol/v1.rb:83):room: room && { name:, within:, safe: room.safe }.docs/protocol/v1/openapi.json: the room object gainssafe(["string","null"], "The engine's line for a safe room, which nothing hostile is born in; null elsewhere."), required, in the examples, with the minor revision note.test/contract/protocol_v1_test.rbcovers it.Sweep.
lib/engine_sweep/worlds/a-stocked-floor.ymlandlib/engine_sweep/scripts/a-floor-stocked-at-layout.yml, with their comments andwhy:notes (below).test/engine_parity/a-floor-stocked-at-layout.checks.json.test/ruby_loop_coverage.ymlif it must list it.engine:rust_gateson it: the doctor and the audit must say what its checks file says.parity/worlds/a-stocked-floor.sqlhere from that branch's loader.Move the pin to this commit.
Out of scope, as the brief says: rest (stage 8; the line's "resting here heals" is true once stage 8 lands), the front end, the room-writer prompt line (C4 b's prompt half), wardens, and any prompt change.
lib/engine_sweep/worlds/a-stocked-floor.yml
lib/engine_sweep/scripts/a-floor-stocked-at-layout.yml