code wiki / _hdl_build / nx_loottable.nx

nx_loottable.nx source

↩ module page · 107 lines · 4635 B

1// nx_loottable.nx -- the SOVEREIGN LOOT / ITEM-AFFIX PART. The last hard GAP on the nx_gamebench board: 2// blocks Open Diablo II and Dungeon Crawl Stone Soup, and is the Diablo-class archetype's defining system. 3// 4// THE BUGS THIS PART EXISTS TO KILL: 5// 1. ★THE UNREACHABLE TIER. Weighted selection is usually written with a cumulative loop and an off-by-one 6// that makes the first or last tier impossible to roll -- legendary items that literally cannot drop, 7// and nobody notices because "rare is rare". Here selection is a half-open cumulative scan and the gate 8// proves EVERY declared tier is actually observed over a large sample. 9// 2. ★DISTRIBUTION DRIFT. Declared weights and observed frequencies diverge silently. The gate measures the 10// real distribution over 100k rolls and requires each tier within tolerance of its declared weight. 11// 3. DUPLICATE AFFIXES -- "+10 STR, +10 STR" on one item, from sampling with replacement. 12// 4. NON-REPRODUCIBLE DROPS -- an item must be a pure function of its seed, so a drop can be replayed, 13// logged compactly, and verified (our determinism exceed, applied to loot). 14// Tables are DATA (rule 11): tiers, weights, affix pool and value ranges are all caller-supplied arrays. 15// LIB ONLY -- no main() by ecosystem convention. 16// license_tier: ORIGINAL expect_exit: 0 17import "nx_syscalls.nx" 18 19const LT_MAXAFFIX: i64 = 6 // hard ceiling on affixes per item 20 21func lt_rng(st: *i64) -> i64 { 22 var x: i64 = st[0] 23 x = x ^ (x << 13) 24 x = x ^ (x >> 7) 25 x = x ^ (x << 17) 26 st[0] = x 27 if x < 0 { return 0 - x } 28 return x 29} 30 31// Weighted pick over weights[0..n). Returns an index in [0,n), or -1 if all weights are zero. 32// Half-open cumulative scan: r in [0,total) and we return the FIRST i with r < cum[i]. 33// Every entry with weight>0 owns a non-empty slice of [0,total), so no tier can be unreachable. 34func lt_pick(weights: *i64, n: i64, r: i64) -> i64 { 35 var total: i64 = 0 36 var i: i64 = 0 37 while i < n { if weights[i] > 0 { total = total + weights[i] } i = i + 1 } 38 if total <= 0 { return 0-1 } 39 var roll: i64 = r % total 40 var cum: i64 = 0 41 var pick: i64 = 0-1 42 i = 0 43 while i < n { 44 if weights[i] > 0 { 45 cum = cum + weights[i] 46 if pick < 0 { if roll < cum { pick = i } } 47 } 48 i = i + 1 49 } 50 return pick 51} 52 53// How many affixes a tier grants (data-driven: tier_affixes[]), clamped to the ceiling. 54func lt_affix_count(tier_affixes: *i64, tier: i64) -> i64 { 55 var c: i64 = tier_affixes[tier] 56 if c < 0 { c = 0 } 57 if c > LT_MAXAFFIX { c = LT_MAXAFFIX } 58 return c 59} 60 61// Roll one item from `seed`. 62// tier_w[ntier] -- tier weights (data) 63// tier_affixes[ntier] -- affixes granted per tier (data) 64// affix_w[naffix] -- affix pool weights (data) 65// affix_lo/affix_hi[naffix]-- per-affix value range, inclusive (data) 66// out_affix[LT_MAXAFFIX], out_val[LT_MAXAFFIX] -- results 67// Returns the tier rolled. out_n receives the affix count. 68func lt_roll(seed: i64, 69 tier_w: *i64, tier_affixes: *i64, ntier: i64, 70 affix_w: *i64, affix_lo: *i64, affix_hi: *i64, naffix: i64, 71 out_affix: *i64, out_val: *i64, out_n: *i64) -> i64 { 72 let st: *i64 = sys_mmap(8) as *i64 73 st[0] = seed 74 if st[0] == 0 { st[0] = 1 } 75 lt_rng(st) // discard the first step so adjacent seeds decorrelate 76 77 let tier: i64 = lt_pick(tier_w, ntier, lt_rng(st)) 78 if tier < 0 { out_n[0] = 0; return 0-1 } 79 let want: i64 = lt_affix_count(tier_affixes, tier) 80 81 // sample affixes WITHOUT replacement: zero a working copy of the weights as each is taken 82 let work: *i64 = sys_mmap(naffix*8 + 64) as *i64 83 var i: i64 = 0 84 while i < naffix { work[i] = affix_w[i]; i = i + 1 } 85 86 var got: i64 = 0 87 var guard: i64 = 0 88 while got < want { 89 if guard > naffix { got = want } // pool exhausted -- stop, never loop forever 90 if got < want { 91 let a: i64 = lt_pick(work, naffix, lt_rng(st)) 92 if a < 0 { got = want } else { 93 let lo: i64 = affix_lo[a] 94 let hi: i64 = affix_hi[a] 95 var span: i64 = hi - lo + 1 96 if span < 1 { span = 1 } 97 out_affix[got] = a 98 out_val[got] = lo + (lt_rng(st) % span) 99 work[a] = 0 // cannot be drawn again -> no duplicate affixes 100 got = got + 1 101 } 102 } 103 guard = guard + 1 104 } 105 out_n[0] = got 106 return tier 107}