Skip to content

Instantly share code, notes, and snippets.

@luckasRanarison
Last active June 19, 2023 08:24
Show Gist options
  • Save luckasRanarison/45ab1f9c83ded86031b668ce81f2f83c to your computer and use it in GitHub Desktop.
Save luckasRanarison/45ab1f9c83ded86031b668ce81f2f83c to your computer and use it in GitHub Desktop.
Terminal snake game in Rust using crossterm
[package]
name = "snake-term"
version = "0.1.0"
edition = "2021"
[dependencies]
crossterm = "0.26.1"
rand = "0.8.5"
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