Repository navigation
Lay out a floor to its own count, and keep a moved doorway's lock - #94
Merged
Merged
Conversation
A place its world gives a `floors` row (migration 20261030200000) is laid out by the new floor module instead of interior: exactly `rooms` rooms on one storey, a near-square grid walked as the serpentine, each room's danger and hazard from the floor's own words through Parameters (the model's picks are never read for a floor), its sorts dealt from the floor's own building kind, and a way down rolled from every room but the entry, which keeps a doorway spare as the entry does. Every draw is the place's own Roll::INTERIOR generator; interior's Builder is opened to the crate and no draw of it moved (vectors/interior.json and every existing golden and records file are unchanged). open_the_way_in routes the doorway to the floor's `down` place, or to a room of it, onto the way-down room, and every other doorway onto the doorstep. For every place, floor or not, a moved doorway now keeps each side's barrier, key, and hazard (so a one-way hazard keeps its direction), every game's passages through it, and every toll that named it; before, it kept only its distance and travel method, and a toll on the old row failed the delete. A floor is laid out once, as any place is: a later game, or a changed `floors` row, reads the stored rooms. The store loads `floors` where the database has it (OPTIONAL_LOADED) and leaves its facts out of the shape only when the table is whole (OPTIONAL_SHAPES), so the game at its current pin still opens and a floors table declared any other way is refused. SCHEMA_VERSION and shape.txt do not move. Every world dump carries the migration, applied through sqlite exactly as Rails' SQLite adapter issued it. New world two-floors-and-a-key (the game's loader at server 3d357bd without the floor keys, then the migration, the floors rows, their footprints and kind) and scripts a-floor-laid-out-to-its-count and a-keyed-stair-onto-a-place-stays-keyed, in PASSING with goldens and records.
…erver 5678109 The game's pin PR (renderedstep/server#369) reads the floor keys, so the dump is now the one its own loader writes, floors rows, footprints and kind included. The rows are the same; only timestamps and the order the floors table and its constraints are dumped in moved. No golden or records file moves.
calebl
added a commit
to renderedstep/server
that referenced
this pull request
Oct 30, 2026
The merge adds only the engine's CI, its fight simulator, its README and the two-floors-and-a-key world dump rebuilt from this branch's loader to the head pinned before; no golden, script or vector portion this repository vendors changed.
calebl
added a commit
to renderedstep/server
that referenced
this pull request
Oct 30, 2026
Pin renderedstep/engine#94's head (9a665dd): lay out a floor to its own count, and keep a moved doorway's lock
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 2 of the dungeon plan, engine half: floor layout. A place its world gives a
floorsrow is laid out by the engine as exactly that many rooms on one storey, with a way down rolled onto a room that is never the entry. It is laid out once, the first time anybody reaches it, and every later game in the world reads the stored rooms. Doorways moved onto a place's rooms now keep their lock, key and hazard. That carry-over applies to every place, not only floors. A world with nofloorsrow lays out, plays and prompts byte for byte as before.What changed
floor.rs(new, pure).floor::lay_out(place, floor)returns aLayoutand the way-down room index.roomscells are kept, so the last line may be short. Column widths and row depths come from the existingshare.MINIMUM_SIDEpaces a room. Otherwise it isfloor::footprint(rooms)= (6 × columns, 6 × rows).close_connectivityand the four-exit cap.danger/hazardwords through the existingdanger::in_a_place/hazard_in_a_place, with a flat gradient. The model's picks are never read for a floor.kind::dealfrom the floor place's ownkindwhen that is a building word. Otherwise the model'splace_kinddeals them, as before.Roll::INTERIORgenerator, in this order: widths, depths, serpentine, each room's danger then hazard, way down, extra doors.MOST_ROOMS= 1000.interior.rs. TheBuilderis opened to the crate (Builder::new,grid,way_down) so the floor path reuses its pieces. No draw moved: the 360vectors/interior.jsoncases and all 100 existing goldens and records files are unchanged.realize.rslay_out. When the place has afloorsrow, it takes the floor path and passes the way-down room and the floor'sdown_location_idtoopen_the_way_in. Otherwise it takes the interior path exactly as before.open_the_way_in: routing. The doorway whose far end is the floor'sdownplace, or a room of it (when the lower floor was laid out first), opens onto the way-down room. Every other doorway opens onto the doorstep (entry first), and the doorstep no longer includes the way-down room.open_the_way_in: carry-over, for every place. A moved doorway keeps, side by side:barrierandkey_template_id;hazardandhazard_dieon the side that had them, so a one-way hazard keeps its direction;playthrough_passagesthrough it (re-inserted onto the new row, ids kept, as a shuffle does);playthrough_tollsthat named it (re-pointed to the new row).Before this change, a moved doorway kept only its distance and travel method. A toll on the old row also made the delete fail its foreign key:
playthrough_tollshas noON DELETE. A doorway that has nowhere to land is still dropped, and its tolls are left naming no doorway.Generated once. Layout still runs only for a place with no rooms (
laid_out). A floor reached again, in any game, is read from its rows. Nothing re-lays it out, deals it again or rewrites it, even after itsfloorsrow changes.Store.
floorsis an optionally loaded table (store::OPTIONAL_LOADED,store::OPTIONAL_SHAPES), loaded where the database has it and read as empty where it does not.shapeleaves its facts out only when every one of them is there, so afloorstable declared any other way, or missing its unique index, is still a difference and is refused.SCHEMA_VERSIONandshape.txtdo not move, so the game at its current pin still opens.Migration 20261030200000 (
CreateFloors) is applied to every world dump inparity/worlds/. Each dump re-dumped byte-identically before the migration was applied. The SQL is exactly what Rails' SQLite adapter issued: I ran the migration below withbin/rails db:migrateon a copy of a world, and its dump was byte-identical to the one my SQL produced.Sweep. New world
two-floors-and-a-keyand two new scripts, both inPASSINGwith goldens and records files.3d357bdloading the world file below without itsfloor:and floorkind:keys, which that loader cannot read yet. Then the migration and the rows the new loader should write were added:floorsrows (Upper: 4 rooms, uneasy, none, down → Lower; Lower: 2 rooms, dangerous, unlit);kind = 'warren'.a-floor-laid-out-to-its-count(14 steps).offers: locked), and the stair key opens it.a-keyed-stair-onto-a-place-stays-keyed(7 steps, an ordinary place with nofloor:). This is the carry-over on its own.hazards: 1).expect:in both scripts was checked against the engine's dumps (21 expectations, 0 mismatches). The records show the moved stair's rows: landing→entrykeyed, cellar key,drop, 4; entry→landingkeyed, cellar key, no hazard.Tests
floor::tests.MINIMUM_SIDE.cave.tests/floors.rs, on the live path with a scripted model.cave, with uneasy-table dangers and no flood, despite the picks. The footprint is 12×12, no doorway is left on the floor place, and the way in lands on the entry. The way down lands on a room other than the entry and is keyed with the stair key on both sides.floorsrow is rewritten (9 rooms, dangerous, flooded). Then three games, the first among them, walk in. A second game's walk-in makes one call (the arrival). The rooms and their doorways are byte-identical afterwards, and the location count does not move.tests/engine.rs). A database withoutfloors(table dropped, version row removed) opens and plays. Afloorstable withroomsnullable is refused (found column floors.rooms integer pk=0), and so is one missing its unique index.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: 102 of 106 agree. The other four are runner-only, as before.SERVER_COMMIT(ccc92ed), with the extension built from this checkout.parity/runner.sh: 104 of 106 agree.a-floor-laid-out-to-its-countanda-keyed-stair-onto-a-place-stays-keyedare "not in the checkout yet".engine:rust_gates: PASSED for 104 scripts.SERVER_COMMITdoes not move: the game at its current pin (ccc92ed) has nofloorstable, and the engine opens it and plays as before.What the server pin PR must change
Migration
db/migrate/20261030200000_create_floors.rb, with this exact version and declaration so the dumps here stay byte-true (store::OPTIONAL_SHAPESholds every fact of it).db/schema.rbmoves to2026_10_30_200000.Floormodel, with a model test and a dice-free factory.Floor belongs_to :locationandbelongs_to :down_location, class_name: "Location", optional: true.Location has_one :floor, dependent: :destroy.roomsan integer in1..1000(floor::MOST_ROOMS), and at least 2 whendown_locationis set.dangerinLocation::Parameters' danger keys (safe uneasy dangerous), allow nil.hazardinLocation::Parameters::HAZARDS(none flooded unlit silent airless), allow nil.down_locationnot the floor itself, in the same story, and itself a floor.Location'svalidates :kindmust also accept aLocation::Kind::BUILDINGSword on a location that has a floor: a floor'skind:is a building (warren), which deals its rooms' sorts.Loader (
lib/world_seed/loader.rb). This is the shape in plan §1.1. Thefloor:keys at this stage are onlyrooms,danger,hazardanddown. Refuse any other key underfloor:; the later keys arrive with their stages.Refuse:
roomsor one outside 1..1000, androoms < 2withdown;dangerorhazardword outside the tables;downthat is not afloor:location of this file, names itself, is not joined to this floor by a connection, or makes a cycle;width,depth,x,yorz, or with adetail_levelother thanstub;parent:is a floor;kind:is not aLocation::Kind::BUILDINGSword (validate_kinds!keepsKINDSfor every other location).Write:
floorsrow (find_or_initialize_by(location:)), every key in both directions on every load: an absent key writes nil, and removingfloor:deletes the row;width = 6 × columns,depth = 6 × rows, where columns is the smallest c with c² ≥ n, rows = ⌈n ÷ c⌉, and n =roomsclamped as in item 2 (floor::footprint).A laid-out floor's footprint and rooms are never rewritten. The re-seed rule is principle 7: a re-seed that changes
roomsafter layout changes the row only.Tests: seeds a floor with its row and footprint; refuses each case above; re-seed after layout leaves the rooms alone.
Exporter (
lib/world_seed/exporter.rb). For a location with a floor, emitfloor: { rooms, danger, hazard, down }.compact, withdownby name. Omit the floor'swidth/depth(the loader derives them) and its rooms (the engine lays them out). Round-trip test.Doctor findings (report, never repair):
rooms(clamped), for example because a re-seed changed it after layout;downis not a floor;downbut no doorway from one of its rooms todownor a room ofdown, and one whose way down lands on its entry.Each finding gets a doctor test.
Ruby
Location::Generator#open_the_way_in!: the same carry-over.ways_inreads both directed rows of each doorway.barrierandkey_template, and withhazard/hazard_dieon the side that had them. A side that was never written takes the other side's barrier and key with no hazard.downplace (or a room of it) goes onto the way-down room. The Ruby generator does not lay floors out (the engine does), so this is only the routing rule, if the Ruby path keeps a reader of it.Copies of a world:
Story::Fork,Story::SnapshotandPlaythrough::SqlitePackagecopyfloorswith the locations they point at.Sweep.
lib/engine_sweep/worlds/two-floors-and-a-key.ymland the two scriptslib/engine_sweep/scripts/a-floor-laid-out-to-its-count.ymlanda-keyed-stair-onto-a-place-stays-keyed.yml, with their comments andwhy:notes (below).test/engine_parity/<script>.*for both.test/ruby_loop_coverage.ymlif it lists scripts.parity/worlds/two-floors-and-a-key.sqlhere from that branch's loader, as Let a story set its own people and thing limits #90 did fora-crowded-cellar, so the dump is the loader's own.Move the pin to this commit. A later engine change may then move
SCHEMA_VERSIONto 20261030200000 and putfloorsintoshape.txtin place ofOPTIONAL_SHAPES.Out of scope, as the brief says:
monster_level(stage 3);floorssends byte-identical requests.No model was called.
lib/engine_sweep/worlds/two-floors-and-a-key.yml
lib/engine_sweep/scripts/a-floor-laid-out-to-its-count.yml
lib/engine_sweep/scripts/a-keyed-stair-onto-a-place-stays-keyed.yml