rsnaker/game_logic/
random_helper.rs1use crate::controls::speed::Speed;
8use crate::game_logic::game_options::{DISPLAYABLE_EMOJI, GameOptions};
9use crate::game_logic::logger::log_configuration::LogLevel;
10use rand::{RngExt, rng};
11use std::ops::RangeInclusive;
12
13#[must_use]
18pub fn weighted_range(choices: &[(u32, RangeInclusive<u16>)]) -> u16 {
19 let total_weight: u32 = choices.iter().map(|(w, _)| *w).sum();
20 let roll: u32 = rng().random_range(1..=total_weight);
21 let mut cumulative = 0;
22 for (weight, range) in choices {
23 cumulative += *weight;
24 if roll <= cumulative {
25 return rng().random_range(range.clone());
26 }
27 }
28 *choices[0].1.start()
29}
30
31#[must_use]
36pub fn generate_random_game_options() -> GameOptions {
37 let speed = match rng().random_range(1..=100) {
38 1..=20 => Speed::Slow,
39 21..=60 => Speed::Normal,
40 61..=90 => Speed::Fast,
41 _ => Speed::Crazy,
42 };
43
44 let head_idx = rng().random_range(0..DISPLAYABLE_EMOJI.len());
45 let mut body_idx = rng().random_range(0..DISPLAYABLE_EMOJI.len());
46 while body_idx == head_idx {
47 body_idx = rng().random_range(0..DISPLAYABLE_EMOJI.len());
48 }
49 let head_symbol = DISPLAYABLE_EMOJI[head_idx].to_string();
50 let body_symbol = DISPLAYABLE_EMOJI[body_idx].to_string();
51
52 let snake_length = weighted_range(&[
54 (45, 3..=12), (30, 13..=30), (15, 31..=75), (7, 76..=150), (3, 151..=300), ]);
60
61 let life = weighted_range(&[
62 (40, 2..=5), (30, 6..=12), (15, 1..=1), (10, 13..=25), (5, 26..=50), ]);
68
69 let nb_of_fruits = weighted_range(&[
70 (40, 3..=8), (30, 9..=20), (15, 1..=2), (10, 21..=50), (5, 51..=100), ]);
76
77 let fruit_duration_seconds = weighted_range(&[
78 (50, 15..=30), (25, 5..=14), (20, 31..=50), (5, 51..=60), ]);
83
84 let snake_growth_factor = weighted_range(&[
85 (60, 1..=1), (30, 2..=3), (10, 4..=6), ]);
89
90 let fruit_timer = rng().random_bool(0.8);
91 let negative_size_fruits = rng().random_bool(0.5);
92 let caps_fps = true;
93
94 let mut options = GameOptions {
95 speed,
96 head_symbol,
97 body_symbol,
98 snake_length,
99 life,
100 nb_of_fruits,
101 no_fruit_timer: !fruit_timer,
102 fruit_timer,
103 fruit_duration_seconds,
104 snake_growth_factor,
105 no_negative_size_fruits: !negative_size_fruits,
106 negative_size_fruits,
107 log_level: LogLevel::Off,
108 no_caps_fps: !caps_fps,
109 caps_fps,
110 load: None,
111 random: true,
112 };
113 options.validate();
114 options
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120 use unicode_segmentation::UnicodeSegmentation;
121
122 #[test]
123 fn test_weighted_range_bounds() {
124 let ranges = [(50, 5..=10), (50, 20..=30)];
125 for _ in 0..100 {
126 let val = weighted_range(&ranges);
127 assert!((5..=10).contains(&val) || (20..=30).contains(&val));
128 }
129 }
130
131 #[test]
132 fn test_generate_random_game_options() {
133 for _ in 0..100 {
134 let opt = generate_random_game_options();
135 assert!(opt.snake_length >= 2 && opt.snake_length <= 999);
136 assert!(opt.life >= 1 && opt.life <= 99);
137 assert!(opt.nb_of_fruits >= 1 && opt.nb_of_fruits <= 999);
138 assert!(opt.fruit_duration_seconds >= 1 && opt.fruit_duration_seconds <= 60);
139 assert!(opt.snake_growth_factor >= 1 && opt.snake_growth_factor <= 10);
140 assert_eq!(opt.head_symbol.graphemes(true).count(), 1);
141 assert_eq!(opt.body_symbol.graphemes(true).count(), 1);
142 assert_ne!(opt.head_symbol, opt.body_symbol);
143 assert!(opt.caps_fps);
144 assert_eq!(opt.log_level, LogLevel::Off);
145 assert!(
146 opt.random,
147 "Generated options must have random flag set to true"
148 );
149 assert_eq!(opt.no_fruit_timer, !opt.fruit_timer);
150 assert_eq!(opt.no_negative_size_fruits, !opt.negative_size_fruits);
151 }
152 }
153}