Skip to content

Stock a floor with safe rooms and healing things when it is laid out - #99

Merged
calebl merged 3 commits into
mainfrom
fm/ta-floors-s7-engine
Oct 30, 2026
Merged

calebl merged 3 commits into
mainfrom
fm/ta-floors-s7-engine

Conversation

@calebl

@calebl calebl commented Oct 30, 2026

Copy link
Copy Markdown
Contributor

Stage 7 of the dungeon plan, engine half: floor supplies, healing items and safe rooms.

  • A floor's row names its supplies: floors.safe_rooms, and floors.healing_count with healing_name and healing_description.
  • They are placed when the floor is laid out, and never again (VISION.md principle 7). Layout happens once per world, on the first step anybody takes onto the floor. No later turn of any game stocks the floor again. A game that takes a healing thing takes its own copy of it.
  • A safe room is laid out safe, with no hazard, population: nobody and locations.sanctuary set. Nobody is born in a sanctuary, whatever its population says.
  • A healing thing is a world row lying in its room: use_kind: healing, bulk: handy, the supply marker items.supply = 'floor', and no kit_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.
  • The player learns a room is safe from engine copy on the room panel. No model is told. The room writer is not told about safe rooms in this change; that prompt line is a separate change for the owner.

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_KEY and TYPESAFE_API_KEY unset.

The words

The plan's §4.3 draft, verbatim (glance::SAFE_ROOM):

A safe room: nothing hostile is born here, and resting here heals.

  • Where it shows: the glance's room, as here.safe, while the player stands in a room whose locations.sanctuary is set. It is None everywhere else.
  • Never in a request. tests/floor_supplies.rs walks every room of the floor and checks that no request contains safe room, sanctuary or resting here heals. The sweep's safe-room reply asserts prompt_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).

  1. "What Is Lying Here", the last bullet: every room of the story. A supply row has no kit_key, so it is among the names this line lists, as any thing a world file seeds is today (realization.rs items_instructions, the taken list). It is listed once per row, so two draughts are named twice:
    -- Never name it after a person or after a place. Already spoken for in this story, so do not reuse: Odile
    +- Never name it after a person or after a place. Already spoken for in this story, so do not reuse: Odile, red draught, red draught
    Flag for the owner. On the dungeon's floor 1 (10 draughts) this repeats the name ten times. The list is capped at 20 thing names, so the draughts can push real names out of it. I left it alone because the brief rules out changing model-read text. The fix would be one filter (supply rows out of 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 no kit_key.
  2. "Already Here", in a room holding a healing thing. It is listed among the loose things, as the kit's loose things are:
     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.
    • In a room whose kit drew nothing (a sparse room can), the whole "Already Here, Decided By The Game" block now appears for the draught alone.
    • With it, the "What Is Lying Here" first line gains its existing , besides the ones Already Here above: those are written already, and a thing named again is not a new one.
    • Neither happened in the sweep: every room of it is furnished.
  3. A safe room gets the existing nobody-here instruction instead of the people block (data/location/generator.yml nobody_here), because nobody is born there:
     ## Who Is Here
    -Write EXACTLY 3 people who are in this place right now.
    -… (the people block and "Their Four Objects of Desire")
    +Write NOBODY into this place. There is nobody here, and the description
    +should read like somewhere nobody is standing.

Nothing else in any request moved. In particular, the allowance line still reads List AT MOST 3 portable things in a room holding a draught.

What changed

  • floor.rs.
    • Floor gains safe_rooms and healing.
    • Laid gains safe and healing: room indices, in the order drawn.
    • Both are drawn from the place's own generator, last, after the guards' room. First the safe rooms are drawn, from every room but the entry, the way-down room and the guards' room. Each safe room is set to danger safe with no hazard. Then one room each for the healing things is drawn, from every room but the entry.
    • Each draw takes distinct rooms, and the count is clamped to the rooms there are. A count of 0 throws no die. So every floor without supplies, and every place without a floors row, lays out exactly as before.
  • turn/told/realize.rs.
    • lay_out reads the floor row's supplies, then writes each safe room with population: nobody and sanctuary: true.
    • A new leave_supply writes one world item per healing room, placed by spot::place as any thing is. It runs after the doorways and the guards.
    • A healing count with no healing_name leaves nothing.
    • slots draws nobody for a sanctuary, whatever its population says, so a sanctuary: true the world file sets on any location holds too.
    • furnish is unchanged: it skips a room only when the room holds a kit row, and a supply row has no kit_key, so a stocked room is still furnished.
  • realization.rs.
    • FLOOR_SUPPLY ("floor").
    • bespoke leaves out a row with supply. That one predicate is what items_instructions (the allowance it offers) and admit_items (what it admits) count, so the room's 3 and the story's limit both exempt supplies.
    • already_here lists supply rows among the loose things.
  • turn.rs. A game's copy of a thing carries supply. The copy names every column it copies, and would otherwise panic on the new one.
  • glance.rs. Here::safe and SAFE_ROOM.
  • store.rs.
    • All six new columns join OPTIONAL_COLUMNS, so a database with or without them opens.
    • SCHEMA_VERSION and shape.txt do not move.
    • The game at its current pin opens and plays as before: nothing is stocked there.
  • Migration 20261031150000 (AddSuppliesToFloors), applied to all 35 world dumps in parity/worlds/. I used the same method as stages 5 and 6:
    • Each dump was first checked to re-dump byte-identically.
    • Then bin/rails db:migrate ran on a copy of it in a scratch checkout of the game at SERVER_COMMIT (383a67d), holding only this migration.
    • For the 29 dumps without 20261029154235, its version row was added before the run and deleted after.
    • Each dump now has:
      • on floors: "safe_rooms" integer DEFAULT 0 NOT NULL, "healing_count" integer DEFAULT 0 NOT NULL, "healing_name" varchar and "healing_description" text;
      • "sanctuary" boolean DEFAULT FALSE NOT NULL on locations;
      • "supply" varchar on items;
      • the version row.
    • The version is the latest Rails accepts today: it refuses a timestamp more than a day ahead. It follows 20261031120000, the latest migration at SERVER_COMMIT. Check it against a freshly pulled main before the pin PR.
  • README. The floor and glance rows of the module table.

Things a reviewer should know

  • The draws come after the guards, not before them as the plan's §1.3 list ordered them.
    • Put last, they move none of stage 3's draws, so every guarded floor already laid out in a sweep is unchanged.
    • The safe-room draw can then leave out the guards' room. A posted guard may be hostile, and a safe room says nothing hostile is born there.
  • A healing room may also be a safe room, or the way-down room. The plan excludes only the entry, and the sweep's room 4 is both safe and stocked.
  • Counts larger than the rooms are clamped, not refused.
    • safe_rooms is capped at the rooms other than the entry, the way down and the guards' room.
    • healing is capped at one per non-entry room.
    • The plan's loader rule (safe_rooms ≤ rooms − 2) is the server's to enforce.
  • "Nothing hostile is born here" holds on the engine's side for any sanctuary: no slots are drawn. It does not stop a story character the world file stands there, or one a room writer names (admit_known). That is the doctor finding below.
  • A game's copy of a draught keeps supply. A copy has a playthrough_id, so no cap counts it either way. The marker says where the thing came from.

What did not move

  • No golden.
    • parity --check parity: AGREED 109 of 113. The other four are runner-only, as before.
  • 33 records files gain only "supply": null on the item rows and "sanctuary": 0 on the location rows they write. I checked every one: with those two keys removed, each file is identical to main'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-floor and script a-floor-stocked-at-layout, in PASSING with 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), without safe_rooms: and healing:, which that loader does not read. Then the migration ran, and one UPDATE floors set safe_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):

  1. Onto the floor. The floor is laid out and stocked. Room 1, the entry, holds only its kit (coin, rock shelf, rubble, stalagmite). Its request has no Loose, and could be picked up: red draught.
  2. Room 2: a draught, and its kit.
    • Its request includes Loose, and could be picked up: red draught. and List AT MOST 3 portable things.
    • The reply lists three things, and all three are admitted.
    • here is the draught, the kit (rock shelf, rubble, shard of pottery) and the three.
  3. /take red draught: carrying: [red draught]. This is the game's copy.
  4. Room 3 holds no draught, and its request lists none lying there.
  5. Room 4, the safe room.
    • Its request includes Loose, and could be picked up: red draught and Write NOBODY into this place., and excludes safe room and sanctuary.
    • present: [].
    • here is the draught and the kit.
  6. A second game walks onto the floor. It is not written again (no room request), and room 1 is as it was.
  7. The second game in room 2 finds the draught the first game took still lying there (the world's row), and only one.

The records file holds the rows the layout wrote: exactly one location with sanctuary: 1, and exactly two items with supply: "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.rs asserts it. I checked every expect: against the engine's dumps: 15 expectations, 0 mismatches.

Tests

  • floor.rs.
    • a_floor_is_laid_out_to_its_count_and_holds_every_rule now 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:
      • the clamped counts;
      • distinct rooms;
      • never the entry;
      • a safe room never the way down or the guards' room;
      • every safe room safe with no hazard.
    • New 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):
    • Stocked once.
      • One sanctuary (room 4: safe, no hazard, nobody).
      • Two draughts with the marker, each in its own room, neither the entry, each placed.
      • A second game writes no room and leaves every row the layout wrote as it was, with still exactly two draughts.
    • The room panel says so, and no request does. here.safe is SAFE_ROOM in room 4 and None in rooms 1–3. No request of the walk contains safe room, sanctuary or resting here heals. The safe room's request carries the nobody-here instruction.
    • No limit counts a draught, and its room is still furnished.
      • The story's things_limit is set to 1. Without the exemption, the two draughts would make the room writer's allowance 0.
      • Room 2's request still offers AT MOST 1 portable thing, and lists the draught as loose.
      • Of two things offered, one is admitted.
      • The room still holds its kit rows.
    • A safe room births nobody whatever its population says. Its population is set to a crowd; the request still says nobody, and a person in the reply is not admitted.
    • A database from before the migration lays the floor out bare: no safe room, no draught. It plays four rooms and copies their things.
  • Store (tests/engine.rs).
    • A database from before the migration opens and plays.
    • A floors.safe_rooms declared nullable, a locations.sanctuary declared varchar, or an items.supply declared boolean is refused.
    • every_world_has_the_shape_the_engine_is_written_against holds for all 36 dumps.

Local runs, all with both provider keys unset:

  • cargo fmt --check, cargo clippy --workspace --all-targets --locked -- -D warnings and cargo 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.
  • CI's runner job, run locally against the game at 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_COMMIT does not move. The game's extension builds against this checkout unchanged:

  • glance::Here only gains a field;
  • the extension's glance_json builds the room's JSON by name, so Playthrough::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.

  1. 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_COLUMNS holds all six. db/schema.rb moves to 2026_10_31_150000.

    class AddSuppliesToFloors < ActiveRecord::Migration[8.1]
      def change
        add_column :floors, :safe_rooms, :integer, default: 0, null: false
        add_column :floors, :healing_count, :integer, default: 0, null: false
        add_column :floors, :healing_name, :string
        add_column :floors, :healing_description, :text
        add_column :locations, :sanctuary, :boolean, default: false, null: false
        add_column :items, :supply, :string
      end
    end
  2. Models, each with a model test:

    • Floor validates safe_rooms and healing_count as integers ≥ 0, and requires healing_name and healing_description when healing_count > 0.
    • Item gains SUPPLIES = %w[floor], validates supply in it allowing nil, and #supply?.
    • A dice-free factory trait for a supply item and for a sanctuary.
  3. Loader (lib/world_seed/loader.rb). The floor: keys gain safe_rooms and a healing: block (count, name, description), written in both directions on every load; absent writes 0 / 0, nil, nil. Refuse:

    • a non-integer or negative safe_rooms;
    • safe_rooms above rooms − 2 (the plan's rule; the engine clamps whatever it is given);
    • a healing: that is not a mapping;
    • a non-integer or negative count;
    • a blank name or description when count > 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 unsets sanctuary on a room, of a floor already laid out. Those are layout rows, not file rows (Item::TemplateRefresh must leave supply rows alone, as it leaves kit rows).

    Tests:

    • it seeds;
    • each refusal above;
    • a re-seed of a laid-out floor leaves its supplies.
  4. Exporter (lib/world_seed/exporter.rb:598). Emit safe_rooms when it is non-zero, healing: {count, name, description} when the count is non-zero, and sanctuary: true on any location that has it, except a floor's own laid-out rooms, which the exporter does not write. Round-trip test.

  5. Registries' exemption.

    • Item.bespoke (app/models/item.rb:376) becomes portable.where(kit_key: nil, supply: nil), so Item::Registry#room_for (registry.rb:138) and #story_item_count (:176) leave supplies out, as the engine's realization::bespoke does.
    • Item::NOT_COPIED (item.rb:587) already copies every other column, so a game's copy carries supply. Check that Story::Fork, Story::Snapshot and Playthrough::SqlitePackage copy locations.sanctuary and items.supply; they do if they copy columns generically.
    • Any Ruby mirror of the slot draw draws nobody for a sanctuary.
    • Registry tests at the default limit and with things_limit: 1, with two supply rows lying in the room.
  6. Doctor findings (report, never repair), with tests:

    • sanctuary_with_a_hostile_resident (warning): a location with sanctuary where a hostile character stands, world or game (a living hostile character there, or one a game's NPC state makes hostile).
    • Also worth having, though the brief does not ask for them:
      • supply_with_a_kit_key (error): a supply row with a kit_key, which furnish would 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.
  7. Protocol and glance (the front end's rendering is stage 9, out of scope here):

    • The extension's glance_json (ext/renderedstep/src/lib.rs:1349) adds "safe": here.safe to the room object.
    • Playthrough::Glance::Room (glance.rb:30) gains :safe, and its console to_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 gains safe (["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.rb covers it.
  8. Sweep.

    • Copy lib/engine_sweep/worlds/a-stocked-floor.yml and lib/engine_sweep/scripts/a-floor-stocked-at-layout.yml, with their comments and why: notes (below).
    • Vendor this commit's script, golden and records file.
    • Write test/engine_parity/a-floor-stocked-at-layout.checks.json.
    • Update test/ruby_loop_coverage.yml if it must list it.
    • Run engine:rust_gates on it: the doctor and the audit must say what its checks file says.
    • Then rebuild parity/worlds/a-stocked-floor.sql here from that branch's loader.
  9. 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
# A floor stocked when it is laid out. The Stocked Floor is a FLOOR, laid out
# by the engine the first time anybody walks into it, as four rooms. Its
# `safe_rooms: 1` makes one of them, never the entry, a safe room: safe, no
# hazard, nobody born in it (`population: nobody`) and `sanctuary` set. Its
# `healing:` block leaves two red draughts lying in two rooms, never the
# entry, as the world's own rows: they carry the supply marker and no
# `kit_key`, so the room is still furnished from its sort and neither the
# room's three-thing allowance nor the story's thing limit counts them. The
# layout is written once, for the world: no later turn of any game stocks
# the floor again.
format: 2
universe:
  physics: Ordinary physics.
  technology: Lanterns, iron keys and hand tools.
  weapons: Short blades and cudgels.
  geographies: A dungeon cut down into black stone under a lantern-lit landing.
  civilizations: Delvers who sell what they bring up.
  history: The dungeon was cut long ago by people nobody remembers.
  economics: Coin for what comes up from below.
  politics: Nobody governs the dungeon.
  religion: Delvers leave a coin on the landing for luck.
  races:
  - name: Delvers
    description: People who go down into the dungeon for a living.
story:
  title: A Stocked Floor
  genre: dungeon crawl
  start_time: '2026-10-31T08:00:00Z'
  preface: Somebody left draughts about the first floor, and a room nothing is born in.
  summary: Odile walks every room of the first floor.
opening_scene:
  location: The Landing
  characters: [Odile]
  description: You stand on the lantern-lit landing above the first floor.
  summary: Odile reaches the landing.
characters:
- fullname: Odile
  nickname: Odile
  race: Delvers
  age: 29
  sex: female
  is_protagonist: true
  is_companion: false
  backstory: A delver who has never been below the first floor.
  personality: Careful and stubborn.
  appearance: A lantern on a pole and a short blade.
  likes: A clear way back.
  dislikes: Doors she cannot open.
  fears: Running out of draughts.
  stats: {level: 3, hit_die: 8, strength: 14, dexterity: 12, will: 12}
locations:
- name: The Landing
  detail_level: realized
  population: nobody
  opening: true
  description: A square landing of black stone lit by one lantern, a stair going down from it.
  teaser: A lantern-lit landing.
  lore: Delvers gather here before they go down.
- name: The Stocked Floor
  detail_level: stub
  kind: warren
  teaser: The first level of the dungeon, where somebody has left supplies about.
  floor:
    rooms: 4
    danger: uneasy
    hazard: none
    safe_rooms: 1
    healing:
      count: 2
      name: red draught
      description: A stoppered glass vial of thick red liquid, warm to the touch. Drinking it closes wounds.
connections:
- between: [The Landing, The Stocked Floor]
  distance: adjacent
  travel_method: taking stairs
lib/engine_sweep/scripts/a-floor-stocked-at-layout.yml
# A FLOOR IS STOCKED ONCE, WHEN IT IS LAID OUT. The Stocked Floor says
# `safe_rooms: 1` and leaves two red draughts (`healing: count: 2`). Its
# layout, on the first step onto it, rolls them into its rooms off the place's
# own dice, after every other draw: the entry, room 1, gets neither; room 4 is
# made the safe room and room 2 and room 4 each get a draught. They are the
# world's own rows, written with the floor and never again: a second game
# walks the same floor and finds the same two, and nothing new. Each room is
# still furnished from its sort as it is first written, and a draught leaves
# the room writer its whole allowance of three things.
story: A Stocked Floor
steps:
- id: onto-the-stocked-floor
  why: >-
    The floor is laid out on this turn and its supplies written with it. The
    entry is never stocked: the room writer is told of no draught lying
    here, and the room holds only its kit.
  type: "/move The Stocked Floor"
  browser:
    token: into-the-stocked-floor
    realizes: The Stocked Floor
    replies:
    - purpose: location
      content:
        description: A level of black stone passages.
        lore: The first floor was cut by people nobody remembers.
        place_kind: warren
    - purpose: location
      prompt_excludes:
      - 'Loose, and could be picked up: red draught'
      content:
        description: A low chamber of black stone at the foot of the stair.
        lore: Delvers pass through here on their way in.
        items: []
        people: []
    - purpose: arrival
      unavailable: true
  expect:
    location: The Stocked Floor room 1 (realized)
    here:
    - coin
    - rock shelf
    - rubble
    - stalagmite
- id: a-room-with-a-draught-and-its-kit
  why: >-
    Room 2 holds a draught and is still furnished from its sort. The room
    writer is told of the draught through Already Here, as of any loose
    thing, and is still offered three things of its own, all three of which
    are admitted.
  type: "/move The Stocked Floor room 2"
  browser:
    token: stocked-floor-room-2
    realizes: The Stocked Floor room 2
    replies:
    - purpose: location
      prompt_includes:
      - 'Loose, and could be picked up: red draught.'
      - List AT MOST 3 portable things
      content:
        description: A cramped chamber, a rock shelf along one wall and rubble heaped in a corner.
        lore: Somebody sheltered here once.
        items:
        - name: tallow candle
          description: A stub of yellow tallow candle, its wick black.
          use_kind: ordinary
        - name: coil of twine
          description: A coil of waxed twine, still tight.
          use_kind: ordinary
        - name: chalk stick
          description: A stick of white chalk worn down at one end.
          use_kind: ordinary
        people: []
    - purpose: arrival
      unavailable: true
  expect:
    location: The Stocked Floor room 2 (realized)
    here:
    - chalk stick
    - coil of twine
    - red draught
    - rock shelf
    - rubble
    - shard of pottery
    - tallow candle
- id: take-the-draught
  why: >-
    The first game takes its own copy of the draught. The world's row stays
    where the layout left it.
  type: "/take red draught"
  expect:
    carrying:
    - red draught
- id: a-room-with-nothing-left-in-it
  type: "/move The Stocked Floor room 3"
  browser:
    token: stocked-floor-room-3
    realizes: The Stocked Floor room 3
    replies:
    - purpose: location
      prompt_excludes:
      - 'Loose, and could be picked up: red draught'
      content:
        description: A long chamber where a ledge runs above a stalagmite.
        lore: Bats roost here.
        items: []
        people: []
    - purpose: arrival
      unavailable: true
  expect:
    location: The Stocked Floor room 3 (realized)
    here:
    - bat skull
    - bone
    - ledge
    - rock shelf
    - stalagmite
- id: the-safe-room
  why: >-
    Room 4 is the safe room: nobody is born in it, so the room writer is given
    the existing nobody-here instruction, and it holds the other draught.
    Nothing tells the room writer it is safe.
  type: "/move The Stocked Floor room 4"
  browser:
    token: stocked-floor-room-4
    realizes: The Stocked Floor room 4
    replies:
    - purpose: location
      prompt_includes:
      - 'Loose, and could be picked up: red draught'
      - Write NOBODY into this place.
      prompt_excludes:
      - safe room
      - sanctuary
      content:
        description: A still chamber around a dark pool, the air quiet.
        lore: Nothing comes in here.
        items: []
        people: []
    - purpose: arrival
      unavailable: true
  expect:
    location: The Stocked Floor room 4 (realized)
    present: []
    here:
    - bone
    - coin
    - crack
    - pool
    - red draught
    - rubble
    - stalagmite
- id: a-second-game-finds-the-floor-as-it-was-stocked
  why: >-
    The floor is not laid out or stocked again: the second game finds room 2
    as the layout left it, its draught still lying there, and only one.
  player: second
  type: "/move The Stocked Floor"
  expect:
    location: The Stocked Floor room 1 (realized)
    here:
    - coin
    - rock shelf
    - rubble
    - stalagmite
- id: and-the-draught-the-first-game-took
  player: second
  type: "/move The Stocked Floor room 2"
  expect:
    location: The Stocked Floor room 2 (realized)
    carrying: []
    here:
    - chalk stick
    - coil of twine
    - red draught
    - rock shelf
    - rubble
    - shard of pottery
    - tallow candle

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
calebl merged commit adaf92d into main Oct 30, 2026
4 checks passed
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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant