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::sprites::map::Map;
11//! use std::sync::{Arc, RwLock};
12//! use ratatui::layout::Rect;
13//! use rsnaker::graphics::graphic_block::Position;
14//!
15//! let case_size = 2;
16//! let map_size = Rect::new(0, 0, 200, 10);
17//! let map = Arc::new(RwLock::new(Map::new(case_size, map_size)));
18//! let number_of_fruits_to_manage = 10;
19//! let mut manager = FruitsManager::new(number_of_fruits_to_manage, map.clone());
20//!
21//! // Simulate eating fruits
22//! let position = Position { x: 10, y: 20 };
23//! if let Some(eaten) = manager.eat_some_fruits(&position) {
24//!     manager.replace_fruits(&eaten);
25//! }
26//! ```
27//!
28
29use crate::graphics::graphic_block::Position;
30use crate::graphics::sprites::fruit::{Fruit, FRUITS_SCORES_PROBABILITIES};
31use crate::graphics::sprites::map::Map;
32use rand::{rng, Rng};
33use ratatui::buffer::Buffer;
34use ratatui::layout::Rect;
35use ratatui::prelude::Widget;
36use ratatui::widgets::WidgetRef;
37use std::sync::{Arc, RwLock};
38
39/// Manages fruit objects within the game logic.
40/// Map outlive fruits, so 'b lifetime >= 'a (fruits) lifetime
41pub struct FruitsManager<'a, 'b: 'a> {
42    fruits: Vec<Fruit<'a>>,      // List of fruits currently in the game_logic
43    carte: Arc<RwLock<Map<'b>>>, // Reference to the game_logic map
44}
45
46impl<'a, 'b> FruitsManager<'a, 'b> {
47    /// Creates a new `FruitsManager` with a given number of fruits.
48    /// # Panics
49    /// if guard cannot be got for Map (whenever a previous panic poisoned guard)
50    /// # Example
51    /// ```
52    /// use std::sync::{Arc, RwLock};
53    /// use ratatui::layout::Rect;
54    /// use rsnaker::game_logic::fruits_manager::FruitsManager;
55    /// use rsnaker::graphics::sprites::map::Map;
56    /// let x =42;
57    /// let map = Arc::new(RwLock::new(Map::new(2, Rect::new(0,0, 160,10))));
58    /// let manager = FruitsManager::new(3, map);
59    /// ```
60    #[must_use]
61    pub fn new(nb: u16, carte: Arc<RwLock<Map<'b>>>) -> Self {
62        let mut fruits: Vec<Fruit> = Vec::with_capacity(nb as usize);
63        {
64            let c = carte.read().unwrap();
65            for _ in 0..nb {
66                fruits.push(Self::spawn_random(&c));
67            }
68        }
69        Self { fruits, carte }
70    }
71
72    /// Spawns a fruit at a random position on the map.
73    fn spawn_random(carte: &Map) -> Fruit<'a> {
74        let position = Self::generate_position_rounded_by_cs(carte);
75        let mut rng = rng();
76        let random_value: u16 = rng.random_range(1..100);
77        let mut cumulative_probability = 0;
78        for &(image, score, probability, size_effect) in FRUITS_SCORES_PROBABILITIES {
79            cumulative_probability += probability;
80            if random_value <= cumulative_probability {
81                return Fruit::new(score, size_effect, position, image);
82            }
83        }
84        // Default fallback fruit
85        let (image, score, _, size_effect) = FRUITS_SCORES_PROBABILITIES[0];
86        Fruit::new(score, size_effect, position, image)
87    }
88
89    /// Replaces eaten fruits with new random ones and ensures balance.
90    /// # Panics
91    /// if guard cannot be got for Map (whenever a previous panic poisoned guard)
92    pub fn replace_fruits(&mut self, fruits_to_remove: &[Fruit<'a>]) {
93        let nb = fruits_to_remove.len();
94        self.fruits
95            .retain(|fruit| !fruits_to_remove.contains(fruit));
96        {
97            let carte_guard = self.carte.read().unwrap();
98            for _ in 0..nb {
99                self.fruits.push(Self::spawn_random(&carte_guard));
100            }
101        }
102    }
103
104    /// Returns a list of fruits at the given position, copying them to avoid lock contention.
105    #[must_use]
106    pub fn eat_some_fruits(&self, position: &Position) -> Option<Vec<Fruit<'a>>> {
107        let eaten: Vec<Fruit<'a>> = self
108            .fruits
109            .iter()
110            .filter(|x| x.is_at_position(position))
111            .cloned()
112            .collect();
113        if eaten.is_empty() { None } else { Some(eaten) }
114    }
115
116    /// Generates a random valid position for spawning fruits.
117    fn generate_position_rounded_by_cs(carte: &Map) -> Position {
118        let mut rng = rng();
119        let cs = carte.get_case_size();
120        let csy = 1;
121        let width = carte.area().width;
122        let height = carte.area().height;
123        let mut max_index_x = (width / cs).saturating_sub(cs);
124        let mut max_index_y = (height / csy).saturating_sub(csy);
125        // Ensure a valid range for generation
126        if max_index_x <= 1 {
127            max_index_x = 2;
128        }
129        if max_index_y <= 1 {
130            max_index_y = 2;
131        }
132        Position {
133            x: rng.random_range(1..max_index_x) * cs,
134            y: rng.random_range(1..max_index_y) * csy,
135        }
136    }
137    pub(crate) fn resize_to_terminal(&mut self) {
138        //change the position of all fruits to avoid no eatable/unreachable fruits
139        for f in &mut self.fruits {
140            f.set_position(Self::generate_position_rounded_by_cs(
141                &self.carte.read().unwrap(),
142            ));
143        }
144    }
145}
146
147/// Implements `WidgetRef` for rendering fruits on the screen.
148impl<'a> WidgetRef for FruitsManager<'a, 'a> {
149    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
150        for fruit in &self.fruits {
151            fruit.render_ref(area, buf);
152        }
153    }
154}
155
156/// Implements `Widget` for compatibility with older versions.
157impl<'a> Widget for FruitsManager<'a, 'a> {
158    fn render(self, area: Rect, buf: &mut Buffer) {
159        self.render_ref(area, buf);
160    }
161}
162
163impl<'a> Widget for &FruitsManager<'a, 'a> {
164    fn render(self, area: Rect, buf: &mut Buffer) {
165        self.render_ref(area, buf);
166    }
167}
168
169/// Test part:
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use std::sync::Arc;
174
175    /// Mock definitions
176    /// Not really need it there,but for example, on how to share ressources for test
177    fn mock_map() -> Arc<RwLock<Map<'static>>> {
178        Arc::new(RwLock::new(Map::new(2, Rect::new(0, 0, 160, 12))))
179    }
180
181    fn dummy_position() -> Position {
182        Position { x: 10, y: 10 }
183    }
184
185    #[test]
186    fn test_new_creates_correct_number_of_fruits() {
187        let map = mock_map();
188        let manager = FruitsManager::new(5, map);
189        assert_eq!(manager.fruits.len(), 5);
190    }
191
192    #[test]
193    fn test_replace_fruits_removes_and_adds_new() {
194        let map = mock_map();
195        let mut manager = FruitsManager::new(3, Arc::clone(&map));
196        let fruits_to_remove = vec![manager.fruits[0].clone()];
197        manager.replace_fruits(&fruits_to_remove);
198        assert_eq!(manager.fruits.len(), 3);
199        assert!(!manager.fruits.contains(&fruits_to_remove[0]));
200    }
201
202    #[test]
203    fn test_eat_some_fruits_returns_correct_fruit() {
204        let map = mock_map();
205        let mut manager = FruitsManager::new(3, Arc::clone(&map));
206        let fruit = Fruit::new(10, 1, dummy_position(), "🍎");
207        manager.fruits[0] = fruit.clone();
208        let result = manager.eat_some_fruits(&dummy_position());
209        assert!(result.is_some());
210        assert!(result.unwrap().contains(&fruit));
211    }
212
213    #[test]
214    fn test_eat_some_fruits_returns_none_if_no_fruit() {
215        let map = mock_map();
216        let manager = FruitsManager::new(3, Arc::clone(&map));
217        let result = manager.eat_some_fruits(&Position { x: 999, y: 999 });
218        assert!(result.is_none());
219    }
220}