Improved snake example (#231)
This commit is contained in:
parent
05d28b4c5a
commit
91d07275ad
@ -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));
|
||||
}
|
||||
game_over_screen()
|
||||
// Check if the snake head position is out of bounds.
|
||||
if map.is_position_out_of_bounds(snake.head_position()) {
|
||||
break;
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
// Wait for some time.
|
||||
thread::sleep(time::Duration::from_millis(200));
|
||||
}
|
||||
|
||||
show_game_over_screen(snake.len())
|
||||
}
|
||||
|
||||
/// 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)
|
||||
)
|
||||
}
|
||||
|
@ -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) {
|
||||
/// Draws the map border.
|
||||
pub fn draw_border(&self) -> Result<()> {
|
||||
for y in 0..self.height {
|
||||
queue!(
|
||||
stdout(),
|
||||
Goto(x as u16, y as u16),
|
||||
Goto(0, y),
|
||||
PrintStyledFont("█".magenta()),
|
||||
Goto(self.width - 1, y),
|
||||
PrintStyledFont("█".magenta())
|
||||
)?;
|
||||
} else {
|
||||
free_positions.insert(format!("{},{}", x, y), Position::new(x, y));
|
||||
}
|
||||
}
|
||||
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;
|
||||
/// 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
|
||||
}
|
||||
|
||||
return false;
|
||||
/// 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
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
/// 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;
|
||||
|
||||
fn draw_food(&self) -> Result<()> {
|
||||
queue!(
|
||||
stdout(),
|
||||
Goto(self.foot_pos.x as u16, self.foot_pos.y as u16),
|
||||
PrintStyledFont("$".green())
|
||||
)
|
||||
// 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(());
|
||||
}
|
||||
}
|
||||
|
@ -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] = [
|
||||
" ▄████ ▄▄▄ ███▄ ▄███▓▓█████ ▒█████ ██▒ █▓▓█████ ██▀███ ",
|
||||
" ██▒ ▀█▒▒████▄ ▓██▒▀█▀ ██▒▓█ ▀ ▒██▒ ██▒▓██░ █▒▓█ ▀ ▓██ ▒ ██▒ ",
|
||||
" ▒██░▄▄▄░▒██ ▀█▄ ▓██ ▓██░▒███ ▒██░ ██▒ ▓██ █▒░▒███ ▓██ ░▄█ ▒ ",
|
||||
" ░▓█ ██▓░██▄▄▄▄██ ▒██ ▒██ ▒▓█ ▄ ▒██ ██░ ▒██ █░░▒▓█ ▄ ▒██▀▀█▄ ",
|
||||
" ░▒▓███▀▒ ▓█ ▓██▒▒██▒ ░██▒░▒████▒ ░ ████▓▒░ ▒▀█░ ░▒████▒░██▓ ▒██▒ ",
|
||||
" ░▒ ▒ ▒▒ ▓▒█░░ ▒░ ░ ░░░ ▒░ ░ ░ ▒░▒░▒░ ░ ▐░ ░░ ▒░ ░░ ▒▓ ░▒▓░ ",
|
||||
" ░ ░ ▒ ▒▒ ░░ ░ ░ ░ ░ ░ ░ ▒ ▒░ ░ ░░ ░ ░ ░ ░▒ ░ ▒░ ",
|
||||
" ░ ░ ░ ░ ▒ ░ ░ ░ ░ ░ ░ ▒ ░░ ░ ░░ ░ ",
|
||||
" ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ░ ",
|
||||
" ░ ",
|
||||
];
|
||||
|
@ -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();
|
||||
|
||||
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 length.
|
||||
pub fn len(&self) -> usize {
|
||||
self.fragments.len()
|
||||
}
|
||||
|
||||
if index == 0 {
|
||||
self.parent_pos = snake_part.position.clone();
|
||||
|
||||
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,
|
||||
/// The current snake direction.
|
||||
pub fn direction(&self) -> Direction {
|
||||
self.direction
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
/// Updates the snake direction.
|
||||
pub fn set_direction(&mut self, direction: Direction) {
|
||||
self.direction = direction;
|
||||
}
|
||||
|
||||
pub fn draw_snake(&mut self) -> Result<()> {
|
||||
for snake_part in self.snake_parts.iter_mut() {
|
||||
snake_part.position.draw("■")?;
|
||||
}
|
||||
Ok(())
|
||||
/// 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;
|
||||
}
|
||||
|
||||
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));
|
||||
/// 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;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn get_parts(&self) -> &Vec<Part> {
|
||||
return &self.snake_parts;
|
||||
/// 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)?;
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
/// Redraws the snake.
|
||||
pub fn draw(&self) -> Result<()> {
|
||||
for fragment in &self.fragments {
|
||||
fragment.draw()?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
103
examples/program_examples/snake/src/types.rs
Normal file
103
examples/program_examples/snake/src/types.rs
Normal 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(()),
|
||||
}
|
||||
}
|
||||
}
|
@ -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 }
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user