nx_mountain_chain.nx source
↩ module page · 420 lines · 17367 B
1// nx_mountain_chain.nx -- specialized mountain-chain generator.
2//
3// Eleventh kind-specific generator per
4// feedback-kind-specific-generators-not-broad-noise. A mountain
5// chain is NOT FBM noise -- it has a primary RIDGE polyline, height
6// envelope along the ridge, perpendicular falloff to neighbouring
7// valleys, and tributary ridges branching off the main spine.
8//
9// Generator architecture:
10// - Main ridge polyline: hash-deterministic meandering line with
11// mild perturbation, similar to nx_river_carve but no downhill
12// bias (mountains follow tectonic stress, not drainage).
13// - Per-segment height: bell curve along the ridge length; peak at
14// mid-ridge, taper toward both ends.
15// - Perpendicular falloff: smoothstep over ridge_width on either
16// side.
17// - Tributary ridges: N branches at hash-determined indices along
18// the main spine, each at a hash-determined angle off-perpendicular.
19//
20// Record layout (variable-length; first 8 i64 are header):
21// rec[0] = n_main_points number of main-ridge polyline points
22// rec[1] = n_tributaries count of branch ridges
23// rec[2] = peak_height_q14_m max height of main spine
24// rec[3] = ridge_width_q14_m perpendicular falloff distance
25// rec[4] = peak_at_index which main point has the global max
26// rec[5] = seed Park-Miller LCG seed
27// rec[6] = reserved
28// rec[7] = reserved
29// rec[8..] = main_points 2 i64 per point (x, z)
30// rec[...] = tributary records 4 i64 per tributary: start_idx,
31// n_tributary_pts, height_factor_q14,
32// reserved
33// rec[...] = tributary points 2 i64 per tributary point (x, z)
34//
35// genealogy_id: anderson_2003_tectonic_geomorphology +
36// himalaya_orogeny_canon + ridge_field_canon
37// lineage_id: nx_mountain_chain_polyline_tributary_v1
38//
39// nx_safety_envelope:
40// intended_use: "Polyline mountain-chain generator with
41// tributary spurs -- kind-specific procgen
42// per cardinal feedback-kind-specific-generators"
43// sil_target: SIL1
44// asil_target: QM
45// dal_target: NONE
46// evidence: [Q14_fixed_point, polyline_classical,
47// tributary_branching_bounded]
48// hazard_register: [bug-tape-chain-self-intersect,
49// bug-tape-elevation-discontinuity-at-spur]
50// residual_risk: "Procgen quality; not safety-runtime."
51// verdict: NOT_YET_EVALUATED
52
53import "nx_syscalls.nx"
54import "nx_tier.nx"
55import "nx_camera_q14.nx"
56const NX_MAGIC_2654435761: i64 = 2654435761
57const NX_MAGIC_1597334677: i64 = 1597334677
58const NX_MAGIC_4500: i64 = 4500
59const NX_MAGIC_18000: i64 = 18000
60const NX_MAGIC_9000: i64 = 9000
61const NX_MAGIC_2000: i64 = 2000
62
63const NX_MC_Q: nx_int = 16384
64
65// Park-Miller LCG.
66const NX_MC_LCG_A: nx_int = 48271
67const NX_MC_LCG_M: nx_int = 2147483647
68
69// Header offsets.
70const NX_MC_OFF_N_MAIN: nx_int = 0
71const NX_MC_OFF_N_TRIBUTARIES: nx_int = 1
72const NX_MC_OFF_PEAK_HEIGHT: nx_int = 2
73const NX_MC_OFF_RIDGE_WIDTH: nx_int = 3
74const NX_MC_OFF_PEAK_AT_INDEX: nx_int = 4
75const NX_MC_OFF_SEED: nx_int = 5
76const NX_MC_HEADER_STRIDE: nx_int = 8
77
78const NX_MC_POINT_STRIDE: nx_int = 2
79const NX_MC_TRIBUTARY_STRIDE: nx_int = 4
80const NX_MC_TRIB_OFF_START_IDX: nx_int = 0
81const NX_MC_TRIB_OFF_N_PTS: nx_int = 1
82const NX_MC_TRIB_OFF_HEIGHT_F: nx_int = 2
83
84// ===== Hash mixer ==================================================
85func _mc_hash(seed: nx_int, a: nx_int, b: nx_int) -> nx_int {
86 var h: nx_int = seed
87 h = (h * NX_MC_LCG_A + a * NX_MAGIC_2654435761) % NX_MC_LCG_M
88 if h < 0 { h = h + NX_MC_LCG_M }
89 h = (h * NX_MC_LCG_A + b * NX_MAGIC_1597334677) % NX_MC_LCG_M
90 if h < 0 { h = h + NX_MC_LCG_M }
91 return h
92}
93
94// ===== Integer sqrt (Newton) =======================================
95func _mc_isqrt(n: nx_int) -> nx_int {
96 if n <= 0 { return 0 }
97 var x: nx_int = n
98 var y: nx_int = (x + 1) / 2
99 while y < x {
100 x = y
101 y = (x + n / x) / 2
102 }
103 return x
104}
105
106// ===== Distance from point to segment ==============================
107func _mc_dist_sq_to_segment(
108 px: nx_int, py: nx_int,
109 x0: nx_int, y0: nx_int, x1: nx_int, y1: nx_int
110) -> nx_int {
111 let ex: nx_int = x1 - x0
112 let ey: nx_int = y1 - y0
113 let edge_sq: nx_int = ex * ex + ey * ey
114 if edge_sq == 0 {
115 let dx: nx_int = px - x0
116 let dy: nx_int = py - y0
117 return dx * dx + dy * dy
118 }
119 let qx: nx_int = px - x0
120 let qy: nx_int = py - y0
121 let dot: nx_int = qx * ex + qy * ey
122 var t: nx_int = dot
123 if t < 0 { t = 0 }
124 if t > edge_sq { t = edge_sq }
125 let cx: nx_int = x0 + ex * t / edge_sq
126 let cy: nx_int = y0 + ey * t / edge_sq
127 let rx: nx_int = px - cx
128 let ry: nx_int = py - cy
129 return rx * rx + ry * ry
130}
131
132// ===== Generator ==================================================
133// Caller provides:
134// seed Park-Miller LCG seed
135// start_x, start_z Q14 metres start of ridge
136// end_x, end_z Q14 metres end of ridge
137// n_segments number of main-ridge segments (n_main_points = n_segments + 1)
138// peak_height_q14_m peak elevation at ridge centre
139// ridge_width_q14_m perpendicular falloff distance
140// n_tributaries branch count (each = 4 segments by default)
141// meander_amp_cd meander amplitude in centi-degrees off-bearing
142// out output buffer (caller must size enough; see below)
143// max_capacity_i64 out buffer size in i64
144//
145// Total i64 needed:
146// 8 (header) + (n_segments + 1) * 2 (main points)
147// + n_tributaries * (4 + 5 * 2) (each tributary = 5 points = 10 i64)
148//
149// Returns 1 on success, 0 if max_capacity is insufficient.
150func nx_mountain_chain_generate(
151 seed: nx_int,
152 start_x: nx_int, start_z: nx_int,
153 end_x: nx_int, end_z: nx_int,
154 n_segments: nx_int,
155 peak_height_q14_m: nx_int,
156 ridge_width_q14_m: nx_int,
157 n_tributaries: nx_int,
158 meander_amp_cd: nx_int,
159 out: *i64,
160 max_capacity_i64: nx_int
161) -> nx_int {
162 let q: nx_int = NX_MC_Q
163 if n_segments <= 0 { return 0 }
164 let n_main_points: nx_int = n_segments + 1
165 let trib_segs: nx_int = 4
166 let trib_pts: nx_int = trib_segs + 1
167 let needed: nx_int =
168 NX_MC_HEADER_STRIDE +
169 n_main_points * NX_MC_POINT_STRIDE +
170 n_tributaries * (NX_MC_TRIBUTARY_STRIDE + trib_pts * NX_MC_POINT_STRIDE)
171 if needed > max_capacity_i64 { return 0 }
172
173 // Header.
174 out[NX_MC_OFF_N_MAIN] = n_main_points
175 out[NX_MC_OFF_N_TRIBUTARIES] = n_tributaries
176 out[NX_MC_OFF_PEAK_HEIGHT] = peak_height_q14_m
177 out[NX_MC_OFF_RIDGE_WIDTH] = ridge_width_q14_m
178 out[NX_MC_OFF_PEAK_AT_INDEX] = n_main_points / 2
179 out[NX_MC_OFF_SEED] = seed
180 out[6] = 0
181 out[7] = 0
182
183 // Base bearing from start to end (centi-degrees).
184 let dx: nx_int = end_x - start_x
185 let dz: nx_int = end_z - start_z
186 var abs_dx: nx_int = dx
187 if abs_dx < 0 { abs_dx = 0 - abs_dx }
188 var abs_dz: nx_int = dz
189 if abs_dz < 0 { abs_dz = 0 - abs_dz }
190 var base_bearing: nx_int = 0
191 if abs_dx >= abs_dz {
192 var fine: nx_int = 0
193 if abs_dx > 0 { fine = (abs_dz * NX_MAGIC_4500) / abs_dx }
194 if dx >= 0 {
195 if dz >= 0 { base_bearing = fine }
196 if dz < 0 { base_bearing = 0 - fine }
197 } else {
198 if dz >= 0 { base_bearing = NX_MAGIC_18000 - fine }
199 if dz < 0 { base_bearing = 0 - NX_MAGIC_18000 + fine }
200 }
201 } else {
202 var fine: nx_int = 0
203 if abs_dz > 0 { fine = (abs_dx * NX_MAGIC_4500) / abs_dz }
204 if dz >= 0 {
205 if dx >= 0 { base_bearing = NX_MAGIC_9000 - fine }
206 if dx < 0 { base_bearing = NX_MAGIC_9000 + fine }
207 } else {
208 if dx >= 0 { base_bearing = 0 - NX_MAGIC_9000 + fine }
209 if dx < 0 { base_bearing = 0 - NX_MAGIC_9000 - fine }
210 }
211 }
212
213 // Total path length / n_segments = step length.
214 let total_len_sq: nx_int = dx * dx + dz * dz
215 let total_len: nx_int = _mc_isqrt(total_len_sq)
216 let step_len: nx_int = total_len / n_segments
217
218 // Write main ridge points with hash-perturbed bearing.
219 let main_base: nx_int = NX_MC_HEADER_STRIDE
220 out[main_base ] = start_x
221 out[main_base + 1] = start_z
222 var cx: nx_int = start_x
223 var cz: nx_int = start_z
224 var i: nx_int = 1
225 while i < n_main_points {
226 let h: nx_int = _mc_hash(seed, i, 1)
227 let signed_h: nx_int = (h % (2 * q)) - q
228 let perturb: nx_int = (signed_h * meander_amp_cd) / q
229 let bearing: nx_int = base_bearing + perturb
230 let cos_b: nx_int = nx_camera_cos_q14_centideg(bearing)
231 let sin_b: nx_int = nx_camera_sin_q14_centideg(bearing)
232 cx = cx + (step_len * cos_b) / q
233 cz = cz + (step_len * sin_b) / q
234 out[main_base + i * NX_MC_POINT_STRIDE ] = cx
235 out[main_base + i * NX_MC_POINT_STRIDE + 1] = cz
236 i = i + 1
237 }
238
239 // Tributary records: each branches at a random main-segment index,
240 // angle perpendicular off-bearing, length = step_len * trib_segs.
241 let trib_base: nx_int = main_base + n_main_points * NX_MC_POINT_STRIDE
242 var t: nx_int = 0
243 while t < n_tributaries {
244 let h_idx: nx_int = _mc_hash(seed + 1000, t, 0)
245 let h_ang: nx_int = _mc_hash(seed + 1000, t, 1)
246 let branch_idx: nx_int = 1 + (h_idx % (n_segments - 1))
247 // Tributary angle = perpendicular (+/- 90 deg) with hash-jitter.
248 let perp_sign: nx_int = h_ang % 2
249 var trib_bearing: nx_int = base_bearing + NX_MAGIC_9000
250 if perp_sign == 1 { trib_bearing = base_bearing - NX_MAGIC_9000 }
251 // Height factor: tributaries are 40-70% of main peak.
252 let h_fac_q: nx_int = (q * 4) / 10 + ((h_ang / 7) % (q * 3 / 10))
253
254 // Write tributary header.
255 let this_rec: nx_int = trib_base + t * (NX_MC_TRIBUTARY_STRIDE + trib_pts * NX_MC_POINT_STRIDE)
256 out[this_rec + NX_MC_TRIB_OFF_START_IDX] = branch_idx
257 out[this_rec + NX_MC_TRIB_OFF_N_PTS] = trib_pts
258 out[this_rec + NX_MC_TRIB_OFF_HEIGHT_F] = h_fac_q
259 out[this_rec + 3] = 0
260
261 // Tributary point 0 = main_points[branch_idx].
262 let branch_x: nx_int = out[main_base + branch_idx * NX_MC_POINT_STRIDE ]
263 let branch_z: nx_int = out[main_base + branch_idx * NX_MC_POINT_STRIDE + 1]
264 let trib_pt_base: nx_int = this_rec + NX_MC_TRIBUTARY_STRIDE
265 out[trib_pt_base ] = branch_x
266 out[trib_pt_base + 1] = branch_z
267 var tx: nx_int = branch_x
268 var tz: nx_int = branch_z
269 var p: nx_int = 1
270 while p < trib_pts {
271 let h_p: nx_int = _mc_hash(seed + NX_MAGIC_2000 + t, p, 0)
272 let signed_h: nx_int = (h_p % (2 * q)) - q
273 let perturb: nx_int = (signed_h * meander_amp_cd) / q
274 let bear: nx_int = trib_bearing + perturb
275 let cos_b: nx_int = nx_camera_cos_q14_centideg(bear)
276 let sin_b: nx_int = nx_camera_sin_q14_centideg(bear)
277 tx = tx + (step_len * cos_b) / q
278 tz = tz + (step_len * sin_b) / q
279 out[trib_pt_base + p * NX_MC_POINT_STRIDE ] = tx
280 out[trib_pt_base + p * NX_MC_POINT_STRIDE + 1] = tz
281 p = p + 1
282 }
283 t = t + 1
284 }
285
286 return 1
287}
288
289// ===== Elevation at (px, pz) ======================================
290// Returns Q14 metres height delta from the mountain-chain field.
291// Composes main ridge + all tributaries; max contribution wins.
292func nx_mountain_chain_elevation_at(
293 rec: *i64, px: nx_int, pz: nx_int
294) -> nx_int {
295 let q: nx_int = NX_MC_Q
296 let n_main: nx_int = rec[NX_MC_OFF_N_MAIN]
297 let n_trib: nx_int = rec[NX_MC_OFF_N_TRIBUTARIES]
298 let peak_h: nx_int = rec[NX_MC_OFF_PEAK_HEIGHT]
299 let ridge_w: nx_int = rec[NX_MC_OFF_RIDGE_WIDTH]
300 let peak_at: nx_int = rec[NX_MC_OFF_PEAK_AT_INDEX]
301 if n_main < 2 { return 0 }
302 if ridge_w <= 0 { return 0 }
303 let ridge_w_sq: nx_int = ridge_w * ridge_w
304
305 let main_base: nx_int = NX_MC_HEADER_STRIDE
306 // Find min distance to any main segment.
307 var max_contrib: nx_int = 0
308 var best_seg_idx: nx_int = 0 - 1
309 var i: nx_int = 0
310 while i < n_main - 1 {
311 let x0: nx_int = rec[main_base + i * NX_MC_POINT_STRIDE ]
312 let y0: nx_int = rec[main_base + i * NX_MC_POINT_STRIDE + 1]
313 let x1: nx_int = rec[main_base + (i + 1) * NX_MC_POINT_STRIDE ]
314 let y1: nx_int = rec[main_base + (i + 1) * NX_MC_POINT_STRIDE + 1]
315 let d_sq: nx_int = _mc_dist_sq_to_segment(px, pz, x0, y0, x1, y1)
316 if d_sq < ridge_w_sq {
317 // Bell-curve height envelope along ridge: H(i) = peak * (1 - ((i - peak_at)/(n/2))^2)
318 let half_n: nx_int = (n_main + 1) / 2
319 var pos_offset: nx_int = i - peak_at
320 if pos_offset < 0 { pos_offset = 0 - pos_offset }
321 if half_n <= 0 { half_n }
322 var env_q: nx_int = 0
323 if half_n > 0 {
324 let pos_q: nx_int = (pos_offset * q) / half_n
325 let pos_sq: nx_int = (pos_q * pos_q) / q
326 env_q = q - pos_sq
327 if env_q < 0 { env_q = 0 }
328 }
329 let h_at_ridge: nx_int = (peak_h * env_q) / q
330 // Perpendicular falloff: smoothstep over ridge_width.
331 let falloff_q: nx_int = ((ridge_w_sq - d_sq) * q) / ridge_w_sq
332 // Smoothstep: 3t^2 - 2t^3.
333 let t: nx_int = falloff_q
334 let t_sq: nx_int = (t * t) / q
335 let t_cu: nx_int = (t_sq * t) / q
336 let smooth_q: nx_int = 3 * t_sq - 2 * t_cu
337 let contrib: nx_int = (h_at_ridge * smooth_q) / q
338 if contrib > max_contrib { max_contrib = contrib; best_seg_idx = i }
339 }
340 i = i + 1
341 }
342
343 // Tributaries: same algorithm but with height_factor.
344 let trib_base: nx_int = main_base + n_main * NX_MC_POINT_STRIDE
345 let trib_pts: nx_int = 5
346 var tt: nx_int = 0
347 while tt < n_trib {
348 let this_rec: nx_int = trib_base + tt * (NX_MC_TRIBUTARY_STRIDE + trib_pts * NX_MC_POINT_STRIDE)
349 let h_fac: nx_int = rec[this_rec + NX_MC_TRIB_OFF_HEIGHT_F]
350 let trib_pt_base: nx_int = this_rec + NX_MC_TRIBUTARY_STRIDE
351 let trib_peak_h: nx_int = (peak_h * h_fac) / q
352 var pi: nx_int = 0
353 while pi < trib_pts - 1 {
354 let tx0: nx_int = rec[trib_pt_base + pi * NX_MC_POINT_STRIDE ]
355 let ty0: nx_int = rec[trib_pt_base + pi * NX_MC_POINT_STRIDE + 1]
356 let tx1: nx_int = rec[trib_pt_base + (pi + 1) * NX_MC_POINT_STRIDE ]
357 let ty1: nx_int = rec[trib_pt_base + (pi + 1) * NX_MC_POINT_STRIDE + 1]
358 let td_sq: nx_int = _mc_dist_sq_to_segment(px, pz, tx0, ty0, tx1, ty1)
359 if td_sq < ridge_w_sq {
360 let falloff_q: nx_int = ((ridge_w_sq - td_sq) * q) / ridge_w_sq
361 let t: nx_int = falloff_q
362 let t_sq: nx_int = (t * t) / q
363 let t_cu: nx_int = (t_sq * t) / q
364 let smooth_q: nx_int = 3 * t_sq - 2 * t_cu
365 let contrib: nx_int = (trib_peak_h * smooth_q) / q
366 if contrib > max_contrib { max_contrib = contrib }
367 }
368 pi = pi + 1
369 }
370 tt = tt + 1
371 }
372
373 return max_contrib
374}
375
376// ===== Self-test ====================================================
377func main() -> i64 {
378 let q: nx_int = NX_MC_Q
379 let cap: nx_int = 256
380 let rec: *i64 = (sys_mmap(cap * NX_SIZEOF_NX_INT)) as *i64
381
382 // T1: Generate a 10-segment chain from (0,0) to (1000q, 0) with
383 // peak 1000m, ridge width 100m, 2 tributaries.
384 let ok: nx_int = nx_mountain_chain_generate(
385 42, 0, 0, 1000 * q, 0,
386 10, 1000 * q, 100 * q,
387 2, 800, // ~8 deg meander
388 rec, cap
389 )
390 if ok != 1 { return __syscall(93, 1, 0, 0, 0, 0, 0) }
391 if rec[NX_MC_OFF_N_MAIN] != 11 { return __syscall(93, 2, 0, 0, 0, 0, 0) }
392 if rec[NX_MC_OFF_N_TRIBUTARIES] != 2 { return __syscall(93, 3, 0, 0, 0, 0, 0) }
393
394 // T2: Elevation at the middle of the chain should be near peak.
395 // Mid X = 500q. Sample at exactly the ridge centre (Y=0).
396 let mid_h: nx_int = nx_mountain_chain_elevation_at(rec, 500 * q, 0)
397 if mid_h < 500 * q { return __syscall(93, 10, 0, 0, 0, 0, 0) }
398
399 // T3: Elevation far from chain -> 0.
400 let far_h: nx_int = nx_mountain_chain_elevation_at(rec, 500 * q, 500 * q)
401 if far_h != 0 { return __syscall(93, 20, 0, 0, 0, 0, 0) }
402
403 // T4: Elevation at the chain's start -> low (bell curve taper).
404 let start_h: nx_int = nx_mountain_chain_elevation_at(rec, 0, 0)
405 if start_h >= mid_h { return __syscall(93, 30, 0, 0, 0, 0, 0) }
406
407 // T5: Refusal -- 0 segments returns 0.
408 let ok2: nx_int = nx_mountain_chain_generate(
409 42, 0, 0, 100, 0, 0, 100, 10, 0, 0, rec, cap
410 )
411 if ok2 != 0 { return __syscall(93, 40, 0, 0, 0, 0, 0) }
412
413 // T6: Refusal -- insufficient capacity.
414 let ok3: nx_int = nx_mountain_chain_generate(
415 42, 0, 0, 100, 0, 10, 100, 10, 5, 800, rec, 5
416 )
417 if ok3 != 0 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
418
419 return 0
420}