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}