Last active
June 19, 2023 08:24
-
-
Save luckasRanarison/45ab1f9c83ded86031b668ce81f2f83c to your computer and use it in GitHub Desktop.
Terminal snake game in Rust using crossterm
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
[package] | |
name = "snake-term" | |
version = "0.1.0" | |
edition = "2021" | |
[dependencies] | |
crossterm = "0.26.1" | |
rand = "0.8.5" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::{ | |
io::{stdout, Write}, | |
process, | |
time::Duration, | |
}; | |
use crossterm::{ | |
cursor, | |
event::{self, Event, KeyCode, KeyEvent, KeyModifiers}, | |
execute, | |
style::{self, Color, Print, SetForegroundColor}, | |
terminal, QueueableCommand, Result, | |
}; | |
use rand::{thread_rng, Rng}; | |
const WIDTH: u16 = 26; | |
const HEIGHT: u16 = 10; | |
#[derive(Clone, Copy, PartialEq)] | |
enum Direction { | |
Up, | |
Down, | |
Left, | |
Right, | |
} | |
impl Direction { | |
fn opposite(&self) -> Self { | |
match self { | |
Direction::Up => Direction::Down, | |
Direction::Down => Direction::Up, | |
Direction::Left => Direction::Right, | |
Direction::Right => Direction::Left, | |
} | |
} | |
} | |
struct Cell { | |
value: char, | |
color: Color, | |
x: u16, | |
y: u16, | |
} | |
impl Cell { | |
fn new(value: char, color: Color, x: u16, y: u16) -> Self { | |
Self { value, color, x, y } | |
} | |
fn render(&self) -> Result<()> { | |
stdout() | |
.queue(cursor::MoveTo(self.x, self.y))? | |
.queue(style::SetForegroundColor(self.color))? | |
.queue(Print(self.value))? | |
.flush() | |
} | |
} | |
struct Snake { | |
body: Vec<Cell>, | |
direction: Direction, | |
} | |
impl Snake { | |
fn new(head: Cell) -> Self { | |
Self { | |
body: vec![head], | |
direction: Direction::Down, | |
} | |
} | |
fn head(&self) -> &Cell { | |
self.body.first().unwrap() | |
} | |
fn render(&self) -> Result<()> { | |
for cell in self.body.iter() { | |
cell.render()?; | |
} | |
stdout().flush() | |
} | |
fn step(&mut self, grow: bool) { | |
let head = self.head(); | |
let (head_x, head_y) = match self.direction { | |
Direction::Up => match head.y == 0 { | |
true => (head.x, HEIGHT - 1), | |
false => (head.x, head.y - 1), | |
}, | |
Direction::Down => match head.y == HEIGHT { | |
true => (head.x, 1), | |
false => (head.x, head.y + 1), | |
}, | |
Direction::Left => match head.x == 0 { | |
true => (WIDTH - 1, head.y), | |
false => (head.x - 1, head.y), | |
}, | |
Direction::Right => match head.x == WIDTH { | |
true => (1, head.y), | |
false => (head.x + 1, head.y), | |
}, | |
}; | |
let new_head = Cell::new('O', Color::Blue, head_x, head_y); | |
if !grow { | |
self.body.pop(); | |
} | |
self.body.insert(0, new_head); | |
} | |
fn change_direction(&mut self, key: KeyCode) { | |
let new_direction = match key { | |
KeyCode::Up => Direction::Up, | |
KeyCode::Down => Direction::Down, | |
KeyCode::Left => Direction::Left, | |
KeyCode::Right => Direction::Right, | |
_ => self.direction, | |
}; | |
if new_direction != self.direction.opposite() { | |
self.direction = new_direction; | |
} | |
} | |
} | |
fn check_collision(a: &Cell, b: &Cell) -> bool { | |
a.x == b.x && a.y == b.y | |
} | |
fn init_screen() -> Result<()> { | |
stdout() | |
.queue(terminal::EnterAlternateScreen)? | |
.queue(cursor::Hide)?; | |
terminal::enable_raw_mode() | |
} | |
fn close_screen() -> Result<()> { | |
stdout() | |
.queue(terminal::LeaveAlternateScreen)? | |
.queue(cursor::Show)?; | |
terminal::disable_raw_mode() | |
} | |
fn clear_screen() -> Result<()> { | |
execute!(stdout(), terminal::Clear(terminal::ClearType::All)) | |
} | |
fn draw_border() -> Result<()> { | |
for i in 0..=WIDTH { | |
stdout() | |
.queue(cursor::MoveTo(i, 0))? | |
.queue(SetForegroundColor(Color::White))? | |
.queue(Print("#"))? | |
.queue(cursor::MoveTo(i, HEIGHT))? | |
.queue(Print("#"))?; | |
} | |
for i in 0..=HEIGHT { | |
stdout() | |
.queue(cursor::MoveTo(0, i))? | |
.queue(Print("#"))? | |
.queue(cursor::MoveTo(WIDTH, i))? | |
.queue(Print("#"))?; | |
} | |
stdout().flush() | |
} | |
fn print_end_screen() -> Result<()> { | |
stdout() | |
.queue(cursor::MoveTo((WIDTH / 2) - 5, HEIGHT / 2 - 1))? | |
.queue(SetForegroundColor(Color::Red))? | |
.queue(Print("GAME OVER"))? | |
.queue(cursor::MoveTo((WIDTH / 2) - 6, HEIGHT / 2))? | |
.queue(SetForegroundColor(Color::White))? | |
.queue(Print("r - Restart"))? | |
.queue(cursor::MoveTo((WIDTH / 2) - 5, (HEIGHT / 2) + 1))? | |
.queue(Print("q - Quit"))? | |
.flush() | |
} | |
fn main() -> Result<()> { | |
let (col, row) = terminal::size()?; | |
if col < 26 || row < 10 { | |
println!("The required size is 26x10, please resize your terminal"); | |
process::exit(0); | |
} | |
init_screen()?; | |
let head = Cell::new('O', Color::Blue, WIDTH / 2, HEIGHT / 2); | |
let mut snake = Snake::new(head); | |
let mut apple = Cell::new('o', Color::Red, WIDTH / 2, (HEIGHT / 2) + 3); | |
let mut rng = thread_rng(); | |
loop { | |
clear_screen()?; | |
apple.render()?; | |
snake.render()?; | |
draw_border()?; | |
let timeout = match snake.direction { | |
Direction::Left | Direction::Right => 150 - snake.body.len(), | |
Direction::Up | Direction::Down => 200 - snake.body.len(), | |
}; | |
if event::poll(Duration::from_millis(timeout as u64))? { | |
if let Event::Key(key) = event::read()? { | |
if let KeyEvent { | |
code: KeyCode::Char('q') | KeyCode::Char('c'), | |
modifiers: KeyModifiers::CONTROL, | |
.. | |
} = key | |
{ | |
process::exit(130) | |
} | |
snake.change_direction(key.code); | |
} | |
} | |
let head = snake.head(); | |
let grow = check_collision(head, &apple); | |
if grow { | |
loop { | |
apple.x = rng.gen_range(1..WIDTH); | |
apple.y = rng.gen_range(1..HEIGHT); | |
if !snake.body.iter().any(|cell| check_collision(cell, &apple)) { | |
break; | |
} | |
} | |
} | |
let collide = snake | |
.body | |
.iter() | |
.enumerate() | |
.any(|(i, cell)| i != 0 && check_collision(head, cell)); | |
if collide { | |
break; | |
} | |
snake.step(grow); | |
} | |
print_end_screen()?; | |
loop { | |
if let Event::Key(key) = event::read()? { | |
match key.code { | |
KeyCode::Char('q') => { | |
close_screen()?; | |
process::exit(0); | |
} | |
KeyCode::Char('r') => main()?, | |
_ => (), | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment