fix(cliffs): rotbb collision boxes are ROTATED, not their bounding box (#84) - #88
Merged
Merged
Conversation
#84) The lava perimeter that cost 13 real Vulcanus cliffs their placement was never the lava mask. It was the collision box's shape. rotbb(x, y, size, intersect) (entity-util.lua:9) returns {{cx-x_dist, cy-y_dist}, {cx+x_dist, cy+y_dist}, 1/8} - a rectangle PLUS an orientation of 1/8, i.e. 45 degrees. Sixteen of the twenty cliff orientations are built with it; only the four straight ones are axis-aligned. The port kept only the bounding box, which overruns the rotated rectangle at all four corners, so it rejected cliffs on lava the game never looks at. CLIFF_ORIENTATION_COLLISION_BOX remains the correct BROAD phase - wouldCollide derives its tile rect from a fixed-point floor over an inclusive rectangle, which cliffCollisionTileBox reproduces. cliffBoxCoversTile adds the narrow phase (separating-axis over the two world axes and the rectangle's own two). | | AABB (before) | oriented (after) | | ------------------- | ------------- | ---------------- | | recall | 0.9675 | 0.9758 | | precision | 0.9743 | 0.9727 | | [0,0] recall | 0.9788 | 1.0000 | | level-sweep recall | 0.951 @ lvl20 | ~1.000 everywhere | Clears 13 of 13 false rejections while keeping 182 of the 185 rejections that remove genuine false positives - the correct shape, not a loosening that trades precision for recall. How it was found, because the route matters more than the fix. The standing explanation - in this spec's own comments and in the notes - was that the tile resolver was "off by about one tile SOMEWHERE". A new dense capture (oracle-vulcanus-lava-boundary, radius-4 neighbourhoods around the 35 accusing tiles) found ZERO lava mismatches over 994 positions and 35/35 at the accusing tiles themselves. The mask was innocent. It was worth capturing because the existing 381-position survey structurally could not answer the question: its sensitivity was measured by planting scale factors on lava's probability, and 1.02 and 1.2 both still pass. Two corrections this forces: - PR #86's "gap 0.067 -> 0.018" is wrong; it is 0.024. That was measured with the over-aggressive AABB rejection, which deleted cells the game keeps and so flattered exactly the ratio it reported. A too-strong correction hides the thing it is correcting. The residual below elevation 120 is real, still open, and now pure over-placement with no recall cost. - "All 13 sit at Chebyshev depth 1 in our lava" was a true measurement that pointed at the wrong suspect. The box's four corners ARE its perimeter, so a corner-shaped box error produces the same signature a one-tile-fat mask would. A statistic can only rule a suspect out if it would come out DIFFERENTLY for each candidate. Checked and cleared on the way: the inclusive-floor fringe is real engine behaviour (disassembly), and chunk ordering is not it (2 of 13 cross a chunk boundary against 50 of 185 in the control - less than baseline). 6 of the 13 also carry a wrong orientation (~23x enrichment over the 2.0% base rate), always a smaller -to-none variant of ours; those belong to the standing orientation residual, which is unchanged. Guards: cliffOrientedBox.spec.ts pins the geometry, and the levels sweep asserts per-level recall > 0.99. Both verified non-vacuous by planting AABB behaviour - they fail with "expected 0 to be greater than 20" and "expected 0.9514 to be greater than 0.99". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt
This was referenced Aug 2, 2026
wormeyman
added a commit
that referenced
this pull request
Aug 2, 2026
) Disassembly, prompted by flagging #88's fix as the thing I was least confident about. It was right to flag: #88 scored best on every metric and was wrong about the mechanism. The engine does NOT collide against rotbb's rectangle rotated 45 degrees. It uses the RAW stored rectangle and discards the orientation tag entirely: 1. EntityMapGenerationTask::tryToAddCliff (0x101625038) loads the orientation's box from proto + 0x5c0 + id*0x48 (20 bytes: four int32 edges at +4, the orientation word at +0x14) and calls wouldCollide with Direction = 0 - literally `mov x4, #0x0`. 2. wouldCollide (0x101625468) forwards box and direction to BoundingBox::BoundingBox(BoundingBox const&, Direction) (0x101c04380), then floors with (box + position) >> 8 over an inclusive tile rect. 3. That constructor zeroes the destination, writes sentinel 0x80010000 into the destination's orientation word, and dispatches through a jump table whose entry 0 is 0 (read at 0x102d01400) - the identity arm, which copies left_top/right_bottom verbatim. The source orientation is never read; the rotate arm is reachable only for a non-zero Direction. Corroborated by the API mirror: BoundingBox is documented as {MapPosition, MapPosition} or {..., RealOrientation} with orientation OPTIONAL, and OrientedCliffPrototype::collision_bounding_box is a plain BoundingBox. | box | false rej | recall | precision | evidence | | ------------------------------ | --------- | ------ | --------- | ---------- | | AABB (until #88) | 13 | 0.9675 | 0.9743 | assumption | | 45-degree oriented rect (#88) | 0 | 0.9758 | 0.9727 | fit only | | raw stored rect (this) | 6 | 0.9720 | 0.9713 | disasm | The middle row is the trap. It shrank the box past the engine's, and the excess shrinkage absorbed a DIFFERENT defect: 4 of the 6 cliffs the correct box still rejects are cells where our orientation disagrees with the game's, so we load the wrong box entirely. Those 4 belong to the standing orientation residual and should stay visible. A model that scores perfectly by hiding a second bug is worse than one that leaves it exposed. Edges are quantised to 1/256 - MapPosition is 8-bit fixed point, so x_dist's sqrt(2) cannot reach the engine at full precision. test/cliffOrientation.spec.ts asserted the OPPOSITE of this until now, on the stated reasoning that the AABB "is what the engine ends up scanning". That block is rewritten: the shipped box is the raw rectangle, it DEPENDS on intersect (the old file asserted independence), and its area is at most half the AABB's - though it is not contained in it, since a small intersect pushes hx past size/2. An attempt to assert containment on every axis failed for exactly that reason. cliffBoxCoversTile and the separating-axis narrow phase are deleted; the broad phase alone is now the whole rule, which is what the engine does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt
wormeyman
added a commit
that referenced
this pull request
Aug 2, 2026
) (#90) * docs(cliffs): shipping orientation is 2.4%, not 2.0% (#84) The banner kept the pre-#88 figure. Since the collision box was corrected to rotbb's rotated rectangle the rejection costs ZERO true positives, so the shipping matched set is identical to the unfiltered one - 1531 either way, 37 wrong orientations, 2.42%. Measured on both paths. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt * fix(cliffs): the collision box is the RAW rectangle - #88 was wrong (#84) Disassembly, prompted by flagging #88's fix as the thing I was least confident about. It was right to flag: #88 scored best on every metric and was wrong about the mechanism. The engine does NOT collide against rotbb's rectangle rotated 45 degrees. It uses the RAW stored rectangle and discards the orientation tag entirely: 1. EntityMapGenerationTask::tryToAddCliff (0x101625038) loads the orientation's box from proto + 0x5c0 + id*0x48 (20 bytes: four int32 edges at +4, the orientation word at +0x14) and calls wouldCollide with Direction = 0 - literally `mov x4, #0x0`. 2. wouldCollide (0x101625468) forwards box and direction to BoundingBox::BoundingBox(BoundingBox const&, Direction) (0x101c04380), then floors with (box + position) >> 8 over an inclusive tile rect. 3. That constructor zeroes the destination, writes sentinel 0x80010000 into the destination's orientation word, and dispatches through a jump table whose entry 0 is 0 (read at 0x102d01400) - the identity arm, which copies left_top/right_bottom verbatim. The source orientation is never read; the rotate arm is reachable only for a non-zero Direction. Corroborated by the API mirror: BoundingBox is documented as {MapPosition, MapPosition} or {..., RealOrientation} with orientation OPTIONAL, and OrientedCliffPrototype::collision_bounding_box is a plain BoundingBox. | box | false rej | recall | precision | evidence | | ------------------------------ | --------- | ------ | --------- | ---------- | | AABB (until #88) | 13 | 0.9675 | 0.9743 | assumption | | 45-degree oriented rect (#88) | 0 | 0.9758 | 0.9727 | fit only | | raw stored rect (this) | 6 | 0.9720 | 0.9713 | disasm | The middle row is the trap. It shrank the box past the engine's, and the excess shrinkage absorbed a DIFFERENT defect: 4 of the 6 cliffs the correct box still rejects are cells where our orientation disagrees with the game's, so we load the wrong box entirely. Those 4 belong to the standing orientation residual and should stay visible. A model that scores perfectly by hiding a second bug is worse than one that leaves it exposed. Edges are quantised to 1/256 - MapPosition is 8-bit fixed point, so x_dist's sqrt(2) cannot reach the engine at full precision. test/cliffOrientation.spec.ts asserted the OPPOSITE of this until now, on the stated reasoning that the AABB "is what the engine ends up scanning". That block is rewritten: the shipped box is the raw rectangle, it DEPENDS on intersect (the old file asserted independence), and its area is at most half the AABB's - though it is not contained in it, since a small intersect pushes hx past size/2. An attempt to assert containment on every axis failed for exactly that reason. cliffBoxCoversTile and the separating-axis narrow phase are deleted; the broad phase alone is now the whole rule, which is what the engine does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 2, 2026
…#84) (#93) #91 handed over one lead: "the defect is in the grid-4 cliff-elevation field, the one input with no per-corner oracle". It is refuted, and so is the idea that there is a single cause. Everything upstream of the crossing test is now measured, not read: - grid-1 `vulcanus_elevation` exact to 4.8e-2 over all 12,675 captured corners - the grid-4 `multisample` min-filter reproduces the game through the CLIFF GENERATOR at both [0,0] and [1500,1500]; grid 1/2/8/centred all score far worse, and `multisample(e,0,0)` is the identity in this channel - `cliffiness_basic` exact to 6.4e-6 - over the 4,266 UNCLAMPED corners. 8,409 of 12,675 sit ON a clamp, so the old "exact" claim was two-thirds vacuous: it was measuring the clamp, not `qmn` - `cliff_smoothing = 1` READ BACK off Vulcanus's own surface. It had been inferred from the prototype default since #28 and never once measured - the smoothing stencil measured on both axes with a delta probe, whose in-chunk-3 arms predict the game places NOTHING and it places nothing - `crossingsForChunk` re-disassembled at 0x10160c9cc (the VA in the notes had moved); `smoothingKnots` matches it line for line - `fixImpossibleCells`'s give-up branch never fires: 0 chunks need a retry With `cliff_smoothing = 0` and every other term real: [0,0] 0 wrong (7 at s=1) [-1200,800] 0 wrong (4 at s=1), precision 1.0000 [1500,1500] 21 wrong (26 at s=1) So two regions carry a smoothing-only defect and one carries a defect that survives smoothing being off - all over-detections, all at the high bands (670/790/1030), margins 0.69-46.6 units, so not float32. Scoring [0,0] alone says "it's the smoothing", which is false for 21 of the 37; two regions out of three agreeing is the same trap as #88. Also re-scopes #84 item 2: the "over-placement below elevation 120" is not spread over the low band, it is one contiguous blob (cells cx 43-48, cy 34-40) where the game places zero cliffs under every setting tried AND under a synthetic cliff_elevation. A field-independent hole is a rule we do not implement, not a field error - and it is not lava. Two new oracle fixtures + provenance; no src change. Claude-Session: https://claude.ai/code/session_015yqZhJCUVLDU5q7SVwAVkj Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 2, 2026
#99 characterised the rule and stopped short of porting it, flagging one open sub-question: whether driving it from our own resource model, rather than the game's entities, is accurate enough. It is - and it costs exactly one cell. Scored across all three oracle regions, driving `makeVulcanusOreRejection` off the same field stack the ore overlay paints from: | region | game | placed | fires | false rejections | surplus | | --- | --- | --- | --- | --- | --- | | [0,0] | 283 | 283 | 0 | 0 | 2 -> 2 | | [1500,1500] | 885 | 900 | 20 | 0 | 42 -> 22 | | [-1200,800] | 401 | 387 | 0 | 0 | 1 -> 1 | Precision at [1500,1500] 0.953 -> 0.975 with the 858 true positives untouched. **Recall is not touched anywhere**, which was the gate: this rule may only ever cost precision. Three variants were scored and the two that lose are kept in the spec rather than dismissed in a comment, because #88/#90 already paid for that lesson here: | variant | fires | correct of 31 | false rejections | | --- | --- | --- | --- | | base box, ores only (SHIPPED) | 20 | 20 | 0 | | base box + geyser | 21 | 20 | 1 | | per-orientation box | 23 | 21 | 2 | The geyser arm is strictly HARMFUL - one more false rejection and not one additional correct suppression - so it is implemented behind `includeGeyser`, defaulting off. The per-orientation rotbb box catches one more true cell and pays two kept cliffs for it; higher `correct` is exactly the trap. Not claimed: the mechanism is still open (the disassembly says cliffs are placed before any resource entity exists), and 11 of the 31 stay unexplained with the box deliberately not widened to cover them. The spec pins that 11. Lands as `CliffBands.cellRejects`, an opaque per-cell predicate beside `tileCollides` - so the shared cliff core stays planet-agnostic, and the model the specs score is the model the renderer ships. The predicate enumerates no entities: the overlapping tiles follow in closed form (2 tiles for an ore against the lava rejection's ~30), guarded by a wider brute-force scan. Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 3, 2026
…0.9961 (#84) (#101) * feat(cliffs): port the ORE -> CLIFF rejection (#84 item 1) #99 characterised the rule and stopped short of porting it, flagging one open sub-question: whether driving it from our own resource model, rather than the game's entities, is accurate enough. It is - and it costs exactly one cell. Scored across all three oracle regions, driving `makeVulcanusOreRejection` off the same field stack the ore overlay paints from: | region | game | placed | fires | false rejections | surplus | | --- | --- | --- | --- | --- | --- | | [0,0] | 283 | 283 | 0 | 0 | 2 -> 2 | | [1500,1500] | 885 | 900 | 20 | 0 | 42 -> 22 | | [-1200,800] | 401 | 387 | 0 | 0 | 1 -> 1 | Precision at [1500,1500] 0.953 -> 0.975 with the 858 true positives untouched. **Recall is not touched anywhere**, which was the gate: this rule may only ever cost precision. Three variants were scored and the two that lose are kept in the spec rather than dismissed in a comment, because #88/#90 already paid for that lesson here: | variant | fires | correct of 31 | false rejections | | --- | --- | --- | --- | | base box, ores only (SHIPPED) | 20 | 20 | 0 | | base box + geyser | 21 | 20 | 1 | | per-orientation box | 23 | 21 | 2 | The geyser arm is strictly HARMFUL - one more false rejection and not one additional correct suppression - so it is implemented behind `includeGeyser`, defaulting off. The per-orientation rotbb box catches one more true cell and pays two kept cliffs for it; higher `correct` is exactly the trap. Not claimed: the mechanism is still open (the disassembly says cliffs are placed before any resource entity exists), and 11 of the 31 stay unexplained with the box deliberately not widened to cover them. The spec pins that 11. Lands as `CliffBands.cellRejects`, an opaque per-cell predicate beside `tileCollides` - so the shared cliff core stays planet-agnostic, and the model the specs score is the model the renderer ships. The predicate enumerates no entities: the overlapping tiles follow in closed form (2 tiles for an ore against the lava rejection's ~30), guarded by a wider brute-force scan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the budget FLIPPED - recall is now the bigger defect (#84) Every cliff defect found since #18 has been a rule the port over-places without (lava collision, the rotbb box shape, the ore suppression), so "find another rejection" has been the shape of the work throughout. After #100 that is no longer where the error is. | region | surplus | missing | lava-killed | ore-killed | never generated | | --- | --- | --- | --- | --- | --- | | [0,0] | 2 | 2 | 2 | 0 | 0 | | [1500,1500] | 22 | 27 | 3 | 0 | 24 | | [-1200,800] | 1 | 15 | 1 | 0 | 14 | | total | 25 | 44 | 6 | 0 | 38 | **The port now misses more cells than it over-places, 44 to 25**, and 38 of the 44 are cells the crossings stage never produces at all - a different defect in a different part of the port from everything solved so far. `[0,0]` generates every cell the game does: its whole miss is the two the lava rejection took, and `neverGenerated` is zero there. All 38 sit in the two far-field regions, which agrees with #93 finding the port exact at [0,0] and [-1200,800] at cliff_smoothing = 0 and still wrong at [1500,1500]. Also closes item 3 (the entity half of `Surface::wouldCollide`) UNPORTED, by size rather than by difficulty - the same move that retired fixImpossibleCells as a suspect. It is a rejection, and rejections can only remove cells: total surplus is 25, which bounds what rocks and craters together could ever be worth against a 44-cell recall gap they cannot touch. The crater arm is settled exactly, since craters are already in the fixtures: all 8 sit in [-1200,800] and not one touches a cell the port over-places, nor any cliff the game kept. Worth zero. The rock arm needs no fixture - the ceiling covers it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the recall gap was a QUERY-WINDOW ARTIFACT - recall is 0.9961 (#84) Supersedes this branch's first commit, whose central claim was wrong. `find_entities_filtered` selects entities whose BOUNDING BOX touches the query area; `placedCells` emits cells whose CENTRE lies inside it. Different inclusion rules, so the fixtures carry cliffs centred just outside the box and every one has been scored as a miss. | region | game rows | centred inside | centred OUTSIDE | | --- | --- | --- | --- | | [0,0] | 283 | 283 | 0 | | [1500,1500] | 885 | 861 | 24 | | [-1200,800] | 401 | 387 | 14 | That is 38 cells - the entire apparent recall gap - and the port places 38 of 38 once the query box includes their centres. Every one is an agreement being scored as a failure. The widening arm is the load-bearing one: "we never looked there" alone is equally consistent with the port being wrong. Corrected budget, both sides scored alike: | region | game | port | matched | surplus | missing | | --- | --- | --- | --- | --- | --- | | [0,0] | 283 | 283 | 281 | 2 | 2 | | [1500,1500] | 861 | 880 | 858 | 22 | 3 | | [-1200,800] | 387 | 387 | 386 | 1 | 1 | | total | 1531 | 1550 | 1525 | 25 | 6 | **Recall 0.9961, precision 0.9839.** The 0.972 recall in the notes divided the same 1525 matches by 1569 rather than 1531 - the match count was never wrong, only the denominator. All 6 missing cells are ones our own lava rejection removed; there is no cell the port simply fails to generate. So precision is the only real defect left. Consequently item 3 (the entity half of Surface::wouldCollide) is RE-OPENED. The earlier commit closed it by size, arguing a rejection cannot help a 44-cell recall gap - that argument died with the gap. With recall at 0.9961 the dominant defect is the 25 surplus cells, which is exactly what a rejection removes. The crater arm stays settled at zero (all 8 sit in [-1200,800], none touches a surplus cell). The rock arm has no oracle capture at all, so capturing one is the next step - now with a 25-cell target rather than a ceiling against it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the rock arm fails on the mechanism's own geometry (#84) Completes the item 3 thread in this branch rather than opening a second PR for it, since the previous commit had just re-opened it. No rock capture is needed to kill it. `computeInternal` runs `generateCliffs` before `generateEntities`, and `apply` runs `applyCliffs` (+124) before `applyEntities` (+164), so within a chunk no rock exists when the cliff is applied. A rock can only block a cliff from an ALREADY-GENERATED NEIGHBOUR, which confines the whole mechanism to cells near a 32-tile chunk border. | | n | near chunk border | | --- | --- | --- | | surplus | 25 | 11 = 44.0% | | matched | 1525 | 673 = 44.1% | The base rate to three significant figures. The surplus has no chunk-border character at all, so the one geometry the mechanism is confined to is not where the errors are. The direct overlap test agrees and is deliberately the weaker arm: 3 of 25 surplus cells overlap a modelled rock against a 6.6% base rate (~1.7 expected), which is nothing - and our rock placement is a salt-dependent roll whose individual positions are unreliable exactly as the geyser's were in #100. So item 3 explains approximately none of the 25, and is closed on the mechanism's geometry rather than on the ceiling argument that died with the recall gap. Remaining unexplained: 25 surplus, 6 missing (all lava-rejection over-rejections), 33 wrong orientations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
wormeyman
added a commit
that referenced
this pull request
Aug 3, 2026
) * feat(cliffs): port the ORE -> CLIFF rejection (#84 item 1) #99 characterised the rule and stopped short of porting it, flagging one open sub-question: whether driving it from our own resource model, rather than the game's entities, is accurate enough. It is - and it costs exactly one cell. Scored across all three oracle regions, driving `makeVulcanusOreRejection` off the same field stack the ore overlay paints from: | region | game | placed | fires | false rejections | surplus | | --- | --- | --- | --- | --- | --- | | [0,0] | 283 | 283 | 0 | 0 | 2 -> 2 | | [1500,1500] | 885 | 900 | 20 | 0 | 42 -> 22 | | [-1200,800] | 401 | 387 | 0 | 0 | 1 -> 1 | Precision at [1500,1500] 0.953 -> 0.975 with the 858 true positives untouched. **Recall is not touched anywhere**, which was the gate: this rule may only ever cost precision. Three variants were scored and the two that lose are kept in the spec rather than dismissed in a comment, because #88/#90 already paid for that lesson here: | variant | fires | correct of 31 | false rejections | | --- | --- | --- | --- | | base box, ores only (SHIPPED) | 20 | 20 | 0 | | base box + geyser | 21 | 20 | 1 | | per-orientation box | 23 | 21 | 2 | The geyser arm is strictly HARMFUL - one more false rejection and not one additional correct suppression - so it is implemented behind `includeGeyser`, defaulting off. The per-orientation rotbb box catches one more true cell and pays two kept cliffs for it; higher `correct` is exactly the trap. Not claimed: the mechanism is still open (the disassembly says cliffs are placed before any resource entity exists), and 11 of the 31 stay unexplained with the box deliberately not widened to cover them. The spec pins that 11. Lands as `CliffBands.cellRejects`, an opaque per-cell predicate beside `tileCollides` - so the shared cliff core stays planet-agnostic, and the model the specs score is the model the renderer ships. The predicate enumerates no entities: the overlapping tiles follow in closed form (2 tiles for an ore against the lava rejection's ~30), guarded by a wider brute-force scan. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the budget FLIPPED - recall is now the bigger defect (#84) Every cliff defect found since #18 has been a rule the port over-places without (lava collision, the rotbb box shape, the ore suppression), so "find another rejection" has been the shape of the work throughout. After #100 that is no longer where the error is. | region | surplus | missing | lava-killed | ore-killed | never generated | | --- | --- | --- | --- | --- | --- | | [0,0] | 2 | 2 | 2 | 0 | 0 | | [1500,1500] | 22 | 27 | 3 | 0 | 24 | | [-1200,800] | 1 | 15 | 1 | 0 | 14 | | total | 25 | 44 | 6 | 0 | 38 | **The port now misses more cells than it over-places, 44 to 25**, and 38 of the 44 are cells the crossings stage never produces at all - a different defect in a different part of the port from everything solved so far. `[0,0]` generates every cell the game does: its whole miss is the two the lava rejection took, and `neverGenerated` is zero there. All 38 sit in the two far-field regions, which agrees with #93 finding the port exact at [0,0] and [-1200,800] at cliff_smoothing = 0 and still wrong at [1500,1500]. Also closes item 3 (the entity half of `Surface::wouldCollide`) UNPORTED, by size rather than by difficulty - the same move that retired fixImpossibleCells as a suspect. It is a rejection, and rejections can only remove cells: total surplus is 25, which bounds what rocks and craters together could ever be worth against a 44-cell recall gap they cannot touch. The crater arm is settled exactly, since craters are already in the fixtures: all 8 sit in [-1200,800] and not one touches a cell the port over-places, nor any cliff the game kept. Worth zero. The rock arm needs no fixture - the ceiling covers it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the recall gap was a QUERY-WINDOW ARTIFACT - recall is 0.9961 (#84) Supersedes this branch's first commit, whose central claim was wrong. `find_entities_filtered` selects entities whose BOUNDING BOX touches the query area; `placedCells` emits cells whose CENTRE lies inside it. Different inclusion rules, so the fixtures carry cliffs centred just outside the box and every one has been scored as a miss. | region | game rows | centred inside | centred OUTSIDE | | --- | --- | --- | --- | | [0,0] | 283 | 283 | 0 | | [1500,1500] | 885 | 861 | 24 | | [-1200,800] | 401 | 387 | 14 | That is 38 cells - the entire apparent recall gap - and the port places 38 of 38 once the query box includes their centres. Every one is an agreement being scored as a failure. The widening arm is the load-bearing one: "we never looked there" alone is equally consistent with the port being wrong. Corrected budget, both sides scored alike: | region | game | port | matched | surplus | missing | | --- | --- | --- | --- | --- | --- | | [0,0] | 283 | 283 | 281 | 2 | 2 | | [1500,1500] | 861 | 880 | 858 | 22 | 3 | | [-1200,800] | 387 | 387 | 386 | 1 | 1 | | total | 1531 | 1550 | 1525 | 25 | 6 | **Recall 0.9961, precision 0.9839.** The 0.972 recall in the notes divided the same 1525 matches by 1569 rather than 1531 - the match count was never wrong, only the denominator. All 6 missing cells are ones our own lava rejection removed; there is no cell the port simply fails to generate. So precision is the only real defect left. Consequently item 3 (the entity half of Surface::wouldCollide) is RE-OPENED. The earlier commit closed it by size, arguing a rejection cannot help a 44-cell recall gap - that argument died with the gap. With recall at 0.9961 the dominant defect is the 25 surplus cells, which is exactly what a rejection removes. The crater arm stays settled at zero (all 8 sit in [-1200,800], none touches a surplus cell). The rock arm has no oracle capture at all, so capturing one is the next step - now with a 25-cell target rather than a ceiling against it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the rock arm fails on the mechanism's own geometry (#84) Completes the item 3 thread in this branch rather than opening a second PR for it, since the previous commit had just re-opened it. No rock capture is needed to kill it. `computeInternal` runs `generateCliffs` before `generateEntities`, and `apply` runs `applyCliffs` (+124) before `applyEntities` (+164), so within a chunk no rock exists when the cliff is applied. A rock can only block a cliff from an ALREADY-GENERATED NEIGHBOUR, which confines the whole mechanism to cells near a 32-tile chunk border. | | n | near chunk border | | --- | --- | --- | | surplus | 25 | 11 = 44.0% | | matched | 1525 | 673 = 44.1% | The base rate to three significant figures. The surplus has no chunk-border character at all, so the one geometry the mechanism is confined to is not where the errors are. The direct overlap test agrees and is deliberately the weaker arm: 3 of 25 surplus cells overlap a modelled rock against a 6.6% base rate (~1.7 expected), which is nothing - and our rock placement is a salt-dependent roll whose individual positions are unreliable exactly as the geyser's were in #100. So item 3 explains approximately none of the 25, and is closed on the mechanism's geometry rather than on the ceiling argument that died with the recall gap. Remaining unexplained: 25 surplus, 6 missing (all lava-rejection over-rejections), 33 wrong orientations. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c * test(cliffs): the orientation residual is not a boundary tie (#84) Stacked on the error-budget branch. The residual's known shape - exactly one edge per wrong cell, always an OVER-detection - has an obvious cheap explanation that turns out to be wrong, and ruling it out eliminates a whole class of cause. `crossesCliff` decides on the sign of `elevation - boundary`. If an endpoint sat within float noise of a band boundary, the ~1e-6 our fields agree to would flip the crossing, and the residual would be an irreducible precision limit with nothing to fix. Measured: every crossing edge in a wrong cell sits at least 0.205 from its boundary, median ~9.9 - four to seven orders of magnitude clear of float noise. For the game to disagree its elevation must differ from ours by more than 0.2, which is a real field or rule difference. Non-vacuity: the overall minimum across all 2,920 crossing edges is 6.4e-3, thirty times tighter, so "far from the boundary" is a property of the wrong cells and not of the sample. Also pins WHICH channel the corner fixture holds, as a number rather than as prose: against our per-tile elevation the worst corner differs by 4.8e-2; against the grid-4 cliff channel the generator actually reads, by 96.09. #83 was the most expensive mistake this subsystem has made and prose is not a guard. The gap that leaves is the point: the grid-4 cliff-elevation channel has NO per-corner oracle, is the only placement input never checked against the game corner by corner, and after the margin result is the only remaining candidate that could move an endpoint the required 0.2. Capturing it is the next step - and not via calculate_tile_properties, which is the 1-tile program that produced the wrong-channel fixture in the first place. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GyN97UwFQmwZs1cg4QHS1c --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.
The lava perimeter that cost 13 real Vulcanus cliffs their placement was never the lava mask. It was the collision box's shape.
The bug
rotbb(x, y, size, intersect)(entity-util.lua:9) returns{{cx-x_dist, cy-y_dist}, {cx+x_dist, cy+y_dist}, 1/8}— a rectangle plus an orientation of 1/8, i.e. 45°. Sixteen of the twenty cliff orientations are built with it; only the four straight ones are axis-aligned. The port kept only the bounding box, which overruns the rotated rectangle at all four corners — so it rejected cliffs on lava the game never looks at.CLIFF_ORIENTATION_COLLISION_BOXremains the correct broad phase (wouldCollidederives its tile rect from a fixed-point floor over an inclusive rectangle).cliffBoxCoversTileadds the narrow phase.[0,0]recallClears 13 of 13 false rejections while keeping 182 of the 185 rejections that remove genuine false positives — the correct shape, not a loosening that trades precision for recall.
How it was found
The standing explanation — in the spec's own comments and in the notes — was that the tile resolver was "off by about one tile SOMEWHERE". A new dense capture (
oracle-vulcanus-lava-boundary, radius-4 neighbourhoods around the 35 accusing tiles) found zero lava mismatches over 994 positions, and 35/35 agreement at the accusing tiles themselves. The mask was innocent.That capture was worth making because the existing 381-position survey structurally could not answer the question: its sensitivity was measured by planting scale factors on
lava's probability, and1.02and1.2both still pass.Two corrections this forces
Checked and cleared on the way
(box + position) >> 8over an inclusive rect, already established by disassembly.-to-nonevariant of ours. A wrong orientation means the wrong box, so the two defects compound; those 6 belong to the standing orientation residual, which is unchanged.Guards
cliffOrientedBox.spec.tspins the geometry; the levels sweep asserts per-level recall > 0.99. Both verified non-vacuous by planting AABB behaviour — they fail withexpected 0 to be greater than 20andexpected 0.9514 to be greater than 0.99.pnpm run verifygreen: 1314 passed / 3 skipped, 0 warnings.🤖 Generated with Claude Code
https://claude.ai/code/session_01WRKSNgkidxc6daeHGJHqpt