Two-lane packing, and the shelves ship with the export
Eric's wall answered the orientation question with a screenshot full of red: the capacity model assumed one flat stack per opening, so his spine-out cubes read 200% full. His reviewer's prescription, built to its acceptance cases: each opening packs a FLAT lane (thinnest axes against interior height, claiming the widest flat box's width) and a STANDING lane beside it (boxes on edge against the remaining width; standing boxes must fit height and depth upright). The largest- footprint class lies flat, smaller boxes stand, any box's lane is flippable per-assignment (▬/▮ toggle in the opening view, ✱ marks an override), and ⚠ now means NO packing fits — not merely "tall stack". Unmeasured boxes take no lane but keep their honesty tag; dual fill bars show each lane's budget; a broken opening is never offered by the suggester. On the real wall: the double-wides and half the cubes went green (7 games = 13.12" flat + 2.91" standing), and the remaining warnings mark cubes that genuinely hold 12-16 boxes. Both reviewer acceptance tests pass verbatim. And the export gains the humanity Eric asked for: game pages say where each box lives, containers list their contents, and a shelves page draws the wall as it physically stands — proportional cells, zones, descriptions, every opening linking its residents. Publishing a shelf layout is a choice: --no-shelves keeps the layer out. "shelves" joins "art" as a reserved slug. 387 tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
co-authored by
Claude Fable 5
parent
d6ff5e73e9
commit
e90a53fb6c
+1
-1
@@ -94,7 +94,7 @@ def test_report_buckets_misfits_and_unknowns(tmp_path, capsys):
|
||||
summary = run_dims_report(cfg)
|
||||
assert summary["misfits"] == ["Monster Box"]
|
||||
out = capsys.readouterr().out
|
||||
assert "doesn't fit: Monster Box" in out
|
||||
assert "Monster Box" in out # named as unplaceable/misfit
|
||||
assert "unshelved" in out # the conflicting + absent games have no home
|
||||
assert "can't be size-verified" in out and "Never Measured" in out
|
||||
|
||||
|
||||
+60
-2
@@ -312,7 +312,7 @@ def test_cli_export_wiring(tmp_path, monkeypatch):
|
||||
|
||||
received = {}
|
||||
|
||||
def fake_run_export(cfg, out, *, title):
|
||||
def fake_run_export(cfg, out, *, title, include_shelves=True):
|
||||
received.update(out=out, title=title)
|
||||
return {"games": 0, "downloaded": 0, "failures": 0, "removed": 0}
|
||||
|
||||
@@ -325,7 +325,7 @@ def test_cli_export_wiring(tmp_path, monkeypatch):
|
||||
assert received["title"] == "T" and received["out"] == tmp_path / "s"
|
||||
assert runner.invoke(app, ["export"]).exit_code != 0 # --out is required
|
||||
|
||||
def failing_run_export(cfg, out, *, title):
|
||||
def failing_run_export(cfg, out, *, title, include_shelves=True):
|
||||
return {"games": 1, "downloaded": 0, "failures": 2, "removed": 0}
|
||||
|
||||
monkeypatch.setattr("bggpipe.export.run_export", failing_run_export)
|
||||
@@ -346,3 +346,61 @@ def test_colophon_card_closes_the_shelf(tmp_path):
|
||||
assert (out / "art" / "bggpipe-piper.jpg").exists()
|
||||
# the filter only ever hides <a class="game"> — the colophon is a div
|
||||
assert '<div class="game colophon"' in index
|
||||
|
||||
|
||||
def test_shelves_export_and_optout(tmp_path):
|
||||
"""The wall ships with the site — locations on game pages, a shelves
|
||||
page drawn as the physical arrangement — unless the owner opts out
|
||||
(publishing a shelf layout is a choice)."""
|
||||
from bggpipe.shelves import save_furniture, save_locations
|
||||
|
||||
cfg = _cfg(tmp_path)
|
||||
save_furniture(
|
||||
cfg,
|
||||
[
|
||||
{
|
||||
"name": "Den Kallax",
|
||||
"description": "the wall behind the couch",
|
||||
"openings": [
|
||||
{
|
||||
"id": "den-a1",
|
||||
"label": "A1",
|
||||
"zone": "oversize",
|
||||
"width_in": 26.5,
|
||||
"height_in": 13.25,
|
||||
"depth_in": 15.4,
|
||||
},
|
||||
{
|
||||
"id": "den-b1",
|
||||
"label": "B1",
|
||||
"zone": "cubes",
|
||||
"width_in": 13.25,
|
||||
"height_in": 13.25,
|
||||
"depth_in": 15.4,
|
||||
},
|
||||
],
|
||||
}
|
||||
],
|
||||
)
|
||||
save_locations(cfg, {"240:24621": {"opening_id": "den-b1", "note": ""}})
|
||||
out = tmp_path / "site"
|
||||
run_export(cfg, out, fetch=_cdn([]), sleep=lambda s: None)
|
||||
|
||||
shelves = (out / "shelves" / "index.html").read_text()
|
||||
assert "Den Kallax" in shelves and "the wall behind the couch" in shelves
|
||||
assert "Britannia" in shelves # the resident links from its opening
|
||||
assert 'href="../britannia/"' in shelves
|
||||
detail = (out / "britannia" / "index.html").read_text()
|
||||
assert "where it lives" in detail and "Den Kallax · B1" in detail
|
||||
# containment renders on both pages
|
||||
local = (out / "britannia-2" / "index.html").read_text()
|
||||
assert "in this box" not in local # the localgame contains nothing
|
||||
index = (out / "index.html").read_text()
|
||||
assert "see the shelves" in index
|
||||
|
||||
# opt-out: no shelves page, no location lines, no link
|
||||
out2 = tmp_path / "site2"
|
||||
run_export(cfg, out2, include_shelves=False, fetch=_cdn([]), sleep=lambda s: None)
|
||||
assert not (out2 / "shelves").exists()
|
||||
assert "where it lives" not in (out2 / "britannia" / "index.html").read_text()
|
||||
assert "see the shelves" not in (out2 / "index.html").read_text()
|
||||
|
||||
+45
-10
@@ -59,23 +59,58 @@ def test_fits_opening_speaks_the_house_language():
|
||||
assert fits_opening(big, TRAVEL) is None
|
||||
|
||||
|
||||
def test_opening_report_counts_stack_and_warns():
|
||||
def test_opening_report_packs_two_lanes():
|
||||
thin = _entry("Thin", 11, 11, 2)
|
||||
thick = _entry("Thick", 11, 11, 4)
|
||||
unmeasured = _entry("Mystery", w=None)
|
||||
report = opening_report(KALLAX, [("a", thin), ("b", thick), ("c", unmeasured)])
|
||||
assert report["games"] == 3
|
||||
assert report["stacked_in"] == 6
|
||||
assert report["flat_in"] == 6 # both lie flat: 2 + 4 within 13.25
|
||||
assert report["unmeasured"] == 1
|
||||
assert report["overfull"] is False
|
||||
# seven thick boxes overflow a 13.25" interior
|
||||
stack = [(str(i), thick) for i in range(7)]
|
||||
assert opening_report(KALLAX, stack)["overfull"] is True
|
||||
# a misfit is named even when the stack has room
|
||||
# a misfit is named even when the lanes have room
|
||||
report = opening_report(KALLAX, [("a", _entry("Too Long", 11.5, 17, 2.25))])
|
||||
assert report["misfits"] == ["Too Long"]
|
||||
|
||||
|
||||
def test_reviewer_lane_cases():
|
||||
"""The two acceptance cases: 3 flat + 4 standing in a 26.8" wide
|
||||
passes; 12 standard boxes FORCED all-flat in a cube still warns."""
|
||||
wide = {
|
||||
"id": "w",
|
||||
"label": "top",
|
||||
"zone": "",
|
||||
"width_in": 26.8,
|
||||
"height_in": 13.25,
|
||||
"depth_in": 15.4,
|
||||
}
|
||||
big = [_entry(f"Big{i}", 11.6, 11.6, 2.8) for i in range(3)]
|
||||
small = [_entry(f"Small{i}", 11.5, 9, 2) for i in range(4)]
|
||||
report = opening_report(
|
||||
wide,
|
||||
[(f"b{i}", g) for i, g in enumerate(big)]
|
||||
+ [(f"s{i}", g) for i, g in enumerate(small)],
|
||||
)
|
||||
assert report["overfull"] is False and report["laneless"] == []
|
||||
assert report["flat_in"] == 8.4 # 3 × 2.8 lying flat
|
||||
assert report["standing_in"] == 8 # 4 × 2 on edge beside the stack
|
||||
assert sorted(set(report["lanes"].values())) == ["flat", "standing"]
|
||||
|
||||
# 12 standard boxes forced flat: only ~4 fit the height; warn
|
||||
twelve = [(f"g{i}", _entry(f"G{i}", 11.6, 11.6, 2.8)) for i in range(12)]
|
||||
forced = opening_report(
|
||||
KALLAX, twelve, overrides={f"g{i}": "flat" for i in range(12)}
|
||||
)
|
||||
assert forced["overfull"] is True
|
||||
assert len(forced["laneless"]) == 8 # 4 fit the 13.25" height, 8 cannot
|
||||
|
||||
# unforced, the same twelve mostly still can't fit a single cube —
|
||||
# flat takes 4, standing takes what remains of the width, rest warn
|
||||
free = opening_report(KALLAX, twelve)
|
||||
assert free["overfull"] is True
|
||||
assert len(free["laneless"]) > 0
|
||||
|
||||
|
||||
def test_effective_location_inherits_from_container():
|
||||
games = {
|
||||
"173634": _entry("Trove", bgg_id=173634),
|
||||
@@ -401,7 +436,7 @@ def test_stored_in_chain_resolves_to_the_outermost_box(tmp_path):
|
||||
assert {g["name"] for g in resident} == {"Trove", "Insert", "Minis"}
|
||||
# only the physical box is stacked and counted
|
||||
assert state["units"][0]["openings"][0]["games"] == 1
|
||||
assert state["units"][0]["openings"][0]["stacked_in"] == 4
|
||||
assert state["units"][0]["openings"][0]["flat_in"] == 4
|
||||
assert state["unshelved"] == []
|
||||
|
||||
# deep cycles refuse: Trove into Minis would close the loop
|
||||
@@ -787,9 +822,9 @@ def test_reunification_outranks_tightest_fit_and_full_shelves_stop_lying(tmp_pat
|
||||
|
||||
state = web.get("/api/shelves").json()
|
||||
exp = next(g for g in state["unshelved"] if g["name"].endswith("Tower"))
|
||||
# A1 holds the series-mate BUT its remaining height (13.25 - 3 - 11 < 2)
|
||||
# can't take the expansion: honesty beats reunification, so the empty
|
||||
# twin cube leads and A1 is not offered at all
|
||||
# A1 holds the series-mate BUT its packing already fails (the base
|
||||
# is laneless behind the hog): a broken opening is never offered, so
|
||||
# honesty beats reunification and the empty twin cube leads
|
||||
offered = [s["id"] for s in exp["suggestions"]]
|
||||
assert ids["A1"] not in offered
|
||||
assert offered[0] == ids["A2"]
|
||||
|
||||
Reference in New Issue
Block a user