nx_beach_zone.nx source
↩ module page · 305 lines · 16101 B
1// nx_beach_zone.nx -- specialized water-land transition generator.
2//
3// Third demonstration of the kind-specific-generator cardinal
4// `feedback-kind-specific-generators-not-broad-noise` after
5// nx_forest_layout (scattered features) and nx_river_carve (polyline
6// path). Beach is the 3rd shape variant: BANDED ZONE -- a horizontal
7// elevation band between water and upland.
8//
9// Beach-internal logic in v1:
10// - Zone classification by elevation relative to water level:
11// below water_level -> UNDERWATER
12// water_level..water_level+tidal_height -> TIDAL
13// tidal_top..max_beach_height -> BEACH
14// above max_beach_height -> UPLAND (no override)
15// - 4 beach types (SANDY / TIDAL_FLAT / PEBBLE / ROCKY) producing
16// different material per zone (sand vs gravel vs rock)
17// - Optional height-smoothing pulls upland gradually toward water
18// level inside the beach zone, so the transition isn't a cliff
19//
20// Abstract material codes (NOT game block IDs) -- caller maps to
21// their voxel palette. Keeps this primitive substrate-pure so it
22// composes with non-voxel applications too (scientific terrain
23// visualisers, map renderers, etc.).
24//
25// V2 follow-ups (queued):
26// - Dune barriers behind the beach zone (composes a wind-direction
27// hint with nx_wind_erosion v2)
28// - Per-tide-cycle wet/dry striping in the TIDAL band
29// - Beach width modulation by slope (gentle slope -> wide beach;
30// steep slope -> narrow / pocket beach)
31// - River-mouth detection (beach widens at delta junctions; reads
32// nx_river_carve segment ends)
33// - Cliff-face emergence (ROCKY beach + steep slope -> exposed
34// bedrock cliffs instead of beach)
35//
36// Loss audit: Q14 integer arithmetic; ramp interpolation is exact in
37// the documented bands. Classification is sealed-enum so no
38// "interpolated zone" half-state.
39//
40// genealogy_id: bascom_1980_waves_beaches +
41// komar_1998_beach_processes_canon +
42// usgs_coastal_classification
43// lineage_id: nx_beach_zone_banded_q14_v1
44//
45// nx_safety_envelope:
46// intended_use: "Banded beach-zone classifier (foreshore,
47// intertidal, supratidal) for coastal procgen"
48// sil_target: SIL1
49// asil_target: QM
50// dal_target: NONE
51// evidence: [Q14_fixed_point, sealed_zone_enum,
52// tide_height_based_classification]
53// hazard_register: [bug-tape-tide-extreme-misclassified,
54// bug-tape-zone-boundary-discontinuity]
55// residual_risk: "Procgen quality only."
56// verdict: NOT_YET_EVALUATED
57
58import "nx_syscalls.nx"
59import "nx_tier.nx"
60
61// ===== Q14 ==========================================================
62const NX_BEACH_Q: nx_int = 16384
63
64// ===== Zone sealed enum =============================================
65// Vertical bands above/below water level.
66const NX_BEACH_ZONE_UNDERWATER: nx_int = 0 // below water_level
67const NX_BEACH_ZONE_TIDAL: nx_int = 1 // wave-washed wet zone
68const NX_BEACH_ZONE_BEACH: nx_int = 2 // dry-sand zone above tidal
69const NX_BEACH_ZONE_UPLAND: nx_int = 3 // beyond max_beach_height, no override
70
71const NX_BEACH_ZONE_COUNT: nx_int = 4
72
73// ===== Beach-type sealed enum =======================================
74// Which kind of beach this is. Drives material per zone.
75const NX_BEACH_TYPE_SANDY: nx_int = 0 // soft sand, classic shoreline
76const NX_BEACH_TYPE_TIDAL_FLAT: nx_int = 1 // wide gentle, mud / wet sand
77const NX_BEACH_TYPE_PEBBLE: nx_int = 2 // shingle / pebble
78const NX_BEACH_TYPE_ROCKY: nx_int = 3 // rocky shoreline, exposed rock
79
80const NX_BEACH_TYPE_COUNT: nx_int = 4
81
82// ===== Abstract material codes ======================================
83// Substrate-pure return values; caller maps to their voxel block IDs.
84// E.g. for nx_voxel_chunk:
85// PASSTHROUGH -> caller's default
86// WATER -> NX_BLOCK_WATER
87// SAND -> NX_BLOCK_SAND
88// GRAVEL -> NX_BLOCK_DIRT (substrate lacks a gravel block; v2 adds)
89// ROCK -> NX_BLOCK_STONE
90const NX_BEACH_MATERIAL_PASSTHROUGH: nx_int = 0
91const NX_BEACH_MATERIAL_WATER: nx_int = 1
92const NX_BEACH_MATERIAL_SAND: nx_int = 2
93const NX_BEACH_MATERIAL_GRAVEL: nx_int = 3
94const NX_BEACH_MATERIAL_ROCK: nx_int = 4
95
96const NX_BEACH_MATERIAL_COUNT: nx_int = 5
97
98// ===== Validity predicates ==========================================
99func nx_beach_zone_is_valid(z: nx_int) -> nx_int {
100 if z == NX_BEACH_ZONE_UNDERWATER { return 1 }
101 if z == NX_BEACH_ZONE_TIDAL { return 1 }
102 if z == NX_BEACH_ZONE_BEACH { return 1 }
103 if z == NX_BEACH_ZONE_UPLAND { return 1 }
104 return 0
105}
106
107func nx_beach_type_is_valid(t: nx_int) -> nx_int {
108 if t == NX_BEACH_TYPE_SANDY { return 1 }
109 if t == NX_BEACH_TYPE_TIDAL_FLAT { return 1 }
110 if t == NX_BEACH_TYPE_PEBBLE { return 1 }
111 if t == NX_BEACH_TYPE_ROCKY { return 1 }
112 return 0
113}
114
115func nx_beach_material_is_valid(m: nx_int) -> nx_int {
116 if m == NX_BEACH_MATERIAL_PASSTHROUGH { return 1 }
117 if m == NX_BEACH_MATERIAL_WATER { return 1 }
118 if m == NX_BEACH_MATERIAL_SAND { return 1 }
119 if m == NX_BEACH_MATERIAL_GRAVEL { return 1 }
120 if m == NX_BEACH_MATERIAL_ROCK { return 1 }
121 return 0
122}
123
124// ===== Zone classifier =============================================
125// Returns NX_BEACH_ZONE_* given heights in Q14 metres.
126//
127// water_level_q14_m: elevation of water surface
128// tidal_height_q14_m: height ABOVE water_level that the TIDAL band reaches
129// (typical ~2 m for normal ocean tides)
130// max_beach_height_q14_m: elevation ABOVE water_level where the
131// BEACH band ends and UPLAND begins (typical
132// ~5-15 m for sandy beaches, more for tidal
133// flats)
134//
135// Refuses (returns UPLAND) for nonsensical configs where tidal_height
136// or max_beach_height are <= 0 -- treats every height as upland so
137// downstream is safe.
138func nx_beach_zone_classify(
139 height_q14_m: nx_int,
140 water_level_q14_m: nx_int,
141 tidal_height_q14_m: nx_int,
142 max_beach_height_q14_m: nx_int
143) -> nx_int {
144 if tidal_height_q14_m <= 0 { return NX_BEACH_ZONE_UPLAND }
145 if max_beach_height_q14_m <= tidal_height_q14_m { return NX_BEACH_ZONE_UPLAND }
146
147 if height_q14_m < water_level_q14_m { return NX_BEACH_ZONE_UNDERWATER }
148
149 let above_water: nx_int = height_q14_m - water_level_q14_m
150 if above_water <= tidal_height_q14_m { return NX_BEACH_ZONE_TIDAL }
151 if above_water <= max_beach_height_q14_m { return NX_BEACH_ZONE_BEACH }
152 return NX_BEACH_ZONE_UPLAND
153}
154
155// ===== Material lookup =============================================
156// Given a classified zone + a beach type, return the abstract material
157// code for the surface block at this voxel.
158//
159// Tabulated rather than computed: sandy/tidal-flat beaches have sand
160// in all wet+dry zones; pebble has gravel; rocky has exposed rock.
161// UNDERWATER is always WATER. UPLAND is always PASSTHROUGH (caller's
162// terrain material wins).
163func nx_beach_material(zone: nx_int, beach_type: nx_int) -> nx_int {
164 if zone == NX_BEACH_ZONE_UNDERWATER { return NX_BEACH_MATERIAL_WATER }
165 if zone == NX_BEACH_ZONE_UPLAND { return NX_BEACH_MATERIAL_PASSTHROUGH }
166
167 // TIDAL + BEACH zones routed by beach_type.
168 if beach_type == NX_BEACH_TYPE_SANDY { return NX_BEACH_MATERIAL_SAND }
169 if beach_type == NX_BEACH_TYPE_TIDAL_FLAT { return NX_BEACH_MATERIAL_SAND }
170 if beach_type == NX_BEACH_TYPE_PEBBLE { return NX_BEACH_MATERIAL_GRAVEL }
171 if beach_type == NX_BEACH_TYPE_ROCKY {
172 // Rocky beaches: TIDAL zone is wet rock (sand-like wash);
173 // BEACH zone is exposed bedrock.
174 if zone == NX_BEACH_ZONE_TIDAL { return NX_BEACH_MATERIAL_SAND }
175 return NX_BEACH_MATERIAL_ROCK
176 }
177 return NX_BEACH_MATERIAL_PASSTHROUGH
178}
179
180// ===== Height smoothing ============================================
181// Caller adds this delta to the base height to flatten upland into
182// the beach zone gradually (avoids cliff edges at the shoreline).
183//
184// In UPLAND or UNDERWATER zones, returns 0 (no smoothing applied;
185// terrain stands as-is).
186//
187// In BEACH zone, pulls height down by smoothing_strength_q14 *
188// (1 - (above_water / max_beach_height)) so deep-beach points (close
189// to max_beach_height) are barely smoothed and water-edge points
190// (close to water_level + tidal_height) are smoothed most.
191//
192// In TIDAL zone, smooth fully toward water_level + tidal_height/2
193// (mid-tidal) to flatten the wet zone.
194func nx_beach_height_smooth(
195 height_q14_m: nx_int,
196 water_level_q14_m: nx_int,
197 tidal_height_q14_m: nx_int,
198 max_beach_height_q14_m: nx_int,
199 smoothing_strength_q14_m: nx_int
200) -> nx_int {
201 let zone: nx_int = nx_beach_zone_classify(
202 height_q14_m, water_level_q14_m, tidal_height_q14_m, max_beach_height_q14_m
203 )
204
205 if zone == NX_BEACH_ZONE_UNDERWATER { return 0 }
206 if zone == NX_BEACH_ZONE_UPLAND { return 0 }
207
208 let above_water: nx_int = height_q14_m - water_level_q14_m
209
210 if zone == NX_BEACH_ZONE_TIDAL {
211 // Pull toward water_level + tidal_height/2.
212 let target: nx_int = tidal_height_q14_m / 2
213 let diff: nx_int = target - above_water
214 // Full smoothing strength applied.
215 return diff * smoothing_strength_q14_m / NX_BEACH_Q
216 }
217
218 // BEACH zone: smoothing proportional to "closeness to tidal".
219 // strength = full at above_water = tidal_top,
220 // zero at above_water = max_beach_height.
221 let zone_span: nx_int = max_beach_height_q14_m - tidal_height_q14_m
222 if zone_span <= 0 { return 0 }
223 let depth_into_beach: nx_int = above_water - tidal_height_q14_m
224 // factor: (zone_span - depth_into_beach) / zone_span, in Q14.
225 let factor: nx_int = (zone_span - depth_into_beach) * NX_BEACH_Q / zone_span
226 // Pull height down toward tidal_top by smoothing_strength * factor.
227 let pull: nx_int = (depth_into_beach * factor) / NX_BEACH_Q
228 return 0 - (pull * smoothing_strength_q14_m / NX_BEACH_Q)
229}
230
231// ===== Self-test ====================================================
232func main() -> i64 {
233 let q: nx_int = NX_BEACH_Q
234
235 // Setup: water level = 0; tidal extends 2 m above; beach extends
236 // to 10 m above. All in Q14 metres.
237 let water: nx_int = 0
238 let tidal: nx_int = 2 * q
239 let beach_top: nx_int = 10 * q
240
241 // T1: Zone classification.
242 if nx_beach_zone_classify(0 - 5 * q, water, tidal, beach_top) != NX_BEACH_ZONE_UNDERWATER { return __syscall(93, 1, 0, 0, 0, 0, 0) }
243 if nx_beach_zone_classify(0, water, tidal, beach_top) != NX_BEACH_ZONE_TIDAL { return __syscall(93, 2, 0, 0, 0, 0, 0) }
244 if nx_beach_zone_classify(q, water, tidal, beach_top) != NX_BEACH_ZONE_TIDAL { return __syscall(93, 3, 0, 0, 0, 0, 0) }
245 if nx_beach_zone_classify(3 * q, water, tidal, beach_top) != NX_BEACH_ZONE_BEACH { return __syscall(93, 4, 0, 0, 0, 0, 0) }
246 if nx_beach_zone_classify(9 * q, water, tidal, beach_top) != NX_BEACH_ZONE_BEACH { return __syscall(93, 5, 0, 0, 0, 0, 0) }
247 if nx_beach_zone_classify(50 * q, water, tidal, beach_top) != NX_BEACH_ZONE_UPLAND { return __syscall(93, 6, 0, 0, 0, 0, 0) }
248
249 // T2: Validity predicates.
250 if nx_beach_zone_is_valid(NX_BEACH_ZONE_UNDERWATER) != 1 { return __syscall(93, 10, 0, 0, 0, 0, 0) }
251 if nx_beach_zone_is_valid(NX_BEACH_ZONE_UPLAND) != 1 { return __syscall(93, 11, 0, 0, 0, 0, 0) }
252 if nx_beach_zone_is_valid(99) != 0 { return __syscall(93, 12, 0, 0, 0, 0, 0) }
253 if nx_beach_type_is_valid(NX_BEACH_TYPE_SANDY) != 1 { return __syscall(93, 13, 0, 0, 0, 0, 0) }
254 if nx_beach_type_is_valid(NX_BEACH_TYPE_ROCKY) != 1 { return __syscall(93, 14, 0, 0, 0, 0, 0) }
255 if nx_beach_type_is_valid(99) != 0 { return __syscall(93, 15, 0, 0, 0, 0, 0) }
256 if nx_beach_material_is_valid(NX_BEACH_MATERIAL_WATER) != 1 { return __syscall(93, 16, 0, 0, 0, 0, 0) }
257 if nx_beach_material_is_valid(NX_BEACH_MATERIAL_SAND) != 1 { return __syscall(93, 17, 0, 0, 0, 0, 0) }
258 if nx_beach_material_is_valid(99) != 0 { return __syscall(93, 18, 0, 0, 0, 0, 0) }
259
260 // T3: Material routing. UNDERWATER always WATER; UPLAND always
261 // PASSTHROUGH (regardless of beach type).
262 if nx_beach_material(NX_BEACH_ZONE_UNDERWATER, NX_BEACH_TYPE_SANDY) != NX_BEACH_MATERIAL_WATER { return __syscall(93, 20, 0, 0, 0, 0, 0) }
263 if nx_beach_material(NX_BEACH_ZONE_UNDERWATER, NX_BEACH_TYPE_ROCKY) != NX_BEACH_MATERIAL_WATER { return __syscall(93, 21, 0, 0, 0, 0, 0) }
264 if nx_beach_material(NX_BEACH_ZONE_UPLAND, NX_BEACH_TYPE_SANDY) != NX_BEACH_MATERIAL_PASSTHROUGH { return __syscall(93, 22, 0, 0, 0, 0, 0) }
265 if nx_beach_material(NX_BEACH_ZONE_UPLAND, NX_BEACH_TYPE_PEBBLE) != NX_BEACH_MATERIAL_PASSTHROUGH { return __syscall(93, 23, 0, 0, 0, 0, 0) }
266
267 // T4: Beach-type material variants.
268 if nx_beach_material(NX_BEACH_ZONE_BEACH, NX_BEACH_TYPE_SANDY) != NX_BEACH_MATERIAL_SAND { return __syscall(93, 30, 0, 0, 0, 0, 0) }
269 if nx_beach_material(NX_BEACH_ZONE_BEACH, NX_BEACH_TYPE_TIDAL_FLAT) != NX_BEACH_MATERIAL_SAND { return __syscall(93, 31, 0, 0, 0, 0, 0) }
270 if nx_beach_material(NX_BEACH_ZONE_BEACH, NX_BEACH_TYPE_PEBBLE) != NX_BEACH_MATERIAL_GRAVEL { return __syscall(93, 32, 0, 0, 0, 0, 0) }
271 if nx_beach_material(NX_BEACH_ZONE_BEACH, NX_BEACH_TYPE_ROCKY) != NX_BEACH_MATERIAL_ROCK { return __syscall(93, 33, 0, 0, 0, 0, 0) }
272
273 // T5: ROCKY beach -- TIDAL band is still wet sand (wash zone),
274 // not exposed rock.
275 if nx_beach_material(NX_BEACH_ZONE_TIDAL, NX_BEACH_TYPE_ROCKY) != NX_BEACH_MATERIAL_SAND { return __syscall(93, 40, 0, 0, 0, 0, 0) }
276 // SANDY in tidal -> sand.
277 if nx_beach_material(NX_BEACH_ZONE_TIDAL, NX_BEACH_TYPE_SANDY) != NX_BEACH_MATERIAL_SAND { return __syscall(93, 41, 0, 0, 0, 0, 0) }
278
279 // T6: Smoothing -- upland + underwater untouched.
280 if nx_beach_height_smooth(50 * q, water, tidal, beach_top, q) != 0 { return __syscall(93, 50, 0, 0, 0, 0, 0) }
281 if nx_beach_height_smooth(0 - 5 * q, water, tidal, beach_top, q) != 0 { return __syscall(93, 51, 0, 0, 0, 0, 0) }
282
283 // T7: BEACH-zone smoothing -- negative delta (pulls down).
284 let s_beach: nx_int = nx_beach_height_smooth(3 * q, water, tidal, beach_top, q)
285 if s_beach >= 0 { return __syscall(93, 60, 0, 0, 0, 0, 0) }
286 // At the upland edge (just below beach_top), almost no smoothing.
287 let s_top: nx_int = nx_beach_height_smooth(9 * q + q / 2, water, tidal, beach_top, q)
288 // Should be small (close to 0 -- the depth_into_beach * factor /
289 // Q is small when factor approaches 0).
290 if s_top < 0 - 500 { return __syscall(93, 61, 0, 0, 0, 0, 0) } // not too aggressive
291
292 // T8: Refusal paths -- nonsensical configs route to UPLAND.
293 if nx_beach_zone_classify(5 * q, water, 0, beach_top) != NX_BEACH_ZONE_UPLAND { return __syscall(93, 70, 0, 0, 0, 0, 0) }
294 if nx_beach_zone_classify(5 * q, water, 0 - 1, beach_top) != NX_BEACH_ZONE_UPLAND { return __syscall(93, 71, 0, 0, 0, 0, 0) }
295 if nx_beach_zone_classify(5 * q, water, beach_top, beach_top) != NX_BEACH_ZONE_UPLAND { return __syscall(93, 72, 0, 0, 0, 0, 0) }
296
297 // T9: Water-level offset (water at +50 m, classifier still works).
298 let high_water: nx_int = 50 * q
299 if nx_beach_zone_classify(45 * q, high_water, tidal, beach_top) != NX_BEACH_ZONE_UNDERWATER { return __syscall(93, 80, 0, 0, 0, 0, 0) }
300 if nx_beach_zone_classify(51 * q, high_water, tidal, beach_top) != NX_BEACH_ZONE_TIDAL { return __syscall(93, 81, 0, 0, 0, 0, 0) }
301 if nx_beach_zone_classify(55 * q, high_water, tidal, beach_top) != NX_BEACH_ZONE_BEACH { return __syscall(93, 82, 0, 0, 0, 0, 0) }
302 if nx_beach_zone_classify(70 * q, high_water, tidal, beach_top) != NX_BEACH_ZONE_UPLAND { return __syscall(93, 83, 0, 0, 0, 0, 0) }
303
304 return 0
305}