Initial commit
This commit is contained in:
commit
c5d1d1dfbf
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
*.lock
|
|
@ -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"
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue