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_GAP: i32 = 1;
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();
let (max_x, max_y) = Self::get_limits(area.width, area.height);
if max_x == 0 && max_y == 0 {
return;
}
(self.x, self.y) = Self::step_along_edge(self.x, self.y, max_x, max_y, 1);
}
pub fn render(&self, frame: &mut ratatui::Frame, area: &Rect) {
for (x, y) in self.get_positions(area.width, area.height) {
frame.render_widget(Paragraph::new("🐍"), Rect::new(x, y, 2, 1));
}
}
fn get_limits(width: u16, height: u16) -> (u16, u16) {
let max_x = width.saturating_sub(2);
let max_y = height.saturating_sub(1);
if width <= 2 || height <= 1 {
return (0, 0);
}
(max_x, max_y)
}
#[must_use]
pub fn step_along_edge(x: u16, y: u16, max_x: u16, max_y: u16, delta: i32) -> (u16, u16) {
let x = x.min(max_x);
let mut y = y.min(max_y);
if x > 0 && x < max_x && y > 0 && y < max_y {
y = 0;
}
let perimeter = 2 * (max_x + max_y);
if perimeter == 0 {
return (x, y);
}
let index = if y == 0 {
x
} else if x == max_x {
max_x + y
} else if y == max_y {
max_x + max_y + (max_x - x)
} else if x == 0 {
2 * max_x + max_y + (max_y - y)
} else {
return (x, y); };
let new_index = u16::try_from((i32::from(index) + delta).rem_euclid(i32::from(perimeter)))
.expect("Maths error");
if new_index <= max_x {
(new_index, 0)
} else if new_index <= max_x + max_y {
(max_x, new_index - max_x)
} else if new_index <= 2 * max_x + max_y {
(max_x - (new_index - max_x - max_y), max_y)
} else {
(0, max_y - (new_index - 2 * max_x - max_y))
}
}
fn is_vertical(x: u16, y: u16, max_x: u16, max_y: u16) -> bool {
(x == max_x && y > 0) || (x == 0 && y > 0 && y < max_y)
}
#[must_use]
pub fn get_positions(&self, width: u16, height: u16) -> Vec<(u16, u16)> {
let (max_x, max_y) = Self::get_limits(width, height);
if max_x == 0 && max_y == 0 {
return vec![(0, 0); TOTAL_SEGMENTS];
}
let mut positions = Vec::with_capacity(TOTAL_SEGMENTS);
let (mut curr_x, mut curr_y) = (self.x, self.y);
curr_x = curr_x.min(max_x);
curr_y = curr_y.min(max_y);
for i in 0..TOTAL_SEGMENTS {
positions.push((curr_x, curr_y));
if i < TOTAL_SEGMENTS - 1 {
let offset: i32 = if Self::is_vertical(curr_x, curr_y, max_x, max_y) {
1 + SEGMENT_GAP
} else {
2 + (SEGMENT_GAP * 2)
};
(curr_x, curr_y) = Self::step_along_edge(curr_x, curr_y, max_x, max_y, -offset);
}
}
positions
}
}