Improved snake example (#231)

This commit is contained in:
Zrzka 2019-09-18 19:31:49 +02:00 committed by Timon
parent 05d28b4c5a
commit 91d07275ad
6 changed files with 594 additions and 263 deletions

View File

@ -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<String, Position> =
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<Direction> {
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<Event> {
// 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<Size> {
/// 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<u16> {
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::<u16>()
.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::<usize>().unwrap();
let parsed_height = height.parse::<usize>().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)
)
}

View File

@ -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<Food>,
}
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<String, Position>) -> 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<String, Position>) -> 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(());
}
}

View File

@ -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] = [
" ▄████ ▄▄▄ ███▄ ▄███▓▓█████ ▒█████ ██▒ █▓▓█████ ██▀███ ",
" ██▒ ▀█▒▒████▄ ▓██▒▀█▀ ██▒▓█ ▀ ▒██▒ ██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒ ",
" ▒██░▄▄▄░▒██ ▀█▄ ▓██ ▓██░▒███ ▒██░ ██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒ ",
" ░▓█ ██▓░██▄▄▄▄██ ▒██ ▒██ ▒▓█ ▄ ▒██ ██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄ ",
" ░▒▓███▀▒ ▓█ ▓██▒▒██▒ ░██▒░▒████▒ ░ ████▓▒░ ▒▀█░ ░▒████▒░██▓ ▒██▒ ",
" ░▒ ▒ ▒▒ ▓▒█░░ ▒░ ░ ░░░ ▒░ ░ ░ ▒░▒░▒░ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░ ",
" ░ ░ ▒ ▒▒ ░░ ░ ░ ░ ░ ░ ░ ▒ ▒░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░ ",
" ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░░ ░ ░░ ░ ",
" ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ",
"",
];

View File

@ -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<P: Into<Position>>(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<Part>,
pub parent_pos: Position,
/// The snake fragments. Head index is always 0.
fragments: Vec<Fragment>,
/// 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<String, Position>,
) -> 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<bool> {
// 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<Part> {
return &self.snake_parts;
/// Redraws the snake.
pub fn draw(&self) -> Result<()> {
for fragment in &self.fragments {
fragment.draw()?;
}
Ok(())
}
}

View File

@ -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<D: Display + Clone>(&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<KeyEvent> for Direction {
type Error = ();
fn try_from(value: KeyEvent) -> std::result::Result<Self, Self::Error> {
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<char> for Direction {
type Error = ();
fn try_from(value: char) -> std::result::Result<Self, Self::Error> {
match value {
'w' => Ok(Direction::Up),
'a' => Ok(Direction::Left),
's' => Ok(Direction::Down),
'd' => Ok(Direction::Right),
_ => Err(()),
}
}
}

View File

@ -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 }
}
}