rsnaker/graphics/sprites/
snake_body.rs

1use crate::controls::direction::Direction;
2use crate::graphics::graphic_block::{GraphicBlock, Position};
3use crate::graphics::sprites::map::Map;
4use ratatui::buffer::Buffer;
5use ratatui::layout::Rect;
6use ratatui::prelude::{Style, Widget};
7use ratatui::style::{Color, Modifier};
8use ratatui::widgets::WidgetRef;
9
10/// A struct representing the snake's body in the game.
11/// It is composed of multiple `GraphicBlock` elements that make up the snake's segments.
12/// The body can move, grow, and check for overlaps with itself.
13///
14/// # Fields
15/// - `body`: A vector of `GraphicBlock` elements representing the segments of the snake's body.
16/// - `CASE_SIZE`: The size of each segment of the snake's body in pixels.
17/// - `position_ini`: The initial position of the snake's head.
18/// - `size_ini`: The initial size of the snake (the number of body segments).
19#[derive(Clone)]
20pub struct SnakeBody<'a> {
21    pub(crate) body: Vec<GraphicBlock<'a>>,
22    case_size: u16,
23    position_ini: Position,
24    size_ini: u16,
25}
26
27impl<'a> SnakeBody<'a> {
28    /// Creates a new `SnakeBody` instance with the specified body image, head image, number of segments,
29    /// initial position, and case size.
30    ///
31    /// # Parameters
32    /// - `body_image`: The image for the body segments of the snake.
33    /// - `head_image`: The image for the snake's head.
34    /// - `nb`: The number of body segments.
35    /// - `position`: The initial position of the snake's head.
36    /// - `CASE_SIZE`: The size of each body segment in pixels.
37    ///
38    /// # Returns
39    /// A new `SnakeBody` instance with the specified parameters.
40    #[must_use]
41    pub fn new(
42        body_image: &'a str,
43        head_image: &'a str,
44        nb: u16,
45        position: Position,
46        case_size: u16,
47    ) -> SnakeBody<'a> {
48        let snake_style = Style {
49            fg: Some(Color::White),
50            bg: Some(Color::Reset), // <- important if the global style has bg
51            underline_color: Some(Color::White),
52            add_modifier: Modifier::ITALIC,
53            sub_modifier: Modifier::BOLD,
54        };
55        let Position { x, y } = position;
56        let mut body = Vec::with_capacity(nb as usize);
57        body.push(GraphicBlock::new(
58            Position { x, y },
59            head_image,
60            snake_style,
61        ));
62        for i in 1..nb {
63            body.push(GraphicBlock::new(
64                Position {
65                    //safer as will limit to 0
66                    x: x.saturating_sub(case_size * i),
67                    y,
68                },
69                body_image,
70                snake_style,
71            ));
72        }
73        SnakeBody {
74            body,
75            case_size,
76            position_ini: position,
77            size_ini: nb,
78        }
79    }
80
81    /// Resets the snake's body to its initial position and size.
82    /// The head is placed at the initial position, and the body segments are repositioned accordingly.
83    pub fn reset(&mut self) {
84        self.body.truncate(self.size_ini as usize);
85        self.body[0].set_position(self.position_ini.clone());
86        for i in 1..self.size_ini {
87            self.body[i as usize].set_position(Position {
88                x: self.position_ini.x.saturating_sub(self.case_size * i),
89                y: self.position_ini.y,
90            });
91        }
92    }
93
94    /// Updates the positions of the body segments to simulate the movement of the snake.
95    /// The body segments "follow" the previous segment.
96    /// Add one previous not shown elements by enabling it (to avoid a big increase in tail as +10)
97    ///
98    /// # Parameters
99    /// - `previous_head`: The position of the previous head of the snake.
100    pub fn ramping_body(&mut self, previous_head: &Position) {
101        let mut current = previous_head.clone();
102        let mut previous = current;
103        let has_grown_by_one = false;
104        for i in 1..self.body.len() {
105            current = self.body[i].get_position().clone();
106            self.body[i].set_position(previous);
107            previous = current;
108            if !self.body[i].enabled && !has_grown_by_one {
109                self.body[i].enable();
110            }
111        }
112    }
113
114    /// Checks if the snake's head overlaps with any part of its body.
115    ///
116    /// # Returns
117    /// - `false` if the head does not overlap with the body.
118    /// - `true` if the head overlaps with any part of the body.
119    #[must_use]
120    pub fn is_snake_eating_itself(&self) -> bool {
121        let head = self.body[0].get_position();
122        for b in self.body.iter().skip(1) {
123            if head == b.get_position() {
124                return true;
125            }
126        }
127        false
128    }
129
130    /// Moves the snake's head left by one case and updates the body accordingly.
131    pub fn left(&mut self) {
132        let current = &self.body[0].get_position().clone();
133        self.body[0].position.x -= self.case_size;
134        self.ramping_body(current);
135    }
136
137    /// Moves the snake's head right by one case and updates the body accordingly.
138    pub fn right(&mut self) {
139        let current = &self.body[0].get_position().clone();
140        self.body[0].position.x += self.case_size;
141        self.ramping_body(current);
142    }
143
144    /// Moves the snake's head up by one case/line and updates the body accordingly.
145    pub fn up(&mut self) {
146        let current = &self.body[0].get_position().clone();
147        self.body[0].position.y -= 1;
148        self.ramping_body(current);
149    }
150
151    /// Moves the snake's head down by one case/line and updates the body accordingly.
152    pub fn down(&mut self) {
153        let current = &self.body[0].get_position().clone();
154        self.body[0].position.y += 1;
155        self.ramping_body(current);
156    }
157
158    /// Moves the snake in the specified direction and checks if the snake's head has moved outside the map
159    /// or overlapped with its body. If the snake moves out of bounds, its position is reversed.
160    ///
161    /// # Parameters
162    /// - `direction`: The direction in which to move the snake.
163    /// - `carte`: The map used to check if the snake's head is out of bounds.
164    ///
165    /// # Returns
166    /// - `&Position` the new snake's head position.
167    #[allow(clippy::trivially_copy_pass_by_ref)]
168    pub fn ramp(&mut self, direction: &Direction, carte: &Map) -> &Position {
169        match direction {
170            Direction::Up => self.up(),
171            Direction::Down => self.down(),
172            Direction::Left => self.left(),
173            Direction::Right => self.right(),
174        }
175        if carte.out_of_map(self.body[0].get_position()) {
176            let new_position = carte.out_of_map_reverse_position(self.body[0].get_position());
177            self.body[0].set_position(new_position);
178        }
179        self.body[0].get_position()
180    }
181
182    /// A backup plan in case the widget reference is unstable, by cloning the snake body.
183    fn _get_widget(&self) -> impl Widget + 'a {
184        self.clone()
185    }
186
187    /// Change the snake size by adding/removing a specified number of segments to its body.
188    ///
189    /// # Parameters
190    /// - `nb`:The number of segments to add or to remove to the snake's body.
191    /// # Panics
192    /// If no element in Snake, as we keep a minimum size `size_ini`,
193    /// when resizing down should not happen
194    pub fn relative_size_change(&mut self, nb: i16) {
195        if nb > 0 {
196            for _ in 0..nb {
197                let mut block_to_add = self
198                    .body
199                    .last()
200                    .expect("Snake body has no elements ! Something went wrong")
201                    .clone();
202                //To show later, snake body one by one
203                block_to_add.disable();
204                self.body.push(block_to_add);
205            }
206        } else {
207            //We must remove some element, but keeping a minimum length for the snake
208            #[allow(clippy::cast_sign_loss)]
209            let sub = self.body.len().saturating_sub((-nb) as usize);
210            let to_keep = if sub < self.size_ini as usize {
211                self.size_ini as usize
212            } else {
213                sub
214            };
215            self.body.truncate(to_keep);
216        }
217    }
218}
219
220/// Only needed for backwards compatibility
221impl Widget for SnakeBody<'_> {
222    fn render(self, area: Rect, buf: &mut Buffer) {
223        self.render_ref(area, buf);
224    }
225}
226impl Widget for &SnakeBody<'_> {
227    fn render(self, area: Rect, buf: &mut Buffer) {
228        self.render_ref(area, buf);
229    }
230}
231//In general, where you expect a widget to immutably work on its data,we recommended implementing
232// Widget for a reference to the widget (impl Widget for &MyWidget).
233// If you need to store state between draw calls, implement StatefulWidget if you want the Widget
234// to be immutable,
235// or implement Widget for a mutable reference to the widget (impl Widget for &mut MyWidget).
236// If you want the widget to be mutable.
237// The mutable widget pattern is used infrequently in apps
238// but can be quite useful.
239// A blanket implementation of Widget for &W where W implements WidgetRef is provided.
240// The Widget trait is also implemented for &str and String types.
241
242impl WidgetRef for SnakeBody<'_> {
243    fn render_ref(&self, area: Rect, buf: &mut Buffer) {
244        for body_element in &self.body {
245            body_element.render_ref(area, buf);
246        }
247    }
248}