use std::collections::{HashSet, VecDeque};
use std::error::Error;
use std::io::{stdout, Stdout, Write};
use std::{thread, time::Duration};
use rand::Rng;
use termion::event::Key;
use termion::input::TermRead;
use termion::raw::{IntoRawMode, RawTerminal};
use termion::screen::IntoAlternateScreen;
use termion::{async_stdin, clear, color, cursor, terminal_size, AsyncReader};
#[derive(Debug, PartialEq)]
enum KeyPress {
Direction(Direction),
Q,
P,
Other,
None,
}
#[derive(Debug, PartialEq)]
enum Direction {
Up,
Down,
Left,
Right,
}
struct GameInput {
input: termion::input::Keys<AsyncReader>,
}
impl GameInput {
fn get_keypress(&mut self) -> KeyPress {
match self.input.by_ref().last() {
Some(Ok(key)) => match key {
Key::Char('q') => KeyPress::Q,
Key::Char('p') => KeyPress::P,
Key::Up => KeyPress::Direction(Direction::Up),
Key::Down => KeyPress::Direction(Direction::Down),
Key::Left => KeyPress::Direction(Direction::Left),
Key::Right => KeyPress::Direction(Direction::Right),
_ => KeyPress::Other,
},
_ => KeyPress::None,
}
}
}
struct GameOutput {
output: termion::screen::AlternateScreen<RawTerminal<Stdout>>,
}
impl GameOutput {
fn render(&mut self) {
self.output.flush().unwrap();
}
fn clear_screen(&mut self) {
write!(self.output, "{}{}", clear::All, cursor::Hide).unwrap();
}
fn reset_terminal(&mut self) {
write!(
self.output,
"{}{}{}",
termion::cursor::Show,
termion::cursor::Goto(1, 1),
termion::clear::All
)
.unwrap();
}
fn draw_border(&mut self, xmin: u16, xmax: u16, ymin: u16, ymax: u16) {
for i in xmin - 1..=xmax + 1 {
for j in ymin - 1..=ymax + 1 {
match i {
n if (n == xmin - 1 || n == xmax + 1) => write!(
self.output,
"{goto}{bgColor} ",
goto = cursor::Goto(i, j),
bgColor = color::Bg(color::White),
)
.unwrap(),
_ => (),
};
match j {
n if (n == ymin - 1 || n == ymax + 1) => write!(
self.output,
"{goto}{bgColor} ",
goto = cursor::Goto(i, j),
bgColor = color::Bg(color::White),
)
.unwrap(),
_ => (),
};
}
}
write!(self.output, "{}", color::Bg(color::Reset),).unwrap()
}
fn draw_food(&mut self, food: &GridCell) {
write!(
self.output,
"{goto}{bgColor}{fgColor}{food_char}{fgreset}{bgreset}",
goto = cursor::Goto(food.x, food.y),
bgColor = color::Bg(color::Red),
fgColor = color::Fg(color::LightGreen),
food_char = '\u{00D3}',
fgreset = color::Fg(color::Reset),
bgreset = color::Bg(color::Reset),
)
.unwrap();
}
fn draw_snake(&mut self, snake: &VecDeque<GridCell>) {
let mut segments: usize = 0;
let len = snake.len();
for segment in snake {
let segment_char = match segments {
0 => 'S',
num if num == len - 1 => 'e',
1 => 'n',
num if num == len - 2 => 'k',
_ => 'a',
};
write!(
self.output,
"{goto}{bgColor}{segment_char}{reset}",
goto = cursor::Goto(segment.x, segment.y),
bgColor = color::Bg(color::Green),
segment_char = segment_char,
reset = color::Bg(color::Reset),
)
.unwrap();
segments += 1;
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone)]
struct GridCell {
x: u16,
y: u16,
}
struct Game {
grid: HashSet<GridCell>,
snake: VecDeque<GridCell>,
food: GridCell,
direction: Direction,
input: GameInput,
output: GameOutput,
min_width: u16,
min_height: u16,
max_width: u16,
max_height: u16,
speed: u64,
}
impl Game {
fn new(
input: GameInput,
output: GameOutput,
term_max_x: u16,
term_max_y: u16,
playable: f64,
) -> Game {
let min_width = (2.0 + ((term_max_x - 1) as f64 * (1.0 - playable))) as u16;
let min_height = (2.0 + ((term_max_y - 1) as f64 * (1.0 - playable))) as u16;
let max_width = ((term_max_x - 1) as f64 * playable) as u16;
let max_height = ((term_max_y - 1) as f64 * playable) as u16;
let mut grid = HashSet::new();
for i in min_width..=max_width {
for j in min_height..=max_height {
grid.insert(GridCell { x: i, y: j });
}
}
let mut snake = VecDeque::new();
let init_size = 5;
for i in 1..=init_size {
snake.push_front(GridCell {
x: max_width - i,
y: (max_height + min_height) / 2,
});
}
for seg in &snake {
grid.remove(seg);
}
let food = Game::generate_random_food(&grid);
let direction = Direction::Left;
let speed = 60;
Game {
grid,
snake,
food,
direction,
input,
output,
min_width,
min_height,
max_width,
max_height,
speed,
}
}
fn generate_random_food(grid: &HashSet<GridCell>) -> GridCell {
let mut rng = rand::thread_rng();
let grid_list: Vec<&GridCell> = grid.iter().by_ref().collect();
let random_index = rng.gen_range(0..grid_list.len());
grid_list[random_index].clone()
}
fn move_snake(&mut self) {
let head = self.snake.front().unwrap();
let new_head = match self.direction {
Direction::Right => {
let x = if head.x == self.max_width {
self.min_width
} else {
head.x + 1
};
let y = head.y;
GridCell { x, y }
}
Direction::Left => {
let x = if head.x == self.min_width {
self.max_width
} else {
head.x - 1
};
let y = head.y;
GridCell { x, y }
}
Direction::Up => {
let x = head.x;
let y = if head.y == self.min_height {
self.max_height
} else {
head.y - 1
};
GridCell { x, y }
}
Direction::Down => {
let x = head.x;
let y = if head.y == self.max_height {
self.min_height
} else {
head.y + 1
};
GridCell { x, y }
}
};
self.grid.remove(&new_head);
self.snake.push_front(new_head);
let old_tail = self.snake.pop_back().unwrap();
self.grid.insert(old_tail);
}
fn check_collision(&mut self) -> bool {
let mut collision = false;
let head = self.snake.front().unwrap();
for segment in self.snake.range(1..) {
if head == segment {
collision = true;
break;
}
}
collision
}
fn vertical(&self) -> bool {
match self.direction {
Direction::Up | Direction::Down => true,
Direction::Left | Direction::Right => false,
}
}
fn play(&mut self) {
self.output.clear_screen();
self.output.render();
'mainloop: loop {
match self.input.get_keypress() {
KeyPress::P => loop {
thread::sleep(Duration::from_millis(10));
match self.input.get_keypress() {
KeyPress::None | KeyPress::Other => (),
_ => break,
}
},
KeyPress::Q => break 'mainloop,
KeyPress::Direction(Direction::Up) if !self.vertical() => {
self.direction = Direction::Up;
}
KeyPress::Direction(Direction::Down) if !self.vertical() => {
self.direction = Direction::Down;
}
KeyPress::Direction(Direction::Left) if self.vertical() => {
self.direction = Direction::Left;
}
KeyPress::Direction(Direction::Right) if self.vertical() => {
self.direction = Direction::Right;
}
_ => (),
}
self.move_snake();
if self.check_collision() {
break 'mainloop;
}
if self.snake.front().unwrap() == &self.food {
for seg in &self.snake {
self.grid.remove(seg);
}
self.snake.push_back(self.snake.back().unwrap().clone());
self.food = Game::generate_random_food(&self.grid);
self.grid.remove(&self.food);
}
self.output.clear_screen();
self.output.draw_border(
self.min_width,
self.max_width,
self.min_height,
self.max_height,
);
self.output.draw_snake(&self.snake);
self.output.draw_food(&self.food);
self.output.render();
thread::sleep(Duration::from_millis(if self.vertical() {
self.speed + 20
} else {
self.speed
}));
}
self.output.reset_terminal();
}
}
fn main() -> Result<(), Box<dyn Error>> {
let input = async_stdin().keys();
let output = stdout().into_raw_mode()?.into_alternate_screen()?;
let term_size = terminal_size()?;
let playable = 0.7;
let mut game = Game::new(
GameInput { input },
GameOutput { output },
term_size.0,
term_size.1,
playable,
);
game.play();
Ok(())
}