diff --git a/examples/program_examples/snake/src/main.rs b/examples/program_examples/snake/src/main.rs index ea782cc..0cc6abd 100644 --- a/examples/program_examples/snake/src/main.rs +++ b/examples/program_examples/snake/src/main.rs @@ -1,134 +1,187 @@ -use std::collections::HashMap; +//! The snake game. +//! +//! This is not a properly designed game! Mainly game loop, input events +//! handling, UI separation, ... The main purpose of this example is to +//! test the `crossterm` crate and demonstrate some of the capabilities. +use std::convert::TryFrom; use std::io::{stdout, Write}; use std::iter::Iterator; use std::{thread, time}; +use crossterm::{ + execute, input, style, AsyncReader, Clear, ClearType, Color, Crossterm, Goto, InputEvent, + KeyEvent, PrintStyledFont, RawScreen, Result, Show, +}; + use map::Map; use snake::Snake; -use variables::{Direction, Position, Size}; - -use crossterm::{ - execute, input, style, AsyncReader, Clear, ClearType, Color, Colorize, Crossterm, Goto, - InputEvent, KeyEvent, PrintStyledFont, RawScreen, Result, Show, -}; +use types::Direction; mod map; mod messages; mod snake; -mod variables; +mod types; + +/// An input (user) event. +#[derive(Debug)] +pub enum Event { + /// User wants to change the snake direction. + UpdateSnakeDirection(Direction), + /// User wants to quite the game. + QuitGame, +} fn main() -> Result<()> { - let map_size = ask_size()?; - - // screen has to be in raw mode in order for the key presses not to be printed to the screen. - let _raw = RawScreen::into_raw_mode(); + // Print the welcome screen and ask for the map size. let crossterm = Crossterm::new(); + let (map_width, map_height) = ask_for_map_size(crossterm.terminal().terminal_size())?; + // Switch screen to the raw mode to avoid printing key presses on the screen + // and hide the cursor. + let _raw = RawScreen::into_raw_mode(); crossterm.cursor().hide()?; - // initialize free positions for the game map. - let mut free_positions: HashMap = - HashMap::with_capacity((map_size.width * map_size.height) as usize); + // Draw the map border. + let mut map = Map::new(map_width, map_height); + map.draw_border()?; - // render the map - let mut map = Map::new(map_size); - map.render_map(&mut free_positions)?; - - let mut snake = Snake::new(); - - // remove snake coords from free positions. - for part in snake.get_parts().iter() { - free_positions.remove_entry(format!("{},{}", part.position.x, part.position.y).as_str()); - } - - map.spawn_food(&free_positions)?; + // Create a new snake, draw it and spawn some food. + let mut snake = Snake::new(map_width, map_height); + snake.draw()?; + map.spawn_food(&snake)?; + // Game loop let mut stdin = crossterm.input().read_async(); - let mut snake_direction = Direction::Right; - - // start the game loop; draw, move snake and spawn food. loop { - if let Some(new_direction) = update_direction(&mut stdin) { - snake_direction = new_direction; - } + // Handle the next user input event (if there's any). + match next_event(&mut stdin, snake.direction()) { + Some(Event::UpdateSnakeDirection(direction)) => snake.set_direction(direction), + Some(Event::QuitGame) => break, + _ => {} + }; - snake.move_snake(&snake_direction, &mut free_positions)?; - - if map.is_out_of_bounds(snake.snake_parts[0].position) { + // Update the snake (move & redraw). If it returns `false` -> new head + // collides with the snake body -> can't eat self -> quit the game loop. + if !snake.update()? { break; } - snake.draw_snake()?; - - if snake.has_eaten_food(map.foot_pos) { - map.spawn_food(&free_positions)?; + // Check if the snake ate some food. + if snake.head_position() == map.food_position() { + // Tell the snake to grow ... + snake.set_ate_food(true); + // ... and spawn new food. + map.spawn_food(&snake)?; } - thread::sleep(time::Duration::from_millis(400)); + // Check if the snake head position is out of bounds. + if map.is_position_out_of_bounds(snake.head_position()) { + break; + } + + // Wait for some time. + thread::sleep(time::Duration::from_millis(200)); } - game_over_screen() + + show_game_over_screen(snake.len()) } -fn update_direction(reader: &mut AsyncReader) -> Option { - let pressed_key = reader.next(); - - if let Some(InputEvent::Keyboard(KeyEvent::Char(character))) = pressed_key { - return Some(match character { - 'w' => Direction::Up, - 'a' => Direction::Left, - 's' => Direction::Down, - 'd' => Direction::Right, - _ => return None, - }); - } else if let Some(InputEvent::Keyboard(key)) = pressed_key { - return Some(match key { - KeyEvent::Up => Direction::Up, - KeyEvent::Left => Direction::Left, - KeyEvent::Down => Direction::Down, - KeyEvent::Right => Direction::Right, - _ => return None, - }); +/// Returns a next user event (if there's any). +fn next_event(reader: &mut AsyncReader, snake_direction: Direction) -> Option { + // The purpose of this loop is to consume events that are not actionable. Let's + // say that the snake is moving to the right and the user hits the right arrow + // key three times and then the up arrow key. The up arrow key would be handled + // in the 4th iteration of the game loop. That's not what we really want and thus + // we are consuming all events here till we find an actionable one or none. + while let Some(event) = reader.next() { + match event { + InputEvent::Keyboard(KeyEvent::Char(character)) => { + if let Ok(new_direction) = Direction::try_from(character) { + if snake_direction.can_change_to(new_direction) { + return Some(Event::UpdateSnakeDirection(new_direction)); + } + } + } + InputEvent::Keyboard(KeyEvent::Esc) => return Some(Event::QuitGame), + InputEvent::Keyboard(key) => { + if let Ok(new_direction) = Direction::try_from(key) { + if snake_direction.can_change_to(new_direction) { + return Some(Event::UpdateSnakeDirection(new_direction)); + } + } + } + _ => {} + }; } - None } -fn ask_size() -> Result { +/// Asks the user for a single map dimension. If the input can't be parsed or is outside +/// of the `min..=default_max` range, `min` or `default_max` is returned. +fn ask_for_map_dimension(name: &str, min: u16, default_max: u16, pos: (u16, u16)) -> Result { + let message = format!( + "Enter map {} (min: {}, default/max: {}):", + name, min, default_max + ); + let message_len = message.chars().count() as u16; + + execute!( + stdout(), + Goto(pos.0, pos.1), + PrintStyledFont(style(message).with(Color::Green)), + Goto(pos.0 + message_len + 1, pos.1) + )?; + + let dimension = input() + .read_line()? + .parse::() + .map(|x| { + if x > default_max { + default_max + } else if x < min { + min + } else { + x + } + }) + .unwrap_or(default_max); + + Ok(dimension) +} + +/// Prints the welcome screen and asks the user for the map size. +fn ask_for_map_size(terminal_size: (u16, u16)) -> Result<(u16, u16)> { + let mut row = 0u16; + execute!( stdout(), Clear(ClearType::All), - Goto(0, 0), - PrintStyledFont(style(format!("{}", messages::SNAKERS.join("\n\r"))).with(Color::Cyan)), - Goto(0, 15), - PrintStyledFont("Enter map width:".green().on_yellow()), - Goto(17, 15) + Goto(0, row), + PrintStyledFont(style(format!("{}", messages::SNAKE.join("\n\r"))).with(Color::Cyan)) )?; - let width = input().read_line().unwrap(); - - execute!( - stdout(), - PrintStyledFont("\r\nEnter map height:".green().on_yellow()), - Goto(17, 17) - )?; - - let height = input().read_line().unwrap(); - - // parse input - let parsed_width = width.parse::().unwrap(); - let parsed_height = height.parse::().unwrap(); + row += messages::SNAKE.len() as u16 + 2; + let width = ask_for_map_dimension("width", 10, terminal_size.0, (0, row))?; + row += 2; + let height = ask_for_map_dimension("height", 10, terminal_size.1, (0, row))?; execute!(stdout(), Clear(ClearType::All))?; - Ok(Size::new(parsed_width, parsed_height)) + Ok((width, height)) } -fn game_over_screen() -> Result<()> { +/// Prints the game over screen. +fn show_game_over_screen(score: usize) -> Result<()> { execute!( stdout(), Clear(ClearType::All), Goto(0, 0), - PrintStyledFont(style(format!("{}", messages::END_MESSAGE.join("\n\r"))).with(Color::Red)), - Show + PrintStyledFont(style(format!("{}", messages::GAME_OVER.join("\n\r"))).with(Color::Red)), + Goto(0, messages::GAME_OVER.len() as u16 + 2), + PrintStyledFont( + style(format!("Your score is {}. You can do better!", score)).with(Color::Red) + ), + Show, + Goto(0, messages::GAME_OVER.len() as u16 + 4) ) } diff --git a/examples/program_examples/snake/src/map.rs b/examples/program_examples/snake/src/map.rs index 5fb9958..e552ae3 100644 --- a/examples/program_examples/snake/src/map.rs +++ b/examples/program_examples/snake/src/map.rs @@ -1,64 +1,124 @@ -use std::collections::HashMap; use std::io::{stdout, Write}; use crossterm::{queue, Colorize, Goto, PrintStyledFont, Result}; -use rand; -use rand::distributions::{IndependentSample, Range}; +use rand::{ + self, + distributions::{IndependentSample, Range}, +}; -use super::variables::{Position, Size}; +use super::snake::Snake; +use super::types::Position; +/// A food. +struct Food { + /// The food position. + position: Position, +} + +impl Food { + /// Creates a new food with the given `position`. + fn new(position: Position) -> Self { + Food { position } + } + + /// Draws the food. + fn draw(&self) -> Result<()> { + queue!( + stdout(), + Goto(self.position.x, self.position.y), + PrintStyledFont("❖".green()) + ) + } +} + +/// A world map. pub struct Map { - pub size: Size, - pub foot_pos: Position, + /// The map width. + width: u16, + /// The map height. + height: u16, + /// Food. + food: Option, } impl Map { - pub fn new(size: Size) -> Self { + /// Crates a new map with the given `width` & `height`. + pub fn new(width: u16, height: u16) -> Self { Map { - size: size, - foot_pos: Position::new(0, 0), + width, + height, + food: None, } } - // render the map on the screen. - pub fn render_map(&mut self, free_positions: &mut HashMap) -> Result<()> { - for y in 0..self.size.height { - for x in 0..self.size.height { - if (y == 0 || y == self.size.height - 1) || (x == 0 || x == self.size.width - 1) { - queue!( - stdout(), - Goto(x as u16, y as u16), - PrintStyledFont("█".magenta()) - )?; - } else { - free_positions.insert(format!("{},{}", x, y), Position::new(x, y)); - } - } + /// Draws the map border. + pub fn draw_border(&self) -> Result<()> { + for y in 0..self.height { + queue!( + stdout(), + Goto(0, y), + PrintStyledFont("█".magenta()), + Goto(self.width - 1, y), + PrintStyledFont("█".magenta()) + )?; + } + for x in 0..self.width { + queue!( + stdout(), + Goto(x, 0), + PrintStyledFont("█".magenta()), + Goto(x, self.height - 1), + PrintStyledFont("█".magenta()) + )?; } Ok(()) } - pub fn is_out_of_bounds(&self, new_pos: Position) -> bool { - if (new_pos.x == 0 || new_pos.x == self.size.width) - || (new_pos.y == 0 || new_pos.y == self.size.height) - { - return true; - } - - return false; + /// Check if the given `position` is out of bounds. + /// + /// Every map has a border and out of bounds means that the position + /// is inside the border. + pub fn is_position_out_of_bounds(&self, position: Position) -> bool { + position.x == 0 + || position.y == 0 + || position.x >= self.width - 1 + || position.y >= self.height - 1 } - pub fn spawn_food(&mut self, free_positions: &HashMap) -> Result<()> { - let index = Range::new(0, free_positions.len()).ind_sample(&mut rand::thread_rng()); - self.foot_pos = free_positions.values().skip(index).next().unwrap().clone(); - self.draw_food() + /// Returns food position. + /// + /// # Panics + /// + /// It's forbidden to call this function before calling the `spawn_food()` function. + /// Considered as a programmer error and will panic. + pub fn food_position(&self) -> Position { + self.food.as_ref().unwrap().position } - fn draw_food(&self) -> Result<()> { - queue!( - stdout(), - Goto(self.foot_pos.x as u16, self.foot_pos.y as u16), - PrintStyledFont("$".green()) - ) + /// Spawns a new food and draws it. + /// + /// The `snake` argument is used to check that the food position doesn't collide + /// with any snake fragment. + pub fn spawn_food(&mut self, snake: &Snake) -> Result<()> { + let free_area_width = self.width - 2; + let free_area_height = self.height - 2; + let free_area_position_count = free_area_width * free_area_height; + + // Naive implementation, but enough for an example + let position = loop { + let index = Range::new(0, free_area_position_count).ind_sample(&mut rand::thread_rng()); + let x = index % free_area_width + 1; + let y = index / free_area_width + 1; + let position = (x, y).into(); + + if !snake.fragment_exists_at_position(position) { + break position; + } + }; + + let food = Food::new(position); + food.draw()?; + self.food = Some(food); + return Ok(()); } } diff --git a/examples/program_examples/snake/src/messages.rs b/examples/program_examples/snake/src/messages.rs index 57cc544..9471eb4 100644 --- a/examples/program_examples/snake/src/messages.rs +++ b/examples/program_examples/snake/src/messages.rs @@ -1,29 +1,28 @@ -pub const SNAKERS: [&str; 11] = [ - " ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ", - "▐░░░░░░░░░░░▌▐░░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌", - "▐░█▀▀▀▀▀▀▀▀▀ ▐░▌░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀▀▀", - "▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌▐░▌", - "▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄█░▌▐░▌░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░█▄▄▄▄▄▄▄▄▄", - "▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌", - " ▀▀▀▀▀▀▀▀▀█░▌▐░▌ ▐░▌ ▐░▌▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀█░█▀▀ ▀▀▀▀▀▀▀▀▀█░▌", - " ▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌", - " ▄▄▄▄▄▄▄▄▄█░▌▐░▌ ▐░▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▄▄▄▄▄▄▄▄▄█░▌", - "▐░░░░░░░░░░░▌▐░▌ ▐░░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌", - " ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀ ▀ ▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀", +//! Game ASCII art messages. +//! +//! These messages were generated on the http://patorjk.com/software/taag/ site. + +pub const SNAKE: [&str; 9] = [ + " ███████╗███╗ ██╗ █████╗ ██╗ ██╗███████╗ ", + " ██╔════╝████╗ ██║██╔══██╗██║ ██╔╝██╔════╝ ", + " ███████╗██╔██╗ ██║███████║█████╔╝ █████╗ ", + " ╚════██║██║╚██╗██║██╔══██║██╔═██╗ ██╔══╝ ", + " ███████║██║ ╚████║██║ ██║██║ ██╗███████╗ ", + " ╚══════╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝╚══════╝ ", + " ", + " Snake controls: WASD or arrow keys. ", + " Hit Esc to quit the game when playing. ", ]; -pub const END_MESSAGE: [&str; 11] = -[ - " ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄ ▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄ ▄ ▄▄▄▄▄▄▄▄▄▄▄ ▄▄▄▄▄▄▄▄▄▄▄ ", - " ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌▐░░▌ ▐░░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ", - " ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌▐░▌░▌ ▐░▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀▀▀▀█░▌ ", - " ▐░▌ ▐░▌ ▐░▌▐░▌▐░▌ ▐░▌▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ", - " ▐░▌ ▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ ", - " ▐░▌▐░░░░░░░░▌▐░░░░░░░░░░░▌▐░▌ ▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░░░░░░░░░░░▌ ", - " ▐░▌ ▀▀▀▀▀▀█░▌▐░█▀▀▀▀▀▀▀█░▌▐░▌ ▀ ▐░▌▐░█▀▀▀▀▀▀▀▀▀ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░█▀▀▀▀▀▀▀▀▀ ▐░█▀▀▀▀█░█▀▀ ", - " ▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ▐░▌ ", - " ▐░█▄▄▄▄▄▄▄█░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░█▄▄▄▄▄▄▄▄▄ ▐░█▄▄▄▄▄▄▄█░▌ ▐░▐░▌ ▐░█▄▄▄▄▄▄▄▄▄ ▐░▌ ▐░▌ ", - " ▐░░░░░░░░░░░▌▐░▌ ▐░▌▐░▌ ▐░▌▐░░░░░░░░░░░▌ ▐░░░░░░░░░░░▌ ▐░▌ ▐░░░░░░░░░░░▌▐░▌ ▐░▌ ", - " ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀▀▀▀▀▀▀▀▀▀▀ ▀ ▀ ", - +pub const GAME_OVER: [&str; 10] = [ + " ▄████ ▄▄▄ ███▄ ▄███▓▓█████ ▒█████ ██▒ █▓▓█████ ██▀███ ", + " ██▒ ▀█▒▒████▄ ▓██▒▀█▀ ██▒▓█ ▀ ▒██▒ ██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒ ", + " ▒██░▄▄▄░▒██ ▀█▄ ▓██ ▓██░▒███ ▒██░ ██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒ ", + " ░▓█ ██▓░██▄▄▄▄██ ▒██ ▒██ ▒▓█ ▄ ▒██ ██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄ ", + " ░▒▓███▀▒ ▓█ ▓██▒▒██▒ ░██▒░▒████▒ ░ ████▓▒░ ▒▀█░ ░▒████▒░██▓ ▒██▒ ", + " ░▒ ▒ ▒▒ ▓▒█░░ ▒░ ░ ░░░ ▒░ ░ ░ ▒░▒░▒░ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░ ", + " ░ ░ ▒ ▒▒ ░░ ░ ░ ░ ░ ░ ░ ▒ ▒░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░ ", + " ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░░ ░ ░░ ░ ", + " ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ", + " ░ ", ]; diff --git a/examples/program_examples/snake/src/snake.rs b/examples/program_examples/snake/src/snake.rs index 658a703..9f29efc 100644 --- a/examples/program_examples/snake/src/snake.rs +++ b/examples/program_examples/snake/src/snake.rs @@ -1,91 +1,260 @@ -use std::collections::HashMap; +use std::fmt; use crossterm::Result; -use super::variables::{Direction, Position}; +use super::types::{Direction, Position}; -pub struct Part { - pub position: Position, +/// A snake fragment kind. +/// +/// Describes how a snake fragment is visualized. +#[derive(Debug, Clone, Copy)] +enum FragmentKind { + Horizontal, + Vertical, + Left, + Right, + Up, + Down, + UpToRight, + UpToLeft, + DownToRight, + DownToLeft, + LeftToUp, + LeftToDown, + RightToUp, + RightToDown, } -impl Part { - pub fn new(x: usize, y: usize) -> Part { - Part { - position: Position::new(x, y), +impl FragmentKind { + /// Creates a snake fragment kind from the snake head direction. + fn with_head_direction(direction: Direction) -> Self { + match direction { + Direction::Up => FragmentKind::Up, + Direction::Down => FragmentKind::Down, + Direction::Left => FragmentKind::Left, + Direction::Right => FragmentKind::Right, } } } +impl fmt::Display for FragmentKind { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let repr = match self { + FragmentKind::Horizontal => "━", + FragmentKind::Vertical => "┃", + FragmentKind::Up => "╻", + FragmentKind::Down => "╹", + FragmentKind::Left => "╺", + FragmentKind::Right => "╸", + FragmentKind::UpToRight | FragmentKind::LeftToDown => "┏", + FragmentKind::UpToLeft | FragmentKind::RightToDown => "┓", + FragmentKind::DownToRight | FragmentKind::LeftToUp => "┗", + FragmentKind::DownToLeft | FragmentKind::RightToUp => "┛", + }; + write!(f, "{}", repr) + } +} + +/// A snake fragment. +#[derive(Debug)] +struct Fragment { + /// The fragment position in the terminal. + position: Position, + /// The fragment kind. + kind: FragmentKind, +} + +impl Fragment { + /// Creates a `Fragment` from the given `position` and `kind`. + fn new>(position: P, kind: FragmentKind) -> Fragment { + Fragment { + position: position.into(), + kind, + } + } + + /// Draws the fragment. + fn draw(&self) -> Result<()> { + self.position.draw(self.kind) + } + + /// Clears the fragment from the screen. + fn clear(&self) -> Result<()> { + self.position.clear_char() + } + + /// Creates a new head `Fragment` from the given snake `direction`. + /// + /// Assumes that the `self` is the current snake head. + fn with_head_direction(&self, direction: Direction) -> Fragment { + let position = match direction { + Direction::Up => (self.position.x, self.position.y - 1), + Direction::Down => (self.position.x, self.position.y + 1), + Direction::Left => (self.position.x - 1, self.position.y), + Direction::Right => (self.position.x + 1, self.position.y), + }; + + Fragment { + position: position.into(), + kind: FragmentKind::with_head_direction(direction), + } + } + + /// Updates the fragment kind with the given snake `direction` and redraws + /// it. + /// + /// Assumes that the `self` is the current snake head (soon to be the old + /// one = 2nd fragment). + /// + /// # Examples + /// + /// Let's say we have a snake `━╸` that is moving to the `Right` and the new + /// direction is `Up`. We can't create / draw a new head only, because then + /// the snake will look like this one: + /// + /// ``` + /// ╻ + /// ━╸ + /// ``` + /// + /// We have to change the current head kind `╻` to `┛` to get a snake like this + /// one: + /// + /// ``` + /// ╻ + /// ━┛ + /// ``` + fn update_with_head_direction(&mut self, direction: Direction) -> Result<()> { + let new_kind = match (self.kind, direction) { + (FragmentKind::Left, Direction::Left) => FragmentKind::Horizontal, + (FragmentKind::Right, Direction::Right) => FragmentKind::Horizontal, + (FragmentKind::Up, Direction::Up) => FragmentKind::Vertical, + (FragmentKind::Down, Direction::Down) => FragmentKind::Vertical, + (FragmentKind::Left, Direction::Up) => FragmentKind::LeftToUp, + (FragmentKind::Left, Direction::Down) => FragmentKind::LeftToDown, + (FragmentKind::Right, Direction::Up) => FragmentKind::RightToUp, + (FragmentKind::Right, Direction::Down) => FragmentKind::RightToDown, + (FragmentKind::Up, Direction::Left) => FragmentKind::UpToLeft, + (FragmentKind::Up, Direction::Right) => FragmentKind::UpToRight, + (FragmentKind::Down, Direction::Left) => FragmentKind::DownToLeft, + (FragmentKind::Down, Direction::Right) => FragmentKind::DownToRight, + (kind, _) => kind, + }; + self.kind = new_kind; + self.draw() + } +} + +/// A snake. +#[derive(Debug)] pub struct Snake { - pub snake_parts: Vec, - pub parent_pos: Position, + /// The snake fragments. Head index is always 0. + fragments: Vec, + /// The current direction. + direction: Direction, + /// Says if the snake ate some food or not. + ate_food: bool, } impl Snake { - pub fn new() -> Snake { + /// Creates a new snake. + pub fn new(map_width: u16, map_height: u16) -> Snake { + let center_x = map_width / 2; + let center_y = map_height / 2; + + let parts = vec![ + Fragment::new((center_x, center_y), FragmentKind::Right), + Fragment::new((center_x - 1, center_y), FragmentKind::Left), + ]; + Snake { - snake_parts: vec![Part::new(9, 10), Part::new(8, 10)], - parent_pos: Position::new(0, 0), + fragments: parts, + direction: Direction::Right, + ate_food: false, } } - pub fn move_snake( - &mut self, - direction: &Direction, - free_positions: &mut HashMap, - ) -> Result<()> { - let count = self.snake_parts.len(); + /// The current snake length. + pub fn len(&self) -> usize { + self.fragments.len() + } - for (index, ref mut snake_part) in self.snake_parts.iter_mut().enumerate() { - if index == count - 1 { - snake_part.position.remove()?; - free_positions.insert( - format!("{},{}", snake_part.position.x, snake_part.position.y), - snake_part.position, - ); - } + /// The current snake direction. + pub fn direction(&self) -> Direction { + self.direction + } - if index == 0 { - self.parent_pos = snake_part.position.clone(); + /// Updates the snake direction. + pub fn set_direction(&mut self, direction: Direction) { + self.direction = direction; + } - match direction { - &Direction::Up => snake_part.position.y -= 1, - &Direction::Down => snake_part.position.y += 1, - &Direction::Left => snake_part.position.x -= 1, - &Direction::Right => snake_part.position.x += 1, - } + /// Sets if the snake ate food. + /// + /// If set to `true`, the next `update()` call will move the head, but + /// won't move the tail. + pub fn set_ate_food(&mut self, ate_food: bool) { + self.ate_food = ate_food; + } - free_positions.remove_entry( - format!("{},{}", snake_part.position.x, snake_part.position.y).as_str(), - ); - } else { - let new_pos = self.parent_pos.clone(); - self.parent_pos = snake_part.position.clone(); - snake_part.position = new_pos; + /// The snake head position. + pub fn head_position(&self) -> Position { + self.fragments[0].position + } + + /// Returns `true` if there's an existing snake fragment at the + /// given `position`. + pub fn fragment_exists_at_position(&self, position: Position) -> bool { + for fragment in &self.fragments { + if fragment.position == position { + return true; } } - Ok(()) + false } - pub fn draw_snake(&mut self) -> Result<()> { - for snake_part in self.snake_parts.iter_mut() { - snake_part.position.draw("■")?; - } - Ok(()) - } + /// Moves the snake and redraws updated fragments only. + /// + /// Returns `Ok(true)` if the snake was updated. Returns `Ok(false)` if the + /// new head position collides with the existing snake fragments. + pub fn update(&mut self) -> Result { + // Get the current head, update fragment kind and redraw it. + // Let's say that the snake is moving down (╻). We want to draw a full + // vertical line (┃) at the current head position. + let current_head = self.fragments.first_mut().unwrap(); + current_head.update_with_head_direction(self.direction)?; - pub fn has_eaten_food(&mut self, food_pos: Position) -> bool { - if self.snake_parts[0].position.x == food_pos.x - && self.snake_parts[0].position.y == food_pos.y - { - self.snake_parts.push(Part::new(1, 1)); - return true; + // Create & draw the new head. + let new_head = current_head.with_head_direction(self.direction); + new_head.draw()?; + + // Check if the new head collides with existing snake fragments + if self.fragment_exists_at_position(new_head.position) { + // Collision, new snake head collides with existing fragment + return Ok(false); } - return false; + self.fragments.insert(0, new_head); + + if self.ate_food { + // Snake ate some food, we just set the state to false and + // do nothing (no tail movement) + self.ate_food = false; + } else { + // Snake didn't eat any food, tail is moving, which means that + // we are going to clear the fragment from the screen and drop it + let tail = self.fragments.pop().unwrap(); + tail.clear()?; + } + + Ok(true) } - pub fn get_parts(&self) -> &Vec { - return &self.snake_parts; + /// Redraws the snake. + pub fn draw(&self) -> Result<()> { + for fragment in &self.fragments { + fragment.draw()?; + } + Ok(()) } } diff --git a/examples/program_examples/snake/src/types.rs b/examples/program_examples/snake/src/types.rs new file mode 100644 index 0000000..23ab049 --- /dev/null +++ b/examples/program_examples/snake/src/types.rs @@ -0,0 +1,103 @@ +use std::convert::TryFrom; +use std::fmt::Display; +use std::io::{stdout, Write}; + +use crossterm::{style, Color, Crossterm, KeyEvent, Result, TerminalCursor}; + +/// Position in the terminal window. +#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)] +pub struct Position { + /// The position column index (0 based). + pub x: u16, + /// The position row index (0 based). + pub y: u16, +} + +impl Position { + /// Creates a new position from the given `x` & `y`. + pub fn new(x: u16, y: u16) -> Position { + Position { x, y } + } + + /// Draws the given `value` at this position. + pub fn draw(&self, value: D) -> Result<()> { + let cursor = TerminalCursor::new(); + cursor.goto(self.x, self.y)?; + + print!("{}", style(value).with(Color::Red)); + stdout().flush()?; + Ok(()) + } + + /// Clears character (writes single space) at this position. + pub fn clear_char(&self) -> Result<()> { + let crossterm = Crossterm::new(); + crossterm.cursor().goto(self.x, self.y)?; + crossterm.terminal().write(" ")?; + Ok(()) + } +} + +/// Crates a `Position` from a `(u16, u16)` tuple. +impl From<(u16, u16)> for Position { + fn from(pos: (u16, u16)) -> Self { + Position::new(pos.0, pos.1) + } +} + +/// A snake direction. +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Direction { + Up, + Down, + Left, + Right, +} + +impl Direction { + /// Returns `true` if the direction is vertical (`Up` or `Down`). + fn is_vertical(&self) -> bool { + self == &Direction::Up || self == &Direction::Down + } + + /// Returns `true` if the direction can be changed to the given + /// `direction`. + /// + /// It's allowed to change direction from vertical to horizontal + /// and vice versa. It's not allowed to change the `Right` direction + /// to either `Left` or `Right`, but it's allowed to change it + /// to either `Up` or `Down`. + pub fn can_change_to(&self, direction: Direction) -> bool { + self.is_vertical() != direction.is_vertical() + } +} + +/// Tries to create a `Direction` from the `KeyEvent` (arrow keys). +impl TryFrom for Direction { + type Error = (); + + fn try_from(value: KeyEvent) -> std::result::Result { + match value { + KeyEvent::Up => Ok(Direction::Up), + KeyEvent::Left => Ok(Direction::Left), + KeyEvent::Down => Ok(Direction::Down), + KeyEvent::Right => Ok(Direction::Right), + _ => Err(()), + } + } +} + +/// Tries to create a `Direction` from the `char` (WASD keys). +impl TryFrom for Direction { + type Error = (); + + fn try_from(value: char) -> std::result::Result { + match value { + 'w' => Ok(Direction::Up), + 'a' => Ok(Direction::Left), + 's' => Ok(Direction::Down), + 'd' => Ok(Direction::Right), + _ => Err(()), + } + } +} diff --git a/examples/program_examples/snake/src/variables.rs b/examples/program_examples/snake/src/variables.rs deleted file mode 100644 index 12976e4..0000000 --- a/examples/program_examples/snake/src/variables.rs +++ /dev/null @@ -1,53 +0,0 @@ -use std::io::stdout; -use std::io::Write; - -use crossterm::{style, Color, Crossterm, Result, TerminalCursor}; - -#[derive(Copy, Clone, Debug)] -pub enum Direction { - Up = 0, - Down = 1, - Left = 2, - Right = 3, -} - -#[derive(Copy, Clone, Debug, PartialOrd, PartialEq)] -pub struct Position { - pub x: usize, - pub y: usize, -} - -impl Position { - pub fn new(x: usize, y: usize) -> Position { - Position { x, y } - } - - pub fn draw(&self, val: &str) -> Result<()> { - let cursor = TerminalCursor::new(); - cursor.goto(self.x as u16, self.y as u16)?; - - print!("{}", style(val).with(Color::Red)); - stdout().flush()?; - Ok(()) - } - - pub fn remove(&self) -> Result<()> { - let crossterm = Crossterm::new(); - - crossterm.cursor().goto(self.x as u16, self.y as u16)?; - crossterm.terminal().write(" ")?; - Ok(()) - } -} - -#[derive(Copy, Clone)] -pub struct Size { - pub width: usize, - pub height: usize, -} - -impl Size { - pub fn new(width: usize, height: usize) -> Size { - Size { width, height } - } -}