Skip to content

Commit 0694ec1

Browse files
wormeymanclaude
andcommitted
Record phase 5 in CLAUDE.md, and correct two comments (#225)
Three documentation changes. No behaviour, and engine.wasm is unchanged. CLAUDE.md's Rust engine section stopped at phase 4 and still listed Vulcanus as future work, so the repo's own map said six ported layers did not exist. Adds a phase 5 section with the 24 frozen counts' four readings, the biome layer's real cache and why it is the only one, the pre-volcano split, and the cliff multisample grid. Fixes an inverted comment on the ashlands basis leaf, found by review on #285. It said the input scale "divides by the volcanism scale multiplier" where the code has that multiplier in the NUMERATOR. The code was right and matched both the TypeScript and the Lua; only the comment was wrong. Worth more than a typo fix: scale_multiplier is exactly 1 at the default preset, which is what every fixture and the poison gate run at, so m/50/3 and 1/50/3/m are bit-identical throughout and an edit "fixing" the code to match the comment would diverge from the game only at a non-default volcanism frequency, with a green gate. Corrects the crack layer's #269 note, which blamed the wrong number. It said the layer is discriminating because its INPUT scales (0.3 * 0.325 and 0.6 * 0.325) are not f32-exact. That is wrong, and measurement says the discriminator is the OUTPUT scale alone: basis_noise returns an f32, so a power-of-two output scale is a pure exponent shift and narrowing the product is the identity, while any other output scale can leave the f32 grid. Over 90,000 samples at a fixed input scale, output scales 1, 0.5, 0.25, 2, 4 and 64 change 0.00% of products; 0.6 changes 79.88%, 0.75 and 3 change 56.32%, 150 changes 97.46%, 125 changes 98.38%. Holding the output scale at 1 and sweeping input scales 0.125, 0.205, 0.51, 0.6, 1.5 and 0.002 changes 0.00% every time. So exactly ONE of the crack layer's twelve basis calls is exposed: hairline_cracks' first term at output scale 0.6, where 80.10% of products differ. The other eleven sit at 1, 0.5 or 0.25. That is a one-term explanation for the one bad count in that layer, and why the four fields built only from power-of-two sites score 15, 40, 10 and 8 rather than 3. The wrong reasoning is recorded in the comment rather than quietly replaced, because it is the plausible guess: the input scale is the number that looks inexact. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X2wz8eFazisYuENEkmBvvG
1 parent 9e4b8c8 commit 0694ec1

3 files changed

Lines changed: 123 additions & 11 deletions

File tree

CLAUDE.md

Lines changed: 80 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1202,7 +1202,7 @@ Field labels carry in-game tooltip text via `FInfo` (an `info` prop on
12021202
`EnemyValueRow`, an `info:` entry in `controlCatalog.ts` for the enemy-base
12031203
autoplace rows).
12041204

1205-
### The Rust/WASM noise engine (`crates/`) - phases 1-3 expressions landed
1205+
### The Rust/WASM noise engine (`crates/`) - phases 1-4 landed, phase 5 in progress
12061206

12071207
A Cargo workspace at the repository root, landed empty on purpose (#219) so the
12081208
gate was proven green on `main` before any port code depended on it. Two crates:
@@ -1294,6 +1294,85 @@ been red whether or not the argmax had a control at all. `POISONED_TESTS` now
12941294
carries FULL test paths rather than bare `fixtures::` names, so a control can
12951295
live beside its op.
12961296

1297+
**Phase 5 (#225) ports Vulcanus, and its EXPRESSION chain down to elevation is
1298+
in.** Landed: `vulcanus_helpers`, `vulcanus_cracks`, `vulcanus_climate`,
1299+
`vulcanus_spawn`, `vulcanus_biomes`, `vulcanus_elevation`, plus
1300+
`vulcanus_temperature` on the elevation module. `vulcanus_shared` needed no
1301+
port - it is `starting_spot_at_angle`, done in #279 - and `vulcanus_seed`
1302+
landed in phase 2. Still out: `vulcanus_resources`, `tiles/vulcanus_catalog`,
1303+
and the cliff, resource and rock stacks.
1304+
1305+
Tier 1 grades **24 named fields** across six fixtures. Every count was measured
1306+
again on the TypeScript side against the same fixture and all 24 agree, so they
1307+
are the distance BOTH ports sit from the game rather than a gap between them.
1308+
1309+
**Four things this phase measured that are worth more than the counts:**
1310+
1311+
- **A second, independent fixture points at #269.** `hairline_cracks` scores
1312+
**3 of 61** and is the shallowest expression in its layer - a bare `plasma`,
1313+
nothing composed on top - so the weakness cannot come from the crack file.
1314+
`plasma` subtracts two `basis_noise_expr` results, and that adapter returns
1315+
the un-narrowed f64 product #269 is about. The crack layer calls it at
1316+
`0.3 * 0.325` and `0.6 * 0.325`, neither exact in f32, which is exactly the
1317+
case #269 records `oracle-basis` as blind to by construction. Do not chase
1318+
the five crack counts before #269 is settled.
1319+
- **A clamp flatters a count, and here it is measurable.** The three clamped
1320+
biomes score 403, 402 and 408 of 434 against their own unclamped sources at
1321+
128, 107 and 127 - the same quantity, times 2, clamped. Nothing improved
1322+
between them: the clamp saturates at 0 or 1 over most of the map and a
1323+
saturated position is exact for free. Read `*_biome_full` as the port's score
1324+
and `*_biome` as what the consumer needs. Same effect in `starting_area`
1325+
(371 of 410) against the unclamped `ashlands_start` (61) feeding it.
1326+
- **The oracle cannot see elevation's `-500` clamp**, and that was checked
1327+
rather than assumed. `vulcanus_elevation` is `max(-500, elev)` and the
1328+
captured `elev` bottoms out at **-58.77**, so the two columns are the same
1329+
field at all 434 positions - 0 of 434 differ - and a port that dropped the
1330+
`max` would score 113 either way. Both are graded anyway; the clamp's real
1331+
test lives in the module, constructing the case the fixture does not.
1332+
- **A discrete output scores like one.** `mountain_volcano_spots` at 359 of 434
1333+
is the highest UNCLAMPED count in the Vulcanus port, because it is dominated
1334+
by which single candidate survives per region - a choice a sub-ULP error
1335+
almost never changes. The same property `voronoi_cell_id` has.
1336+
1337+
**`detailNoise` is the reading to carry out of this phase.** It has the
1338+
SMALLEST residual of its three helper fields (7.778e-5) and the FEWEST exact
1339+
matches (**1 of 38**), where `mountainPlasma` has 2.807e-3 and 7 of 38. A field
1340+
can be uniformly close and almost never right, which is the argument for
1341+
counting matches rather than bounding error, stated in one number.
1342+
1343+
Read elevation's worst residual of 1.332e-1 against its scale before reacting:
1344+
the field spans -58 to +1024, so that is ~1.3e-4 relative, the same order as
1345+
every layer above it. An absolute bound would need re-tuning per field for
1346+
scale alone - a third reason not to use one.
1347+
1348+
**`vulcanus_biomes` keeps a REAL cache, and it is the only layer that does.**
1349+
Every other ported layer evaluates top to bottom into locals, because every read
1350+
is at the same `(x, y)`. `raw_spots` is not: it reads selected spots from up to
1351+
four neighbouring regions, which is genuine cross-position state. The region
1352+
cache is a `RefCell<BTreeMap>` so `eval` can stay `&self` while the density and
1353+
favorability closures handed to `select_spots` borrow it. `BTreeMap` rather than
1354+
`HashMap` deliberately - nothing iterates it today, but a determinism-critical
1355+
port should not carry a container whose iteration order is unspecified.
1356+
1357+
`volcano_area` is evaluated at every spot candidate and pulls the whole
1358+
pre-volcano chain at that candidate; the TypeScript memoizes those and the port
1359+
recomputes them. **Nothing on the render path reaches this layer yet**, so it is
1360+
correct-first on purpose. If it ever joins a per-pixel render that is the first
1361+
measurement to take - `multioctave_noise`'s own docs record what happened last
1362+
time a per-call rebuild went unmeasured, which was 20x.
1363+
1364+
**The mountains pre-volcano split is load-bearing.** `mountain_volcano_spots`
1365+
depends on the mountains biome and the mountains biome folds the volcano field
1366+
back in; the Lua breaks that with a PRE-volcano stage that `volcano_area` reads.
1367+
Collapsing the two is an infinite recursion, which announces itself - reading
1368+
`volcano_area` off the POST-volcano raw does not.
1369+
1370+
**`cliff_elevation` is a separate entry point, not a convenience.**
1371+
`multisample`'s offsets are in the CONSUMING program's grid units, so the cliff
1372+
generator's 4-tile lattice moves the field 16 tiles for a `dx` of 4 (#83). The
1373+
tile and terrain channels pass 1; cliffs pass 4; both go through one code path
1374+
with the grid as a parameter.
1375+
12971376
**Tier 3 now covers both preview PNGs**, which is what #224's gate asks for.
12981377
`test/wasmFulgoraRenderParity.spec.ts` renders through the real boundary and
12991378
compares against the images Factorio itself produced:

crates/fmw-noise/src/expressions/vulcanus_elevation.rs

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -141,8 +141,16 @@ impl<'a> VulcanusElevation<'a> {
141141
biomes,
142142
climate,
143143
mountain_basis: BasisLeaf::new(seed0, 13_423, 1.0 / 500.0, 250.0),
144-
// Note the input scale divides by the ashlands LOCAL scale of 3, and
145-
// by the volcanism scale multiplier - not by the crack scale.
144+
// The input scale MULTIPLIES by the volcanism scale multiplier and
145+
// divides by 50 and by the ashlands LOCAL scale of 3 - not by the
146+
// crack scale, and the multiplier is not a divisor.
147+
//
148+
// Spelled out because no test here can tell the two apart:
149+
// `scale_multiplier` is exactly 1 at the default preset, which is
150+
// what every fixture and the poison gate run at, so
151+
// `m / 50 / 3` and `1 / 50 / 3 / m` are bit-identical throughout.
152+
// The forms only diverge at a non-default volcanism FREQUENCY
153+
// slider, and they would diverge with the whole gate green.
146154
ashlands_basis: BasisLeaf::new(
147155
seed0,
148156
12_643,

crates/fmw-noise/src/fixtures.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2434,14 +2434,39 @@ fn reproduces_the_vulcanus_crack_layer_at_every_captured_position() {
24342434
// 5.460e-4, 6.387e-4.
24352435
//
24362436
// `hairlineCracks` at 3 of 61 is the weakest and it is the SHALLOWEST
2437-
// expression in the layer - a bare `plasma` with nothing composed on top.
2438-
// That points at the plasma adapter rather than at anything this file
2439-
// builds, and specifically at #269: `basis_noise_expr` returns an
2440-
// un-narrowed f64 product where the game narrows to f32, and `plasma` is
2441-
// two of those subtracted. The crack layer's scales are 0.3 * 0.325 and
2442-
// 0.6 * 0.325, neither exact in f32, so this layer is exactly the case the
2443-
// #269 comment says the existing `oracle-basis` fixture is blind to. Do not
2444-
// chase these five counts before that one is settled.
2437+
// expression in the layer - a bare `plasma` with nothing composed on top -
2438+
// so the weakness cannot come from anything this file builds. It points at
2439+
// the plasma adapter, and specifically at #269: `basis_noise_expr` returns
2440+
// an un-narrowed f64 product where the game narrows to f32, and `plasma` is
2441+
// two of those subtracted.
2442+
//
2443+
// **Which call sites #269 can reach is decided by the OUTPUT scale alone,
2444+
// and it is a clean rule.** `basis_noise` returns an f32, so multiplying by
2445+
// a POWER OF TWO is a pure exponent shift and can never leave the f32 grid:
2446+
// narrowing that product is the identity. Any other output scale can.
2447+
// Measured over 90,000 samples at a fixed input scale, output scale
2448+
// 1 / 0.5 / 0.25 / 2 / 4 / 64 each change **0.00%** of products, while 0.6
2449+
// changes 79.88%, 0.75 and 3 change 56.32%, 150 changes 97.46% and 125
2450+
// changes 98.38%. Holding the output scale at 1 and sweeping the INPUT
2451+
// scale over 0.125, 0.205, 0.51, 0.6, 1.5 and 0.002 changes 0.00% every
2452+
// time - the input scale decides which noise value you get, never whether
2453+
// the product is representable.
2454+
//
2455+
// So of the twelve `basis_noise_expr` calls this layer makes, exactly ONE
2456+
// is exposed: `hairline_cracks`'s first term, at output scale 0.6, where
2457+
// 80.10% of products differ. The other eleven sit at 1, 0.5 or 0.25 and are
2458+
// blind by construction. That is a one-term explanation for the one bad
2459+
// count here, and it is why the four fields built only from power-of-two
2460+
// sites score 15, 40, 10 and 8 rather than 3.
2461+
//
2462+
// It also explains why `oracle-basis` cannot grade this: that fixture was
2463+
// captured at output scale 1. Do not chase `hairlineCracks` before #269 is
2464+
// settled, and do not expect the other four to move when it is.
2465+
//
2466+
// An earlier draft of this comment blamed the layer's INPUT scales
2467+
// (0.3 * 0.325 and 0.6 * 0.325, neither exact in f32). That was wrong, and
2468+
// it is recorded rather than quietly deleted because it is the plausible
2469+
// guess: the input scale is the number that looks inexact.
24452470
type C = CrackFields;
24462471
for (key, want_exact, select) in [
24472472
(

0 commit comments

Comments
 (0)