use rand::Rng;
use std::collections::{HashSet, VecDeque};
use std::hash::Hash;
use std::{thread, time::Duration};
use crate::game_input::{self, KeyPress};
use crate::game_output;
const INIT_SNAKE_SIZE: u16 = 5;
#[derive(Debug, PartialEq)]
enum GameState {
PreGame,
InProgress,
GameOver,
}
#[derive(Debug, PartialEq)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
impl Direction {
fn vertical(&self) -> bool {
match self {
Self::Up | Self::Down => true,
Self::Left | Self::Right => false,
}
}
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub struct GridCell {
pub x: u16,
pub y: u16,
}
pub struct Game {
state: GameState,
grid: HashSet<GridCell>,
snake: VecDeque<GridCell>,
food: GridCell,
direction: Direction,
input: game_input::GameInput,
output: game_output::GameOutput,
min_width: u16,
min_height: u16,
max_width: u16,
max_height: u16,
speed: u64,
}
impl Game {
pub fn new(
input: game_input::GameInput,
output: game_output::GameOutput,
term_max_x: u16,
term_max_y: u16,
playable: f64,
speed: u64,
) -> Game {
let state = GameState::PreGame;
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 = Self::create_grid(min_width, min_height, max_width, max_height);
let snake = Self::create_snake(min_height, max_width, max_height);
for seg in &snake {
grid.remove(seg);
}
let food = Game::generate_random_food(&grid);
grid.remove(&food);
let direction = Direction::Left;
Game {
state,
grid,
snake,
food,
direction,
input,
output,
min_width,
min_height,
max_width,
max_height,
speed,
}
}
pub fn run(&mut self) {
loop {
match self.state {
GameState::PreGame => self.state = GameState::InProgress,
GameState::InProgress => {
let quit = self.play();
if quit {
break;
}
}
GameState::GameOver => {
let keep_playing = self.game_over();
if keep_playing {
self.restart();
} else {
break;
}
}
}
}
self.output.reset_terminal();
}
fn create_grid(min_w: u16, min_h: u16, max_w: u16, max_h: u16) -> HashSet<GridCell> {
let mut grid = HashSet::new();
for i in min_w..=max_w {
for j in min_h..=max_h {
grid.insert(GridCell { x: i, y: j });
}
}
grid
}
fn create_snake(min_h: u16, max_w: u16, max_h: u16) -> VecDeque<GridCell> {
let mut snake = VecDeque::new();
for i in 1..=INIT_SNAKE_SIZE {
snake.push_front(GridCell {
x: max_w - i,
y: (max_h + min_h) / 2,
});
}
snake
}
fn restart(&mut self) {
self.state = GameState::InProgress;
self.grid = Self::create_grid(
self.min_width,
self.min_height,
self.max_width,
self.max_height,
);
self.snake = Self::create_snake(self.min_height, self.max_width, self.max_height);
for seg in &self.snake {
self.grid.remove(seg);
}
self.food = Game::generate_random_food(&self.grid);
self.grid.remove(&self.food);
self.direction = Direction::Left;
}
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) -> GridCell {
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);
self.snake.pop_back().unwrap()
}
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 game_over(&mut self) -> bool {
let mut keep_playing = false;
self.output.clear_screen();
self.output.draw_game_over_message(self.snake.len());
self.output.render();
loop {
match self.input.get_keypress() {
KeyPress::Pause => {
keep_playing = true;
break;
}
KeyPress::Quit => break,
_ => (),
}
thread::sleep(Duration::from_millis(10));
}
keep_playing
}
fn play(&mut self) -> bool {
self.output.clear_screen();
self.output.draw_border(
self.min_width,
self.max_width,
self.min_height,
self.max_height,
);
self.output.draw_food(&self.food);
self.output.render();
'mainloop: loop {
match self.input.get_keypress() {
KeyPress::Pause => loop {
match self.input.get_keypress() {
KeyPress::None | KeyPress::Other => (),
_ => break,
}
thread::sleep(Duration::from_millis(10));
},
KeyPress::Quit => return true,
KeyPress::DirectionKey(Direction::Up) if !self.direction.vertical() => {
self.direction = Direction::Up;
}
KeyPress::DirectionKey(Direction::Down) if !self.direction.vertical() => {
self.direction = Direction::Down;
}
KeyPress::DirectionKey(Direction::Left) if self.direction.vertical() => {
self.direction = Direction::Left;
}
KeyPress::DirectionKey(Direction::Right) if self.direction.vertical() => {
self.direction = Direction::Right;
}
_ => (),
}
let old_tail = self.move_snake();
if self.check_collision() {
self.state = GameState::GameOver;
break 'mainloop;
}
if self.snake.front().unwrap() == &self.food {
for seg in &self.snake {
self.grid.remove(seg);
}
self.snake.push_back(old_tail);
self.grid.insert(self.food);
self.food = Game::generate_random_food(&self.grid);
self.output.draw_food(&self.food);
self.grid.remove(&self.food);
} else {
self.output.undraw(&old_tail);
self.grid.insert(old_tail);
}
self.output.draw_snake(&self.snake);
self.output.render();
thread::sleep(Duration::from_millis(if self.direction.vertical() {
self.speed + 20
} else {
self.speed
}));
}
false
}
}