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}