nx_worldgen_tunelib.nx source
↩ module page · 325 lines · 14531 B
1// nx_worldgen_tunelib.nx -- shared library for the worldgen knob-tuning
2// loop (no main; consumed by nx_worldgen_tuner, its KAT test, and
3// nx_worldgen_refine). The capability this closes (operator 2026-06-10
4// "build the team's capabilities to handle your callouts"): the refine
5// loop's auto-filed verdict NAMES a weak axis; this library lets a team
6// organ OPTIMIZE the generator's exposed knobs against exactly that axis
7// -- verdict -> assignment -> tuner -> re-verdict, with the 8-axis grader
8// as referee and no LLM anywhere in the loop.
9//
10// Knobs (all DATA, swept under the verdict):
11// river_depth -- channel incision depth (P4)
12// rill_depth -- drainage texture depth (P4)
13// rill_radius -- drainage density (poisson) (P4)
14// detail_amp -- fine-octave relief (nx_sig_detail)
15//
16// Score = (grade, named-axis value) lexicographic: grade dominates;
17// the banked refine axis breaks ties, so the tuner climbs the axis the
18// verdict called out without ever trading the grade away.
19// license_tier: ORIGINAL
20
21import "nx_syscalls.nx"
22import "nx_tier.nx"
23import "nx_procgen_preset.nx"
24import "nx_procgen_water.nx"
25import "nx_procgen_biome.nx"
26import "nx_procgen_features.nx"
27import "nx_world_quality_grader.nx"
28const TL_MAGIC_1024: i64 = 1024
29const TL_MAGIC_4096: i64 = 4096
30const TL_MAGIC_4095: i64 = 4095
31
32const TL_W: i64 = 64
33const TL_H: i64 = 64
34const TL_RELIEF: i64 = 1024
35const TL_SEEDS: i64 = 3
36
37// knob vector layout (preset/density ride along for provenance)
38const TL_K_RIVER_DEPTH: i64 = 0
39const TL_K_RILL_DEPTH: i64 = 1
40const TL_K_RILL_RADIUS: i64 = 2
41const TL_K_DETAIL_AMP: i64 = 3
42const TL_K_PEAKS_EXTRA: i64 = 4
43const TL_K_AMP_BOOST: i64 = 5
44const TL_K_TERRACE: i64 = 6
45const TL_K_COUNT: i64 = 7
46
47// ---- full knob-explicit world composition (the tuner's eval body; also
48// the recipe wr_eval_cfg consumes so gate metric == tuned metric) -------
49func tl_compose_world(seed: i64, preset: i64, density: i64,
50 knobs: *i64, wmask: *i64) -> *Landscape {
51 let land: *Landscape = nx_procgen_landscape_cfg(seed, preset, TL_W, TL_H, density,
52 knobs[TL_K_PEAKS_EXTRA],
53 knobs[TL_K_AMP_BOOST],
54 knobs[TL_K_TERRACE])
55 nx_water_compose_cfg(land.heightmap, TL_W, TL_H, seed, preset, wmask,
56 knobs[TL_K_RIVER_DEPTH],
57 knobs[TL_K_RILL_DEPTH],
58 knobs[TL_K_RILL_RADIUS])
59 nx_sig_detail(land.heightmap, TL_W, TL_H, seed, knobs[TL_K_DETAIL_AMP])
60 return land
61}
62
63// grade ONE (preset, density, knobs) config over TL_SEEDS seeds, biome +
64// feature axes fed; returns worst grade, copies worst-seed verdict into
65// out12. Same seed law as nx_worldgen_refine (comparability).
66func tl_eval(preset: i64, density: i64, knobs: *i64, out12: *i64) -> i64 {
67 var worst: i64 = NX_GRADE_S + 1
68 let tmp: *i64 = sys_mmap(8 * 12) as *i64
69 let bm: *i64 = sys_mmap(TL_W * TL_H * 8) as *i64
70 let wmask: *i64 = sys_mmap(TL_W * TL_H * 8) as *i64
71 let hist: *i64 = sys_mmap(NX_FEAT_N_KINDS * 8) as *i64
72 var s: i64 = 0
73 while s < TL_SEEDS {
74 let seed: i64 = 1000 + s * 777 + preset * 31 + density
75 let land: *Landscape = tl_compose_world(seed, preset, density, knobs, wmask)
76 nx_biome_map_build(land.heightmap, TL_W, TL_H, seed, bm)
77 var nf: i64 = land.n_features
78 if nf > 0 {
79 let kinds: *i64 = sys_mmap(nf * 8) as *i64
80 nx_feature_assign_with_water(land.feature_xs, land.feature_ys, nf, bm, wmask, TL_W, TL_H, seed, kinds)
81 nx_feature_hist_build(kinds, nf, hist)
82 }
83 if nf <= 0 {
84 var hk: i64 = 0
85 while hk < NX_FEAT_N_KINDS { hist[hk] = 0; hk = hk + 1 }
86 }
87 nx_world_quality_grade(land.heightmap, TL_W, TL_H, TL_RELIEF, bm, hist, NX_FEAT_N_KINDS, tmp)
88 if tmp[0] < worst {
89 worst = tmp[0]
90 var c: i64 = 0
91 while c < 12 { out12[c] = tmp[c]; c = c + 1 }
92 }
93 s = s + 1
94 }
95 return worst
96}
97
98// ---- refine-axis -> verdict-offset mapping (the "named axis") --------
99// Returns the out12 offset whose value the tuner maximizes as tiebreak.
100// FLATNESS is lower-better; tl_axis_value inverts it so "bigger = better"
101// holds for every axis uniformly.
102func tl_axis_offset(refine_axis: i64) -> i64 {
103 if refine_axis == NX_WQG_REFINE_MORE_RELIEF { return NX_WQG_OFF_DIVERSITY }
104 if refine_axis == NX_WQG_REFINE_MORE_FEATURES { return NX_WQG_OFF_EXTREMES }
105 if refine_axis == NX_WQG_REFINE_ADD_DETAIL { return NX_WQG_OFF_LOCAL_VAR }
106 if refine_axis == NX_WQG_REFINE_SMOOTH_NOISE { return NX_WQG_OFF_LOCAL_VAR }
107 if refine_axis == NX_WQG_REFINE_LESS_FLATNESS { return NX_WQG_OFF_FLATNESS_PENALTY }
108 if refine_axis == NX_WQG_REFINE_FIX_FRACTAL { return NX_WQG_OFF_FRACTAL_DIM }
109 if refine_axis == NX_WQG_REFINE_FIX_BIOMES { return NX_WQG_OFF_BIOME_COHERENCE }
110 if refine_axis == NX_WQG_REFINE_MORE_VARIETY { return NX_WQG_OFF_FEATURE_VARIETY }
111 if refine_axis == NX_WQG_REFINE_IMPROVE_READ { return NX_WQG_OFF_READABILITY }
112 return NX_WQG_OFF_LOCAL_VAR // NONE: the historical weak axis
113}
114
115func tl_axis_value(out12: *i64, refine_axis: i64) -> i64 {
116 let off: i64 = tl_axis_offset(refine_axis)
117 let v: i64 = out12[off]
118 if off == NX_WQG_OFF_FLATNESS_PENALTY { return NX_WQG_Q - v }
119 return v
120}
121
122// Mean axis health (all 8 axes, big-is-better orientation) -- the third
123// score layer: among equal (grade, named-axis) configs the HEALTHIER
124// world wins, so the tuner cannot trade sibling axes for free (measured
125// 2026-06-10: deep rills bought ext by gutting READABILITY; the verdict
126// loop re-targeted, and this layer makes such trades cost at tune time).
127func tl_health(out12: *i64) -> i64 {
128 var sum: i64 = out12[NX_WQG_OFF_DIVERSITY]
129 sum = sum + out12[NX_WQG_OFF_LOCAL_VAR]
130 sum = sum + out12[NX_WQG_OFF_EXTREMES]
131 sum = sum + (NX_WQG_Q - out12[NX_WQG_OFF_FLATNESS_PENALTY])
132 sum = sum + out12[NX_WQG_OFF_FRACTAL_DIM]
133 sum = sum + out12[NX_WQG_OFF_BIOME_COHERENCE]
134 sum = sum + out12[NX_WQG_OFF_FEATURE_VARIETY]
135 sum = sum + out12[NX_WQG_OFF_READABILITY]
136 return sum / 8
137}
138
139// STRICT 3-layer lexicographic score: grade >> named axis >> health.
140// Layer widths use Q+1 so a lower layer can never flip a higher one.
141// The axis layer is CLAMPED at the grader's own WIN floor: surplus past
142// a win buys no grade, so chasing it (measured 2026-06-10: read 10595
143// via the all-off corner, paying TWO losses for worthless surplus) is a
144// score bug, not a strategy. At/above the floor, configs tie on layer 2
145// and HEALTH picks the balanced one.
146// LANDMINE (2026-06-10, found by KAT4 + _ts_diag): `s + call(...) * lw`
147// miscompiles -- the multiplier lands on the call result TWICE
148// ((grade+axis)*lw^2 measured, exit-4 stable). Calls are hoisted to
149// temps before any arithmetic; min-repro filed in the pm row.
150func tl_score(grade: i64, out12: *i64, refine_axis: i64) -> i64 {
151 let lw: i64 = NX_WQG_Q + 1
152 let lw2: i64 = lw * lw
153 var av: i64 = tl_axis_value(out12, refine_axis)
154 if av > NX_WQG_AXIS_WIN_FLOOR { av = NX_WQG_AXIS_WIN_FLOOR }
155 let hv: i64 = tl_health(out12)
156 let gl: i64 = grade * lw2
157 let al: i64 = av * lw
158 var s: i64 = gl + al
159 s = s + hv
160 return s
161}
162
163// ---- tiny conf I/O (key=value lines, values integer) ------------------
164// reads <key>=<int> from a conf buffer; returns fallback when missing.
165func tl_conf_get(buf: *u8, n: i64, key: *u8, fallback: i64) -> i64 {
166 var klen: i64 = 0
167 while key[klen] != (0 as u8) { klen = klen + 1 }
168 var i: i64 = 0
169 while i < n {
170 // at line start? (i == 0 or previous is \n)
171 var at_start: i64 = 0
172 if i == 0 { at_start = 1 }
173 if i > 0 { if buf[i - 1] == (10 as u8) { at_start = 1 } }
174 if at_start == 1 {
175 var m: i64 = 0
176 while m < klen {
177 if i + m >= n { m = klen + 1 } else {
178 if buf[i + m] != key[m] { m = klen + 1 } else { m = m + 1 }
179 }
180 }
181 if m == klen {
182 if i + klen < n {
183 if buf[i + klen] == (61 as u8) { // '='
184 var v: i64 = 0
185 var neg: i64 = 0
186 var j: i64 = i + klen + 1
187 if j < n { if buf[j] == (45 as u8) { neg = 1; j = j + 1 } }
188 var any: i64 = 0
189 while j < n {
190 let c: i64 = buf[j] as i64
191 if c >= 48 {
192 if c <= 57 { v = v * 10 + (c - 48); any = 1; j = j + 1 } else { j = n }
193 } else { j = n }
194 }
195 if any == 1 {
196 if neg == 1 { return 0 - v }
197 return v
198 }
199 }
200 }
201 }
202 }
203 i = i + 1
204 }
205 return fallback
206}
207
208// read whole small file into buf (cap bytes); returns length or -1
209func tl_read_file(path: *u8, buf: *u8, cap: i64) -> i64 {
210 let fd: i64 = sys_openat_rd(path)
211 if fd < 0 { return 0 - 1 }
212 var total: i64 = 0
213 var n: i64 = sys_read(fd, buf, cap)
214 while n > 0 {
215 total = total + n
216 if total < cap { n = sys_read(fd, ((buf as i64) + total) as *u8, cap - total) } else { n = 0 }
217 }
218 sys_close(fd)
219 return total
220}
221
222// find "<key>=" at a line start; returns offset of the value's first
223// char, or -1 when the key is absent.
224func tl_conf_find_val(buf: *u8, n: i64, key: *u8) -> i64 {
225 var klen: i64 = 0
226 while key[klen] != (0 as u8) { klen = klen + 1 }
227 var i: i64 = 0
228 while i < n {
229 var at_start: i64 = 0
230 if i == 0 { at_start = 1 }
231 if i > 0 { if buf[i - 1] == (10 as u8) { at_start = 1 } }
232 if at_start == 1 {
233 var m: i64 = 0
234 while m < klen {
235 if i + m >= n { m = klen + 1 } else {
236 if buf[i + m] != key[m] { m = klen + 1 } else { m = m + 1 }
237 }
238 }
239 if m == klen {
240 if i + klen < n {
241 if buf[i + klen] == (61 as u8) { return i + klen + 1 }
242 }
243 }
244 }
245 i = i + 1
246 }
247 return 0 - 1
248}
249
250// match a NUL-terminated word at buf[off..] (stops the conf value at \n)
251func tl_word_at(buf: *u8, n: i64, off: i64, word: *u8) -> i64 {
252 var k: i64 = 0
253 while word[k] != (0 as u8) {
254 if off + k >= n { return 0 }
255 if buf[off + k] != word[k] { return 0 }
256 k = k + 1
257 }
258 if off + k < n {
259 if buf[off + k] != (10 as u8) { return 0 }
260 }
261 return 1
262}
263
264// parse refine_axis=<NAME> (the banked verdict's string form) -> enum
265func tl_conf_get_refine(buf: *u8, n: i64, fallback: i64) -> i64 {
266 let off: i64 = tl_conf_find_val(buf, n, "refine_axis" as *u8)
267 if off < 0 { return fallback }
268 if tl_word_at(buf, n, off, "MORE_RELIEF" as *u8) == 1 { return NX_WQG_REFINE_MORE_RELIEF }
269 if tl_word_at(buf, n, off, "MORE_FEATURES" as *u8) == 1 { return NX_WQG_REFINE_MORE_FEATURES }
270 if tl_word_at(buf, n, off, "LESS_FLATNESS" as *u8) == 1 { return NX_WQG_REFINE_LESS_FLATNESS }
271 if tl_word_at(buf, n, off, "ADD_DETAIL" as *u8) == 1 { return NX_WQG_REFINE_ADD_DETAIL }
272 if tl_word_at(buf, n, off, "SMOOTH_NOISE" as *u8) == 1 { return NX_WQG_REFINE_SMOOTH_NOISE }
273 if tl_word_at(buf, n, off, "FIX_FRACTAL" as *u8) == 1 { return NX_WQG_REFINE_FIX_FRACTAL }
274 if tl_word_at(buf, n, off, "FIX_BIOMES" as *u8) == 1 { return NX_WQG_REFINE_FIX_BIOMES }
275 if tl_word_at(buf, n, off, "MORE_VARIETY" as *u8) == 1 { return NX_WQG_REFINE_MORE_VARIETY }
276 if tl_word_at(buf, n, off, "IMPROVE_READ" as *u8) == 1 { return NX_WQG_REFINE_IMPROVE_READ }
277 return fallback
278}
279
280func tl_grade_name(g: i64) -> *u8 {
281 if g == NX_GRADE_F { return "F" as *u8 }
282 if g == NX_GRADE_D { return "D" as *u8 }
283 if g == NX_GRADE_C { return "C" as *u8 }
284 if g == NX_GRADE_B { return "B" as *u8 }
285 if g == NX_GRADE_A { return "A" as *u8 }
286 if g == NX_GRADE_S { return "S" as *u8 }
287 return "?" as *u8
288}
289
290func tl_refine_name(r: i64) -> *u8 {
291 if r == NX_WQG_REFINE_NONE { return "NONE" as *u8 }
292 if r == NX_WQG_REFINE_MORE_RELIEF { return "MORE_RELIEF" as *u8 }
293 if r == NX_WQG_REFINE_MORE_FEATURES { return "MORE_FEATURES" as *u8 }
294 if r == NX_WQG_REFINE_LESS_FLATNESS { return "LESS_FLATNESS" as *u8 }
295 if r == NX_WQG_REFINE_ADD_DETAIL { return "ADD_DETAIL" as *u8 }
296 if r == NX_WQG_REFINE_SMOOTH_NOISE { return "SMOOTH_NOISE" as *u8 }
297 if r == NX_WQG_REFINE_FIX_FRACTAL { return "FIX_FRACTAL" as *u8 }
298 if r == NX_WQG_REFINE_FIX_BIOMES { return "FIX_BIOMES" as *u8 }
299 if r == NX_WQG_REFINE_MORE_VARIETY { return "MORE_VARIETY" as *u8 }
300 if r == NX_WQG_REFINE_IMPROVE_READ { return "IMPROVE_READ" as *u8 }
301 return "?" as *u8
302}
303
304// load the banked knob conf into knobs[]; missing file/keys -> the
305// preset-table defaults (config hierarchy: banked conf > preset table).
306func tl_load_knobs(conf_path: *u8, preset: i64, knobs: *i64) -> i64 {
307 knobs[TL_K_RIVER_DEPTH] = nx_water_default_river_depth(preset)
308 knobs[TL_K_RILL_DEPTH] = nx_water_default_rill_depth()
309 knobs[TL_K_RILL_RADIUS] = nx_water_default_rill_radius(preset)
310 knobs[TL_K_DETAIL_AMP] = 0
311 knobs[TL_K_PEAKS_EXTRA] = 0
312 knobs[TL_K_AMP_BOOST] = TL_MAGIC_1024
313 knobs[TL_K_TERRACE] = 0
314 let buf: *u8 = sys_mmap(TL_MAGIC_4096)
315 let n: i64 = tl_read_file(conf_path, buf, TL_MAGIC_4095)
316 if n <= 0 { return 0 }
317 knobs[TL_K_RIVER_DEPTH] = tl_conf_get(buf, n, "river_depth" as *u8, knobs[TL_K_RIVER_DEPTH])
318 knobs[TL_K_RILL_DEPTH] = tl_conf_get(buf, n, "rill_depth" as *u8, knobs[TL_K_RILL_DEPTH])
319 knobs[TL_K_RILL_RADIUS] = tl_conf_get(buf, n, "rill_radius" as *u8, knobs[TL_K_RILL_RADIUS])
320 knobs[TL_K_DETAIL_AMP] = tl_conf_get(buf, n, "detail_amp" as *u8, knobs[TL_K_DETAIL_AMP])
321 knobs[TL_K_PEAKS_EXTRA] = tl_conf_get(buf, n, "peaks_extra" as *u8, knobs[TL_K_PEAKS_EXTRA])
322 knobs[TL_K_AMP_BOOST] = tl_conf_get(buf, n, "amp_boost" as *u8, knobs[TL_K_AMP_BOOST])
323 knobs[TL_K_TERRACE] = tl_conf_get(buf, n, "terrace_steps" as *u8, knobs[TL_K_TERRACE])
324 return 1
325}