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}