# ============================================================================ # numeric.ludic — three "polish" numeric types, written in Ludic: # Huge.* — idle/incremental big numbers (mantissa x 10^exponent) # Angle.* — an auto-wrapping angle in radians # Percent.* — a value clamped to [0, 1] # # ludicc splices this file into any program that mentions `Huge.*`, `Angle.*` # or `Percent.*`. It builds on the deterministic fixed-point `Math.*` namespace # (spliced inline, no runtime of its own), so it works in a plain tool as well # as a game. The namespaces (emit_call.ludic) alias each method to the matching # `huge_*` / `angle_*` / `percent_*` function below. # ============================================================================ # ---- Huge: mantissa + exponent, for idle-game magnitudes ------------------- # # A value is m x 10^e with the mantissa m normalized to [1, 10) (or 0 for zero); # e is an unbounded int, so magnitudes far past 1e308 are representable. The # mantissa is a Q16.16 `fixed`, giving ~4 significant digits — this is a # DISPLAY-SCALE number for showing 1.23e45, NOT a lockstep-exact value; keep it # out of the deterministic simulation and use BigInt/Decimal when exactness # matters. const HUGE_ONE: fixed = 1.0 const HUGE_TEN: fixed = 10.0 property Huge { m: fixed = 0.0, e: int = 0 } # normalize (mantissa, exponent) so the mantissa lands in [1, 10) function huge_make(m: fixed, e: int) -> Huge { let h = new Huge if m == 0.0 { h.m = 0.0; h.e = 0; return h } var neg = false var mm = m if mm < 0.0 { neg = true; mm = 0.0 - mm } var ee = e while mm >= HUGE_TEN { mm = mm / HUGE_TEN; ee = ee + 1 } while mm < HUGE_ONE { mm = mm * HUGE_TEN; ee = ee - 1 } if neg { mm = 0.0 - mm } h.m = mm; h.e = ee return h } # an int -> Huge (built via i64 so the whole int range normalizes cleanly) function huge_from(value: int) -> Huge { if value == 0 { return huge_make(0.0, 0) } var neg = false var v: long = value if v < 0 { neg = true; v = 0 - v } var e = 0 var p: long = 1 while p * 10 <= v { p = p * 10; e = e + 1 } let raw: long = (v * 65536) / p # v / 10^e in Q16.16 (in [1,10)) let rawi: int = raw var m: fixed = as_fixed(rawi) if neg { m = 0.0 - m } return huge_make(m, e) } function huge_sign(a: Huge) -> int { if a.m > 0.0 { return 1 } if a.m < 0.0 { return -1 } return 0 } function huge_neg(a: Huge) -> Huge { return huge_make(0.0 - a.m, a.e) } function huge_mul(a: Huge, b: Huge) -> Huge { if a.m == 0.0 or b.m == 0.0 { return huge_make(0.0, 0) } return huge_make(a.m * b.m, a.e + b.e) } function huge_add(a: Huge, b: Huge) -> Huge { if a.m == 0.0 { return b } if b.m == 0.0 { return a } var big = a var small = b if b.e > a.e { big = b; small = a } let diff = big.e - small.e if diff > 8 { return big } # negligible at display precision var sm = small.m var k = 0 while k < diff { sm = sm / HUGE_TEN; k = k + 1 } return huge_make(big.m + sm, big.e) } function huge_sub(a: Huge, b: Huge) -> Huge { return huge_add(a, huge_neg(b)) } function huge_cmp(a: Huge, b: Huge) -> int { let sa = huge_sign(a) let sb = huge_sign(b) if sa != sb { if sa > sb { return 1 }; return -1 } if sa == 0 { return 0 } var c = 0 if a.e != b.e { if a.e > b.e { c = 1 } else { c = 0 - 1 } } else { if a.m > b.m { c = 1 } else { if a.m < b.m { c = 0 - 1 } else { c = 0 } } } if sa < 0 { c = 0 - c } return c } function huge_mantissa(a: Huge) -> fixed { return a.m } function huge_exp(a: Huge) -> int { return a.e } # scientific text like "1.23e45" (two-decimal mantissa); plain "0" for zero function huge_str(a: Huge) -> pointer { if a.m == 0.0 { return "0" } var neg = "" var m = a.m if m < 0.0 { neg = "-"; m = 0.0 - m } var ip: int = floor(m) # 1..9 let frac: fixed = m - fixed(ip) var dd: int = floor(frac * 100.0 + 0.5) # round the two decimals to nearest var ex = a.e if dd >= 100 { dd = 0; ip = ip + 1 } # rounding carried into the ones place if ip >= 10 { ip = 1; ex = ex + 1 } # ...and on into the exponent var ds = string(dd) if dd < 10 { ds = "0" + ds } return neg + string(ip) + "." + ds + "e" + string(ex) } # ---- Angle: an auto-wrapping radian angle ---------------------------------- const ANGLE_PI: fixed = 3.14159265 const ANGLE_TAU: fixed = 6.28318531 # wrap any radian value into the half-open range [-pi, pi) function angle_wrap(a: fixed) -> fixed { var x = a while x >= ANGLE_PI { x = x - ANGLE_TAU } while x < 0.0 - ANGLE_PI { x = x + ANGLE_TAU } return x } function angle_from_degrees(d: fixed) -> fixed { return angle_wrap(Math.deg_to_rad(d)) } function angle_to_degrees(a: fixed) -> fixed { return Math.rad_to_deg(a) } function angle_sin(a: fixed) -> fixed { return Math.sin(a) } function angle_cos(a: fixed) -> fixed { return Math.cos(a) } function angle_add(a: fixed, b: fixed) -> fixed { return angle_wrap(a + b) } # the shortest signed rotation from `a` to `b`, in [-pi, pi) function angle_diff(a: fixed, b: fixed) -> fixed { return angle_wrap(b - a) } # interpolate from `a` toward `b` along the shortest arc (t is a fixed 0..1) function angle_lerp(a: fixed, b: fixed, t: fixed) -> fixed { return angle_wrap(a + angle_diff(a, b) * t) } # ---- Percent: a value clamped to [0, 1] ------------------------------------ const PCT_ONE: fixed = 1.0 # clamp any fixed into [0, 1] function percent_clamp(v: fixed) -> fixed { if v < 0.0 { return 0.0 } if v > PCT_ONE { return PCT_ONE } return v } # num / den as a clamped ratio (0 when den is 0) function percent_of(num: fixed, den: fixed) -> fixed { if den == 0.0 { return 0.0 } return percent_clamp(num / den) } # linear interpolation a..b by a clamped t function percent_lerp(a: fixed, b: fixed, t: fixed) -> fixed { let p = percent_clamp(t) return a + (b - a) * p } # value scaled by a clamped percent function percent_apply(value: fixed, p: fixed) -> fixed { return value * percent_clamp(p) }