Skip to content

Commit b545a3a

Browse files
pdbethkeclaude
andcommitted
fix: the Brief lists a wagon once, not four times
A regression I introduced this morning, found by reading the page a model actually gets rather than by running the suite. `Furnishing` projects its footprint into four wall segments so movement, line of sight, cover moves and Area Of Effect keep working --- they all read `scene.walls`. `Brief.features` reads `scene.walls` too and was never told the difference, so the page said: Photographer's wagon: 2.7m away, would give you cover 2/4 Photographer's wagon: 1.6m away, would not shield you from where they are Photographer's wagon: 3.1m away, would give you cover 2/4 Photographer's wagon: 4.1m away, would not shield you from where they are Four entries for one wagon, at four distances, contradicting each other about whether it is worth hiding behind --- because each is a different EDGE and the far ones genuinely shield nobody. Sixteen of the twenty-two lines in that section were the same three objects repeated. One object, one line: nearest edge for the distance, best cover any edge offers for the worth. Standing behind a wagon means standing behind the side facing the shooting. ONLY THE DERIVED EDGES FOLD, and getting that wrong cost a building. A first cut folded on `part_of` alone and silently deleted "C.S. Fly's photograph gallery" from the page, because it shares an owner with the boarding house wall. Those two were AUTHORED separately --- somebody named them separately because a fighter relates to them separately. A furnishing's edges were never authored at all. The fold now matches on ids that name a FURNISHING, which is the same distinction Krackle already draws in `isFurnishingEdge`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Lvepxj9B2mDc4cRN3jaWwn
1 parent 475fd0d commit b545a3a

2 files changed

Lines changed: 125 additions & 2 deletions

File tree

kirby_combat/brief.py

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,30 @@ def sightings(self) -> list[str]:
221221
p for p in (self.position_of(e.id) for e in self._enemies)
222222
if p is not None
223223
]
224+
# ONE THING, ONE LINE. A `Furnishing` projects its footprint into
225+
# four wall segments so movement and line of sight keep working,
226+
# and this reads `scene.walls` -- so a wagon arrived as four
227+
# planks at four distances, two of them saying "would not shield
228+
# you" because they are the far edges of the same object. A reader
229+
# cannot act on that, and the page is what a chooser acts on.
230+
#
231+
# Folded by `part_of`, which already means "a face of that
232+
# structure": nearest edge for the distance, best cover any edge
233+
# offers for the worth. Standing behind a wagon means standing
234+
# behind the side that faces the shooting.
235+
# ONLY THE DERIVED EDGES FOLD. A `Furnishing`'s four edges were
236+
# never authored --- they are one object's outline --- so they are
237+
# one line. A BUILDING's faces were: somebody wrote "C.S. Fly's
238+
# boarding house (west wall)" and "C.S. Fly's photograph gallery"
239+
# as separate walls because a fighter relates to them separately,
240+
# and folding them by `part_of` silently deleted the gallery from
241+
# the page. The same distinction Krackle draws in
242+
# `isFurnishingEdge`: an id that names a FURNISHING, not any
243+
# `part_of` at all.
244+
furnishing_ids = {
245+
f.id for f in (getattr(self.scene, "furnishings", None) or [])
246+
}
247+
best: dict[str, tuple] = {}
224248
out = []
225249
for wall in self.features:
226250
name = getattr(wall, "name", None) or getattr(wall, "id", "?")
@@ -250,8 +274,18 @@ def sightings(self) -> list[str]:
250274
hardness = f"; BODY {body} to break through"
251275
elif defense is not None:
252276
hardness = f"; DEF {defense}"
253-
out.append(f"{name}: {distance:.1f}m away, {worth}{hardness}")
254-
return out
277+
line = f"{name}: {distance:.1f}m away, {worth}{hardness}"
278+
owner = getattr(wall, "part_of", None)
279+
if owner not in furnishing_ids:
280+
out.append(line)
281+
continue
282+
# Keep the best of the faces: more cover wins, and among equals
283+
# the nearer one -- which is the face a mover would actually
284+
# put between himself and the threat.
285+
prior = best.get(owner)
286+
if prior is None or (level, -distance) > (prior[0], -prior[1]):
287+
best[owner] = (level, distance, line)
288+
return out + [entry[2] for entry in best.values()]
255289

256290
@property
257291
def bearings(self) -> list[EnemyBearing]:
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
"""A wagon is one wagon on the page, not four planks.
2+
3+
`Furnishing` projects its footprint into wall segments so that movement,
4+
line of sight, cover moves and Area Of Effect keep working -- they all
5+
read `scene.walls`. `Brief.features` reads `scene.walls` too, and it was
6+
never told the difference, so a Brief listed:
7+
8+
Photographer's wagon: 2.7m away, would give you cover 2/4
9+
Photographer's wagon: 1.6m away, would not shield you from where they are
10+
Photographer's wagon: 3.1m away, would give you cover 2/4
11+
Photographer's wagon: 4.1m away, would not shield you from where they are
12+
13+
Four entries for one wagon, at four distances, contradicting each other
14+
about whether it is worth hiding behind -- because each is a different
15+
EDGE of the same footprint and an edge on the far side genuinely does not
16+
shield you.
17+
18+
A reader cannot act on that, and the page is what a chooser acts on. The
19+
whole object is one feature: the nearest edge is how far away it is, and
20+
the best cover any edge offers is what it is worth.
21+
22+
Introduced by the footprint change on 2026-09-10 and caught by reading
23+
the page a model actually gets.
24+
"""
25+
from __future__ import annotations
26+
27+
from kirby_combat.brief import Brief
28+
29+
30+
def _page():
31+
from examples.the_shootout_we_can_publish import the_fight
32+
33+
pages: list[str] = []
34+
35+
class Peek:
36+
def choose(self, situation):
37+
if not pages:
38+
pages.append(situation.brief().render())
39+
return situation.menu[0].action_id
40+
41+
the_fight(59, chooser=Peek())
42+
return pages[0]
43+
44+
45+
def _feature_lines(page: str) -> list[str]:
46+
out, seen = [], False
47+
for line in page.splitlines():
48+
if line.startswith("What is around you:"):
49+
seen = True
50+
continue
51+
if seen:
52+
if not line.startswith(" "):
53+
break
54+
out.append(line.strip())
55+
return out
56+
57+
58+
def test_each_thing_appears_once():
59+
lines = _feature_lines(_page())
60+
names = [line.split(":")[0] for line in lines]
61+
duplicated = {n for n in names if names.count(n) > 1}
62+
assert not duplicated, f"listed more than once: {sorted(duplicated)}"
63+
64+
65+
def test_the_wagon_is_still_there():
66+
"""Deduplicating must not delete the thing."""
67+
names = [line.split(":")[0] for line in _feature_lines(_page())]
68+
assert any("wagon" in n.lower() for n in names), names
69+
70+
71+
def test_a_thing_is_worth_the_best_cover_any_of_it_offers():
72+
"""One edge of a wagon faces away and shields nobody; standing behind
73+
the wagon means standing behind the edge that does. Reporting the
74+
useless edge would tell a reader the wagon is worthless."""
75+
lines = _feature_lines(_page())
76+
wagon = next(line for line in lines if "wagon" in line.lower())
77+
assert "would give you cover" in wagon, wagon
78+
79+
80+
def test_a_buildings_named_faces_are_NOT_folded_together():
81+
"""Only the DERIVED edges fold. A furnishing's four edges were never
82+
authored -- they are one object's outline. A building's faces were:
83+
somebody wrote "C.S. Fly's boarding house (west wall)" and "C.S. Fly's
84+
photograph gallery" as separate walls because a fighter relates to
85+
them separately, and folding by `part_of` deleted the gallery from the
86+
page entirely."""
87+
names = [line.split(":")[0] for line in _feature_lines(_page())]
88+
assert any("gallery" in n.lower() for n in names), names
89+
assert any("boarding house" in n.lower() for n in names), names

0 commit comments

Comments
 (0)