Skip to main content

thrust_rl/env/games/snake/
snake.rs

1//! Snake entity and movement logic
2//!
3//! This module handles individual snake behavior, movement,
4//! collision detection, and growth mechanics.
5
6use std::collections::VecDeque;
7
8use super::types::{Direction, Position};
9
10/// Individual snake in the game
11#[derive(Debug, Clone)]
12pub struct Snake {
13    /// Snake ID (for multi-agent games)
14    pub id: usize,
15    /// Head position
16    pub head: Position,
17    /// Body positions (deque for efficient head/tail operations)
18    pub body: VecDeque<Position>,
19    /// Current direction
20    pub direction: Direction,
21    /// Length of snake
22    pub length: usize,
23    /// Whether the snake is alive
24    pub alive: bool,
25}
26
27impl Snake {
28    /// Create new snake at position
29    pub fn new(id: usize, start_pos: Position, start_direction: Direction) -> Self {
30        let mut body = VecDeque::new();
31        body.push_back(start_pos);
32
33        Self { id, head: start_pos, body, direction: start_direction, length: 1, alive: true }
34    }
35
36    /// Move snake in current direction
37    pub fn move_forward(&mut self) {
38        let new_head = self.head.add_direction(self.direction);
39
40        // Add new head position
41        self.body.push_front(new_head);
42        self.head = new_head;
43
44        // Remove tail if not growing
45        if self.body.len() > self.length {
46            self.body.pop_back();
47        }
48    }
49
50    /// Move snake in current direction with wraparound boundaries (torus)
51    pub fn move_forward_wrap(&mut self, width: i32, height: i32) {
52        let new_head = self.head.add_direction(self.direction).wrap(width, height);
53
54        // Add new head position
55        self.body.push_front(new_head);
56        self.head = new_head;
57
58        // Remove tail if not growing
59        if self.body.len() > self.length {
60            self.body.pop_back();
61        }
62    }
63
64    /// Change direction (with validation)
65    pub fn change_direction(&mut self, new_direction: Direction) {
66        // Prevent immediate reversal
67        if new_direction != self.direction.opposite() {
68            self.direction = new_direction;
69        }
70    }
71
72    /// Grow snake by one segment
73    pub fn grow(&mut self) {
74        self.length += 1;
75    }
76
77    /// Check if snake collides with walls
78    pub fn collides_with_wall(&self, width: i32, height: i32) -> bool {
79        !self.head.in_bounds(width, height)
80    }
81
82    /// Check if snake collides with itself
83    pub fn collides_with_self(&self) -> bool {
84        // Check if head collides with any body segment
85        self.body.iter().skip(1).any(|&pos| pos == self.head)
86    }
87
88    /// Check if snake collides with another snake
89    pub fn collides_with_other(&self, other: &Snake) -> bool {
90        // Check head collision with other snake's body
91        other.body.iter().any(|&pos| pos == self.head)
92    }
93
94    /// Check if snake eats food at position
95    pub fn eats_food(&self, food_pos: &Position) -> bool {
96        self.head == *food_pos
97    }
98
99    /// Get all positions occupied by snake
100    pub fn get_all_positions(&self) -> Vec<Position> {
101        self.body.iter().cloned().collect()
102    }
103
104    /// Get snake's current length
105    pub fn len(&self) -> usize {
106        self.length
107    }
108
109    /// Whether the snake has no body segments.
110    ///
111    /// A live snake always has at least one segment, so this is effectively
112    /// always `false`; provided to satisfy the `len`/`is_empty` convention.
113    pub fn is_empty(&self) -> bool {
114        self.length == 0
115    }
116
117    /// Check if snake is alive
118    pub fn is_alive(&self) -> bool {
119        self.alive
120    }
121}
122
123/// Food pellet in the game
124#[derive(Debug, Clone)]
125pub struct Food {
126    /// Grid cell the food occupies. Guaranteed to be in-bounds and not
127    /// overlap any live snake segment by construction (see
128    /// [`Food::generate_random`]).
129    pub position: Position,
130}
131
132impl Food {
133    /// Create food at position
134    pub fn new(position: Position) -> Self {
135        Self { position }
136    }
137
138    /// Generate random food position avoiding snakes
139    pub fn generate_random(
140        width: i32,
141        height: i32,
142        snakes: &[Snake],
143        rng: &mut impl rand::Rng,
144    ) -> Self {
145        loop {
146            let x = rng.random_range(0..width);
147            let y = rng.random_range(0..height);
148            let pos = Position::new(x, y);
149
150            // Check if position is occupied by any snake
151            let occupied = snakes.iter().any(|snake| snake.get_all_positions().contains(&pos));
152
153            if !occupied {
154                return Self::new(pos);
155            }
156        }
157    }
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_snake_creation() {
166        let pos = Position::new(5, 5);
167        let snake = Snake::new(0, pos, Direction::Right);
168
169        assert_eq!(snake.id, 0);
170        assert_eq!(snake.head, pos);
171        assert_eq!(snake.direction, Direction::Right);
172        assert_eq!(snake.length, 1);
173        assert_eq!(snake.body.len(), 1);
174        assert!(snake.is_alive());
175    }
176
177    #[test]
178    fn test_snake_movement() {
179        let start_pos = Position::new(5, 5);
180        let mut snake = Snake::new(0, start_pos, Direction::Right);
181
182        snake.move_forward();
183        assert_eq!(snake.head, Position::new(6, 5));
184        assert_eq!(snake.body.len(), 1); // Tail removed since not growing
185
186        snake.grow();
187        snake.move_forward();
188        assert_eq!(snake.head, Position::new(7, 5));
189        assert_eq!(snake.body.len(), 2); // Body grew
190        assert_eq!(snake.length, 2);
191    }
192
193    #[test]
194    fn test_snake_direction_change() {
195        let mut snake = Snake::new(0, Position::new(5, 5), Direction::Right);
196
197        // Valid direction change
198        snake.change_direction(Direction::Down);
199        assert_eq!(snake.direction, Direction::Down);
200
201        // Invalid direction change (immediate reversal should be ignored)
202        snake.change_direction(Direction::Up);
203        assert_eq!(snake.direction, Direction::Down); // Should remain Down
204    }
205
206    #[test]
207    fn test_snake_collision_detection() {
208        let mut snake = Snake::new(0, Position::new(1, 1), Direction::Right);
209        snake.grow();
210        snake.move_forward(); // head now at (2, 1) — last in-bounds column on 3x3
211        assert!(!snake.collides_with_wall(3, 3), "(2, 1) is in-bounds on 3x3");
212
213        // Wall collision — step off the right edge
214        snake.move_forward(); // head now at (3, 1) — off the right edge
215        assert!(snake.collides_with_wall(3, 3), "(3, 1) is out-of-bounds on 3x3");
216
217        // Self collision - grow long enough to loop back onto own body
218        let mut snake2 = Snake::new(0, Position::new(5, 5), Direction::Right);
219        snake2.grow();
220        snake2.grow();
221        snake2.grow();
222        snake2.grow(); // length is now 5 (initial 1 + 4 grows)
223        snake2.move_forward(); // head (6, 5)
224        snake2.change_direction(Direction::Down);
225        snake2.move_forward(); // head (6, 6)
226        snake2.change_direction(Direction::Left);
227        snake2.move_forward(); // head (5, 6)
228        snake2.change_direction(Direction::Up);
229        snake2.move_forward(); // head back at (5, 5) — collides with body tail
230        assert!(snake2.collides_with_self());
231    }
232
233    #[test]
234    fn test_snake_food_eating() {
235        let snake_pos = Position::new(5, 5);
236        let food_pos = Position::new(5, 5);
237        let snake = Snake::new(0, snake_pos, Direction::Right);
238
239        assert!(snake.eats_food(&food_pos));
240
241        let food_pos2 = Position::new(6, 5);
242        assert!(!snake.eats_food(&food_pos2));
243    }
244
245    #[test]
246    fn test_food_generation() {
247        let mut rng = rand::rng();
248        let snakes = vec![
249            Snake::new(0, Position::new(0, 0), Direction::Right),
250            Snake::new(1, Position::new(2, 2), Direction::Right),
251        ];
252
253        let food = Food::generate_random(5, 5, &snakes, &mut rng);
254        let food_pos = food.position;
255
256        // Food should be within bounds
257        assert!(food_pos.in_bounds(5, 5));
258
259        // Food should not be on any snake
260        for snake in &snakes {
261            assert!(!snake.get_all_positions().contains(&food_pos));
262        }
263    }
264}