## Pure unit tests for the range-weighted tile draw in ## `movements/the_floor_is_lava_ring.nim` (the new TFIL-RING mover). ## ## NO battle, NO Java, NO server. Run with: ## nim c -r common_libs/tests/test_tfil_ring_weights.nim ## ## These pin the two contracts that make the mover safe to ship: ## 1. the weight SHAPE (flat top exactly 1.0 in band, Gaussian falloff ## outside, uniform as T -> inf, uniform for pools smaller than 4); ## 2. the draw can only ever return an index INSIDE the candidate pool, so it ## can never pick a tile the unweighted mover would have rejected. import std/[math, random] import movements/the_floor_is_lava_ring var failures = 0 proc check(name: string, ok: bool) = if ok: echo "PASS: ", name else: echo "FAIL: ", name; inc failures const Lo = 100.0 Hi = 200.0 K = 60.0 # ── 1. flat top is exactly 1.0 across the band ─────────────────────────────── proc testFlatTop() = var allOne = true var d = Lo while d <= Hi: if rangeWeight(d, Lo, Hi, K) != 1.0: allOne = false d += 2.5 check "flat top: weight is exactly 1.0 for every d in [lo, hi]", allOne check "flat top: lower edge d == lo is 1.0", rangeWeight(Lo, Lo, Hi, K) == 1.0 check "flat top: upper edge d == hi is 1.0", rangeWeight(Hi, Lo, Hi, K) == 1.0 # The flat top is what preserves the within-band hedge: all in-band tiles are # weighted identically, so the random draw still spreads across the band. check "flat top: two different in-band tiles are equally weighted", rangeWeight(120.0, Lo, Hi, K) == rangeWeight(180.0, Lo, Hi, K) # ── 2. weights fall off outside the band ───────────────────────────────────── proc testFalloff() = let wBelow = rangeWeight(Lo - 10.0, Lo, Hi, K) let wAbove = rangeWeight(Hi + 10.0, Lo, Hi, K) check "falloff: below lo is strictly between 0 and 1", wBelow > 0.0 and wBelow < 1.0 check "falloff: above hi is strictly between 0 and 1", wAbove > 0.0 and wAbove < 1.0 check "falloff: symmetric (lo-d == d-hi)", abs(wBelow - wAbove) < 1e-12 check "falloff: monotone decreasing away from the band", rangeWeight(Lo - 40.0, Lo, Hi, K) < rangeWeight(Lo - 20.0, Lo, Hi, K) and rangeWeight(Hi + 40.0, Lo, Hi, K) < rangeWeight(Hi + 20.0, Lo, Hi, K) check "falloff: exactly 1.0 at the band edge, < 1.0 just outside", rangeWeight(Lo, Lo, Hi, K) == 1.0 and rangeWeight(Lo - 0.001, Lo, Hi, K) < 1.0 # ── 3. T -> infinity gives uniform weights (the old distribution) ──────────── proc testTemperature() = let dists = @[50.0, 120.0, 180.0, 260.0, 400.0] let wHuge = bandWeights(dists, Lo, Hi, K, 1e9) var allOne = true for w in wHuge: if abs(w - 1.0) > 1e-6: allOne = false check "T -> inf: every weight collapses to 1.0 (uniform)", allOne let wTiny = bandWeights(dists, Lo, Hi, K, 0.05) check "T -> 0: in-band tiles dominate the outside tiles", wTiny[1] > wTiny[0] and wTiny[2] > wTiny[3] and wTiny[2] > wTiny[4] # In-band weight is exactly 1.0 regardless of T (1 ^ anything == 1). let wDefault = bandWeights(dists, Lo, Hi, K, 0.4) check "in-band weight is exactly 1.0 at the default T", wDefault[1] == 1.0 # ── 4. small pools are forced uniform (melee safety) ───────────────────────── proc testSmallPoolUniform() = let w2 = bandWeights(@[50.0, 900.0], Lo, Hi, K, 0.4) check "pool of 2 -> uniform (never shaped)", w2.len == 2 and w2[0] == 1.0 and w2[1] == 1.0 let w3 = bandWeights(@[50.0, 150.0, 900.0], Lo, Hi, K, 0.4) check "pool of 3 -> uniform (never shaped)", w3[0] == 1.0 and w3[1] == 1.0 and w3[2] == 1.0 let w4 = bandWeights(@[50.0, 150.0, 900.0, 950.0], Lo, Hi, K, 0.4) check "pool of 4 IS shaped (shaping starts at MinRingPool)", w4[1] > w4[0] and w4[1] > w4[2] # ── 5. the chosen index is ALWAYS inside the candidate pool ────────────────── proc testIndexInsidePool() = let dists = @[50.0, 120.0, 180.0, 260.0, 400.0, 900.0] let w = bandWeights(dists, Lo, Hi, K, 0.4) var rng = initRand(12345) var allIn = true for _ in 0..<20000: let idx = weightedIndex(w, rand(rng, 1.0)) if idx < 0 or idx >= dists.len: allIn = false check "random draws: chosen index is always within candidates", allIn # Edge variates and degenerate weight vectors must stay in range too. check "u == 0 -> index 0", weightedIndex(w, 0.0) == 0 # A near-1 variate returns the LAST tile only when that tile has weight; use a # uniform vector so the expectation is unambiguous. check "u just below 1 -> last index (uniform weights)", weightedIndex(@[1.0, 1.0, 1.0, 1.0], 0.999999) == 3 check "empty pool -> 0", weightedIndex(newSeq[float](), 0.5) == 0 check "all-zero weights -> a valid in-pool index", weightedIndex(@[0.0, 0.0, 0.0], 0.5) in 0..2 check "single weight -> index 0", weightedIndex(@[1.0], 0.99) == 0 # The `var Rand` wrapper must agree on the range contract. var rng2 = initRand(1) let idx2 = weightedIndex(w, rng2) check "var Rand wrapper returns an in-pool index", idx2 >= 0 and idx2 < w.len # ── 6. the default T actually tilts the draw toward the band ───────────────── proc testTilt() = # Two in-band tiles, three far. Uniform would pick an in-band tile 2/5 = 40% # of the time; the default T = 0.4 must push that well above 80%. let dists = @[60.0, 120.0, 180.0, 500.0, 700.0] let w = bandWeights(dists, Lo, Hi, K, 0.4) var rng = initRand(999) var inBand = 0 const N = 20000 for _ in 0..80% vs 40%)", inBand.float / N.float > 0.8 # ── 7. OFF path (temp <= 0) is uniform, matching the old control arm ───────── proc testOffPathUniform() = let dists = @[50.0, 120.0, 180.0, 260.0, 400.0] let wOff = bandWeights(dists, Lo, Hi, K, 0.0) var allOne = true for w in wOff: if w != 1.0: allOne = false check "temp <= 0 (OFF path) -> uniform weights", allOne # ── run ────────────────────────────────────────────────────────────────────── testFlatTop() testFalloff() testTemperature() testSmallPoolUniform() testIndexInsidePool() testTilt() testOffPathUniform() if failures == 0: echo "\nAll tfil-ring weight checks passed." else: echo "\n", failures, " tfil-ring weight check(s) FAILED." quit(1)