Skip to content

fix(noise): fastCbrt's exponent is f32(1/3), with the exact oracle fastApprox never had (#163) - #164

Merged
wormeyman merged 2 commits into
mainfrom
fix/fastcbrt-exponent-oracle
Aug 5, 2026
Merged

fix(noise): fastCbrt's exponent is f32(1/3), with the exact oracle fastApprox never had (#163)#164
wormeyman merged 2 commits into
mainfrom
fix/fastcbrt-exponent-oracle

Conversation

@wormeyman

@wormeyman wormeyman commented Aug 5, 2026

Copy link
Copy Markdown
Owner

Closes #163. Closes #161. First conversion of the kind #162 asks for.

#161 is closed rather than merely advanced: the spec now carries the pre-9b49ebb implementation and asserts it fails the committed values on BOTH fastapprox series, with the correct exponent supplied in each - so the rounding change is adjudicated against the game independently of the exponent fix. Margins are 71/123 and 50/123.

fastApprox is consumed by the resource spot_height chain and had no exact ground truth - every fixture over it compares with a tolerance wide enough to hide the ~1e-5 effects that were actually in question. This adds that ground truth and then uses it.

The probe

Samples the noise machine's own ^ operator directly, as x ^ <exponent> routed onto elevation, instead of inferring it through the patch chain where a dozen other f32 steps can mask the effect. 4 series x 123 positions, compared with toBe after Math.fround - no tolerance anywhere.

Positions are deliberately adversarial, because a plain grid does not discriminate: 12 evenly spaced points scored 12/12 for both candidate cube-root exponents, and only Math.cbrt (0/12) failed. The committed set includes points where a double 1/3 differs from f32(1/3), and points where the pre-9b49ebb single-rounding fastapprox differs from the per-operation rounding that replaced it (~30% of inputs).

#163: the exponent

fastCbrt passed a JS double 1/3. The game reaches this through Math::powSafe(float, float) - both parameters are float, and the multiply is fmul s0, s0, s1 at single precision - so it computes with f32(1/3) = 0.3333333432674408. Wrong on ~3.0% of inputs, by up to 7.8e-3 absolute.

At the 24 positions chosen because the two candidates differ: the double scores 0/24, f32(1/3) scores 24/24. Settled against the game, not the disassembly.

#161: it recovers most of the absolute-error regression

9b49ebb improved relative error sharply but regressed worst-absolute on 3 of 4 regularPatches cases, which read as the price of a change the binary requires. It was not - it was a second, independent bug in the same file showing through:

case pre-9b49ebb post-9b49ebb this PR
iron-ore/123456 0.6898 0.8159 0.6491
iron-ore/777771 0.6860 0.8096 0.6493
uranium-ore/777771 0.6135 0.4755 0.4724
uranium-ore/123456 0.3788 0.4790 0.4790

Three of four are now better than the original baseline on absolute error as well as relative. ABS_TOL headroom goes 0.18 -> 0.35.

Two findings that were NOT predicted

Both now pinned at 123/123:

  • x ^ 0.5 is an exact sqrt, not fastapprox. The spec first asserted fastapprox here, on the assumption that any non-integral exponent reaches the pair, and the game refuted it at the very first position.
  • An integral exponent takes powSafe's fcvtzs/scvtf fast path - exact exponentiation by squaring, never touching fastapprox.

So ^ has three behaviours, and a Lua ^ 0.5 or ^ <integer> must not be ported as fastPow. Both existing sites are already correct (resourceMath.ts writes regular_rq_factor ^ 2 as a plain multiplication); nothing recorded why until now.

That last point also closes a natural-looking inference: fastPow's other two call sites pass an integer octaves, so "the game uses squaring for integers, those must be wrong too" is very tempting. It is false - swapping the multioctave norm to squaring makes its oracle error 20x worse (2.630e-5 -> 5.332e-4, failing both gates), so that normalisation does not route through powSafe at all. The test carries both halves.

Worth knowing before merging

pnpm run verify exits 0 both with and without the fastCbrt fix (200 files / 1726 tests). The only thing in the repo that can see this change is the new spec. That is the weakness #162 tracks, and it is why the fixture came first here rather than the one-line fix.

Captured on 2.1.12 build 87038, refs:sync --check in sync. 2.1.13 shipped the same day; its changelog has no map-generation, noise, terrain, autoplace, cliff or map-exchange entries.

… exact oracle (#161, #163)

Adds the f32-EXACT ground truth fastApprox never had, and uses it to settle two
open questions about the file.

The probe samples the noise machine's own `^` operator as `x ^ <exponent>`
rather than going through the resource spot_height chain, so a 1e-5 effect is
visible instead of being buried under a dozen other f32 steps. 4 series x 123
positions, compared with toBe after Math.fround - no tolerance anywhere.

#163: fastCbrt passed a double 1/3 where the game's Math::powSafe(float, float)
takes an f32 exponent (the multiply is `fmul s0, s0, s1`). At the 24 fixture
positions chosen because the two candidates differ, the double scores 0/24 and
f32(1/3) scores 24/24.

#161: the fix also recovers most of the absolute-error regression that made
commit 9b49ebb look like a trade-off. regularPatches, before -> after:

  iron-ore/123456     abs 0.8159 -> 0.6491
  uranium-ore/123456  abs 0.4790 -> 0.4790
  iron-ore/777771     abs 0.8096 -> 0.6493
  uranium-ore/777771  abs 0.4755 -> 0.4724

Three of four are now better than the ORIGINAL pre-9b49ebb baseline on absolute
error as well as relative; ABS_TOL headroom goes 0.18 -> 0.35. So "relative
improves, absolute regresses" was not an inherent cost of the rounding change -
it was a second, independent bug in the same file showing through, which a
tolerance-based suite cannot distinguish.

Two findings that were NOT predicted, both now pinned at 123/123:

- `x ^ 0.5` is an EXACT sqrt, not fastapprox. The spec first asserted fastapprox
  and the game refuted it at the first position.
- An integral exponent takes powSafe's fcvtzs/scvtf fast path and is exact
  exponentiation by squaring, never touching fastapprox.

So `^` has three behaviours, and a Lua `^ 0.5` or `^ <integer>` must not be
ported as fastPow. Both existing sites were already correct; nothing recorded
why until now.

pnpm run verify exits 0 (200 files / 1726 tests) both with and without the
fastCbrt fix, which is exactly the weakness #162 tracks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DutxVPSqj12PtwNE46RtSs
The oracle-fastpow fixture was captured after the per-operation rounding change,
so on its own it confirms the current implementation rather than choosing
between the two. This carries the pre-9b49ebb single-rounding implementation
verbatim from 9b49ebb^ and asserts it FAILS the committed values at many
positions.

The two disagree on ~30% of inputs and the fixture's positions were chosen to
include them, so the guard is not thin. It is the same shape as the existing
double-1/3 guard: an exact-match test is only worth as much as the proof that a
wrong model would miss.

Answers the part of #161 that the fixture alone left open.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DutxVPSqj12PtwNE46RtSs
@wormeyman
wormeyman merged commit b2fced7 into main Aug 5, 2026
6 checks passed
wormeyman added a commit that referenced this pull request Aug 5, 2026
…e-7 to EXACTLY 0 (#162) (#165)

The formula was already right; the precision of its intermediate steps was the
whole error. The noise machine evaluates in f32 registers, and reproducing that
takes the residual to exactly 0 on all 404 committed oracle samples (three
sweeps: 121 + 121 + 162). The f64 form matched only 285 of them.

Same class of fix as fastApprox's per-operation rounding in #164.

The spec's ceiling was the thing that hid this. It asserted `worst < 8e-3`
against an actual worst of ~9.5e-7 - a ceiling ~8400x looser than the real
error, which would have accepted almost any regression. The three sweeps now
assert f32 equality with `toBe`, plus a guard that the f64 arithmetic FAILS the
fixture, so the exact assertions cannot quietly become vacuous.

Two hand-derived assertions moved from precision 10 to 6, and this is not a
loosening: they compare against exact decimals like -2 and -1.0, but
intermediates such as `0.5 - 0.6` and `1 - 1.2` are not representable at f32, so
the game's own answer is -2.000000476837158. The exact decimal is the wrong
target for an f32 computation. Asserting the exact f32 result instead would mean
recomputing `f32(5 * f32(f32(1) - f32(1.2)))` in the test - re-implementing the
function and checking it against itself - so those stay formula-SHAPE guards
(which axis drives the min, that pmax=inf does not clamp) while bit-exactness
lives in the oracle sweeps.

This changes shipped tile-autoplace output (sand-1's coastal term). pnpm run
verify exits 0: 200 files / 1728 tests.


Claude-Session: https://claude.ai/code/session_01DutxVPSqj12PtwNE46RtSs

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant