use crate::controls::speed::Speed;
use crate::game_logic::game_options::{DISPLAYABLE_EMOJI, GameOptions};
use crate::game_logic::logger::log_configuration::LogLevel;
use rand::{RngExt, rng};
use std::ops::RangeInclusive;
#[must_use]
pub fn weighted_range(choices: &[(u32, RangeInclusive<u16>)]) -> u16 {
let total_weight: u32 = choices.iter().map(|(w, _)| *w).sum();
let roll: u32 = rng().random_range(1..=total_weight);
let mut cumulative = 0;
for (weight, range) in choices {
cumulative += *weight;
if roll <= cumulative {
return rng().random_range(range.clone());
}
}
*choices[0].1.start()
}
#[must_use]
pub fn generate_random_game_options() -> GameOptions {
let speed = match rng().random_range(1..=100) {
1..=20 => Speed::Slow,
21..=60 => Speed::Normal,
61..=90 => Speed::Fast,
_ => Speed::Crazy,
};
let head_idx = rng().random_range(0..DISPLAYABLE_EMOJI.len());
let mut body_idx = rng().random_range(0..DISPLAYABLE_EMOJI.len());
while body_idx == head_idx {
body_idx = rng().random_range(0..DISPLAYABLE_EMOJI.len());
}
let head_symbol = DISPLAYABLE_EMOJI[head_idx].to_string();
let body_symbol = DISPLAYABLE_EMOJI[body_idx].to_string();
let snake_length = weighted_range(&[
(45, 3..=12), (30, 13..=30), (15, 31..=75), (7, 76..=150), (3, 151..=300), ]);
let life = weighted_range(&[
(40, 2..=5), (30, 6..=12), (15, 1..=1), (10, 13..=25), (5, 26..=50), ]);
let nb_of_fruits = weighted_range(&[
(40, 3..=8), (30, 9..=20), (15, 1..=2), (10, 21..=50), (5, 51..=100), ]);
let fruit_duration_seconds = weighted_range(&[
(50, 15..=30), (25, 5..=14), (20, 31..=50), (5, 51..=60), ]);
let snake_growth_factor = weighted_range(&[
(60, 1..=1), (30, 2..=3), (10, 4..=6), ]);
let fruit_timer = rng().random_bool(0.8);
let negative_size_fruits = rng().random_bool(0.5);
let caps_fps = true;
let mut options = GameOptions {
speed,
head_symbol,
body_symbol,
snake_length,
life,
nb_of_fruits,
no_fruit_timer: !fruit_timer,
fruit_timer,
fruit_duration_seconds,
snake_growth_factor,
no_negative_size_fruits: !negative_size_fruits,
negative_size_fruits,
log_level: LogLevel::Off,
no_caps_fps: !caps_fps,
caps_fps,
load: None,
random: true,
};
options.validate();
options
}
#[cfg(test)]
mod tests {
use super::*;
use unicode_segmentation::UnicodeSegmentation;
#[test]
fn test_weighted_range_bounds() {
let ranges = [(50, 5..=10), (50, 20..=30)];
for _ in 0..100 {
let val = weighted_range(&ranges);
assert!((5..=10).contains(&val) || (20..=30).contains(&val));
}
}
#[test]
fn test_generate_random_game_options() {
for _ in 0..100 {
let opt = generate_random_game_options();
assert!(opt.snake_length >= 2 && opt.snake_length <= 999);
assert!(opt.life >= 1 && opt.life <= 99);
assert!(opt.nb_of_fruits >= 1 && opt.nb_of_fruits <= 999);
assert!(opt.fruit_duration_seconds >= 1 && opt.fruit_duration_seconds <= 60);
assert!(opt.snake_growth_factor >= 1 && opt.snake_growth_factor <= 10);
assert_eq!(opt.head_symbol.graphemes(true).count(), 1);
assert_eq!(opt.body_symbol.graphemes(true).count(), 1);
assert_ne!(opt.head_symbol, opt.body_symbol);
assert!(opt.caps_fps);
assert_eq!(opt.log_level, LogLevel::Off);
assert!(
opt.random,
"Generated options must have random flag set to true"
);
assert_eq!(opt.no_fruit_timer, !opt.fruit_timer);
assert_eq!(opt.no_negative_size_fruits, !opt.negative_size_fruits);
}
}
}