rs_snake 1.0.1

The classic 'Snake' game as a terminal application.
Documentation
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,
}

// TODO: still need to figure out how to abstract this part properly
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,
        }
    }
}

// TODO: still need to figure out how to abstract this part properly
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;

        // Initialize game grid
        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 });
            }
        }

        // Initialize snake
        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,
            });
        }

        // Update grid by removing cells occupied by snake
        for seg in &snake {
            grid.remove(seg);
        }

        // Generate food in a random cell
        let food = Game::generate_random_food(&grid);

        // Initialize starting movement direction
        let direction = Direction::Left;

        // Initialize game speed
        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) {
        // Get current head
        let head = self.snake.front().unwrap();

        // Create new head based on direction
        let new_head = match self.direction {
            // If snake is at an edge, wrap around to other side
            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 }
            }
        };

        // Remove new head from grid
        self.grid.remove(&new_head);
        // Push new head to start of snake
        self.snake.push_front(new_head);

        // Remove old tail from snake
        let old_tail = self.snake.pop_back().unwrap();
        // Put old tail back into grid
        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) {
        // Initial render to clear screen
        self.output.clear_screen();
        self.output.render();

        // Start of main loop
        'mainloop: loop {
            // Handle user input
            match self.input.get_keypress() {
                // Pause the game
                KeyPress::P => loop {
                    // Sleep here to let input thread have some control
                    thread::sleep(Duration::from_millis(10));
                    match self.input.get_keypress() {
                        KeyPress::None | KeyPress::Other => (),
                        _ => break,
                    }
                },
                // Quit the game
                KeyPress::Q => break 'mainloop,
                // Get pressed direction key
                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);
            }

            // Clear screen
            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
            }));
        }

        // Reset terminal
        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(())
}