commit c5d1d1dfbfca853a84003fa373c651393f8255eb Author: Augusto Gunsch Date: Sun Nov 28 23:00:39 2021 -0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..27aff38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +*.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..9515810 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "snakers" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tui = "0.16" +termion = "1.5" +rand = "0.8.4" diff --git a/src/event.rs b/src/event.rs new file mode 100644 index 0000000..7497841 --- /dev/null +++ b/src/event.rs @@ -0,0 +1,49 @@ +use std::io; +use std::thread; +use std::sync::mpsc; +use termion::input::TermRead; +use termion::event::Key; + +pub struct Events { + rx: mpsc::Receiver, +} + +impl Events { + pub fn new() -> Events { + let (tx, rx) = mpsc::channel(); + + thread::spawn(move || { + let stdin = io::stdin(); + + for key in stdin.keys() { + match key { + Ok(key) => if let Err(_) = tx.send(key) { return; }, + Err(_) => panic!("Failed to read stdin"), + } + } + }); + + Events { + rx, + } + } + + pub fn next(&self) -> Option { + let key = self.rx.try_recv(); + match key { + Ok(key) => Some(key), + Err(err) => match err { + mpsc::TryRecvError::Empty => None, + _ => panic!("Channel closed") + }, + } + } + + pub fn last(&self) -> Option { + let mut key = self.next(); + while let Some(k) = self.next() { + key = Some(k); + } + key + } +} diff --git a/src/food.rs b/src/food.rs new file mode 100644 index 0000000..f161789 --- /dev/null +++ b/src/food.rs @@ -0,0 +1,49 @@ +use crate::vec2::Vec2; +use rand::prelude::*; +use tui::widgets::canvas::{Context}; +use tui::style::Color; + +use crate::SCREEN_WIDTH; +use crate::SCREEN_HEIGHT; + +pub struct Food { + pos: Vec2, + spoil_in: i32, +} + +impl Food { + pub fn draw(&self, ctx: &mut Context) { + if self.spoiled() { + ctx.print(self.pos.x, self.pos.y, "*", Color::Green); + } else { + ctx.print(self.pos.x, self.pos.y, "*", Color::Red); + } + } + + pub fn new() -> Self { + let mut rng = thread_rng(); + let x = rng.gen_range(-(SCREEN_WIDTH/2.0) as i32..(SCREEN_WIDTH/2.0) as i32); + let y = rng.gen_range(-(SCREEN_HEIGHT/2.0) as i32..(SCREEN_HEIGHT/2.0) as i32); + + Self { + pos: Vec2::new(x as f64, y as f64), + spoil_in: rng.gen_range(50..300), + } + } + + pub fn spoiled(&self) -> bool { + self.spoil_in >= 0 + } + + pub fn rotten(&self) -> bool { + self.spoil_in <= -100 + } + + pub fn tick_spoil(&mut self) { + self.spoil_in -= 1; + } + + pub fn get_pos(&self) -> &Vec2 { + &self.pos + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..69e3fbe --- /dev/null +++ b/src/main.rs @@ -0,0 +1,95 @@ +mod snake; +mod vec2; +mod event; +mod food; + +use std::sync::Mutex; +use std::io; +use std::thread; +use std::time::Duration; +use tui::Terminal; +use tui::backend::TermionBackend; +use tui::widgets::{Block, Borders}; +use tui::style::Color; +use tui::widgets::canvas::{Canvas}; +use termion::screen::AlternateScreen; +use termion::raw::IntoRawMode; +use termion::event::Key; + +use event::Events; +use snake::Snake; +use vec2::Vec2; +use food::Food; + +const SCREEN_WIDTH: f64 = 360.0; +const SCREEN_HEIGHT: f64 = 180.0; + +fn main() -> Result<(), io::Error> { + let stdout = io::stdout().into_raw_mode()?; + let stdout = AlternateScreen::from(stdout); + let backend = TermionBackend::new(stdout); + let mut terminal = Terminal::new(backend)?; + + let mut snake = Snake::new(0.0, 0.0, Color::LightBlue); + let mut food_pool: Vec = Vec::new(); + + while food_pool.len() < 15 { + food_pool.push(Food::new()); + } + + let food_pool = Mutex::new(food_pool); + + let events = Events::new(); + loop { + thread::sleep(Duration::from_millis(15)); + + terminal.draw(|f| { + let canvas = Canvas::default() + .block(Block::default() + .title("Canvas") + .borders(Borders::ALL)) + .x_bounds([-SCREEN_WIDTH/2.0, SCREEN_WIDTH/2.0]) + .y_bounds([-SCREEN_HEIGHT/2.0, SCREEN_HEIGHT/2.0]) + .paint(|ctx| { + //ctx.draw(&snake); + snake.draw_big(ctx); + + for food in &*food_pool.lock().unwrap() { + food.draw(ctx); + } + }); + + f.render_widget(canvas, f.size()); + })?; + + snake.advance(); + + if snake.collides_with(&snake) { + return Ok(()); + } + + for key in events.last() { + match key { + Key::Ctrl(c) => if c == 'c' { return Ok(()); }, + Key::Up => snake.change_direction(Vec2::new(0.0, 1.0)), + Key::Left => snake.change_direction(Vec2::new(-1.0, 0.0)), + Key::Down => snake.change_direction(Vec2::new(0.0, -1.0)), + Key::Right => snake.change_direction(Vec2::new(1.0, 0.0)), + _ => {} + } + } + let mut food_pool = food_pool.lock().unwrap(); + snake.try_eat(&mut food_pool); + + for food in &mut *food_pool { + food.tick_spoil(); + } + + for i in 0..food_pool.len() { + if food_pool[i].rotten() { + food_pool.swap_remove(i); + food_pool.push(Food::new()); + } + } + } +} diff --git a/src/snake.rs b/src/snake.rs new file mode 100644 index 0000000..8e06cf8 --- /dev/null +++ b/src/snake.rs @@ -0,0 +1,183 @@ +use std::ptr; +use std::collections::LinkedList; +use tui::widgets::canvas::{Shape, Painter, Line, Context}; +use tui::style::Color; +use crate::vec2::Vec2; +use crate::food::Food; + +pub struct Snake { + body: LinkedList, + color: Color, + direction: Vec2, + speed: Vec2, +} + +impl Shape for Snake { + fn draw(&self, painter: &mut Painter) { + let mut body = self.body.iter(); + let mut last = body.next().unwrap(); + + for node in body { + let line = Line { + x1: last.x, + y1: last.y, + x2: node.x, + y2: node.y, + color: self.color, + }; + line.draw(painter); + last = node; + } + + let head = self.get_head(); + let head_point = Line { + x1: head.x, + y1: head.y, + x2: head.x, + y2: head.y, + color: Color::LightYellow + }; + + head_point.draw(painter); + } +} + +impl Snake { + fn draw_segment(&self, node1: Vec2, node2: Vec2, ctx: &mut Context) { + let x1 = node1.x as i32; + let x2 = node2.x as i32; + let y1 = node1.y as i32; + let y2 = node2.y as i32; + + let x_range = if x2 > x1 { + x1..=x2 + } else { + x2..=x1 + }; + let y_range = if y2 > y1 { + y1..=y2 + } else { + y2..=y1 + }; + + if x2 == x1 { + for y in y_range { + ctx.print(node1.x as f64, y as f64, "|", self.color); + } + } else { + for x in x_range { + ctx.print(x as f64, node2.y as f64, "-", self.color); + } + } + } + + pub fn draw_big(&self, ctx: &mut Context) { + let mut body = self.body.iter(); + let mut last = body.next().unwrap(); + + for node in body { + self.draw_segment(*last, *node, ctx); + last = node; + } + ctx.layer(); + + let head = self.get_head(); + ctx.print(head.x, head.y, "*", Color::LightYellow); + } + + pub fn new(x: f64, y: f64, color: Color) -> Self { + Self { + body: LinkedList::from([Vec2::new(x, y), Vec2::new(x-5.0, y)]), + color, + direction: Vec2::new(1.0, 0.0), + speed: Vec2::from_single(1.0), + } + } + + pub fn change_direction(&mut self, direction: Vec2) { + if self.direction != direction && self.direction != -direction { + self.direction = direction; + + // Create a new edge at current head position + let head = self.body.pop_front().unwrap(); + self.body.push_front(head); + self.body.push_front(head); + } + } + + pub fn grow(&mut self, multiplier: Vec2) { + let mut prev_tail = self.body.pop_back().unwrap(); + let tail = self.body.pop_back().unwrap(); + + if tail == prev_tail { + self.body.push_back(tail); + self.grow(multiplier); + return; + } + + prev_tail += tail.direction_to(&prev_tail) * multiplier; + self.body.push_back(tail); + self.body.push_back(prev_tail); + } + + pub fn advance(&mut self) { + let mut head = self.body.pop_front().unwrap(); + head += self.direction * self.speed; + self.body.push_front(head); + + self.grow(-self.speed); + } + + fn nodes_collide(target: &Vec2, node1: &Vec2, node2: &Vec2) -> bool { + if node1.x == node2.x && node1.x == target.x { + if node1.y < node2.y { + target.y >= node1.y && target.y <= node2.y + } else { + target.y <= node1.y && target.y >= node2.y + } + } else if node1.y == node2.y && node1.y == target.y { + if node1.x < node2.x { + target.x >= node1.x && target.x <= node2.x + } else { + target.x <= node1.x && target.x >= node2.x + } + } else { + false + } + } + + pub fn collides_with(&self, other: &Self) -> bool { + let my_head = self.get_head(); + + let mut nodes = other.body.iter(); + let mut last = nodes.next().unwrap(); + + // If checking against itself, skip first section + if ptr::eq(self, other) { + last = nodes.next().unwrap(); + } + + for node in nodes { + if Self::nodes_collide(my_head, last, node) { + return true; + } + last = node; + } + + false + } + + pub fn get_head(&self) -> &Vec2 { + self.body.iter().next().unwrap() + } + + pub fn try_eat(&mut self, food_pool: &mut Vec) { + for i in 0..food_pool.len() { + if (*food_pool[i].get_pos() - *self.get_head()).len() <= self.speed.x + 2.0 { + food_pool.swap_remove(i); + self.grow(Vec2::from_single(12.0)); + break; + } + } + } +} diff --git a/src/vec2.rs b/src/vec2.rs new file mode 100644 index 0000000..ff1878f --- /dev/null +++ b/src/vec2.rs @@ -0,0 +1,167 @@ +use std::ops::{Add, AddAssign, Neg, Mul, Div, Sub, SubAssign}; +use std::cmp::PartialEq; + +#[derive(Debug, Copy, Clone)] +pub struct Vec2 { + pub x: f64, + pub y: f64 +} + +impl Vec2 { + pub fn new(x: f64, y: f64) -> Self { + Self { x, y } + } + + pub fn from_single(a: f64) -> Self { + Self { x: a, y: a } + } + + pub fn origin() -> Self { + Self { x: 0.0, y: 0.0 } + } + + pub fn len(&self) -> f64 { + (self.x.powf(2.0) + self.y.powf(2.0)).sqrt() + } + + pub fn normal(&self) -> Self { + let hipothenuse = self.len(); + if hipothenuse > 0.0 { + Self { + x: self.x / hipothenuse, + y: self.y / hipothenuse, + } + } else { + self.clone() + } + } + + pub fn direction_to(&self, other: &Self) -> Self { + (*other - *self).normal() + } +} + +impl Add for Vec2 { + type Output = Self; + + fn add(self, rhs: Self) -> Self { + Self { x: self.x + rhs.x, y: self.y + rhs.y } + } +} + +impl Sub for Vec2 { + type Output = Self; + + fn sub(self, rhs: Self) -> Self { + Self {x: self.x - rhs.x, y: self.y - rhs.y} + } +} + +impl AddAssign for Vec2 { + fn add_assign(&mut self, rhs: Self) { + self.x += rhs.x; + self.y += rhs.y; + } +} + +impl SubAssign for Vec2 { + fn sub_assign(&mut self, rhs: Self) { + self.x -= rhs.x; + self.y -= rhs.y; + } +} + +impl PartialEq for Vec2 { + fn eq(&self, other: &Self) -> bool { + self.x == other.x && self.y == other.y + } +} + +impl Mul for Vec2 { + type Output = Self; + + fn mul(self, rhs: Self) -> Self { + Self { x: self.x * rhs.x, y: self.y * rhs.y } + } +} + +impl Div for Vec2 { + type Output = Self; + + fn div(self, rhs: Self) -> Self { + Self { + x: if rhs.x > 0.0 { self.x / rhs.x } else { 0.0 }, + y: if rhs.y > 0.0 { self.y / rhs.y } else { 0.0 }, + } + } +} + +impl Neg for Vec2 { + type Output = Self; + + fn neg(self) -> Self { + Self { + x: -self.x, + y: -self.y, + } + } +} + +#[cfg(test)] +mod tests { + use super::Vec2; + + #[test] + fn direction_0deg() { + let v1 = Vec2::origin(); + let v2 = Vec2::new(2.0, 0.0); + let result = Vec2::new(1.0, 0.0); + + assert_eq!(result, v1.direction_to(&v2)); + } + + #[test] + fn direction_45deg() { + let v1 = Vec2::origin(); + let v2 = Vec2::from_single(1.0); + let result = Vec2::from_single(1.0/(2.0_f64).sqrt()); + + assert_eq!(result, v1.direction_to(&v2)); + } + + #[test] + fn direction_90deg() { + let v1 = Vec2::origin(); + let v2 = Vec2::new(0.0, 23.121); + let result = Vec2::new(0.0, 1.0); + + assert_eq!(result, v1.direction_to(&v2)); + } + + #[test] + fn direction_180deg() { + let v1 = Vec2::origin(); + let v2 = Vec2::new(-82.1, 0.0); + let result = Vec2::new(-1.0, 0.0); + + assert_eq!(result, v1.direction_to(&v2)); + } + + #[test] + fn direction_270deg() { + let v1 = Vec2::origin(); + let v2 = Vec2::new(0.0, -2.8); + let result = Vec2::new(0.0, -1.0); + + assert_eq!(result, v1.direction_to(&v2)); + } + + #[test] + fn direction_not_origin() { + let v1 = Vec2::from_single(1.0); + let v2 = Vec2::from_single(3.0); + let result = Vec2::from_single(1.0/(2.0_f64).sqrt()); + + assert_eq!(result, v1.direction_to(&v2)); + } +}