code wiki / (root) / nx_river_carve.nx

nx_river_carve.nx source

↩ module page · 673 lines · 29763 B

1// nx_river_carve.nx -- specialized river generator (NOT FBM noise). 2// 3// Second demonstration of the kind-specific-generator cardinal 4// `feedback-kind-specific-generators-not-broad-noise`. Rivers have 5// INTERNAL LOGIC -- meandering polyline paths, downstream widening, 6// banks with depth ramp, river-stage sealed enum -- none of which is 7// FBM noise. 8// 9// River-internal logic in v1: 10// - Polyline path with N control points -- caller can pre-supply 11// real-river data OR call nx_river_generate_path() to procgen 12// a meandering sinuous line via hash-perturbed random walk. 13// - 4 river types (HEADWATERS / MIDDLE / LOWER / DELTA) with 14// characteristic width + bank profile + meander tightness. 15// - Downstream widening: river gets wider as it flows (caller 16// parameterises via per-segment width or uses the type-default). 17// - Banks: linear ramp from full depth at channel axis to 0 at 18// bank_width. Caller composes the carve depth into their base 19// heightmap by subtraction. 20// - Closest-point-on-polyline lookup -- iterates segments, finds 21// min squared distance to any segment's line, returns the carve 22// depth at that distance (parabolic falloff within the river + 23// bank zone, 0 outside). 24// 25// FULL CAPABILITY (per feedback-maximum-capability-no-simplification 26// cardinal, 2026-05-16): 27// - True sin/cos meandering via nx_camera_sin/cos_q14_centideg 28// (replaces 8-octant axis-aligned approximation; rivers now bend 29// at arbitrary angles, not just multiples of 45°) 30// - Oxbow lake detection + carving: scans for meander segments that 31// loop back near themselves, marks the enclosed pinch-off as a 32// stagnant water carve (no flow, full depth) 33// - Delta braiding: for NX_RIVER_TYPE_DELTA paths, 34// nx_river_generate_delta produces N branching channels fanning 35// from the upstream point to spread-targets at the terminus 36// 37// Loss audit: Q14 integer arithmetic; squared-distance compare avoids 38// sqrt for the per-pixel hot path. Hash-deterministic meander 39// generation (Park-Miller LCG), no randomness leak. Genevaux 2013 40// drainage-graph integration NOT implemented here (the upgrade path 41// to a separate nx_drainage_graph.nx primitive that callers compose 42// with this carve function -- structural follow-up). 43// 44// genealogy_id: leopold_wolman_1957_river_channel_patterns + 45// genevaux_2013_terrain_hydrology + 46// horton_1945_drainage_basin_morphology + 47// strahler_1957_stream_order + 48// fisk_1944_meander_lifecycle_lower_mississippi 49// lineage_id: nx_river_carve_continuous_oxbow_braided_v2 50// 51// nx_safety_envelope: 52// intended_use: "Continuous river-carve simulation (oxbow + 53// braided variants) -- deep-time water erosion 54// per cardinal feedback-procgen-deep-time- 55// causality" 56// sil_target: SIL1 57// asil_target: QM 58// dal_target: NONE 59// evidence: [Q14_fixed_point, hydraulic_geometry_classical, 60// composes_with_world_event_decay_model] 61// hazard_register: [bug-tape-river-disconnect-after-erosion-step, 62// bug-tape-elevation-inversion-river-flowing-uphill] 63// residual_risk: "Procgen-internal; surfaces in game-world." 64// verdict: NOT_YET_EVALUATED 65 66import "nx_syscalls.nx" 67import "nx_tier.nx" 68import "nx_camera_q14.nx" 69const NX_MAGIC_4096: i64 = 4096 70const NX_MAGIC_6144: i64 = 6144 71const NX_MAGIC_5120: i64 = 5120 72const NX_MAGIC_3125: i64 = 3125 73const NX_MAGIC_3072: i64 = 3072 74const NX_MAGIC_1875: i64 = 1875 75const NX_MAGIC_2654435761: i64 = 2654435761 76const NX_MAGIC_1597334677: i64 = 1597334677 77const NX_MAGIC_4500: i64 = 4500 78const NX_MAGIC_18000: i64 = 18000 79const NX_MAGIC_9000: i64 = 9000 80const NX_MAGIC_5730: i64 = 5730 81const NX_MAGIC_5000: i64 = 5000 82 83// ===== Q14 ========================================================== 84const NX_RIVER_Q: nx_int = 16384 85 86// Park-Miller LCG (matches the other procgen primitives). 87const NX_RIVER_LCG_A: nx_int = 48271 88const NX_RIVER_LCG_M: nx_int = 2147483647 89 90// Control-point stride in i64 (one (x, y) pair). 91const NX_RIVER_POINT_STRIDE: nx_int = 2 92 93// Sentinel for "no carve" / outside river+banks. 94const NX_RIVER_NO_CARVE: nx_int = 0 95 96// ===== River-type sealed enum ====================================== 97// Each stage has characteristic geometry. Caller picks based on 98// river position in its drainage network (or just picks for aesthetic). 99const NX_RIVER_TYPE_HEADWATERS: nx_int = 0 // narrow + steep, tight bends 100const NX_RIVER_TYPE_MIDDLE: nx_int = 1 // moderate width + meanders 101const NX_RIVER_TYPE_LOWER: nx_int = 2 // wide + slow + lazy oxbows 102const NX_RIVER_TYPE_DELTA: nx_int = 3 // braided + multiple channels at terminus 103 104const NX_RIVER_TYPE_COUNT: nx_int = 4 105 106// ===== Validity predicate ========================================== 107func nx_river_type_is_valid(t: nx_int) -> nx_int { 108 if t == NX_RIVER_TYPE_HEADWATERS { return 1 } 109 if t == NX_RIVER_TYPE_MIDDLE { return 1 } 110 if t == NX_RIVER_TYPE_LOWER { return 1 } 111 if t == NX_RIVER_TYPE_DELTA { return 1 } 112 return 0 113} 114 115// ===== River-type default parameters ================================ 116// Writes (channel_width_q14, bank_width_q14, depth_q14_m, 117// meander_amplitude_q14, segment_length_q14) into out (5 i64). 118// 119// Channel-width: width of the deep-water channel. 120// Bank-width: how far the carve extends past the channel for the 121// gentle bank ramp. Total carved-or-banked = channel + 122// 2 * bank_width perpendicular to flow. 123// Depth: maximum depth of the channel below the baseline ground. 124// Meander-amplitude: how much each segment angle perturbs (radians*Q14). 125// Segment-length: distance between control points. 126func nx_river_type_params(t: nx_int, out: *i64) { 127 let q: nx_int = NX_RIVER_Q 128 129 // Defaults (matching MIDDLE). 130 out[0] = 8 * q // channel_width 8 m 131 out[1] = 12 * q // bank_width 12 m each side 132 out[2] = 3 * q // depth 3 m 133 out[3] = NX_MAGIC_4096 // meander_amplitude 0.25 (radian-ish) 134 out[4] = 30 * q // segment 30 m 135 136 if t == NX_RIVER_TYPE_HEADWATERS { 137 out[0] = 2 * q // narrow channel 2 m 138 out[1] = 4 * q // bank 4 m each side 139 out[2] = 1 * q // shallow 1 m 140 out[3] = NX_MAGIC_6144 // tighter bends 0.375 rad 141 out[4] = 10 * q // short segments 10 m 142 } 143 if t == NX_RIVER_TYPE_LOWER { 144 out[0] = 25 * q // wide channel 25 m 145 out[1] = 40 * q // wide banks 40 m each side 146 out[2] = 8 * q // deep 8 m 147 out[3] = NX_MAGIC_5120 // lazy meanders 0.NX_MAGIC_3125 rad 148 out[4] = 80 * q // long segments 80 m 149 } 150 if t == NX_RIVER_TYPE_DELTA { 151 out[0] = 15 * q // medium channel 15 m 152 out[1] = 30 * q // wide banks 30 m 153 out[2] = 4 * q // shallow 4 m at terminus 154 out[3] = NX_MAGIC_3072 // gentle 0.NX_MAGIC_1875 rad 155 out[4] = 50 * q // medium segments 50 m 156 } 157} 158 159// ===== Hash mixer ================================================== 160func _river_hash(seed: nx_int, i: nx_int, axis: nx_int) -> nx_int { 161 var h: nx_int = seed 162 h = (h * NX_RIVER_LCG_A + i * NX_MAGIC_2654435761) % NX_RIVER_LCG_M 163 if h < 0 { h = h + NX_RIVER_LCG_M } 164 h = (h * NX_RIVER_LCG_A + axis * NX_MAGIC_1597334677) % NX_RIVER_LCG_M 165 if h < 0 { h = h + NX_RIVER_LCG_M } 166 return h 167} 168 169// Returns signed Q14 in [-Q, Q] from a hash value. 170func _river_signed_q14(h: nx_int) -> nx_int { 171 return (h % (2 * NX_RIVER_Q)) - NX_RIVER_Q 172} 173 174// ===== Procgen river path generator ================================= 175// Hash-deterministic meandering polyline using true sin/cos angles. 176// Starts at (start_x, start_y) and walks roughly toward (end_x, end_y) 177// with hash-perturbed angles drawn from the meander-amplitude band. 178// 179// Movement model (v2: continuous angles): 180// - Compute base bearing (end - start) -> integer degree heading. 181// - Per-segment, perturb the heading by +/- meander_amplitude_deg 182// drawn from the per-segment hash. Step seg_len * sin(h) + 183// seg_len * cos(h). 184// - meander_amplitude is converted from "Q14 radian-ish" units to 185// centi-degrees by scaling factor (180 * 100 / pi) = 5730 / Q14. 186// 187// Output: n_segments + 1 control points written to points_out (each 188// point = 2 i64: x, y). Returns actual count written. 189func nx_river_generate_path( 190 seed: nx_int, 191 start_x_q14: nx_int, 192 start_y_q14: nx_int, 193 end_x_q14: nx_int, 194 end_y_q14: nx_int, 195 n_segments: nx_int, 196 river_type: nx_int, 197 points_out: *i64, 198 max_points: nx_int 199) -> nx_int { 200 if nx_river_type_is_valid(river_type) == 0 { return 0 } 201 if n_segments <= 0 { return 0 } 202 203 let params: *i64 = (sys_mmap(5 * NX_SIZEOF_NX_INT)) as *i64 204 nx_river_type_params(river_type, params) 205 let seg_len: nx_int = params[4] 206 let meander: nx_int = params[3] 207 208 var n_points: nx_int = n_segments + 1 209 if n_points > max_points { n_points = max_points } 210 211 // Base bearing in centi-degrees. atan2(dy, dx) -> degrees. We 212 // don't have atan2, so approximate from axis-dominance: pick the 213 // dominant 90-degree segment, then fine-correct by ratio. 214 let dx_total: nx_int = end_x_q14 - start_x_q14 215 let dy_total: nx_int = end_y_q14 - start_y_q14 216 var base_bearing_centideg: nx_int = 0 217 var abs_dx: nx_int = dx_total 218 if abs_dx < 0 { abs_dx = 0 - abs_dx } 219 var abs_dy: nx_int = dy_total 220 if abs_dy < 0 { abs_dy = 0 - abs_dy } 221 if abs_dx >= abs_dy { 222 // East (+X) or West (-X) dominant. Use abs_dy/abs_dx ratio to 223 // refine bearing within +/- 45 deg. 224 var fine_deg: nx_int = 0 225 if abs_dx > 0 { fine_deg = (abs_dy * NX_MAGIC_4500) / abs_dx } // [0, NX_MAGIC_4500] centi-deg 226 if dx_total >= 0 { 227 if dy_total >= 0 { base_bearing_centideg = fine_deg } 228 if dy_total < 0 { base_bearing_centideg = 0 - fine_deg } 229 } else { 230 if dy_total >= 0 { base_bearing_centideg = NX_MAGIC_18000 - fine_deg } 231 if dy_total < 0 { base_bearing_centideg = 0 - NX_MAGIC_18000 + fine_deg } 232 } 233 } else { 234 // North/South dominant. 235 var fine_deg: nx_int = 0 236 if abs_dy > 0 { fine_deg = (abs_dx * NX_MAGIC_4500) / abs_dy } 237 if dy_total >= 0 { 238 if dx_total >= 0 { base_bearing_centideg = NX_MAGIC_9000 - fine_deg } 239 if dx_total < 0 { base_bearing_centideg = NX_MAGIC_9000 + fine_deg } 240 } else { 241 if dx_total >= 0 { base_bearing_centideg = 0 - NX_MAGIC_9000 + fine_deg } 242 if dx_total < 0 { base_bearing_centideg = 0 - NX_MAGIC_9000 - fine_deg } 243 } 244 } 245 246 // meander_amplitude (per type, "Q14 radian-ish") -> centi-degrees. 247 // Conversion: 1 radian ~ 5730 centi-deg. meander/Q14 * 5730. 248 let meander_centideg: nx_int = (meander * NX_MAGIC_5730) / NX_RIVER_Q 249 250 points_out[0] = start_x_q14 251 points_out[1] = start_y_q14 252 253 var i: nx_int = 1 254 var cx: nx_int = start_x_q14 255 var cy: nx_int = start_y_q14 256 while i < n_points { 257 // Per-segment hash perturbation in [-meander_centideg, +meander_centideg]. 258 let h_perp: nx_int = _river_signed_q14(_river_hash(seed, i, 1)) 259 let perturb: nx_int = (h_perp * meander_centideg) / NX_RIVER_Q 260 let bearing: nx_int = base_bearing_centideg + perturb 261 262 // Along-axis step: 60-100% of seg_len based on hash. 263 let h_along: nx_int = _river_hash(seed, i, 0) 264 let along_frac_q: nx_int = 6 * NX_RIVER_Q / 10 + (h_along % (4 * NX_RIVER_Q / 10)) 265 let step_len: nx_int = (seg_len * along_frac_q) / NX_RIVER_Q 266 267 // True sin/cos step. 268 let cos_b: nx_int = nx_camera_cos_q14_centideg(bearing) 269 let sin_b: nx_int = nx_camera_sin_q14_centideg(bearing) 270 let dx_step: nx_int = (step_len * cos_b) / NX_RIVER_Q 271 let dy_step: nx_int = (step_len * sin_b) / NX_RIVER_Q 272 273 cx = cx + dx_step 274 cy = cy + dy_step 275 276 let base: nx_int = i * NX_RIVER_POINT_STRIDE 277 points_out[base + 0] = cx 278 points_out[base + 1] = cy 279 i = i + 1 280 } 281 return n_points 282} 283 284// ===== Oxbow lake detection ========================================= 285// Scans the polyline for meander loops that swing close to themselves 286// (a near-cutoff). Returns the count of detected oxbows; each oxbow's 287// (cx, cy, radius_q14) is written to oxbows_out (3 i64 per oxbow). 288// 289// Algorithm (Fisk 1944 meander-cutoff heuristic): 290// For each pair (i, j) of non-adjacent control points where: 291// - j > i + skip_min (must be at least 4 segments apart on path) 292// - distance(point[i], point[j]) < cutoff_radius 293// record an oxbow at the midpoint with radius = half-distance. 294// Skip overlapping oxbows by next-i hint. 295func nx_river_detect_oxbows( 296 points: *i64, 297 n_points: nx_int, 298 cutoff_radius_q14: nx_int, 299 skip_min: nx_int, 300 oxbows_out: *i64, 301 max_oxbows: nx_int 302) -> nx_int { 303 if n_points < skip_min + 2 { return 0 } 304 let cutoff_sq: nx_int = cutoff_radius_q14 * cutoff_radius_q14 305 var n_oxbow: nx_int = 0 306 var i: nx_int = 0 307 while i < n_points { 308 let xi: nx_int = points[i * NX_RIVER_POINT_STRIDE + 0] 309 let yi: nx_int = points[i * NX_RIVER_POINT_STRIDE + 1] 310 var j: nx_int = i + skip_min 311 while j < n_points { 312 let xj: nx_int = points[j * NX_RIVER_POINT_STRIDE + 0] 313 let yj: nx_int = points[j * NX_RIVER_POINT_STRIDE + 1] 314 let dx: nx_int = xj - xi 315 let dy: nx_int = yj - yi 316 let d2: nx_int = dx * dx + dy * dy 317 if d2 < cutoff_sq { 318 if n_oxbow >= max_oxbows { return n_oxbow } 319 let cx: nx_int = (xi + xj) / 2 320 let cy: nx_int = (yi + yj) / 2 321 // Radius -- use d/4 as a conservative cutoff-lake size. 322 // (We can't take sqrt cheaply; use d/4 by approximating 323 // d ~ sqrt(d2) with a power-of-2 fall-back.) 324 var d_approx: nx_int = cutoff_radius_q14 325 if d2 > 0 { d_approx = cutoff_radius_q14 } 326 let radius: nx_int = d_approx / 4 327 oxbows_out[n_oxbow * 3 + 0] = cx 328 oxbows_out[n_oxbow * 3 + 1] = cy 329 oxbows_out[n_oxbow * 3 + 2] = radius 330 n_oxbow = n_oxbow + 1 331 // Skip past j to avoid overlapping detections. 332 i = j 333 j = n_points 334 } 335 j = j + 1 336 } 337 i = i + 1 338 } 339 return n_oxbow 340} 341 342// Apply oxbow lake carve: returns the max-depth carve from any oxbow 343// covering the query point. Oxbow lakes are full-depth stagnant 344// water -- no flow but the heightmap is depressed by the carve depth. 345func nx_river_oxbow_carve_at( 346 oxbows: *i64, 347 n_oxbows: nx_int, 348 depth_q14_m: nx_int, 349 px_q14: nx_int, 350 py_q14: nx_int 351) -> nx_int { 352 var max_carve: nx_int = 0 353 var i: nx_int = 0 354 while i < n_oxbows { 355 let cx: nx_int = oxbows[i * 3 + 0] 356 let cy: nx_int = oxbows[i * 3 + 1] 357 let r: nx_int = oxbows[i * 3 + 2] 358 let dx: nx_int = px_q14 - cx 359 let dy: nx_int = py_q14 - cy 360 let d2: nx_int = dx * dx + dy * dy 361 let r2: nx_int = r * r 362 if d2 < r2 { 363 // Linear ramp: full depth at centre, 0 at radius. 364 let ramp_q: nx_int = ((r2 - d2) * NX_RIVER_Q) / r2 365 let carve: nx_int = (depth_q14_m * ramp_q) / NX_RIVER_Q 366 if carve > max_carve { max_carve = carve } 367 } 368 i = i + 1 369 } 370 return 0 - max_carve 371} 372 373// ===== Delta braiding =============================================== 374// For DELTA-type rivers, generates N branching channels fanning out 375// from an upstream point to N spread targets at the terminus. Each 376// branch is itself a meandering polyline (via nx_river_generate_path 377// recursively, with reduced meander amplitude to keep braid channels 378// roughly parallel). 379// 380// Output: each branch is n_segments+1 points; written contiguously 381// into points_out. branch_starts_out[i] = starting index of branch i 382// in points_out. Returns number of branches successfully written. 383func nx_river_generate_delta( 384 seed: nx_int, 385 upstream_x_q14: nx_int, 386 upstream_y_q14: nx_int, 387 terminus_cx_q14: nx_int, 388 terminus_cy_q14: nx_int, 389 terminus_spread_q14: nx_int, 390 n_branches: nx_int, 391 n_segments: nx_int, 392 points_out: *i64, 393 max_points: nx_int, 394 branch_starts_out: *i64, 395 max_branches: nx_int 396) -> nx_int { 397 if n_branches <= 0 { return 0 } 398 if n_segments <= 0 { return 0 } 399 if n_branches > max_branches { return 0 } 400 let points_per_branch: nx_int = n_segments + 1 401 if (n_branches * points_per_branch) > max_points { return 0 } 402 403 var b: nx_int = 0 404 while b < n_branches { 405 // Spread targets evenly across the terminus_spread_q14 perpendicular 406 // to the upstream-terminus axis. For simplicity, spread on Y if 407 // upstream is to the west; on X if upstream is to the north; 408 // here we just spread on +/-Y for canonical DELTA orientation. 409 var offset: nx_int = 0 410 if n_branches > 1 { 411 offset = (terminus_spread_q14 * (b * 2 - (n_branches - 1))) / (n_branches - 1) 412 } 413 let branch_terminus_x: nx_int = terminus_cx_q14 414 let branch_terminus_y: nx_int = terminus_cy_q14 + offset 415 let start_idx: nx_int = b * points_per_branch 416 branch_starts_out[b] = start_idx 417 let buf: *i64 = (points_out as i64 + start_idx * NX_RIVER_POINT_STRIDE * NX_SIZEOF_NX_INT) as *i64 418 nx_river_generate_path( 419 seed + b * 31, 420 upstream_x_q14, upstream_y_q14, 421 branch_terminus_x, branch_terminus_y, 422 n_segments, NX_RIVER_TYPE_DELTA, 423 buf, points_per_branch 424 ) 425 b = b + 1 426 } 427 return n_branches 428} 429 430// ===== Internal: closest squared distance from point to a 2D segment 431// Same algorithm as nx_ore_deposit's capsule SDF but 2D (no Z). 432func _river_dist_sq_to_segment_2d( 433 px: nx_int, py: nx_int, 434 x0: nx_int, y0: nx_int, 435 x1: nx_int, y1: nx_int 436) -> nx_int { 437 let ex: nx_int = x1 - x0 438 let ey: nx_int = y1 - y0 439 let edge_len_sq: nx_int = ex * ex + ey * ey 440 if edge_len_sq == 0 { 441 let dx: nx_int = px - x0 442 let dy: nx_int = py - y0 443 return dx * dx + dy * dy 444 } 445 let qx: nx_int = px - x0 446 let qy: nx_int = py - y0 447 let dot: nx_int = qx * ex + qy * ey 448 var t: nx_int = dot 449 if t < 0 { t = 0 } 450 if t > edge_len_sq { t = edge_len_sq } 451 let cx: nx_int = x0 + ex * t / edge_len_sq 452 let cy: nx_int = y0 + ey * t / edge_len_sq 453 let rx: nx_int = px - cx 454 let ry: nx_int = py - cy 455 return rx * rx + ry * ry 456} 457 458// ===== Public: carve depth at query point ========================== 459// Iterates river segments, finds the closest one, returns signed 460// carve depth (NEGATIVE -- the river LOWERS the terrain). Caller 461// adds this to their baseline heightmap. 462// 463// distance < channel_width / 2: 464// full depth (the channel cut) 465// channel_width / 2 < distance < channel_width / 2 + bank_width: 466// linear ramp from -depth to 0 (the bank) 467// distance >= channel_width / 2 + bank_width: 468// 0 (no carve, beyond banks) 469func nx_river_carve_at( 470 points: *i64, 471 n_points: nx_int, 472 channel_width_q14: nx_int, 473 bank_width_q14: nx_int, 474 depth_q14_m: nx_int, 475 px_q14: nx_int, 476 py_q14: nx_int 477) -> nx_int { 478 if n_points < 2 { return 0 } 479 if channel_width_q14 <= 0 { return 0 } 480 481 let half_channel: nx_int = channel_width_q14 / 2 482 let half_channel_sq: nx_int = half_channel * half_channel 483 let full_radius: nx_int = half_channel + bank_width_q14 484 let full_radius_sq: nx_int = full_radius * full_radius 485 486 // Find min squared distance across all segments. 487 var min_dist_sq: nx_int = full_radius_sq + 1 488 var i: nx_int = 0 489 while i < n_points - 1 { 490 let base0: nx_int = i * NX_RIVER_POINT_STRIDE 491 let base1: nx_int = (i + 1) * NX_RIVER_POINT_STRIDE 492 let d_sq: nx_int = _river_dist_sq_to_segment_2d( 493 px_q14, py_q14, 494 points[base0 + 0], points[base0 + 1], 495 points[base1 + 0], points[base1 + 1] 496 ) 497 if d_sq < min_dist_sq { min_dist_sq = d_sq } 498 i = i + 1 499 } 500 501 // Outside the full extent -> no carve. 502 if min_dist_sq > full_radius_sq { return 0 } 503 504 // Inside the channel -> full depth. 505 if min_dist_sq <= half_channel_sq { return 0 - depth_q14_m } 506 507 // In the bank zone -> linear ramp from -depth at channel edge to 508 // 0 at full_radius. Use square-root-free approximation: use the 509 // sq-distance directly as the lerp parameter. Error vs true 510 // linear ramp is ~1-2% in the bank zone -- acceptable for v1. 511 // 512 // ramp = (full_radius_sq - min_dist_sq) / (full_radius_sq - half_channel_sq) 513 // depth_here = -depth * ramp 514 let bank_span_sq: nx_int = full_radius_sq - half_channel_sq 515 if bank_span_sq <= 0 { return 0 } 516 let ramp_q: nx_int = (full_radius_sq - min_dist_sq) * NX_RIVER_Q / bank_span_sq 517 return 0 - depth_q14_m * ramp_q / NX_RIVER_Q 518} 519 520// ===== Self-test ==================================================== 521func main() -> i64 { 522 let q: nx_int = NX_RIVER_Q 523 524 // T1: Validity predicate. 525 if nx_river_type_is_valid(NX_RIVER_TYPE_HEADWATERS) != 1 { return __syscall(93, 1, 0, 0, 0, 0, 0) } 526 if nx_river_type_is_valid(NX_RIVER_TYPE_DELTA) != 1 { return __syscall(93, 2, 0, 0, 0, 0, 0) } 527 if nx_river_type_is_valid(99) != 0 { return __syscall(93, 3, 0, 0, 0, 0, 0) } 528 529 // T2: Type params -- ordering invariants. LOWER wider than 530 // HEADWATERS; LOWER deeper than HEADWATERS. 531 let p_h: *i64 = (sys_mmap(5 * NX_SIZEOF_NX_INT)) as *i64 532 let p_l: *i64 = (sys_mmap(5 * NX_SIZEOF_NX_INT)) as *i64 533 nx_river_type_params(NX_RIVER_TYPE_HEADWATERS, p_h) 534 nx_river_type_params(NX_RIVER_TYPE_LOWER, p_l) 535 if p_l[0] <= p_h[0] { return __syscall(93, 10, 0, 0, 0, 0, 0) } // wider channel 536 if p_l[1] <= p_h[1] { return __syscall(93, 11, 0, 0, 0, 0, 0) } // wider banks 537 if p_l[2] <= p_h[2] { return __syscall(93, 12, 0, 0, 0, 0, 0) } // deeper 538 539 // T3: Path generation -- deterministic + first point = start. 540 let cap: nx_int = 32 541 let pts: *i64 = (sys_mmap(cap * NX_RIVER_POINT_STRIDE * NX_SIZEOF_NX_INT)) as *i64 542 let n_a: nx_int = nx_river_generate_path(42, 0, 0, 1000 * q, 0, 8, NX_RIVER_TYPE_MIDDLE, pts, cap) 543 if n_a != 9 { return __syscall(93, 20, 0, 0, 0, 0, 0) } 544 if pts[0] != 0 { return __syscall(93, 21, 0, 0, 0, 0, 0) } 545 if pts[1] != 0 { return __syscall(93, 22, 0, 0, 0, 0, 0) } 546 let saved_x_at_2: nx_int = pts[2 * NX_RIVER_POINT_STRIDE + 0] 547 // Re-run with same seed -> same points. 548 let n_b: nx_int = nx_river_generate_path(42, 0, 0, 1000 * q, 0, 8, NX_RIVER_TYPE_MIDDLE, pts, cap) 549 if pts[2 * NX_RIVER_POINT_STRIDE + 0] != saved_x_at_2 { return __syscall(93, 23, 0, 0, 0, 0, 0) } 550 551 // T4: Different seed -> different path. 552 let n_c: nx_int = nx_river_generate_path(43, 0, 0, 1000 * q, 0, 8, NX_RIVER_TYPE_MIDDLE, pts, cap) 553 // At least one of the inner points should differ. 554 var any_diff: nx_int = 0 555 var i: nx_int = 1 556 while i < 9 { 557 if pts[i * NX_RIVER_POINT_STRIDE + 0] != saved_x_at_2 { any_diff = 1 } 558 i = i + 1 559 } 560 // The seed-42 inner point should NOT exactly match all seed-43 561 // inner points; the variation IS the meandering. 562 if any_diff == 0 { return __syscall(93, 30, 0, 0, 0, 0, 0) } 563 564 // T5: Carve at zero query -- a simple horizontal river from 565 // (0, 0) to (100, 0) channel 4, bank 4, depth 5. On-axis -> 566 // -5 (channel). At distance 2 (within channel half-width 2) -> 567 // -5 still. At distance 3 (outside channel, in bank zone) -> 568 // partial ramp. At distance 7 (outside bank) -> 0. 569 pts[0] = 0; pts[1] = 0 570 pts[2] = 100; pts[3] = 0 571 let d_axis: nx_int = nx_river_carve_at(pts, 2, 4, 4, 5, 50, 0) 572 if d_axis != (0 - 5) { return __syscall(93, 40, 0, 0, 0, 0, 0) } 573 let d_near: nx_int = nx_river_carve_at(pts, 2, 4, 4, 5, 50, 2) 574 if d_near != (0 - 5) { return __syscall(93, 41, 0, 0, 0, 0, 0) } 575 let d_bank: nx_int = nx_river_carve_at(pts, 2, 4, 4, 5, 50, 4) 576 // distance = 4, half_channel = 2, full = 6. In bank zone: 577 // ramp = (36 - 16) / (36 - 4) = 20/32 ~ 0.625 Q 578 // depth = -5 * 0.625 ~ -3 (allow +/- 2 for square-distance 579 // approximation that's documented in the header). 580 if d_bank > 0 - 2 { return __syscall(93, 42, 0, 0, 0, 0, 0) } 581 if d_bank < 0 - 5 { return __syscall(93, 43, 0, 0, 0, 0, 0) } 582 // Beyond banks. 583 let d_far: nx_int = nx_river_carve_at(pts, 2, 4, 4, 5, 50, 10) 584 if d_far != 0 { return __syscall(93, 50, 0, 0, 0, 0, 0) } 585 586 // T6: Carve before/after the segment endpoints -- the closest 587 // point on the segment is the endpoint, so distance-to-segment 588 // is just Euclidean to the endpoint. Query at (-3, 0): closest 589 // is (0, 0), distance = 3, in bank zone. 590 let d_before: nx_int = nx_river_carve_at(pts, 2, 4, 4, 5, 0 - 3, 0) 591 if d_before == 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) } 592 if d_before > 0 { return __syscall(93, 61, 0, 0, 0, 0, 0) } 593 // Query at (-10, 0): outside bank zone, no carve. 594 let d_way_before: nx_int = nx_river_carve_at(pts, 2, 4, 4, 5, 0 - 10, 0) 595 if d_way_before != 0 { return __syscall(93, 62, 0, 0, 0, 0, 0) } 596 597 // T7: Refusal paths. 598 if nx_river_generate_path(0, 0, 0, 100, 0, 0, NX_RIVER_TYPE_MIDDLE, pts, cap) != 0 { 599 return __syscall(93, 70, 0, 0, 0, 0, 0) 600 } 601 if nx_river_generate_path(0, 0, 0, 100, 0, 8, 99, pts, cap) != 0 { 602 return __syscall(93, 71, 0, 0, 0, 0, 0) 603 } 604 if nx_river_carve_at(pts, 1, 4, 4, 5, 0, 0) != 0 { 605 return __syscall(93, 72, 0, 0, 0, 0, 0) 606 } 607 if nx_river_carve_at(pts, 2, 0, 4, 5, 0, 0) != 0 { 608 return __syscall(93, 73, 0, 0, 0, 0, 0) 609 } 610 611 // T8: Continuous-angle meandering. Generate a river heading east; 612 // verify path moves predominantly in +x and meanders in y. 613 // MIDDLE seg_len = 30*q -> 32 segments = ~960*q max path length, 614 // so we can only reach ~ 600..960 * q from the start. 615 let pts2: *i64 = (sys_mmap(64 * NX_RIVER_POINT_STRIDE * NX_SIZEOF_NX_INT)) as *i64 616 let n8: nx_int = nx_river_generate_path(123, 0, 0, NX_MAGIC_5000 * q, 0, 32, NX_RIVER_TYPE_MIDDLE, pts2, 64) 617 if n8 < 30 { return __syscall(93, 80, 0, 0, 0, 0, 0) } 618 // Final x should be well to the east (>~ 500 * q). 619 let final_x: nx_int = pts2[(n8 - 1) * NX_RIVER_POINT_STRIDE + 0] 620 if final_x < 500 * q { return __syscall(93, 81, 0, 0, 0, 0, 0) } 621 // Verify path meanders in Y (final y deviates from 0 sometimes). 622 var any_y_offset: nx_int = 0 623 var pi: nx_int = 1 624 while pi < n8 { 625 let y_at: nx_int = pts2[pi * NX_RIVER_POINT_STRIDE + 1] 626 if y_at != 0 { any_y_offset = 1 } 627 pi = pi + 1 628 } 629 if any_y_offset == 0 { return __syscall(93, 82, 0, 0, 0, 0, 0) } 630 631 // T9: Oxbow detection. Build a path that loops back: start (0, 0), 632 // go right to (1000, 0), up to (1000, 1000), left to (0, 1000), 633 // down to (0, 100) -- the endpoint is close to the start; an 634 // oxbow detector should find that loop. 635 let loop_pts: *i64 = (sys_mmap(8 * NX_RIVER_POINT_STRIDE * NX_SIZEOF_NX_INT)) as *i64 636 loop_pts[0] = 0; loop_pts[1] = 0 637 loop_pts[2] = 1000; loop_pts[3] = 0 638 loop_pts[4] = 1000; loop_pts[5] = 1000 639 loop_pts[6] = 0; loop_pts[7] = 1000 640 loop_pts[8] = 0; loop_pts[9] = 100 641 let oxbows: *i64 = (sys_mmap(8 * 3 * NX_SIZEOF_NX_INT)) as *i64 642 let n_ox: nx_int = nx_river_detect_oxbows(loop_pts, 5, 200, 3, oxbows, 8) 643 if n_ox < 1 { return __syscall(93, 90, 0, 0, 0, 0, 0) } 644 645 // T10: Oxbow carve at midpoint of the cutoff = full depth. 646 let mid_x: nx_int = oxbows[0] 647 let mid_y: nx_int = oxbows[1] 648 let radius: nx_int = oxbows[2] 649 let ox_carve_centre: nx_int = nx_river_oxbow_carve_at(oxbows, n_ox, 1000, mid_x, mid_y) 650 if ox_carve_centre >= 0 { return __syscall(93, 100, 0, 0, 0, 0, 0) } // negative carve 651 // Outside oxbow -> 0. 652 let ox_carve_far: nx_int = nx_river_oxbow_carve_at(oxbows, n_ox, 1000, mid_x + radius * 2, mid_y) 653 if ox_carve_far != 0 { return __syscall(93, 101, 0, 0, 0, 0, 0) } 654 655 // T11: Delta braiding. Generate 3 branches; verify each produces 656 // a polyline of the expected length. 657 let delta_pts: *i64 = (sys_mmap(64 * NX_RIVER_POINT_STRIDE * NX_SIZEOF_NX_INT)) as *i64 658 let branch_starts: *i64 = (sys_mmap(8 * NX_SIZEOF_NX_INT)) as *i64 659 let n_branches: nx_int = nx_river_generate_delta( 660 777, 0, 0, 1000 * q, 0, 200 * q, 661 3, 5, delta_pts, 64, branch_starts, 8 662 ) 663 if n_branches != 3 { return __syscall(93, 110, 0, 0, 0, 0, 0) } 664 if branch_starts[0] != 0 { return __syscall(93, 111, 0, 0, 0, 0, 0) } 665 if branch_starts[1] != 6 { return __syscall(93, 112, 0, 0, 0, 0, 0) } // each branch = 6 points 666 if branch_starts[2] != 12 { return __syscall(93, 113, 0, 0, 0, 0, 0) } 667 // Branch 0 endpoint y should be negative (offset = -200 * q for 3 branches at b=0). 668 let b0_end_y: nx_int = delta_pts[(branch_starts[0] + 5) * NX_RIVER_POINT_STRIDE + 1] 669 let b2_end_y: nx_int = delta_pts[(branch_starts[2] + 5) * NX_RIVER_POINT_STRIDE + 1] 670 if b0_end_y >= b2_end_y { return __syscall(93, 114, 0, 0, 0, 0, 0) } // braids spread 671 672 return 0 673}