use ratatui::layout::Rect;
use ratatui::widgets::Paragraph;
use std::time::{Duration, Instant};
pub const SPEED_MOVING_SNAKE_SLEEP_TIME_MS: u64 = 50;
const SEGMENT_SPACING: u16 = 4;
const TOTAL_SEGMENTS: usize = 5;
pub struct EdgeSnake {
pub x: u16,
pub y: u16,
last_update: Instant,
frame_duration: Duration,
}
impl Default for EdgeSnake {
fn default() -> Self {
Self::new()
}
}
impl EdgeSnake {
#[must_use]
pub fn new() -> Self {
Self {
x: 0,
y: 0,
last_update: Instant::now(),
frame_duration: Duration::from_millis(SPEED_MOVING_SNAKE_SLEEP_TIME_MS),
}
}
pub fn update(&mut self, area: &Rect) {
if self.last_update.elapsed() < self.frame_duration {
return;
}
self.last_update = Instant::now();
self.x = Self::next_x(self.x, area.width);
self.y = 0;
}
pub fn render(&self, frame: &mut ratatui::Frame, area: &Rect) {
for (x, y) in self.get_positions(area.width) {
frame.render_widget(Paragraph::new("🐍"), Rect::new(x, y, 2, 1));
}
}
fn next_x(x: u16, width: u16) -> u16 {
let max_x = width.saturating_sub(2);
if width <= 2 || x >= max_x { 0 } else { x + 1 }
}
#[must_use]
pub fn get_positions(&self, width: u16) -> Vec<(u16, u16)> {
if width <= 2 {
return Vec::new();
}
let available_positions = width - 1;
let head_x = self.x.min(width - 2);
(0..TOTAL_SEGMENTS)
.map(|segment| {
let offset = (u16::try_from(segment).expect("segment count exceeds u16")
* SEGMENT_SPACING)
% available_positions;
(
(head_x + available_positions - offset) % available_positions,
0,
)
})
.collect()
}
}
#[cfg(test)]
mod tests {
use super::EdgeSnake;
#[test]
fn head_wraps_from_the_right_edge_to_the_left_edge() {
assert_eq!(EdgeSnake::next_x(7, 9), 0);
assert_eq!(EdgeSnake::next_x(6, 9), 7);
}
#[test]
fn body_stays_on_the_top_row_and_wraps_horizontally() {
let mut snake = EdgeSnake::new();
snake.x = 1;
assert_eq!(
snake.get_positions(10),
vec![(1, 0), (6, 0), (2, 0), (7, 0), (3, 0)]
);
}
}