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}