Skip to content

Commit 5d4abb3

Browse files
martex-devclaude
andcommitted
M27: fittings, props, and a hallway that is a place rather than a stroke
Presentation only again -- model.py, map.py and ledger.py untouched. The corridor was a wide dark stroke through the room centres: a line on a diagram rather than somewhere you could walk. The runs of floor that are genuinely outside a room are now computed from the gap between two consecutive shells and drawn as sections -- walls down both sides, deck plating, a dashed guide down the middle and a light overhead -- so they line up with the doorways because they are measured from the same gap those were cut for. Inside a room the walk continues as a band of darker plating across the floor, and each threshold gets hazard chevrons. Rooms went from seven objects to twelve or fourteen: a fittings band against the top wall with vents, conduit runs and an illuminated sign over the door; a third row of furniture; and loose props tucked into the margins the patrol does not use -- chairs, barrels, a cart, a coil of cables, a floor hatch. Eleven new fixture kinds. Each room carries its number as a large faint floor stencil, the same index its callout card shows. The ten humanoid builds gained arms: shoulders, sleeves in the room's colour and hands, which is what was missing when a figure was holding a tablet or a wrench with nothing to hold it with. Two things worth recording: * The overhead light strips were positioned from the top of the floor and the new wall fittings from the content origin, and at the room's height those two coincide -- the lights were being drawn inside the vents. Neither was wrong on its own. The lights now run across the room where a ceiling light would be. * A patch script that asserts its way down a file and writes at the end leaves nothing written when an assertion fires, while the earlier substitutions have already succeeded against the in-memory copy. The run reads as partial and is in fact a no-op, which is the safer failure and the more confusing one. 572 passed, mypy --strict clean. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent e458c22 commit 5d4abb3

5 files changed

Lines changed: 495 additions & 39 deletions

File tree

BUILD_PLAN.md

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
55
This is the executable plan derived from [`docs/`](docs/). The design documents say *what* to build and *why*; this says *in what order*, *with what acceptance test*, and *what changes because of the machine we're actually on*.
66

7-
**Status:** M0–M26 complete; v5 and v6 landed, v7 registered and not yet run. **V6's result does not adjudicate what v6 registered** — its treatment arm did not implement the mechanism it names; see M23. The live path is wired but unspent (mock-driven throughout; the first live run awaits an API key). M12's code-generation half is blocked on both a key and Docker. Nothing below is claimed as done until its acceptance criteria are green in CI.
7+
**Status:** M0–M27 complete; v5 and v6 landed, v7 registered and not yet run. **V6's result does not adjudicate what v6 registered** — its treatment arm did not implement the mechanism it names; see M23. The live path is wired but unspent (mock-driven throughout; the first live run awaits an API key). M12's code-generation half is blocked on both a key and Docker. Nothing below is claimed as done until its acceptance criteria are green in CI.
88

99
---
1010

@@ -1010,6 +1010,52 @@ state into the next interaction. It is measured from the start of each press now
10101010

10111011
---
10121012

1013+
### M27 · Fittings, props, and a hallway that is a place ✅
1014+
Presentation only again — `model.py`, `map.py` and `ledger.py` untouched.
1015+
1016+
**The corridor was a stroke through the room centres.** Wide, dark, and a line on a diagram
1017+
rather than somewhere you could walk. The runs of floor that are genuinely *outside* a room are
1018+
now computed from the gap between two consecutive shells and drawn as sections: walls down both
1019+
sides, deck plating, a dashed guide down the middle and a light overhead. They line up with the
1020+
doorways because they are measured from the same gap the doorways were cut for. Inside a room the
1021+
walk continues as a band of darker plating across its floor, so the route reads as one run rather
1022+
than as rooms with a line between them. Each threshold gets hazard chevrons.
1023+
1024+
**Rooms went from seven objects to twelve.** A fittings band against the top wall — vents, conduit
1025+
runs with a pilot light, an illuminated sign over the door — a third row of furniture, and loose
1026+
props tucked into the margins the patrol does not use: chairs, barrels, a cart, a coil of cables,
1027+
a floor hatch. Eleven new fixture kinds. Each room also carries its number as a large faint floor
1028+
stencil, which is the same index its callout card shows.
1029+
1030+
**The actors have arms.** Ten builds gained shoulders, sleeves in the room's colour, and hands —
1031+
which is what was missing when a figure was holding a tablet or a wrench with nothing to hold it
1032+
with.
1033+
1034+
#### What building it revealed was wrong
1035+
1036+
**1. Two bands were computed from different origins and landed on each other.** The overhead
1037+
light strips were positioned from the top of the floor and the new wall fittings from the content
1038+
origin, and at the room's height those two happen to coincide — the lights were drawn inside the
1039+
vents. Neither was wrong on its own. The lights now run across the room where a ceiling light
1040+
would be, rather than along the wall where the fittings are.
1041+
1042+
**2. A patch script that asserts its way down a file leaves the file half-written.** The
1043+
substitution for the floor stencil did not match, the assertion fired, and because the write is
1044+
at the end nothing at all was applied — but the earlier substitutions had already succeeded
1045+
against the in-memory copy, so the run *looked* partial and was in fact a no-op. Worth recording
1046+
because the failure reads as the opposite of what it is: an aborted all-or-nothing patch is
1047+
safer than a partial one, and it is easy to spend a while looking for changes that were never
1048+
written.
1049+
1050+
**Acceptance**
1051+
- Every test from M22 through M26 still passes.
1052+
- A room holds at least five drawn fixtures, all inside its floor — now twelve to fourteen.
1053+
Tested.
1054+
- No two labels overlap, including the new floor stencils, at any zoom. Tested.
1055+
- Still one self-contained file; still refuses to build on input that does not verify.
1056+
1057+
---
1058+
10131059
## 2. Working agreement
10141060

10151061
- **`main` is always green.** Every milestone lands as a PR with CI passing on Linux and Windows.

src/nullius/station/render.py

Lines changed: 209 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -209,13 +209,19 @@ class Principle:
209209
#: The rest of the interior, as fractions of what is left below that. The
210210
#: off-corridor rooms are shorter than the pipeline rooms and the same layout
211211
#: has to read in both, so the bands are proportional from here down.
212-
BACK_AT, BACK_H = 0.02, 0.19
213-
MID_AT, MID_H = 0.25, 0.17
214-
LANE_AT = 0.50
215-
FRONT_AT, FRONT_H = 0.62, 0.18
212+
WALL_AT, WALL_H = -0.055, 0.075
213+
BACK_AT, BACK_H = 0.05, 0.185
214+
MID_AT, MID_H = 0.27, 0.16
215+
LANE_AT = 0.505
216+
FRONT_AT, FRONT_H = 0.635, 0.175
216217
FEET_AT = 0.965
217218
AGENT_H = 74.0
218219

220+
#: The hallway between two rooms: as wide as the doorways it joins, so the walk
221+
#: reads as one run of floor rather than as a line drawn between two boxes.
222+
HALL = 62.0
223+
HALL_WALL = 8.0
224+
219225
#: How wide a lane an agent paces, as a fraction of the floor, and how long one
220226
#: length of it takes. Agents patrol their own room and never leave it: the
221227
#: token is what moves through the station, and a role walking the corridor
@@ -409,34 +415,127 @@ class Kit:
409415
nothing in it reads as a room that does nothing, which would be a claim, and
410416
a false one."""
411417

418+
wall: tuple[str, ...]
419+
"""Fittings against the top wall: vents, conduit runs, a sign over the door."""
420+
412421
back: tuple[str, ...]
413422
mid: tuple[str, ...]
414423
front: tuple[str, ...]
424+
props: tuple[str, ...] = ()
425+
"""Loose things on the floor, in the margins either side of the walk."""
415426

416427

417428
#: Furniture appropriate to the work. A room absent from this mapping gets
418429
#: :data:`DEFAULT_KIT`, so adding a room to the map draws something rather than
419430
#: leaving an empty box.
420431
FURNITURE: dict[str, Kit] = {
421-
"drafting": Kit(("board", "board", "shelf"), ("crate", "shelf"), ("desk", "desk")),
422-
"screening": Kit(("board", "shelf", "board"), ("cabinet", "crate"), ("desk", "desk")),
423-
"registry": Kit(("cabinet", "cabinet", "cabinet"), ("rack", "shelf"), ("console", "console")),
424-
"workshop": Kit(("rack", "rack", "rack"), ("bench", "crate"), ("bench", "bench")),
425-
"execution": Kit(("rack", "rack", "rack"), ("table", "table"), ("table", "console")),
426-
"analysis": Kit(("screen", "screen", "screen"), ("board", "shelf"), ("console", "console")),
427-
"challenge": Kit(("board", "screen", "board"), ("cabinet", "shelf"), ("console", "desk")),
428-
"blind": Kit(("cabinet", "cabinet", "cabinet"), ("rack", "crate"), ("console", "console")),
429-
"review": Kit(("shelf", "board", "shelf"), ("cabinet", "crate"), ("desk", "desk")),
430-
"record": Kit(("shelf", "shelf", "shelf"), ("cabinet", "cabinet"), ("plinth", "plinth")),
432+
"drafting": Kit(
433+
("vent", "conduit", "sign"),
434+
("board", "board", "shelf"),
435+
("crate", "shelf"),
436+
("desk", "desk"),
437+
("chair", "cables"),
438+
),
439+
"screening": Kit(
440+
("conduit", "sign", "vent"),
441+
("board", "shelf", "board"),
442+
("cabinet", "crate"),
443+
("desk", "desk"),
444+
("chair", "barrel"),
445+
),
446+
"registry": Kit(
447+
("vent", "sign", "conduit"),
448+
("cabinet", "cabinet", "cabinet"),
449+
("rack", "printer"),
450+
("console", "console"),
451+
("chair", "hatch"),
452+
),
453+
"workshop": Kit(
454+
("conduit", "conduit", "vent"),
455+
("rack", "rack", "rack"),
456+
("bench", "crate"),
457+
("bench", "bench"),
458+
("barrel", "cart"),
459+
),
460+
"execution": Kit(
461+
("vent", "conduit", "vent"),
462+
("rack", "rack", "rack"),
463+
("table", "table"),
464+
("table", "console"),
465+
("hatch", "barrel"),
466+
),
467+
"analysis": Kit(
468+
("sign", "conduit", "vent"),
469+
("screen", "screen", "screen"),
470+
("board", "printer"),
471+
("console", "console"),
472+
("chair", "cables"),
473+
),
474+
"challenge": Kit(
475+
("vent", "sign", "conduit"),
476+
("board", "screen", "board"),
477+
("cabinet", "locker"),
478+
("console", "desk"),
479+
("chair", "crate"),
480+
),
481+
"blind": Kit(
482+
("conduit", "vent", "sign"),
483+
("cabinet", "cabinet", "cabinet"),
484+
("rack", "locker"),
485+
("console", "console"),
486+
("hatch", "cables"),
487+
),
488+
"review": Kit(
489+
("vent", "sign", "conduit"),
490+
("shelf", "board", "shelf"),
491+
("cabinet", "crate"),
492+
("desk", "desk"),
493+
("chair", "barrel"),
494+
),
495+
"record": Kit(
496+
("conduit", "sign", "conduit"),
497+
("shelf", "shelf", "shelf"),
498+
("cabinet", "cabinet"),
499+
("plinth", "plinth"),
500+
("cables", "hatch"),
501+
),
431502
"vault": Kit(
432-
("cabinet", "cabinet", "cabinet", "cabinet"), ("cabinet", "cabinet"), ("terminal",)
503+
("vent", "conduit", "vent", "sign"),
504+
("cabinet", "cabinet", "cabinet", "cabinet"),
505+
("locker", "locker", "cabinet"),
506+
("terminal",),
507+
("hatch", "barrel"),
508+
),
509+
"treasury": Kit(
510+
("sign", "conduit", "vent"),
511+
("cabinet", "rack", "cabinet"),
512+
("crate", "crate"),
513+
("console", "desk"),
514+
("cart", "barrel"),
515+
),
516+
"archive": Kit(
517+
("conduit", "vent", "conduit", "sign"),
518+
("shelf", "shelf", "shelf", "shelf"),
519+
("shelf", "shelf"),
520+
("shelf", "crate"),
521+
("cart", "cables"),
522+
),
523+
"oracle": Kit(
524+
("vent", "sign", "vent"),
525+
("cabinet", "cabinet", "cabinet"),
526+
("shelf", "locker"),
527+
("plinth", "plinth"),
528+
("hatch", "barrel"),
433529
),
434-
"treasury": Kit(("cabinet", "rack", "cabinet"), ("crate", "crate"), ("console", "desk")),
435-
"archive": Kit(("shelf", "shelf", "shelf", "shelf"), ("shelf", "shelf"), ("shelf", "crate")),
436-
"oracle": Kit(("cabinet", "cabinet", "cabinet"), ("shelf", "shelf"), ("plinth", "plinth")),
437530
}
438531

439-
DEFAULT_KIT = Kit(("rack", "cabinet", "shelf"), ("crate", "shelf"), ("console", "desk"))
532+
DEFAULT_KIT = Kit(
533+
("vent", "conduit", "sign"),
534+
("rack", "cabinet", "shelf"),
535+
("crate", "shelf"),
536+
("console", "desk"),
537+
("chair", "cables"),
538+
)
440539

441540

442541
def _row(
@@ -479,14 +578,42 @@ def _fixtures(room: Room) -> list[dict[str, Any]]:
479578
kit = FURNITURE.get(room.room_id, DEFAULT_KIT)
480579
floor = _inner(room)
481580
usable = _usable(floor)
581+
wall = _row(kit.wall, floor, _band(floor, WALL_AT), usable * WALL_H, nxt)
482582
back = _row(kit.back, floor, _band(floor, BACK_AT), usable * BACK_H, nxt)
483583
mid = _row(kit.mid, floor, _band(floor, MID_AT), usable * MID_H, nxt)
484584
front = _row(kit.front, floor, _band(floor, FRONT_AT), usable * FRONT_H, nxt)
485-
for fixture in back + mid:
585+
props = _props(kit.props, floor, nxt)
586+
for fixture in wall + back + mid:
486587
fixture["depth"] = "back"
487-
for fixture in front:
588+
for fixture in front + props:
488589
fixture["depth"] = "front"
489-
return back + mid + front
590+
return wall + back + mid + props + front
591+
592+
593+
def _props(
594+
kinds: tuple[str, ...],
595+
floor: Box,
596+
nxt: Callable[[float, float], float],
597+
) -> list[dict[str, Any]]:
598+
"""Loose things, tucked into the corners either side of the walk.
599+
600+
Placed in the margins the patrol does not use, so the floor has something on
601+
it without anybody having to step over it.
602+
"""
603+
out: list[dict[str, Any]] = []
604+
for index, kind in enumerate(kinds):
605+
size = 26.0 + nxt(0.0, 12.0)
606+
left = index % 2 == 0
607+
out.append(
608+
{
609+
"kind": kind,
610+
"x": (floor.x + 6.0) if left else (floor.x + floor.w - 6.0 - size),
611+
"y": _band(floor, 0.36 + 0.3 * (index // 2)) + nxt(0.0, 10.0),
612+
"w": size,
613+
"h": size * 0.7,
614+
}
615+
)
616+
return out
490617

491618

492619
def _sprites(room: Room) -> list[dict[str, Any]]:
@@ -565,6 +692,43 @@ def _doorways(walk: list[Room]) -> dict[str, list[dict[str, Any]]]:
565692
return out
566693

567694

695+
def _hallways(walk: list[Room]) -> list[dict[str, Any]]:
696+
"""The runs of floor between one room and the next.
697+
698+
A corridor drawn as a stroke through the room centres is a line on a
699+
diagram. These are the sections that are actually outside a room — walled,
700+
floored and lit — computed from the gap between two consecutive shells, so
701+
they line up with the doorways cut for them.
702+
"""
703+
out: list[dict[str, Any]] = []
704+
for first, second in pairwise(walk):
705+
one, two = _shell(first), _shell(second)
706+
if abs(one.y - two.y) < 1.0:
707+
lane = lane_y(first)
708+
left, right = (one, two) if two.x > one.x else (two, one)
709+
out.append(
710+
{
711+
"x": left.x + left.w,
712+
"y": lane - HALL / 2,
713+
"w": right.x - (left.x + left.w),
714+
"h": HALL,
715+
"vertical": False,
716+
}
717+
)
718+
else:
719+
top, bottom = (one, two) if two.y > one.y else (two, one)
720+
out.append(
721+
{
722+
"x": one.x + one.w / 2 - HALL / 2,
723+
"y": top.y + top.h,
724+
"w": HALL,
725+
"h": bottom.y - (top.y + top.h),
726+
"vertical": True,
727+
}
728+
)
729+
return [hall for hall in out if hall["w"] > 1 and hall["h"] > 1]
730+
731+
568732
def _is_outer(room: Room, side: str) -> bool:
569733
"""Whether nothing sits on the far side of this wall."""
570734
shell = _shell(room)
@@ -871,6 +1035,21 @@ def _cards(
8711035
kind="status",
8721036
)
8731037
)
1038+
floor = _inner(room)
1039+
labels.append(
1040+
_label(
1041+
f"{index:02d}",
1042+
x=floor.x + floor.w - 12.0,
1043+
y=floor.y + floor.h - 10.0,
1044+
size=54.0,
1045+
fill="stencil",
1046+
owner=room.room_id,
1047+
max_width=90.0,
1048+
weight=700,
1049+
anchor="end",
1050+
kind="stencil",
1051+
)
1052+
)
8741053
role_chips, role_labels, _ = _chip_row(
8751054
chips_text,
8761055
box.x + CARD_PAD,
@@ -1039,6 +1218,7 @@ def plan(station: Station) -> dict[str, Any]:
10391218
for room in walk
10401219
]
10411220
doorways = _doorways(walk)
1221+
halls = _hallways(walk)
10421222
exits = _exits(station, doorways)
10431223
fixed = _exit_labels(exits) + _map_labels(station, exits)
10441224
cards, labels = _cards(station, exits, [label.box for label in fixed])
@@ -1056,6 +1236,12 @@ def plan(station: Station) -> dict[str, Any]:
10561236
# every way out is drawn the same size as every other.
10571237
"doors": exits,
10581238
"doorways": doorways,
1239+
"halls": halls,
1240+
"lanes": {
1241+
room.room_id: round(lane_y(room), 2)
1242+
for room in ROOMS
1243+
if any(gap for gap in doorways[room.room_id])
1244+
},
10591245
"shells": shells,
10601246
"floors": floors,
10611247
"cards": cards,
@@ -1161,6 +1347,7 @@ def _arrivals(station: Station, layout: dict[str, Any]) -> list[float]:
11611347
"faint": "var(--plate-faint)",
11621348
"status": "var(--plate-ink)",
11631349
"badge": "var(--plate)",
1350+
"stencil": "var(--walli)",
11641351
}
11651352

11661353

0 commit comments

Comments
 (0)