Files
settled-reach/server/src/bps.rs
T
jpmschweitzerandClaude Opus 4.8 d6e37c0c0e fix(simulation): prosperity in basis points + shared log10_floor helper (#954)
Address PR #145 review. Convert prosperity from f32 to integer basis
points (matching the existing prosperity_bps, D-010 integer-only),
removing both float-determinism bugs by construction:

- New server/src/bps.rs: log10_floor (integer order-of-magnitude) +
  bps_to_f32 edge helper, with boundary tests.
- city_context_reader: prosperity_baseline_bps (u32, 0-10000). role base
  + pop bonus (400*log10_floor, cap 1200) + symmetric noise via integer
  modulo (fixes the always-negative bug) + clamp [1000,9500]. No floats.
- CityGenerationContext.prosperity_baseline -> prosperity_baseline_bps;
  updated the two test context builders. Not serialized — no wire break.
- district_mix population_tier now uses log10_floor (same determinism
  bug class as the comment claimed to avoid).
- Tests in bps + assert positive noise is achievable (the case the old
  test hid).

cargo check/clippy --all-targets -D warnings clean; 1291 lib tests pass;
fmt clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 14:31:27 +02:00

123 lines
3.5 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Fixed-point / basis-point helpers (D-010 integer arithmetic).
//!
//! The codebase represents fractional values (0.01.0) as **basis points**
//! (`bps`), where `10_000 bps = 1.0`. This avoids f32/f64 in determinism-
//! sensitive paths: given the same inputs, pure integer arithmetic produces
//! identical results on every platform and compiler version.
//!
//! Convention: field names carry the `_bps` suffix; raw bps values are `u32`.
//!
//! # Integer log₁₀
//!
//! [`log10_floor`] is the single reference implementation of `floor(log10(n))`
//! for positive integers. Use this everywhere a "how many orders of magnitude"
//! calculation would otherwise reach for `f64::log10`. District-mix population
//! tiers and prosperity pop-bonuses both need it.
/// Integer floor of log₁₀ for `n ≥ 1`.
///
/// Returns 0 for n = 1..9, 1 for n = 10..99, 2 for n = 100..999, and so on.
/// Panics in debug if `n == 0` (log10(0) is undefined); returns 0 in release.
///
/// No floats, no platform-dependent rounding — deterministic (D-010).
///
/// # Examples
///
/// ```
/// use server::bps::log10_floor;
/// assert_eq!(log10_floor(1), 0);
/// assert_eq!(log10_floor(9), 0);
/// assert_eq!(log10_floor(10), 1);
/// assert_eq!(log10_floor(100), 2);
/// assert_eq!(log10_floor(999), 2);
/// assert_eq!(log10_floor(1_000_000), 6);
/// ```
pub fn log10_floor(n: u64) -> u32 {
debug_assert!(n >= 1, "log10_floor: n must be ≥ 1 (got {n})");
if n == 0 {
return 0;
}
let mut v = n;
let mut result = 0u32;
while v >= 10 {
v /= 10;
result += 1;
}
result
}
/// Convert a bps value (010_000) to a clamped f32 in [0.0, 1.0].
///
/// Only call this at the edge of a system that genuinely needs f32 — the stored
/// representation stays integer. Document why f32 is needed at the call site.
#[inline]
pub fn bps_to_f32(bps: u32) -> f32 {
bps as f32 / 10_000.0
}
// ---------------------------------------------------------------------------
// Tests
// ---------------------------------------------------------------------------
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn log10_floor_single_digits() {
// 1..9 all return 0.
for n in 1u64..10 {
assert_eq!(log10_floor(n), 0, "expected 0 for n={n}");
}
}
#[test]
fn log10_floor_boundary_10() {
assert_eq!(log10_floor(9), 0);
assert_eq!(log10_floor(10), 1);
}
#[test]
fn log10_floor_boundary_100() {
assert_eq!(log10_floor(99), 1);
assert_eq!(log10_floor(100), 2);
}
#[test]
fn log10_floor_boundary_1000() {
assert_eq!(log10_floor(999), 2);
assert_eq!(log10_floor(1_000), 3);
}
#[test]
fn log10_floor_large_values() {
assert_eq!(log10_floor(999_999), 5);
assert_eq!(log10_floor(1_000_000), 6);
assert_eq!(log10_floor(9_999_999), 6);
assert_eq!(log10_floor(10_000_000), 7);
}
#[test]
fn log10_floor_very_large() {
// u64::MAX = 18_446_744_073_709_551_615 → 19 digits → floor = 19.
assert_eq!(log10_floor(u64::MAX), 19);
}
#[test]
fn bps_to_f32_midpoint() {
let f = bps_to_f32(5_000);
assert!((f - 0.5).abs() < 1e-6, "5000 bps should be 0.5, got {f}");
}
#[test]
fn bps_to_f32_full_scale() {
let f = bps_to_f32(10_000);
assert!((f - 1.0).abs() < 1e-6, "10000 bps should be 1.0, got {f}");
}
#[test]
fn bps_to_f32_zero() {
assert_eq!(bps_to_f32(0), 0.0);
}
}