use rand::RngExt;
use crate::game::state::TICK_HZ;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PowerupKind {
Lucky,
Frenzy,
Buff,
GreenCoin,
}
pub const N_KINDS: usize = 4;
const _: () = assert!(PowerupKind::ALL.len() == N_KINDS);
impl PowerupKind {
pub const ALL: [Self; N_KINDS] = [Self::Lucky, Self::Frenzy, Self::Buff, Self::GreenCoin];
pub fn lifetime_ticks(self) -> u32 {
match self {
Self::Lucky | Self::Frenzy | Self::Buff | Self::GreenCoin => 220,
}
}
pub fn mean_cooldown_ticks(self) -> u32 {
match self {
Self::Lucky => 60 * TICK_HZ, Self::Frenzy => 120 * TICK_HZ, Self::Buff => 120 * TICK_HZ, Self::GreenCoin => 240 * TICK_HZ, }
}
pub fn min_cooldown_ticks(self) -> u32 {
5 * TICK_HZ
}
pub fn max_cooldown_ticks(self) -> u32 {
4 * self.mean_cooldown_ticks()
}
}
#[derive(Clone, Debug)]
pub struct Powerup {
pub kind: PowerupKind,
pub spawn_id: u64,
pub frac_x: f32,
pub frac_y: f32,
pub life_ticks: u32,
}
pub fn next_cooldown(kind: PowerupKind) -> u32 {
let mean = kind.mean_cooldown_ticks() as f64;
let min = kind.min_cooldown_ticks();
let max = kind.max_cooldown_ticks();
let u: f64 = rand::rng().random();
let raw = -(1.0 - u).ln() * mean;
raw.round().clamp(min as f64, max as f64) as u32
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn next_cooldown_in_truncated_exponential_range() {
for kind in PowerupKind::ALL {
let min = kind.min_cooldown_ticks();
let max = kind.max_cooldown_ticks();
let mean = kind.mean_cooldown_ticks() as f64;
let n = 10_000;
let mut sum: f64 = 0.0;
for _ in 0..n {
let v = next_cooldown(kind);
assert!(
v >= min && v <= max,
"next_cooldown({kind:?}) = {v} not in [{min}, {max}]"
);
sum += v as f64;
}
let empirical_mean = sum / n as f64;
assert!(
empirical_mean > mean * 0.55 && empirical_mean < mean * 1.25,
"empirical mean {empirical_mean} for {kind:?} too far from {mean}"
);
}
}
}