rsnaker/graphics/menus/
edge_snake.rs1use ratatui::layout::Rect;
2use ratatui::widgets::Paragraph;
3use std::time::{Duration, Instant};
4
5pub const SPEED_MOVING_SNAKE_SLEEP_TIME_MS: u64 = 50;
6const SEGMENT_SPACING: u16 = 4;
8const TOTAL_SEGMENTS: usize = 5;
10
11pub struct EdgeSnake {
13 pub x: u16,
15 pub y: u16,
17 last_update: Instant,
19 frame_duration: Duration,
21}
22
23impl Default for EdgeSnake {
24 fn default() -> Self {
25 Self::new()
26 }
27}
28
29impl EdgeSnake {
30 #[must_use]
32 pub fn new() -> Self {
33 Self {
34 x: 0,
35 y: 0,
36 last_update: Instant::now(),
37 frame_duration: Duration::from_millis(SPEED_MOVING_SNAKE_SLEEP_TIME_MS),
38 }
39 }
40
41 pub fn update(&mut self, area: &Rect) {
43 if self.last_update.elapsed() < self.frame_duration {
44 return;
45 }
46 self.last_update = Instant::now();
47
48 self.x = Self::next_x(self.x, area.width);
49 self.y = 0;
50 }
51
52 pub fn render(&self, frame: &mut ratatui::Frame, area: &Rect) {
53 for (x, y) in self.get_positions(area.width) {
54 frame.render_widget(Paragraph::new("🐍"), Rect::new(x, y, 2, 1));
55 }
56 }
57
58 fn next_x(x: u16, width: u16) -> u16 {
60 let max_x = width.saturating_sub(2);
61 if width <= 2 || x >= max_x { 0 } else { x + 1 }
62 }
63
64 #[must_use]
67 pub fn get_positions(&self, width: u16) -> Vec<(u16, u16)> {
68 if width <= 2 {
69 return Vec::new();
70 }
71
72 let available_positions = width - 1;
73 let head_x = self.x.min(width - 2);
74 (0..TOTAL_SEGMENTS)
75 .map(|segment| {
76 let offset = (u16::try_from(segment).expect("segment count exceeds u16")
77 * SEGMENT_SPACING)
78 % available_positions;
79 (
80 (head_x + available_positions - offset) % available_positions,
81 0,
82 )
83 })
84 .collect()
85 }
86}
87
88#[cfg(test)]
89mod tests {
90 use super::EdgeSnake;
91
92 #[test]
93 fn head_wraps_from_the_right_edge_to_the_left_edge() {
94 assert_eq!(EdgeSnake::next_x(7, 9), 0);
95 assert_eq!(EdgeSnake::next_x(6, 9), 7);
96 }
97
98 #[test]
99 fn body_stays_on_the_top_row_and_wraps_horizontally() {
100 let mut snake = EdgeSnake::new();
101 snake.x = 1;
102
103 assert_eq!(
104 snake.get_positions(10),
105 vec![(1, 0), (6, 0), (2, 0), (7, 0), (3, 0)]
106 );
107 }
108}