code wiki / (root) / nx_procgen_features.nx

nx_procgen_features.nx source

↩ module page · 163 lines · 6951 B

1// nx_procgen_features.nx -- feature KINDS assigned by biome law (PROCGEN 2// arc P3 = EXCEED10 ladder R2; seeds bucket B6 structures). 3// 4// The grader's FEATURE_VARIETY axis (Shannon entropy over a kind 5// histogram) was structurally ABSENT (=0): the landscape's poisson points 6// had positions but no KINDS. This module gives each feature a kind the 7// causal way -- the biome it stands in decides what can grow there, and a 8// seeded jitter stream picks within the biome's mix: 9// 10// TREE -- forests dominant; grassland scatter; treeline thins them 11// ROCK -- deserts, tundra, snowfields, ice; outcrops elsewhere 12// SHRUB -- the understory mix everywhere green 13// WATER -- rare: oasis (desert), melt pond (tundra), swamp pool 14// (tropical) -- placed where water causally collects 15// 16// The mix table is the law (build-intelligence cardinal: a biome with bad 17// features gets a BETTER mix row, never a stripped one). No TREE in 18// DESERT is a tested invariant, not a comment. 19// 20// Deterministic per (features, biome_map, seed): byte-identical kinds. 21// license_tier: ORIGINAL 22 23import "nx_syscalls.nx" 24import "nx_tier.nx" 25import "nx_prng.nx" 26import "nx_procgen_biome.nx" 27const NX_MAGIC_1024: i64 = 1024 28 29// ===== Feature kinds (sealed) ========================================= 30const NX_FEAT_TREE: nx_int = 0 31const NX_FEAT_ROCK: nx_int = 1 32const NX_FEAT_SHRUB: nx_int = 2 33const NX_FEAT_WATER: nx_int = 3 34const NX_FEAT_N_KINDS: nx_int = 4 35 36// Jitter stream offset: kinds draw from their own seed stream so adding 37// a kind never reshuffles the biome map (streams stay independent). 38const NX_FEAT_SEED_OFF: nx_int = 24107 39 40func nx_feature_kind_is_valid(k: nx_int) -> nx_int { 41 if k < 0 { return 0 } 42 if k >= NX_FEAT_N_KINDS { return 0 } 43 return 1 44} 45 46// The biome -> kind mix law. jitter_q10 in [0, 1024). Thresholds are 47// the mix table: e.g. TEMPERATE = 50% tree / 30% shrub / 20% rock. 48// 49// MIX RICHNESS LAW (measured 2026-06-10): the grader's entropy 50// quantisation (bin = total/count, truncating) zeroes the contribution of 51// any kind above 50% share -- and ecologically a monoculture IS poor 52// variety. Real forests carry understory and outcrops; the table keeps 53// every dominant kind at <= ~55% so worlds read rich, which is also what 54// the FEATURE_VARIETY axis rewards. Improve the mix, never strip kinds. 55func nx_feature_kind_pick(biome: nx_int, jitter_q10: nx_int) -> nx_int { 56 if biome == NX_BIOME_DESERT { 57 if jitter_q10 < 512 { return NX_FEAT_ROCK } // 50% 58 if jitter_q10 < 922 { return NX_FEAT_SHRUB } // 40% shrub-steppe 59 return NX_FEAT_WATER // 10% oasis 60 } 61 if biome == NX_BIOME_TUNDRA { 62 if jitter_q10 < 410 { return NX_FEAT_ROCK } // 40% 63 if jitter_q10 < 819 { return NX_FEAT_SHRUB } // 40% 64 return NX_FEAT_WATER // 20% melt pond 65 } 66 if biome == NX_BIOME_GRASSLAND { 67 if jitter_q10 < 512 { return NX_FEAT_SHRUB } // 50% 68 if jitter_q10 < 819 { return NX_FEAT_TREE } // 30% copses 69 return NX_FEAT_ROCK // 20% 70 } 71 if biome == NX_BIOME_BOREAL { 72 if jitter_q10 < 512 { return NX_FEAT_TREE } // 50% 73 if jitter_q10 < 768 { return NX_FEAT_ROCK } // 25% 74 return NX_FEAT_SHRUB // 25% 75 } 76 if biome == NX_BIOME_TEMPERATE { 77 if jitter_q10 < 512 { return NX_FEAT_TREE } // 50% 78 if jitter_q10 < 819 { return NX_FEAT_SHRUB } // 30% 79 return NX_FEAT_ROCK // 20% 80 } 81 if biome == NX_BIOME_TROPICAL { 82 if jitter_q10 < 563 { return NX_FEAT_TREE } // 55% 83 if jitter_q10 < 819 { return NX_FEAT_SHRUB } // 25% 84 return NX_FEAT_WATER // 20% swamp pool 85 } 86 if biome == NX_BIOME_SNOW { 87 // subalpine treeline: conifers climb into the snow band (measured 88 // 2026-06-10: signature basins skew the value-quartile bands so a 89 // big share of cells classify HIGH -- an all-rock snow band made 90 // EVERY preset rock-dominant; the richer treeline mix is both the 91 // ecological truth and the variety the worlds were missing) 92 if jitter_q10 < 410 { return NX_FEAT_ROCK } // 40% 93 if jitter_q10 < 717 { return NX_FEAT_TREE } // 30% snow pines 94 return NX_FEAT_SHRUB // 30% 95 } 96 return NX_FEAT_ROCK // ICE + any future id 97} 98 99// Assign a kind to every feature point. xs/ys/n from the Landscape; 100// biome_map parallel to the heightmap. out_kinds: n i64s. 101func nx_feature_assign(xs: *i64, ys: *i64, n: nx_int, 102 biome_map: *i64, w: nx_int, h: nx_int, 103 seed: nx_int, out_kinds: *i64) -> nx_int { 104 if w <= 0 { return 0 - 1 } 105 if h <= 0 { return 0 - 1 } 106 let st: *i64 = sys_mmap(4 * 8) as *i64 107 nx_prng_init(st, seed + NX_FEAT_SEED_OFF) 108 var i: nx_int = 0 109 while i < n { 110 var x: nx_int = xs[i] 111 var y: nx_int = ys[i] 112 if x < 0 { x = 0 } 113 if x >= w { x = w - 1 } 114 if y < 0 { y = 0 } 115 if y >= h { y = h - 1 } 116 let jitter: nx_int = nx_prng_range(st, NX_MAGIC_1024) 117 out_kinds[i] = nx_feature_kind_pick(biome_map[y * w + x], jitter) 118 i = i + 1 119 } 120 return 0 121} 122 123// Water-aware assignment (P4 composition, ADDITIVE -- the plain 124// nx_feature_assign contract is untouched): a feature standing in a 125// water-mask cell IS water (reeds/pool) regardless of the biome mix -- 126// causality beats the probability table. water_mask: w*h i64s, 1 = 127// channel/pond cell; pass 0 to behave exactly like nx_feature_assign. 128func nx_feature_assign_with_water(xs: *i64, ys: *i64, n: nx_int, 129 biome_map: *i64, water_mask: *i64, 130 w: nx_int, h: nx_int, 131 seed: nx_int, out_kinds: *i64) -> nx_int { 132 let rc: nx_int = nx_feature_assign(xs, ys, n, biome_map, w, h, seed, out_kinds) 133 if rc != 0 { return rc } 134 if (water_mask as i64) == 0 { return 0 } 135 var i: nx_int = 0 136 while i < n { 137 var x: nx_int = xs[i] 138 var y: nx_int = ys[i] 139 if x < 0 { x = 0 } 140 if x >= w { x = w - 1 } 141 if y < 0 { y = 0 } 142 if y >= h { y = h - 1 } 143 if water_mask[y * w + x] == 1 { out_kinds[i] = NX_FEAT_WATER } 144 i = i + 1 145 } 146 return 0 147} 148 149// Histogram over kinds: out_hist[NX_FEAT_N_KINDS], ready for the grader's 150// FEATURE_VARIETY axis. 151func nx_feature_hist_build(kinds: *i64, n: nx_int, out_hist: *i64) -> nx_int { 152 var k: nx_int = 0 153 while k < NX_FEAT_N_KINDS { out_hist[k] = 0; k = k + 1 } 154 var i: nx_int = 0 155 while i < n { 156 let kd: nx_int = kinds[i] 157 if nx_feature_kind_is_valid(kd) == 1 { 158 out_hist[kd] = out_hist[kd] + 1 159 } 160 i = i + 1 161 } 162 return 0 163}