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::io::{stdout, Write};
|
||||||
use std::iter::Iterator;
|
use std::iter::Iterator;
|
||||||
use std::{thread, time};
|
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 map::Map;
|
||||||
use snake::Snake;
|
use snake::Snake;
|
||||||
use variables::{Direction, Position, Size};
|
use types::Direction;
|
||||||
|
|
||||||
use crossterm::{
|
|
||||||
execute, input, style, AsyncReader, Clear, ClearType, Color, Colorize, Crossterm, Goto,
|
|
||||||
InputEvent, KeyEvent, PrintStyledFont, RawScreen, Result, Show,
|
|
||||||
};
|
|
||||||
|
|
||||||
mod map;
|
mod map;
|
||||||
mod messages;
|
mod messages;
|
||||||
mod snake;
|
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<()> {
|
fn main() -> Result<()> {
|
||||||
let map_size = ask_size()?;
|
// Print the welcome screen and ask for the map 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();
|
|
||||||
let crossterm = Crossterm::new();
|
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()?;
|
crossterm.cursor().hide()?;
|
||||||
|
|
||||||
// initialize free positions for the game map.
|
// Draw the map border.
|
||||||
let mut free_positions: HashMap<String, Position> =
|
let mut map = Map::new(map_width, map_height);
|
||||||
HashMap::with_capacity((map_size.width * map_size.height) as usize);
|
map.draw_border()?;
|
||||||
|
|
||||||
// render the map
|
// Create a new snake, draw it and spawn some food.
|
||||||
let mut map = Map::new(map_size);
|
let mut snake = Snake::new(map_width, map_height);
|
||||||
map.render_map(&mut free_positions)?;
|
snake.draw()?;
|
||||||
|
map.spawn_food(&snake)?;
|
||||||
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)?;
|
|
||||||
|
|
||||||
|
// Game loop
|
||||||
let mut stdin = crossterm.input().read_async();
|
let mut stdin = crossterm.input().read_async();
|
||||||
let mut snake_direction = Direction::Right;
|
|
||||||
|
|
||||||
// start the game loop; draw, move snake and spawn food.
|
|
||||||
loop {
|
loop {
|
||||||
if let Some(new_direction) = update_direction(&mut stdin) {
|
// Handle the next user input event (if there's any).
|
||||||
snake_direction = new_direction;
|
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)?;
|
// 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 map.is_out_of_bounds(snake.snake_parts[0].position) {
|
if !snake.update()? {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
snake.draw_snake()?;
|
// Check if the snake ate some food.
|
||||||
|
if snake.head_position() == map.food_position() {
|
||||||
if snake.has_eaten_food(map.foot_pos) {
|
// Tell the snake to grow ...
|
||||||
map.spawn_food(&free_positions)?;
|
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> {
|
/// Returns a next user event (if there's any).
|
||||||
let pressed_key = reader.next();
|
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
|
||||||
if let Some(InputEvent::Keyboard(KeyEvent::Char(character))) = pressed_key {
|
// say that the snake is moving to the right and the user hits the right arrow
|
||||||
return Some(match character {
|
// key three times and then the up arrow key. The up arrow key would be handled
|
||||||
'w' => Direction::Up,
|
// in the 4th iteration of the game loop. That's not what we really want and thus
|
||||||
'a' => Direction::Left,
|
// we are consuming all events here till we find an actionable one or none.
|
||||||
's' => Direction::Down,
|
while let Some(event) = reader.next() {
|
||||||
'd' => Direction::Right,
|
match event {
|
||||||
_ => return None,
|
InputEvent::Keyboard(KeyEvent::Char(character)) => {
|
||||||
});
|
if let Ok(new_direction) = Direction::try_from(character) {
|
||||||
} else if let Some(InputEvent::Keyboard(key)) = pressed_key {
|
if snake_direction.can_change_to(new_direction) {
|
||||||
return Some(match key {
|
return Some(Event::UpdateSnakeDirection(new_direction));
|
||||||
KeyEvent::Up => Direction::Up,
|
}
|
||||||
KeyEvent::Left => Direction::Left,
|
}
|
||||||
KeyEvent::Down => Direction::Down,
|
}
|
||||||
KeyEvent::Right => Direction::Right,
|
InputEvent::Keyboard(KeyEvent::Esc) => return Some(Event::QuitGame),
|
||||||
_ => return None,
|
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
|
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!(
|
execute!(
|
||||||
stdout(),
|
stdout(),
|
||||||
Clear(ClearType::All),
|
Clear(ClearType::All),
|
||||||
Goto(0, 0),
|
Goto(0, row),
|
||||||
PrintStyledFont(style(format!("{}", messages::SNAKERS.join("\n\r"))).with(Color::Cyan)),
|
PrintStyledFont(style(format!("{}", messages::SNAKE.join("\n\r"))).with(Color::Cyan))
|
||||||
Goto(0, 15),
|
|
||||||
PrintStyledFont("Enter map width:".green().on_yellow()),
|
|
||||||
Goto(17, 15)
|
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let width = input().read_line().unwrap();
|
row += messages::SNAKE.len() as u16 + 2;
|
||||||
|
let width = ask_for_map_dimension("width", 10, terminal_size.0, (0, row))?;
|
||||||
execute!(
|
row += 2;
|
||||||
stdout(),
|
let height = ask_for_map_dimension("height", 10, terminal_size.1, (0, row))?;
|
||||||
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();
|
|
||||||
|
|
||||||
execute!(stdout(), Clear(ClearType::All))?;
|
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!(
|
execute!(
|
||||||
stdout(),
|
stdout(),
|
||||||
Clear(ClearType::All),
|
Clear(ClearType::All),
|
||||||
Goto(0, 0),
|
Goto(0, 0),
|
||||||
PrintStyledFont(style(format!("{}", messages::END_MESSAGE.join("\n\r"))).with(Color::Red)),
|
PrintStyledFont(style(format!("{}", messages::GAME_OVER.join("\n\r"))).with(Color::Red)),
|
||||||
Show
|
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 std::io::{stdout, Write};
|
||||||
|
|
||||||
use crossterm::{queue, Colorize, Goto, PrintStyledFont, Result};
|
use crossterm::{queue, Colorize, Goto, PrintStyledFont, Result};
|
||||||
use rand;
|
use rand::{
|
||||||
use rand::distributions::{IndependentSample, Range};
|
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 struct Map {
|
||||||
pub size: Size,
|
/// The map width.
|
||||||
pub foot_pos: Position,
|
width: u16,
|
||||||
|
/// The map height.
|
||||||
|
height: u16,
|
||||||
|
/// Food.
|
||||||
|
food: Option<Food>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Map {
|
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 {
|
Map {
|
||||||
size: size,
|
width,
|
||||||
foot_pos: Position::new(0, 0),
|
height,
|
||||||
|
food: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// render the map on the screen.
|
/// Draws the map border.
|
||||||
pub fn render_map(&mut self, free_positions: &mut HashMap<String, Position>) -> Result<()> {
|
pub fn draw_border(&self) -> Result<()> {
|
||||||
for y in 0..self.size.height {
|
for y in 0..self.height {
|
||||||
for x in 0..self.size.height {
|
queue!(
|
||||||
if (y == 0 || y == self.size.height - 1) || (x == 0 || x == self.size.width - 1) {
|
stdout(),
|
||||||
queue!(
|
Goto(0, y),
|
||||||
stdout(),
|
PrintStyledFont("█".magenta()),
|
||||||
Goto(x as u16, y as u16),
|
Goto(self.width - 1, y),
|
||||||
PrintStyledFont("█".magenta())
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_out_of_bounds(&self, new_pos: Position) -> bool {
|
/// Check if the given `position` is out of bounds.
|
||||||
if (new_pos.x == 0 || new_pos.x == self.size.width)
|
///
|
||||||
|| (new_pos.y == 0 || new_pos.y == self.size.height)
|
/// Every map has a border and out of bounds means that the position
|
||||||
{
|
/// is inside the border.
|
||||||
return true;
|
pub fn is_position_out_of_bounds(&self, position: Position) -> bool {
|
||||||
}
|
position.x == 0
|
||||||
|
|| position.y == 0
|
||||||
return false;
|
|| position.x >= self.width - 1
|
||||||
|
|| position.y >= self.height - 1
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn spawn_food(&mut self, free_positions: &HashMap<String, Position>) -> Result<()> {
|
/// Returns food position.
|
||||||
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();
|
/// # Panics
|
||||||
self.draw_food()
|
///
|
||||||
|
/// 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<()> {
|
/// Spawns a new food and draws it.
|
||||||
queue!(
|
///
|
||||||
stdout(),
|
/// The `snake` argument is used to check that the food position doesn't collide
|
||||||
Goto(self.foot_pos.x as u16, self.foot_pos.y as u16),
|
/// with any snake fragment.
|
||||||
PrintStyledFont("$".green())
|
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(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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 crossterm::Result;
|
||||||
|
|
||||||
use super::variables::{Direction, Position};
|
use super::types::{Direction, Position};
|
||||||
|
|
||||||
pub struct Part {
|
/// A snake fragment kind.
|
||||||
pub position: Position,
|
///
|
||||||
|
/// 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 {
|
impl FragmentKind {
|
||||||
pub fn new(x: usize, y: usize) -> Part {
|
/// Creates a snake fragment kind from the snake head direction.
|
||||||
Part {
|
fn with_head_direction(direction: Direction) -> Self {
|
||||||
position: Position::new(x, y),
|
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 struct Snake {
|
||||||
pub snake_parts: Vec<Part>,
|
/// The snake fragments. Head index is always 0.
|
||||||
pub parent_pos: Position,
|
fragments: Vec<Fragment>,
|
||||||
|
/// The current direction.
|
||||||
|
direction: Direction,
|
||||||
|
/// Says if the snake ate some food or not.
|
||||||
|
ate_food: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Snake {
|
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 {
|
||||||
snake_parts: vec![Part::new(9, 10), Part::new(8, 10)],
|
fragments: parts,
|
||||||
parent_pos: Position::new(0, 0),
|
direction: Direction::Right,
|
||||||
|
ate_food: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn move_snake(
|
/// The current snake length.
|
||||||
&mut self,
|
pub fn len(&self) -> usize {
|
||||||
direction: &Direction,
|
self.fragments.len()
|
||||||
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() {
|
/// The current snake direction.
|
||||||
if index == count - 1 {
|
pub fn direction(&self) -> Direction {
|
||||||
snake_part.position.remove()?;
|
self.direction
|
||||||
free_positions.insert(
|
}
|
||||||
format!("{},{}", snake_part.position.x, snake_part.position.y),
|
|
||||||
snake_part.position,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if index == 0 {
|
/// Updates the snake direction.
|
||||||
self.parent_pos = snake_part.position.clone();
|
pub fn set_direction(&mut self, direction: Direction) {
|
||||||
|
self.direction = direction;
|
||||||
|
}
|
||||||
|
|
||||||
match direction {
|
/// Sets if the snake ate food.
|
||||||
&Direction::Up => snake_part.position.y -= 1,
|
///
|
||||||
&Direction::Down => snake_part.position.y += 1,
|
/// If set to `true`, the next `update()` call will move the head, but
|
||||||
&Direction::Left => snake_part.position.x -= 1,
|
/// won't move the tail.
|
||||||
&Direction::Right => snake_part.position.x += 1,
|
pub fn set_ate_food(&mut self, ate_food: bool) {
|
||||||
}
|
self.ate_food = ate_food;
|
||||||
|
}
|
||||||
|
|
||||||
free_positions.remove_entry(
|
/// The snake head position.
|
||||||
format!("{},{}", snake_part.position.x, snake_part.position.y).as_str(),
|
pub fn head_position(&self) -> Position {
|
||||||
);
|
self.fragments[0].position
|
||||||
} else {
|
}
|
||||||
let new_pos = self.parent_pos.clone();
|
|
||||||
self.parent_pos = snake_part.position.clone();
|
/// Returns `true` if there's an existing snake fragment at the
|
||||||
snake_part.position = new_pos;
|
/// 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<()> {
|
/// Moves the snake and redraws updated fragments only.
|
||||||
for snake_part in self.snake_parts.iter_mut() {
|
///
|
||||||
snake_part.position.draw("■")?;
|
/// Returns `Ok(true)` if the snake was updated. Returns `Ok(false)` if the
|
||||||
}
|
/// new head position collides with the existing snake fragments.
|
||||||
Ok(())
|
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 {
|
// Create & draw the new head.
|
||||||
if self.snake_parts[0].position.x == food_pos.x
|
let new_head = current_head.with_head_direction(self.direction);
|
||||||
&& self.snake_parts[0].position.y == food_pos.y
|
new_head.draw()?;
|
||||||
{
|
|
||||||
self.snake_parts.push(Part::new(1, 1));
|
// Check if the new head collides with existing snake fragments
|
||||||
return true;
|
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> {
|
/// Redraws the snake.
|
||||||
return &self.snake_parts;
|
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