code wiki / (root) / nx_worldgen_tunelib.nx

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}