Skip to content

Commit d70ee77

Browse files
wormeymanclaude
andauthored
test(cliffs): name the ore->cliff mechanism - cliff_removal_probability (#84) (#200)
* test(oracle): add a data-stage prototype override hook (#84) Checkpoint, NOT PR-ready - no regression test yet. `autoplaceControls` and `cliffSettings` are surface settings and can be changed at on_init. A prototype field cannot: it is read at map-gen from the loaded prototype, so the only lever is the data stage. That is the difference between "switch the ore off" and "leave the ore exactly where it is and change one property of it", and only the second can separate a resource's PRESENCE from a specific behaviour of it. - `extraDataLua` is written to data-final-fixes.lua, not data.lua. This mod declares no dependencies, so Factorio may run its data.lua before space-age's, at which point data.raw.resource["tungsten-ore"] does not exist and the override would silently do nothing. - The protos dump gains `cliff_removal_probability` and `map_grid`, read back off the running game, so an override arm has to prove it applied. What it bought, on the blob region with COLLAPSED cliff settings: arm blob cliffs resources removal_prob A control 0/10 335 945 1 B removal_prob = 0, ore present 10/10 345 945 0 C resources OFF 10/10 345 0 1 B leaves 945 resource entities exactly where the control has them and the cliffs come back regardless, and B is indistinguishable from C. So `ResourceEntityPrototype::cliff_removal_probability` is the mechanism behind the ore -> cliff exclusion that #99, #100, #110, #128 and #129 characterised empirically and that vulcanusOreRejection.ts still records as "NOT established". It defaults to 1.0 and no shipped prototype overrides it, which is why the port's unconditional box-overlap rejection is correct as written - the behaviour does not change, the explanation does. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEgTMcwvbm67ddUUtudY6X * test(cliffs): name the ore->cliff mechanism - cliff_removal_probability (#84) `vulcanusOreRejection.ts` opened with "NOT established: the mechanism. This is a characterised empirical rule, not a port of a known engine path." It is established now, and the answer was a documented prototype field: `ResourceEntityPrototype::cliff_removal_probability`, default 1.0, overridden by no shipped prototype - so it is invisible from the data and can only be seen by changing it. Five issues of `autoplace_controls` work could not reach it because a surface lever can only add or remove the ore. A prototype lever leaves the ore in place and changes one thing about it: arm blob cells cliff-vulcanus resources field control 0/10 335 945 1 field = 0 10/10 345 945 0 resources OFF 10/10 345 0 1 The middle arm keeps all 945 resource entities exactly where the control has them and the cliffs return regardless, so no account resting on the ore's PRESENCE survives. It is indistinguishable from the resources-OFF arm, and 345 - 335 is exactly the ten blob cells, so the field accounts for the effect entirely rather than partly. Every arm reads the field back off the running game, so an override that failed to apply cannot be mistaken for a term that does not matter. NO BEHAVIOUR CHANGES. At 1.0 the removal is unconditional, so the ported box-overlap rejection is correct exactly as written. What changes is the record: the disassembly finding (cliffs are computed and placed before any resource entity exists) stops being a dead end and becomes the reason the effect had to be a removal, and the thin n=1 destroy-stage result gains outside support - a field that removes cliffs can only act on cliffs that already exist. Still open, and said so in every place touched: the GEOMETRY the engine removes with. The base collision_box remains an empirical fit and the box still must not be tuned. - `OracleOptions.extraDataLua`, written to data-final-fixes.lua rather than data.lua because this probe mod declares no dependencies and may otherwise load before space-age. - The protos dump gains cliff_removal_probability and map_grid. - test/cliffRemovalProbability.spec.ts, its fixture and provenance. Discrimination checked by planting a confused-arm error: exactly the two mechanism tests fail, and the two that read only the control arm correctly do not. - Stale "mechanism is open" text corrected in vulcanusOreRejection.ts, cliffPlacement.ts, cliffOreActsAtDestroyStage.spec.ts and vulcanus-cliffs-NOTES.md. The point-in-time spec under docs/superpowers/specs/ is left alone by convention. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CEgTMcwvbm67ddUUtudY6X --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 931801a commit d70ee77

9 files changed

Lines changed: 16513 additions & 23 deletions

docs/noise/vulcanus-cliffs-NOTES.md

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2125,20 +2125,52 @@ the best-scoring collision model was the wrong one.
21252125
cliffs for it.** Higher `correct` is exactly the trap: recall is the half that
21262126
must not be traded. Note this means the ore rule and the lava rejection use
21272127
*different* cliff rectangles - the base `collision_box` and the per-orientation
2128-
one respectively - which is only defensible because the ore mechanism is open
2129-
and the base box is the shape it was measured with. If the mechanism is ever
2130-
found, revisit this first.
2128+
one respectively - which is defensible because the base box is the shape the
2129+
rule was measured with, and because naming the mechanism (below) did not name
2130+
its geometry. Revisit this first if the geometry is ever found.
21312131

21322132
### What is NOT claimed
21332133

21342134
**11 of the 31 are still unexplained** and the box is deliberately not widened
21352135
until they fall out: 10 are #99's run remainders and 1 is the cell our ore model
21362136
misses. `test/cliffOreRejection.spec.ts` pins that 11 so the gap stays tracked.
21372137

2138-
The **mechanism is still open**. This ships a characterised empirical rule -
2139-
one-way, additive, local, box-shaped - and the disassembly still says cliffs are
2140-
computed and placed before any resource entity exists, so whatever the engine is
2141-
really doing, it is not the collision test this models.
2138+
The **mechanism is SOLVED as of 2026-08-14**, and the note here used to say it
2139+
was open. It is `ResourceEntityPrototype::cliff_removal_probability`, a
2140+
documented field that defaults to **1.0** and that no shipped prototype
2141+
overrides - so it is invisible from the data and can only be seen by changing
2142+
it. `test/cliffRemovalProbability.spec.ts` holds the arms:
2143+
2144+
| arm | blob cells | cliff-vulcanus | resources | field |
2145+
| --- | --- | --- | --- | --- |
2146+
| control | 0 / 10 | 335 | 945 | 1 |
2147+
| field = 0 | **10 / 10** | 345 | **945** | 0 |
2148+
| resources OFF | 10 / 10 | 345 | 0 | 1 |
2149+
2150+
The middle arm is the whole result: all 945 resource entities stay exactly where
2151+
the control has them and the cliffs come back anyway, so no account resting on
2152+
the ore's mere PRESENCE survives. It is indistinguishable from the
2153+
resources-OFF arm, and 345 - 335 is exactly the ten blob cells, so the field
2154+
accounts for the effect entirely rather than partly.
2155+
2156+
**A prototype lever is a different instrument from a surface lever**, and that
2157+
is why five issues of `autoplace_controls` work could not reach this. A surface
2158+
setting can only add or remove the ore; a prototype field can leave it in place
2159+
and change one thing about it. `OracleOptions.extraDataLua` exists for that now,
2160+
and writes `data-final-fixes.lua` rather than `data.lua`, because the probe mod
2161+
declares no dependencies and may otherwise load before `space-age`.
2162+
2163+
**Nothing in the port changes.** At 1.0 the removal is unconditional, so the
2164+
box-overlap rejection is correct as written. Two things do change: the
2165+
disassembly finding below (cliffs are computed and placed before any resource
2166+
entity exists, so this is not the engine's collision test) stops being a dead
2167+
end and becomes the reason the effect had to be a *removal*; and the thin n=1
2168+
destroy-stage result gains outside support, since a field that removes cliffs
2169+
can only act on cliffs that already exist.
2170+
2171+
**Still open: the geometry.** Which rectangle the engine removes with is not
2172+
settled by naming the field, so the base-`collision_box` choice above remains an
2173+
empirical fit and the box still must not be tuned.
21422174

21432175
### Where it lives
21442176

src/noise/cliffs/cliffPlacement.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,10 +124,12 @@ export interface CliffBands {
124124
*
125125
* **Deliberately opaque.** This module is planet-agnostic - the corner
126126
* lattice, `crossesCliff` and the orientation table are engine behaviour - and
127-
* the one rule that currently uses this hook is not engine behaviour at all
128-
* but a characterised empirical one (Vulcanus's ORE -> CLIFF suppression, see
129-
* `vulcanusOreRejection.ts`). Keeping it a bare predicate is what stops a
130-
* planet-specific and mechanism-open rule from leaking into the shared core.
127+
* the one rule that currently uses this hook is planet-specific and only
128+
* partly explained (Vulcanus's ORE -> CLIFF suppression, see
129+
* `vulcanusOreRejection.ts` - the mechanism is
130+
* `ResourceEntityPrototype::cliff_removal_probability`, but the geometry it
131+
* removes with is still an empirical fit). Keeping it a bare predicate is what
132+
* stops such a rule from leaking into the shared core.
131133
*
132134
* It runs at the same site as `tileCollides` rather than as a filter over
133135
* `placedCells`' return value so that the model the specs score is the model

src/noise/cliffs/vulcanusOreRejection.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,42 @@
2020
* half-extent is 1.398 against the ores' 0.098: a point-at-tile-centre test
2121
* explains the calcite cells and cannot explain the geyser ones.
2222
*
23-
* **NOT established: the mechanism.** This is a characterised empirical rule,
24-
* not a port of a known engine path, and the difference matters enough to state
25-
* at the top of the file. The obvious candidate is refuted:
23+
* **ESTABLISHED 2026-08-14: the mechanism is
24+
* `ResourceEntityPrototype::cliff_removal_probability`.** It defaults to
25+
* **1.0**, and no shipped prototype overrides it - grepped across `base/`,
26+
* `core/`, `space-age/`, `quality/` and `elevated-rails/` - so it is invisible
27+
* from the data alone and can only be seen by changing it.
28+
*
29+
* Settled by a lever, and specifically by a PROTOTYPE lever rather than a
30+
* surface one (`test/cliffRemovalProbability.spec.ts`). Switching the resources
31+
* off, which is how #99 fixed the direction, removes everything about the ore
32+
* at once and so can never say how. Zeroing one field instead leaves all 945
33+
* resource entities exactly where the control has them:
34+
*
35+
* | arm | blob cells | cliff-vulcanus | resources | field |
36+
* | --- | --- | --- | --- | --- |
37+
* | control | 0 / 10 | 335 | 945 | 1 |
38+
* | field = 0 | **10 / 10** | 345 | **945** | 0 |
39+
* | resources OFF | 10 / 10 | 345 | 0 | 1 |
40+
*
41+
* The zeroed arm is indistinguishable from the no-resources arm, and 345 - 335
42+
* is exactly the ten blob cells, so the field accounts for the effect entirely
43+
* rather than partly. Each arm reads the field back off the running game, so an
44+
* override that failed to apply cannot be mistaken for a term that does not
45+
* matter.
46+
*
47+
* **No code changes, and that is the point of recording it.** At 1.0 the
48+
* removal is unconditional, so the box-overlap rejection below is correct
49+
* exactly as written. What changes is that its SHAPE is explained rather than
50+
* fitted - a placed resource destroys the cliffs it collides with - and that
51+
* the refutation below stops being a dead end and becomes the reason the effect
52+
* had to be a removal at all.
53+
*
54+
* **Still NOT established: the geometry the engine removes with.** The base
55+
* `collision_box` in point 1 below remains an empirical fit rather than a read
56+
* of the code path, and naming the field licenses no tuning of it.
57+
*
58+
* The rival candidate was refuted before any of this, and stays refuted:
2659
* `EntityMapGenerationTask::computeInternal` (`0x101622860`) calls
2760
* `generateCliffs` at `+44` and `generateEntities` at `+148`, and `apply`
2861
* (`0x101623b48`) calls `applyCliffs` at `+124` and `applyEntities` at `+164`,
@@ -58,7 +91,10 @@
5891
* ore-suppressed cell whose neighbour can tell destruction from non-generation
5992
* (`1546,1550.5`, a geyser cell) says DESTROYED, so the effect enters at
6093
* `applyCliffs`/`Surface::wouldCollide` and not at `crossingsForChunk`. n=1 -
61-
* the oracle is thin here and the spec says so.
94+
* the oracle is thin here and the spec says so. **That thin result is now
95+
* corroborated by something other than itself:** a field literally named
96+
* `cliff_removal_probability` can only act on a cliff that already exists, so
97+
* "destroyed rather than never queued" is what the mechanism predicts.
6298
*
6399
* **Consequence for anyone about to widen the box:** it would not be modelling a
64100
* known code path, because the engine's entity collision provably is not this
@@ -72,8 +108,9 @@
72108
* (`CLIFF_ORIENTATION_COLLISION_BOX`). Those are materially different shapes
73109
* - the base box is `+/-0.988 x +/-0.488`, while orientation 4's rotbb is
74110
* `[-3.5,-3,4.5,3]`. The base box is the one the rule was measured with, and
75-
* since the mechanism is open there is nothing that says the ore rule should
76-
* reuse the collision path's shape. `test/cliffOreRejection.spec.ts` scores
111+
* naming the mechanism does not settle its geometry - a removal test need not
112+
* reuse the collision path's shape, and nothing yet says which shape it does
113+
* use. `test/cliffOreRejection.spec.ts` scores
77114
* BOTH so the choice is a recorded measurement rather than an assumption -
78115
* which is the lesson #88/#90 already paid for, where the best-scoring
79116
* collision model was the wrong one because it absorbed an unrelated defect.

test/cliffOreActsAtDestroyStage.spec.ts

Lines changed: 14 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,12 +61,20 @@ import { withCtxDefaults } from "../src/noise/eval/ctx";
6161
* surface before chunk N+1's cliffs are applied, and it still cannot matter).
6262
*
6363
* **The consequence that matters for #84.** The box-overlap model in
64-
* `vulcanusOreRejection.ts` is a characterised empirical rule that does not
65-
* correspond to the engine's collision test. Widening its box until the
66-
* remaining cells fall out would not be modelling a known code path; it would be
67-
* fitting a shape to an unexplained effect, which is exactly what #88 records as
68-
* having shipped a wrong model that scored perfectly. The recall gap is real and
69-
* worth closing - but not that way.
64+
* `vulcanusOreRejection.ts` does not correspond to the engine's collision test.
65+
* Widening its box until the remaining cells fall out would not be modelling a
66+
* known code path; it would be fitting a shape to an effect whose geometry is
67+
* still unknown, which is exactly what #88 records as having shipped a wrong
68+
* model that scored perfectly. The recall gap is real and worth closing - but
69+
* not that way.
70+
*
71+
* **Update 2026-08-14: the effect now has a name, and it predicts this
72+
* result.** `ResourceEntityPrototype::cliff_removal_probability` (default 1.0)
73+
* is the mechanism - see `cliffRemovalProbability.spec.ts`. A field that
74+
* *removes* cliffs can only act on cliffs that already exist, so "destroyed
75+
* rather than never queued" is what it predicts, and this spec's thin n=1
76+
* result stops standing on its own. It does not name the geometry, so the
77+
* warning above about widening the box is unchanged.
7078
*/
7179

7280
const INPUT = { seed0: 123456, startingPositions: [{ x: 0, y: 0 }] };
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
import { describe, expect, it } from "vite-plus/test";
2+
3+
import removal from "./fixtures/oracle-vulcanus-cliff-removal-probability.seed123456.json";
4+
5+
/**
6+
* **The MECHANISM of the ore -> cliff exclusion: `cliff_removal_probability`.**
7+
*
8+
* `cliffOreDirection.spec.ts` settled the DIRECTION - resources suppress
9+
* cliffs, not the reverse - by switching the resources off and regenerating.
10+
* That is as far as switching something off can go. Removing the ore removes
11+
* everything about the ore at once, so it can say which way the effect runs and
12+
* never how.
13+
*
14+
* The lever here is a PROTOTYPE field, which is a different kind of lever:
15+
* every one of the 945 resource entities stays exactly where the control has
16+
* them, and one property of them changes.
17+
* `ResourceEntityPrototype::cliff_removal_probability` defaults to **1.0**, and
18+
* no shipped prototype overrides it - grepped across `base/`, `core/`,
19+
* `space-age/`, `quality/` and `elevated-rails/`. It is therefore invisible
20+
* from the data alone and can only be seen by changing it.
21+
*
22+
* It cannot be changed the way `autoplace_controls` and `cliff_settings` are,
23+
* because those are surface settings and this is read at map-gen from the
24+
* loaded prototype. `OracleOptions.extraDataLua` exists for exactly this, and
25+
* writes `data-final-fixes.lua` rather than `data.lua`: the probe mod declares
26+
* no dependencies, so Factorio may load it before `space-age`, at which point
27+
* `data.raw.resource["tungsten-ore"]` does not exist yet and the override would
28+
* silently edit nothing.
29+
*
30+
* **What this does NOT change: the port.** At 1.0 the removal is
31+
* unconditional, so `vulcanusOreRejection.ts`'s box-overlap rejection is
32+
* correct exactly as written and no code moves. What changes is that its header
33+
* can stop saying the mechanism is unknown, and that the box-overlap SHAPE is
34+
* explained rather than fitted - a placed resource destroys the cliffs it
35+
* collides with.
36+
*/
37+
38+
interface Ent {
39+
x: number;
40+
y: number;
41+
name: string;
42+
}
43+
interface Proto {
44+
type: string;
45+
layers: string[];
46+
box?: { lx: number; ly: number; rx: number; ry: number };
47+
cliff_removal_probability?: number;
48+
map_grid?: boolean;
49+
}
50+
interface Arm {
51+
label: string;
52+
zeroedCliffRemovalProbability: boolean;
53+
autoplaceControls: Record<string, { frequency: number; size: number; richness: number }> | null;
54+
effectiveAutoplace: Record<string, { frequency: number; size: number; richness: number }>;
55+
cliffs: Ent[];
56+
resources: Ent[];
57+
protos: Record<string, Proto>;
58+
}
59+
60+
const arms = removal.cases as unknown as Arm[];
61+
const CONTROL = 0;
62+
const ZEROED = 1;
63+
const ORE_OFF = 2;
64+
65+
const vulcanusCliffs = (a: Arm): number =>
66+
a.cliffs.filter((c) => c.name === "cliff-vulcanus").length;
67+
68+
/**
69+
* The ten cells `#94` found the game leaves empty however `cliff_elevation` is
70+
* routed onto them, and `#99` showed fill completely once the ore is switched
71+
* off. Same list, verbatim, as `cliffOreDirection.spec.ts` - if it drifts, the
72+
* two specs are no longer talking about the same thing.
73+
*/
74+
const BLOB = [
75+
"178,138.5",
76+
"178,142.5",
77+
"178,146.5",
78+
"178,150.5",
79+
"182,138.5",
80+
"182,142.5",
81+
"182,146.5",
82+
"182,150.5",
83+
"186,138.5",
84+
"186,142.5",
85+
];
86+
const blobHits = (a: Arm): number => {
87+
const cells = new Set(
88+
a.cliffs.filter((c) => c.name === "cliff-vulcanus").map((c) => `${String(c.x)},${String(c.y)}`),
89+
);
90+
return BLOB.filter((k) => cells.has(k)).length;
91+
};
92+
93+
describe("the ore -> cliff exclusion is cliff_removal_probability", () => {
94+
/**
95+
* The distinguishing arm. The ore is still there - all 945 of it, the same
96+
* count as the control - and every blob cell gets a cliff anyway. No account
97+
* in which the exclusion is about the ore's PRESENCE survives this.
98+
*/
99+
it("zeroing the field restores every blob cell with the ore still in place", () => {
100+
expect(blobHits(arms[CONTROL])).toBe(0);
101+
expect(blobHits(arms[ZEROED])).toBe(10);
102+
103+
// Non-vacuity, both halves. The ore did NOT go away...
104+
expect(arms[ZEROED].resources.length).toBe(945);
105+
expect(arms[ZEROED].resources.length).toBe(arms[CONTROL].resources.length);
106+
// ...and the arm really did run with the field changed, read back off the
107+
// running game rather than echoed from what was written.
108+
expect(arms[CONTROL].protos["tungsten-ore"].cliff_removal_probability).toBe(1);
109+
expect(arms[ZEROED].protos["tungsten-ore"].cliff_removal_probability).toBe(0);
110+
});
111+
112+
/**
113+
* The field accounts for the effect ENTIRELY, not merely for some of it.
114+
* Zeroing it is indistinguishable from deleting every resource, and the
115+
* difference from the control is exactly the ten blob cells.
116+
*/
117+
it("zeroing the field is indistinguishable from having no resources at all", () => {
118+
expect(vulcanusCliffs(arms[CONTROL])).toBe(335);
119+
expect(vulcanusCliffs(arms[ZEROED])).toBe(345);
120+
expect(vulcanusCliffs(arms[ORE_OFF])).toBe(345);
121+
expect(vulcanusCliffs(arms[ZEROED])).toBe(vulcanusCliffs(arms[ORE_OFF]));
122+
expect(vulcanusCliffs(arms[ZEROED]) - vulcanusCliffs(arms[CONTROL])).toBe(BLOB.length);
123+
124+
// The resources-OFF arm is the known control from cliffOreDirection: it
125+
// reaches the same cliff count by removing the ore rather than by changing
126+
// it, and its field is untouched.
127+
expect(arms[ORE_OFF].resources.length).toBe(0);
128+
expect(arms[ORE_OFF].protos["tungsten-ore"].cliff_removal_probability).toBe(1);
129+
expect(arms[ORE_OFF].effectiveAutoplace["tungsten_ore"]?.size).toBe(0);
130+
expect(arms[CONTROL].effectiveAutoplace["tungsten_ore"]?.size).toBe(1);
131+
});
132+
133+
/**
134+
* The default is what makes the ported rejection unconditional, and it is
135+
* asserted here so that a future Factorio version lowering it - or a mod
136+
* prototype arriving with a different value - fails loudly instead of
137+
* silently invalidating `vulcanusOreRejection.ts`'s "always" assumption.
138+
*/
139+
it("every resource takes the 1.0 default, which is why the port needs no probability", () => {
140+
for (const name of ["tungsten-ore", "calcite", "coal", "sulfuric-acid-geyser"]) {
141+
expect(arms[CONTROL].protos[name].cliff_removal_probability).toBe(1);
142+
}
143+
});
144+
145+
/**
146+
* Recorded here because the same read-back proved it and because it is the
147+
* reason a rendered map preview cannot count resource entities: `map_grid`
148+
* draws solid ores as a 2x2-block checkerboard. Only fluid and vent
149+
* resources opt out, and the geyser is the one in this fixture.
150+
*/
151+
it("map_grid is on for the solid ores and off for the geyser", () => {
152+
for (const name of ["tungsten-ore", "calcite", "coal"]) {
153+
expect(arms[CONTROL].protos[name].map_grid).toBe(true);
154+
}
155+
expect(arms[CONTROL].protos["sulfuric-acid-geyser"].map_grid).toBe(false);
156+
});
157+
});

test/fixtures/PROVENANCE.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -391,6 +391,10 @@
391391
"oracle-fastpow.seed123456.json": {
392392
"factorioVersion": "2.1.12",
393393
"evidence": "stated - captured 2026-08-05 by test/oracle/capture.ts fastpow against the installed binary (2.1.12, build 87038), which pnpm refs:sync --check reported in sync at capture time. Note 2.1.13 released the same day; its changelog contains no map-generation, noise, terrain, autoplace, cliff or map-exchange entries. The probe samples the noise machine's own `^` operator as `x ^ <exponent>` routed onto elevation on the default Nauvis surface, so fastApprox is compared to the operator DIRECTLY rather than through a downstream chain - every other fixture over fastApprox compares with a tolerance wide enough to hide the ~1e-5 effects at issue (#161, #162). Four series: 1/3 (the shipping cube root), 0.5, 2.5 and 2. The exponents are not interchangeable and the fixture is what established that: `^` has THREE behaviours, all at 123/123 - 1/3 and 2.5 reach Math::powSafe(float,float) and so the fastapprox log2/exp2 pair; 0.5 is an exact sqrt; an integral exponent takes powSafe's fcvtzs/scvtf fast path and is exact exponentiation by squaring. The 0.5 result was a refutation - the spec first asserted fastapprox there and the game disagreed at the first position. Positions are deliberately adversarial rather than a plain grid, which does not discriminate (12 evenly spaced points scored 12/12 for both candidate cube-root exponents): they include 24 points where a double 1/3 differs from f32(1/3), which settled #163 at 0/24 against 24/24, and points where the pre-9b49ebb single-rounding fastapprox differs from the per-operation rounding that replaced it (~30% of inputs). Every coordinate is a multiple of 1/2 a tile and therefore exact in the 1/256 MapPosition grid."
394+
},
395+
"oracle-vulcanus-cliff-removal-probability.seed123456.json": {
396+
"factorioVersion": "2.1.14",
397+
"evidence": "captured 2026-08-14 by test/oracle/capture.ts vulcanus-cliff-removal-probability against the installed binary, which pnpm refs:sync --check reported binary/factorio-data/lua-api all in sync at 2.1.14 at capture time; each arm also reads ResourceEntityPrototype::cliff_removal_probability back off the running game, so the version the arm ran under is recorded inside the fixture as well as here"
394398
}
395399
}
396400
}

0 commit comments

Comments
 (0)