use crate::params::ParamState;
pub struct Lcg {
state: u64,
}
impl Lcg {
pub fn new(seed: u64) -> Self {
Self {
state: seed.wrapping_add(1),
}
}
pub fn next_u64(&mut self) -> u64 {
self.state = self
.state
.wrapping_mul(6364136223846793005)
.wrapping_add(1442695040888963407);
self.state
}
pub fn next_f32(&mut self) -> f32 {
(self.next_u64() >> 33) as f32 / (1u64 << 31) as f32
}
pub fn next_normal(&mut self) -> f32 {
let u1 = self.next_f32().max(1e-10);
let u2 = self.next_f32();
let r = (-2.0 * u1.ln()).sqrt();
let theta = 2.0 * std::f32::consts::PI * u2;
r * theta.cos()
}
pub fn sample_normal(&mut self, mean: f32, std: f32) -> f32 {
(mean + self.next_normal() * std).clamp(0.0, 1.0)
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct AnthroDistribution {
pub name: String,
pub height_mean: f32,
pub height_std: f32,
pub weight_mean: f32,
pub weight_std: f32,
pub muscle_mean: f32,
pub muscle_std: f32,
pub age_mean: f32,
pub age_std: f32,
}
impl AnthroDistribution {
pub fn average_adult() -> Self {
Self {
name: "average_adult".into(),
height_mean: 0.50,
height_std: 0.15,
weight_mean: 0.45,
weight_std: 0.18,
muscle_mean: 0.35,
muscle_std: 0.15,
age_mean: 0.40,
age_std: 0.20,
}
}
pub fn young_adult() -> Self {
Self {
name: "young_adult".into(),
height_mean: 0.52,
height_std: 0.12,
weight_mean: 0.42,
weight_std: 0.15,
muscle_mean: 0.40,
muscle_std: 0.12,
age_mean: 0.25,
age_std: 0.08,
}
}
pub fn older_adult() -> Self {
Self {
name: "older_adult".into(),
height_mean: 0.46,
height_std: 0.10,
weight_mean: 0.52,
weight_std: 0.15,
muscle_mean: 0.28,
muscle_std: 0.10,
age_mean: 0.75,
age_std: 0.10,
}
}
pub fn athletic() -> Self {
Self {
name: "athletic".into(),
height_mean: 0.55,
height_std: 0.10,
weight_mean: 0.48,
weight_std: 0.10,
muscle_mean: 0.70,
muscle_std: 0.12,
age_mean: 0.35,
age_std: 0.10,
}
}
pub fn heavy() -> Self {
Self {
name: "heavy".into(),
height_mean: 0.45,
height_std: 0.12,
weight_mean: 0.75,
weight_std: 0.12,
muscle_mean: 0.25,
muscle_std: 0.10,
age_mean: 0.45,
age_std: 0.15,
}
}
pub fn sample(&self, rng: &mut Lcg) -> ParamState {
ParamState::new(
rng.sample_normal(self.height_mean, self.height_std),
rng.sample_normal(self.weight_mean, self.weight_std),
rng.sample_normal(self.muscle_mean, self.muscle_std),
rng.sample_normal(self.age_mean, self.age_std),
)
}
pub fn sample_diverse(&self, count: usize, seed: u64) -> Vec<ParamState> {
if count == 0 {
return Vec::new();
}
let mut rng = Lcg::new(seed);
(0..count)
.map(|i| {
let t = if count == 1 {
0.5_f32
} else {
i as f32 / (count - 1) as f32
};
let height = (self.height_mean - 2.0 * self.height_std + t * 4.0 * self.height_std)
.clamp(0.0, 1.0);
ParamState::new(
height,
rng.sample_normal(self.weight_mean, self.weight_std),
rng.sample_normal(self.muscle_mean, self.muscle_std),
rng.sample_normal(self.age_mean, self.age_std),
)
})
.collect()
}
}
pub struct BodyRandomizer {
pub distribution: AnthroDistribution,
rng: Lcg,
}
impl BodyRandomizer {
pub fn new(distribution: AnthroDistribution, seed: u64) -> Self {
Self {
distribution,
rng: Lcg::new(seed),
}
}
pub fn with_seed(mut self, seed: u64) -> Self {
self.rng = Lcg::new(seed);
self
}
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> ParamState {
self.distribution.sample(&mut self.rng)
}
pub fn generate(&mut self, count: usize) -> Vec<ParamState> {
(0..count).map(|_| self.next()).collect()
}
pub fn standard(seed: u64) -> Self {
Self::new(AnthroDistribution::average_adult(), seed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lcg_deterministic() {
let mut a = Lcg::new(42);
let mut b = Lcg::new(42);
for _ in 0..20 {
assert_eq!(a.next_u64(), b.next_u64());
}
}
#[test]
fn lcg_next_f32_in_range() {
let mut rng = Lcg::new(99);
for _ in 0..1000 {
let v = rng.next_f32();
assert!(v >= 0.0, "value {v} below 0");
assert!(v < 1.0, "value {v} not below 1");
}
}
#[test]
fn sample_clamps_to_unit() {
let dist = AnthroDistribution::average_adult();
let mut rng = Lcg::new(7);
for _ in 0..500 {
let p = dist.sample(&mut rng);
assert!(
(0.0..=1.0).contains(&p.height),
"height out of range: {}",
p.height
);
assert!(
(0.0..=1.0).contains(&p.weight),
"weight out of range: {}",
p.weight
);
assert!(
(0.0..=1.0).contains(&p.muscle),
"muscle out of range: {}",
p.muscle
);
assert!((0.0..=1.0).contains(&p.age), "age out of range: {}", p.age);
}
}
#[test]
fn generate_count_correct() {
let mut randomizer = BodyRandomizer::standard(1);
assert_eq!(randomizer.generate(10).len(), 10);
}
#[test]
fn different_seeds_different_results() {
let mut r1 = BodyRandomizer::standard(1);
let mut r2 = BodyRandomizer::standard(2);
let p1 = r1.next();
let p2 = r2.next();
assert!(
(p1.height - p2.height).abs() > 1e-6
|| (p1.weight - p2.weight).abs() > 1e-6
|| (p1.muscle - p2.muscle).abs() > 1e-6
|| (p1.age - p2.age).abs() > 1e-6,
"Different seeds produced identical ParamState"
);
}
#[test]
fn sample_diverse_count_correct() {
let dist = AnthroDistribution::average_adult();
assert_eq!(dist.sample_diverse(5, 42).len(), 5);
}
#[test]
fn athletic_has_high_muscle() {
assert!(AnthroDistribution::athletic().muscle_mean > 0.5);
}
#[test]
fn older_adult_has_high_age() {
assert!(AnthroDistribution::older_adult().age_mean > 0.6);
}
#[test]
fn standard_randomizer_works() {
let _ = BodyRandomizer::standard(42).next();
}
}