code wiki / (root) / nx_beach_zone.nx

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}