Skip to main content

rsnaker/game_logic/
fruits_manager.rs

1//! # Fruits Manager
2//!
3//! This module defines the `fruits_manager` struct,
4//! which is responsible for handling fruit objects within a game.
5//! It includes methods for spawning, replacing, and interacting with fruits.
6//!
7//! # Example
8//! ```rust
9//! use rsnaker::game_logic::fruits_manager::FruitsManager;
10//! use rsnaker::graphics::graphic_block::Position;
11//! use rsnaker::graphics::sprites::map::Map;
12//! use ratatui::layout::Rect;
13//! use std::sync::{Arc, RwLock};
14//! use std::time::Duration;
15//!
16//! let case_size = 2;
17//! let map_size = Rect::new(0, 0, 200, 10);
18//! let map = Arc::new(RwLock::new(Map::new(case_size, map_size)));
19//! let number_of_fruits_to_manage = 10;
20//! let mut manager = FruitsManager::new(
21//!     number_of_fruits_to_manage,
22//!     map.clone(),
23//!     true,
24//!     5f32,
25//! );
26//!
27//! // Simulate eating fruits
28//! let position = Position { x: 10, y: 20 };
29//! if let Some(eaten) = manager.eat_some_fruits(&position) {
30//!     manager.replace_fruits(&eaten);
31//! }
32//! ```
33
34use crate::graphics::graphic_block::Position;
35use crate::graphics::sprites::fruit::{Fruit, FRUITS_SCORES_PROBABILITIES};
36use crate::graphics::sprites::map::Map;
37use rand::{rng, RngExt};
38use ratatui::buffer::Buffer;
39use ratatui::layout::Rect;
40use ratatui::prelude::Widget;
41use ratatui::widgets::WidgetRef;
42use std::sync::{Arc, RwLock};
43use std::time::Instant;
44
45/// Manages fruit objects within the game logic.
46/// Map outlive fruits, so 'b lifetime >= 'a (fruits) lifetime
47pub struct FruitsManager<'a, 'b: 'a> {
48    fruits: Vec<Fruit<'a>>,      // List of fruits currently in the game_logic
49    carte: Arc<RwLock<Map<'b>>>, // Reference to the game_logic map
50    fruit_timer_enabled: bool,
51    fruit_base_lifetime: f32,
52}
53
54impl<'a, 'b> FruitsManager<'a, 'b> {
55    /// Creates a new `FruitsManager` with a given number of fruits.
56    /// # Panics
57    /// if guard cannot be got for Map (whenever a previous panic poisoned guard)
58    /// # Example
59    /// ```
60    /// use std::sync::{Arc, RwLock};
61    /// use std::time::Duration;
62    /// use ratatui::layout::Rect;
63    /// use rsnaker::game_logic::fruits_manager::FruitsManager;
64    /// use rsnaker::graphics::sprites::map::Map;
65    /// let map = Arc::new(RwLock::new(Map::new(2, Rect::new(0,0, 160,10))));
66    /// let manager = FruitsManager::new(3, map, true, 5f32);
67    /// ```
68    #[must_use]
69    pub fn new(
70        nb: u16,
71        carte: Arc<RwLock<Map<'b>>>,
72        fruit_timer_enabled: bool,
73        fruit_base_lifetime: f32,
74    ) -> Self {
75        let mut fm = Self {
76            fruits: Vec::with_capacity(nb as usize),
77            carte,
78            fruit_timer_enabled,
79            fruit_base_lifetime,
80        };
81        fm.init(nb);
82        fm
83    }
84
85    /// Spawns a fruit at a random position on the map.
86    fn spawn_random(carte: &Map, fruit_timer_enabled: bool, fruit_base_lifetime: f32) -> Fruit<'a> {
87        let position = Self::generate_position_rounded_by_cs(carte);
88        let random_value: u16 = rng().random_range(1..=100);
89        let mut cumulative_probability = 0;
90        for &(image, score, probability, size_effect) in FRUITS_SCORES_PROBABILITIES {
91            cumulative_probability += probability;
92            if random_value <= cumulative_probability {
93                return Fruit::new(
94                    score,
95                    size_effect,
96                    position,
97                    image,
98                    fruit_base_lifetime,
99                    fruit_timer_enabled,
100                );
101            }
102        }
103        // Default fallback fruit
104        let (image, score, _, size_effect) = FRUITS_SCORES_PROBABILITIES[0];
105        Fruit::new(
106            score,
107            size_effect,
108            position,
109            image,
110            fruit_base_lifetime,
111            fruit_timer_enabled,
112        )
113    }
114
115    /// Replaces eaten fruits with new random ones and ensures balance.
116    /// # Panics
117    /// if guard cannot be got for Map (whenever a previous panic poisoned guard)
118    pub fn replace_fruits(&mut self, fruits_to_remove: &[Fruit<'a>]) {
119        if fruits_to_remove.is_empty() {
120            return;
121        }
122        let initial_count = self.fruits.len();
123        self.fruits
124            .retain(|fruit| !fruits_to_remove.contains(fruit));
125
126        self.spawn_missing_fruits(initial_count - self.fruits.len());
127    }
128
129    /// Removes expired fruits and replaces them with fresh ones.
130    pub fn update(&mut self) {
131        if !self.fruit_timer_enabled {
132            return;
133        }
134        let now = Instant::now();
135        let initial_count = self.fruits.len();
136        // Filter
137        self.fruits.retain(|fruit| !fruit.is_expired(now));
138
139        self.spawn_missing_fruits(initial_count - self.fruits.len());
140    }
141
142    /// Refills the fruit vector to its expected capacity by spawning new fruits.
143    fn spawn_missing_fruits(&mut self, count: usize) {
144        if count == 0 {
145            return;
146        }
147        // Lock the map once for all spawns
148        let carte_guard = self.carte.read().unwrap();
149        // Optimize memory allocation to prevent multiple costly reallocations
150        self.fruits.reserve(count);
151
152        for _ in 0..count {
153            self.fruits.push(Self::spawn_random(
154                &carte_guard,
155                self.fruit_timer_enabled,
156                self.fruit_base_lifetime,
157            ));
158        }
159    }
160
161    /// Pauses or resumes all fruit timers.
162    pub fn set_timer_paused(&mut self, paused: bool) {
163        if !self.fruit_timer_enabled {
164            return;
165        }
166
167        let now = Instant::now();
168        for fruit in &mut self.fruits {
169            fruit.set_timer_paused(paused, now);
170        }
171    }
172
173    /// Returns a list of fruits at the given position, copying them to avoid lock contention.
174    #[must_use]
175    pub fn eat_some_fruits(&self, position: &Position) -> Option<Vec<Fruit<'a>>> {
176        let now = Instant::now();
177        let eaten: Vec<Fruit<'a>> = self
178            .fruits
179            .iter()
180            .filter(|x| x.is_at_position(position) && !x.is_expired(now))
181            .cloned()
182            .collect();
183        if eaten.is_empty() { None } else { Some(eaten) }
184    }
185
186    /// Generates a random valid position for spawning fruits.
187    fn generate_position_rounded_by_cs(carte: &Map) -> Position {
188        let mut rng = rng();
189        let cs = carte.get_case_size();
190        let csy = 1;
191        let width = carte.area().width;
192        let height = carte.area().height;
193        //saturating cs to be sure to have the size for the counter (at right position)
194        let mut max_index_x = (width / cs).saturating_sub(cs);
195        let mut max_index_y = (height / csy).saturating_sub(csy);
196        // Ensure a valid range for generation
197        if max_index_x <= 1 {
198            max_index_x = 2;
199        }
200        if max_index_y <= 1 {
201            max_index_y = 2;
202        }
203        Position {
204            x: rng.random_range(1..max_index_x) * cs,
205            y: rng.random_range(1..max_index_y) * csy,
206        }
207    }
208
209    pub(crate) fn reset_to_terminal_size(&mut self) {
210        //change the position of all fruits to avoid no eatable/unreachable fruits
211        for f in &mut self.fruits {
212            f.set_position(Self::generate_position_rounded_by_cs(
213                &self.carte.read().unwrap(),
214            ));
215        }
216    }
217
218    pub(crate) fn reset(&mut self) {
219        let len = u16::try_from(self.fruits.len()).unwrap();
220        self.fruits.clear();
221        self.init(len);
222    }
223
224    /// Returns all current fruits.
225    #[must_use]
226    pub fn get_fruits(&self) -> &Vec<Fruit<'a>> {
227        &self.fruits
228    }
229
230    fn init(&mut self, nb: u16) {
231        for _ in 0..nb {
232            self.fruits.push(Self::spawn_random(
233                &self.carte.read().unwrap(),
234                self.fruit_timer_enabled,
235                self.fruit_base_lifetime,
236            ));
237        }
238    }
239}
240
241/// Implements `WidgetRef` for rendering fruits on the screen.
242impl<'a> WidgetRef for FruitsManager<'a, 'a> {
243    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
244        for fruit in &self.fruits {
245            fruit.render_ref(area, buf);
246        }
247    }
248}
249
250/// Implements `Widget` for compatibility with older versions.
251impl<'a> Widget for FruitsManager<'a, 'a> {
252    fn render(self, area: Rect, buf: &mut Buffer) {
253        self.render_ref(area, buf);
254    }
255}
256
257impl<'a> Widget for &FruitsManager<'a, 'a> {
258    fn render(self, area: Rect, buf: &mut Buffer) {
259        self.render_ref(area, buf);
260    }
261}
262
263/// Test part:
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use std::sync::Arc;
268
269    /// Mock definitions
270    /// Not really need it there,but for example, on how to share resources for test
271    fn mock_map() -> Arc<RwLock<Map<'static>>> {
272        Arc::new(RwLock::new(Map::new(2, Rect::new(0, 0, 160, 12))))
273    }
274
275    fn dummy_position() -> Position {
276        Position { x: 10, y: 10 }
277    }
278
279    #[test]
280    fn test_new_creates_correct_number_of_fruits() {
281        let map = mock_map();
282        let manager = FruitsManager::new(5, map, true, 5f32);
283        assert_eq!(manager.fruits.len(), 5);
284    }
285
286    #[test]
287    fn test_replace_fruits_removes_and_adds_new() {
288        let map = mock_map();
289        let mut manager = FruitsManager::new(3, Arc::clone(&map), true, 5f32);
290        let fruits_to_remove = vec![manager.fruits[0].clone()];
291        manager.replace_fruits(&fruits_to_remove);
292        assert_eq!(manager.fruits.len(), 3);
293        assert!(!manager.fruits.contains(&fruits_to_remove[0]));
294    }
295
296    #[test]
297    fn test_eat_some_fruits_returns_correct_fruit() {
298        let map = mock_map();
299        let mut manager = FruitsManager::new(3, Arc::clone(&map), true, 5f32);
300        let fruit = Fruit::new(10, 1, dummy_position(), "🍎", 5f32, true);
301        manager.fruits[0] = fruit.clone();
302        let result = manager.eat_some_fruits(&dummy_position());
303        assert!(result.is_some());
304        assert!(result.unwrap().contains(&fruit));
305    }
306
307    #[test]
308    fn test_eat_some_fruits_returns_none_if_no_fruit() {
309        let map = mock_map();
310        let manager = FruitsManager::new(3, Arc::clone(&map), true, 5f32);
311        let result = manager.eat_some_fruits(&Position { x: 999, y: 999 });
312        assert!(result.is_none());
313    }
314
315    #[test]
316    fn test_update_replaces_expired_fruit() {
317        let map = mock_map();
318        let mut manager = FruitsManager::new(1, Arc::clone(&map), true, 5f32);
319        manager.fruits[0] = Fruit::new(10, 1, dummy_position(), "🍎", 0f32, true);
320
321        manager.update();
322
323        assert_eq!(manager.fruits.len(), 1);
324        assert!(!manager.fruits[0].is_expired(Instant::now()));
325    }
326}