code wiki / (root) / nx_procgen_preset.nx

nx_procgen_preset.nx source

↩ module page · 299 lines · 12254 B

1// nx_procgen_preset.nx -- Tier-0 procgen landscape with bidirectional 2// progressive disclosure, in pure NishiLang. 3// 4// The cardinal: every Tier-N entry point is a VISIBLE COMPOSITION of 5// Tier-(N+1) primitives in this same file. No opaque internals, no 6// cross-language escape hatch. A new user can call nx_procgen_landscape_default(42) 7// and get a useful landscape; a power user can Read this file, copy the 8// Tier-2 composition body, swap one call, and ship their own preset. 9// Same toolchain, same parser, same compilation. 10// 11// === Tiers exposed by this module ===================================== 12// 13// Tier 0 (just-works) 14// nx_procgen_landscape_default(seed) -> *Landscape 15// 16// Tier 1 (preset + knobs) 17// nx_procgen_landscape(seed, preset, width, height, density_q10) 18// -> *Landscape 19// 20// Tier 2 (compose primitives, no preset) 21// The body of nx_procgen_landscape is a visible inline composition 22// of nx_perlin_* + nx_poisson_disk_*. Copy and modify. 23// 24// Tier 3 (raw nx) 25// import "nx_perlin.nx" 26// import "nx_poisson_disk.nx" 27// ... write your own pipeline. The primitives are in this same 28// runtime/ directory and are .nx source you can read + edit. 29// 30// === The bidirectional invariant ====================================== 31// 32// Top-down: Tier 0 -> Tier 3 is "open the file, see the recipe." 33// Bottom-up: Tier 3 -> Tier 0 is "save your composition as a named preset." 34// See _preset_params() below -- that table IS the preset registry. 35// A power user adds a row, gets a new preset, ships it. 36 37// nx_safety_envelope: 38// intended_use: AUTO_APPLIED -- primitive-specific tuning queued 39// sil_target: SIL1 40// evidence: [bulk_applied_2026-05-16, see-file-comment-for-detail] 41// verdict: NOT_YET_EVALUATED 42 43import "nx_syscalls.nx" 44import "nx_tier.nx" 45import "nx_perlin.nx" 46import "nx_poisson_disk.nx" 47import "nx_procgen_signature.nx" 48const NX_MAGIC_1024: i64 = 1024 49 50// ===== Sealed-enum: presets =========================================== 51// 52// Each preset names a landscape archetype. The mapping from preset -> 53// (octaves, persistence_q10, feature_radius) lives in _preset_params(). 54// To add a preset: extend NX_PROCGEN_N_PRESETS, add a const, add a row 55// to _preset_params. 56 57const NX_PROCGEN_PRESET_FOREST: nx_int = 0 58const NX_PROCGEN_PRESET_DESERT: nx_int = 1 59const NX_PROCGEN_PRESET_MEADOW: nx_int = 2 60const NX_PROCGEN_PRESET_COAST: nx_int = 3 61const NX_PROCGEN_PRESET_MOUNTAIN: nx_int = 4 62const NX_PROCGEN_N_PRESETS: nx_int = 5 63 64func nx_procgen_preset_is_valid(p: nx_int) -> nx_int { 65 if p < 0 { return 0 } 66 if p >= NX_PROCGEN_N_PRESETS { return 0 } 67 return 1 68} 69 70// ===== Default knobs (overridable from Tier 1) ======================== 71// 72// Tier 0 uses these directly. Tier 1 callers override. No magic 73// numbers in the function bodies -- every default is a named const. 74 75const NX_PROCGEN_DEFAULT_WIDTH: nx_int = 64 76const NX_PROCGEN_DEFAULT_HEIGHT: nx_int = 64 77const NX_PROCGEN_DEFAULT_DENSITY_Q10: nx_int = 1024 // 1.0 = preset's natural density 78 79// Q10 step between adjacent grid cells when sampling perlin. 80// 102 = 0.1 in Q10; smaller = lower-frequency landscape. 81const NX_PROCGEN_CELL_STEP_Q10: nx_int = 102 82 83const NX_PROCGEN_MAX_FEATURES: nx_int = 2048 84 85// ===== Output: typed Landscape struct ================================= 86// 87// Quantitative: width / height / heightmap / feature points. 88// Qualitative: preset + seed identify the recipe (provenance). 89 90struct Landscape { 91 width: nx_int, 92 height: nx_int, 93 heightmap: *nx_int, // width * height entries, Q10 elevation 94 n_features: nx_int, 95 feature_xs: *i64, 96 feature_ys: *i64, 97 preset: nx_int, 98 seed: nx_int 99} 100 101// ===== Tier-2 helpers (visible building blocks) ======================= 102// 103// These two are the "primitives within the module." Tier 1 calls them 104// explicitly so the recipe is readable. A user copying the recipe sees 105// exactly which primitives are involved. 106 107// Map preset -> (octaves, persistence_q10, base_radius). 108// This is the preset registry. Bottom-up users add a preset here. 109func _preset_octaves(p: nx_int) -> nx_int { 110 if p == NX_PROCGEN_PRESET_FOREST { return 4 } 111 if p == NX_PROCGEN_PRESET_DESERT { return 2 } 112 if p == NX_PROCGEN_PRESET_MEADOW { return 3 } 113 if p == NX_PROCGEN_PRESET_COAST { return 5 } 114 if p == NX_PROCGEN_PRESET_MOUNTAIN { return 6 } 115 return 4 116} 117 118func _preset_persistence_q10(p: nx_int) -> nx_int { 119 if p == NX_PROCGEN_PRESET_FOREST { return 614 } // ~0.6 -- rough canopy 120 if p == NX_PROCGEN_PRESET_DESERT { return 256 } // ~0.25 -- smooth dunes 121 if p == NX_PROCGEN_PRESET_MEADOW { return 358 } // ~0.35 -- gentle rolls 122 if p == NX_PROCGEN_PRESET_COAST { return 512 } // ~0.5 -- balanced 123 if p == NX_PROCGEN_PRESET_MOUNTAIN { return 768 } // ~0.75 -- jagged 124 return 512 125} 126 127func _preset_base_radius(p: nx_int) -> nx_int { 128 if p == NX_PROCGEN_PRESET_FOREST { return 4 } 129 if p == NX_PROCGEN_PRESET_DESERT { return 10 } 130 if p == NX_PROCGEN_PRESET_MEADOW { return 6 } 131 if p == NX_PROCGEN_PRESET_COAST { return 8 } 132 if p == NX_PROCGEN_PRESET_MOUNTAIN { return 12 } 133 return 8 134} 135 136// Adjust base radius by density knob. density_q10 = 1024 means use the 137// preset's natural radius; 2048 doubles spacing (half as many features); 138// 512 halves spacing (more features). 139func _apply_density(base_radius: nx_int, density_q10: nx_int) -> nx_int { 140 if density_q10 <= 0 { return base_radius } 141 var r: nx_int = (base_radius * density_q10) / NX_MAGIC_1024 142 if r < 1 { r = 1 } 143 return r 144} 145 146// Fill heightmap[w*h] with Q10 perlin-fbm values. Visible Tier-2. 147func _fill_heightmap(state: *PerlinState, w: nx_int, h: nx_int, 148 heightmap: *nx_int, octaves: nx_int, 149 persistence_q10: nx_int) -> nx_int { 150 var y: nx_int = 0 151 while y < h { 152 var x: nx_int = 0 153 while x < w { 154 let x_q10: nx_int = x * NX_PROCGEN_CELL_STEP_Q10 155 let y_q10: nx_int = y * NX_PROCGEN_CELL_STEP_Q10 156 let v: nx_int = nx_perlin_fbm_2d(state, x_q10, y_q10, 157 octaves, persistence_q10) 158 heightmap[y * w + x] = v 159 x = x + 1 160 } 161 y = y + 1 162 } 163 return 0 164} 165 166// ===== Tier 1: preset + knobs ========================================= 167// 168// THIS FUNCTION BODY IS THE TIER-2 RECIPE. Anything a Tier-3 user 169// would write directly with nx_perlin + nx_poisson_disk is visible 170// here. Copy this body, swap a call, ship your own variant. 171 172func nx_procgen_landscape(seed: nx_int, preset: nx_int, 173 width: nx_int, height: nx_int, 174 density_q10: nx_int) -> *Landscape { 175 // Defensive at boundary: preset must be valid; fall back to FOREST. 176 var p: nx_int = preset 177 if nx_procgen_preset_is_valid(p) == 0 { 178 p = NX_PROCGEN_PRESET_FOREST 179 } 180 var w: nx_int = width 181 if w <= 0 { w = NX_PROCGEN_DEFAULT_WIDTH } 182 var h: nx_int = height 183 if h <= 0 { h = NX_PROCGEN_DEFAULT_HEIGHT } 184 var dq: nx_int = density_q10 185 if dq <= 0 { dq = NX_PROCGEN_DEFAULT_DENSITY_Q10 } 186 187 // === Step 1: pull recipe knobs from preset ==================== 188 let octaves: nx_int = _preset_octaves(p) 189 let persistence_q10: nx_int = _preset_persistence_q10(p) 190 let base_radius: nx_int = _preset_base_radius(p) 191 let radius: nx_int = _apply_density(base_radius, dq) 192 193 // === Step 2: heightmap via Perlin fbm ========================= 194 let state: *PerlinState = nx_perlin_alloc(seed) 195 let heightmap: *nx_int = (sys_mmap(w * h * NX_SIZEOF_NX_INT)) as *nx_int 196 _fill_heightmap(state, w, h, heightmap, octaves, persistence_q10) 197 198 // === Step 2b: signature features by constraint (P1) =========== 199 // Peaks/ridge/valley/cliff as composed primitives on top of the fbm 200 // base -- noise gives texture, landmarks give EXTREMES. Recipe and 201 // knob table live in nx_procgen_signature.nx, per-preset. 202 nx_sig_compose(heightmap, w, h, seed, p) 203 204 // === Step 3: feature points via Poisson-disk ================= 205 let xs: *i64 = (sys_mmap(NX_PROCGEN_MAX_FEATURES * NX_SIZEOF_NX_INT)) as *i64 206 let ys: *i64 = (sys_mmap(NX_PROCGEN_MAX_FEATURES * NX_SIZEOF_NX_INT)) as *i64 207 let n_features: nx_int = nx_poisson_disk_sample(seed, w, h, radius, 208 xs, ys, 209 NX_PROCGEN_MAX_FEATURES) 210 211 // === Step 4: wrap in typed Landscape struct ================== 212 let land: *Landscape = (sys_mmap(8 * NX_SIZEOF_NX_INT)) as *Landscape 213 land.width = w 214 land.height = h 215 land.heightmap = heightmap 216 land.n_features = n_features 217 land.feature_xs = xs 218 land.feature_ys = ys 219 land.preset = p 220 land.seed = seed 221 return land 222} 223 224// Knob-explicit Tier-1 (ADDITIVE; v1 contract untouched): same recipe 225// with the landmark knobs surfaced for the tuner/recipe layers. 226func nx_procgen_landscape_cfg(seed: nx_int, preset: nx_int, 227 width: nx_int, height: nx_int, 228 density_q10: nx_int, 229 peaks_extra: nx_int, 230 amp_boost_q10: nx_int, 231 terrace_steps: nx_int) -> *Landscape { 232 var p: nx_int = preset 233 if nx_procgen_preset_is_valid(p) == 0 { p = NX_PROCGEN_PRESET_FOREST } 234 var w: nx_int = width 235 if w <= 0 { w = NX_PROCGEN_DEFAULT_WIDTH } 236 var h: nx_int = height 237 if h <= 0 { h = NX_PROCGEN_DEFAULT_HEIGHT } 238 var dq: nx_int = density_q10 239 if dq <= 0 { dq = NX_PROCGEN_DEFAULT_DENSITY_Q10 } 240 241 let octaves: nx_int = _preset_octaves(p) 242 let persistence_q10: nx_int = _preset_persistence_q10(p) 243 let base_radius: nx_int = _preset_base_radius(p) 244 let radius: nx_int = _apply_density(base_radius, dq) 245 246 let state: *PerlinState = nx_perlin_alloc(seed) 247 let heightmap: *nx_int = (sys_mmap(w * h * NX_SIZEOF_NX_INT)) as *nx_int 248 _fill_heightmap(state, w, h, heightmap, octaves, persistence_q10) 249 nx_sig_compose_cfg(heightmap, w, h, seed, p, peaks_extra, amp_boost_q10, terrace_steps) 250 251 let xs: *i64 = (sys_mmap(NX_PROCGEN_MAX_FEATURES * NX_SIZEOF_NX_INT)) as *i64 252 let ys: *i64 = (sys_mmap(NX_PROCGEN_MAX_FEATURES * NX_SIZEOF_NX_INT)) as *i64 253 let n_features: nx_int = nx_poisson_disk_sample(seed, w, h, radius, 254 xs, ys, 255 NX_PROCGEN_MAX_FEATURES) 256 257 let land: *Landscape = (sys_mmap(8 * NX_SIZEOF_NX_INT)) as *Landscape 258 land.width = w 259 land.height = h 260 land.heightmap = heightmap 261 land.n_features = n_features 262 land.feature_xs = xs 263 land.feature_ys = ys 264 land.preset = p 265 land.seed = seed 266 return land 267} 268 269// ===== Tier 0: just-works ============================================= 270// 271// Single-call entry for new users. Implemented as one Tier-1 call so 272// the recipe stays readable -- there is no hidden default-resolution 273// logic. All defaults are the named consts above. 274 275func nx_procgen_landscape_default(seed: nx_int) -> *Landscape { 276 return nx_procgen_landscape(seed, 277 NX_PROCGEN_PRESET_FOREST, 278 NX_PROCGEN_DEFAULT_WIDTH, 279 NX_PROCGEN_DEFAULT_HEIGHT, 280 NX_PROCGEN_DEFAULT_DENSITY_Q10) 281} 282 283// ===== Accessors (Tier 1 typed reads) ================================= 284// 285// Quantitative: heightmap value at (x, y). 286// Qualitative: classify elevation into Perlin band (VOID/QUIET/...). 287 288func nx_landscape_height_at(land: *Landscape, x: nx_int, y: nx_int) -> nx_int { 289 if x < 0 { return 0 } 290 if x >= land.width { return 0 } 291 if y < 0 { return 0 } 292 if y >= land.height { return 0 } 293 return land.heightmap[y * land.width + x] 294} 295 296func nx_landscape_height_band_at(land: *Landscape, x: nx_int, y: nx_int) -> nx_int { 297 let v: nx_int = nx_landscape_height_at(land, x, y) 298 return nx_perlin_classify(v) 299}