Initial commit

This commit is contained in:
Augusto Gunsch 2021-11-28 23:00:39 -03:00
commit c5d1d1dfbf
No known key found for this signature in database
GPG Key ID: F7EEFE29825C72DC
7 changed files with 556 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/target
*.lock

11
Cargo.toml Normal file
View File

@ -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"

49
src/event.rs Normal file
View File

@ -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<Key>,
}
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<Key> {
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<Key> {
let mut key = self.next();
while let Some(k) = self.next() {
key = Some(k);
}
key
}
}

49
src/food.rs Normal file
View File

@ -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
}
}

95
src/main.rs Normal file
View File

@ -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<Food> = 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());
}
}
}
}

183
src/snake.rs Normal file
View File

@ -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<Vec2>,
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<Food>) {
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;
}
}
}
}

167
src/vec2.rs Normal file
View File

@ -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));
}
}