code wiki / (root) / nx_mountain_chain.nx

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}