Skip to content

Give the Vulcanus cliff-blocking tile set one definition per side (#364) - #368

Merged
wormeyman merged 1 commit into
mainfrom
fix/364-vulcanus-cliff-blocking-tiles
Aug 31, 2026
Merged

Give the Vulcanus cliff-blocking tile set one definition per side (#364)#368
wormeyman merged 1 commit into
mainfrom
fix/364-vulcanus-cliff-blocking-tiles

Conversation

@wormeyman

Copy link
Copy Markdown
Collaborator

Closes #364.

The set of Vulcanus tiles that refuse a cliff - lava and lava-hot - was
written out four times, and nothing checked the four agreed. Two of the four sat
on adjacent lines in fixtures.rs and said the same thing in different
vocabularies, one by enum and one by string literal, so they could drift apart
without either file changing.

Now there is one definition per side, and a test that the two sides agree.

What moved

before after
cliffs/vulcanus_fields.rs inlined Lava | LavaHot calls tile.is_cliff_blocking()
fixtures.rs is_lava, the same match again VulcanusTile::is_cliff_blocking
fixtures.rs want_lava, by name: name == "lava" || ... resolves through the new VulcanusTile::from_name, then asks the same predicate
cliffCatalog.ts unchanged, and still the only TypeScript definition

from_name is the inverse of the existing name(), resolved through
TILE_ORDER so the two cannot disagree. It refuses a name Vulcanus does not
place, which is what turns a typo in a fixture into a failure rather than a
silent false.

The two sides are now compared, not just tidied

fmw-wasm exports vulcanus_cliff_blocking_names_fnv1a64(), an FNV-1a 64 over
the blocking tiles' names, sorted and joined by newlines.
test/wasmVulcanusParity.spec.ts hashes VULCANUS_CLIFF_BLOCKING_TILES through
the module's own fnv1a64 and compares.

Three choices there are deliberate:

  • Names, not a count. A count cannot tell lava from volcanic-folds, and
    a swap is exactly the drift worth catching.
  • The module's own fnv1a64, not a second one in TypeScript. A
    reimplementation is one more thing that can disagree, and its disagreement
    would look identical to real drift.
  • Sorted. Catalog order is ground truth for the argmax tie-break, but a
    Set of two strings does not carry it, so hashing in catalog order would make
    this depend on a fact it is not trying to check.

It can fail, and that was watched rather than assumed

Adding VolcanicFolds to the Rust set and rebuilding the module turns exactly
the new parity test red and leaves the other 11 in that file green. A second
test in the spec is the standing control: it hashes the set with a tile added,
with one removed, and with one swapped, and asserts all three differ from the
module's answer. Without it, a bug that made both sides hash nothing would leave
the comparison green while comparing nothing.

The module grew 1,289 bytes, measured rather than assumed

320,092 to 321,381. Stubbing the new export to return 0 unconditionally and
rebuilding gives 320,135, so:

part bytes
the export symbol and its body 43
the name table, the sort and the hash 1,246
total 1,289

The cost is name()'s 19-arm match pulling every tile name string into the
module. Nothing unaccounted for got linked in. The 512 KB tripwire in
wasmEngine.spec.ts is not close and did not fire.

One thing found on the way, which is NOT folded in

Sweeping the repo for Lava | LavaHot rather than trusting the issue's table
turned up two sites the issue did not list: rocks/vulcanus_placement.rs and
resources/vulcanus_geyser.rs. Both refuse the same two tiles.

They are deliberately left alone, because the three sets are equal by
coincidence rather than by a shared rule. The geyser's own doc comment already
says so - "the forbidden set coincides with the rock overlay's while being
reached by a completely different route" - and the three routes are:

gate what actually decides it
cliffs the cliff's collision mask holds water_tile, and tile_collision_masks.lava() sets it
rocks the rock prototypes' vulcanus_tiles_cold / vulcanus_tiles_hot autoplace lists, whose union is every tile but these
geysers type = "resource" defaults to the mask {resource = true}, which tile_collision_masks.lava() also lists

Change the tile data on any one of those axes and the three come apart. A shared
predicate would then be wrong in two places at once, and silently. That warning
is now written on is_cliff_blocking itself, where the next person to try the
merge will read it.

Gate

pnpm run verify green locally. The four new Rust unit tests cover from_name
in both directions, freeze the set at the lava pair, and check the exported hash
is computed the long way rather than compared against a magic digest.

The set of Vulcanus tiles that refuse a cliff - `lava` and `lava-hot` - was
written out four times, and nothing asserted the four agreed. Two of them sat
on adjacent lines in `fixtures.rs` and said the same thing in two vocabularies,
one by enum and one by string literal, so they could drift apart without either
file changing.

| before | after |
| --- | --- |
| `cliffs/vulcanus_fields.rs` inlined `Lava \| LavaHot` | calls `tile.is_cliff_blocking()` |
| `fixtures.rs` `is_lava`, the same match again | `VulcanusTile::is_cliff_blocking` |
| `fixtures.rs` `want_lava`, by name | resolves through `VulcanusTile::from_name`, then asks the same predicate |
| `cliffCatalog.ts` | unchanged, still the only TypeScript definition |

`from_name` is the inverse of the existing `name()`, resolved through
`TILE_ORDER` so the two cannot disagree. It refuses a name Vulcanus does not
place, which turns a typo in a fixture into a failure rather than a silent
`false`.

## The two sides are compared now, not just tidied

`fmw-wasm` exports `vulcanus_cliff_blocking_names_fnv1a64()`, an FNV-1a 64 over
the blocking tiles' names, sorted and joined by newlines.
`test/wasmVulcanusParity.spec.ts` hashes `VULCANUS_CLIFF_BLOCKING_TILES`
through the module's own `fnv1a64` and compares.

Names rather than a count, because a count cannot tell `lava` from
`volcanic-folds` and a swap is the drift worth catching. The module's own hash
rather than a second FNV-1a in TypeScript, because a reimplementation is one
more thing that can disagree and its disagreement would look identical to real
drift. Sorted, because catalog order is ground truth for the argmax tie-break
but a `Set` of two strings does not carry it, so hashing in catalog order would
make this depend on a fact it is not trying to check.

## Planted, not predicted

Adding `VolcanicFolds` to the Rust set and rebuilding the module turns exactly
the new parity test red and leaves the other eleven in that file green. A
second test is the standing control: it hashes the set with a tile added, with
one removed and with one swapped, and asserts all three differ from the
module's answer. Without it, a bug that made both sides hash nothing would
leave the comparison green while comparing nothing.

## The module grew 1,289 bytes, measured rather than assumed

320,092 to 321,381. Stubbing the export to return 0 unconditionally and
rebuilding gives 320,135, so the export symbol and its body cost 43 bytes and
the name table, the sort and the hash cost 1,246. The cost is `name()`'s
19-arm match pulling every tile name string into the module. Nothing
unaccounted for got linked in, and the 512 KB tripwire in `wasmEngine.spec.ts`
did not fire.

## Two more sites, deliberately left alone

Sweeping for `Lava | LavaHot` rather than trusting the issue's table found two
the issue did not list: `rocks/vulcanus_placement.rs` and
`resources/vulcanus_geyser.rs`. Both refuse the same two tiles, and both keep
their own definition.

The three sets are equal by coincidence, not by a shared rule. The geyser's own
doc comment already says so - "the forbidden set coincides with the rock
overlay's while being reached by a completely different route" - and the three
routes are the cliff's collision mask holding `water_tile`, the rock
prototypes' `vulcanus_tiles_cold` / `vulcanus_tiles_hot` autoplace lists, and
`type = "resource"` defaulting to the mask `{resource = true}`. Change the tile
data on any one of those axes and the three come apart, and a shared predicate
would then be wrong in two places at once, silently. That warning now sits on
`is_cliff_blocking` itself.

Closes #364.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@wormeyman
wormeyman merged commit a9e764f into main Aug 31, 2026
9 checks passed
@wormeyman
wormeyman deleted the fix/364-vulcanus-cliff-blocking-tiles branch August 31, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The Vulcanus cliff-blocking tile set has four definitions and nothing checks they agree

1 participant