A game of Snake written in Rust designed to be played in the terminal
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

404 lines
11 KiB

use std::{env, fs, fs::File, io::{Error, Read, Write}, process::exit, thread, time};
use rand::Rng;
use pancurses::{initscr, endwin, Input, noecho /*, flushinp */};
enum GameEnd {
PlayerQuit,
AteSelf,
HitWall,
}
/// Different ways for the game to end
impl GameEnd {
fn tell(&self) -> &'static str {
match self {
GameEnd::PlayerQuit => "quit",
GameEnd::AteSelf => "ate yourself",
GameEnd::HitWall => "hit the wall",
}
}
}
#[derive(Debug)]
struct Vec2 {
x: i32,
y: i32,
}
impl Vec2 {
/// Initialize a Vec2 with the values you provide
/// Syntax: Vec2::new(X, Y)
/// Returns: Vec2
fn new(x: i32, y: i32) -> Vec2 {
Vec2 { x, y }
}
/// Check if the X and Y of one Vec2 is equal to another
/// Syntax: Vec2_1.equal_xy(&Vec2_2)
/// Returns: bool
fn equal_xy(&self, cmp_vec: &Vec2) -> bool {
self.x == cmp_vec.x && self.y == cmp_vec.y
}
}
/// Sets the direction of the snake head safely
/// as to not run into itself and end the game
fn set_direction(dir: &mut Vec2, new_dir: Vec2) {
if new_dir.x != -dir.x || new_dir.y != -dir.y {
dir.x = new_dir.x;
dir.y = new_dir.y;
}
}
/// Places the food in a random spot on the screen
fn place_food(food: &mut Vec2, window_size: &Vec2, snake: &Vec<Vec2>) {
// Place the food
food.x = rand::thread_rng().gen_range(1..window_size.x-1);
food.y = rand::thread_rng().gen_range(1..window_size.y-1);
// Check if food is under snake, recursively try again if so
for s in snake {
if food.equal_xy(s) {
place_food(food, window_size, snake)
}
}
}
/// Retrieve the high score from a high_score file
/// If the file doesn't exist, create one
fn get_hi_score() -> usize {
// Check if the high_score file exists
let mut file = match File::open("high_score") {
Ok(file) => file,
// No? Create one
Err(_) => {
match File::create("high_score") {
Ok(mut file) => {
match file.write_all(b"5") {
Ok(_) => (),
// And if writing to it fails... "fuck it".
Err(err) => panic!("Failed to write to hi_score: {}", err),
}
file
},
// If creating it fails... "fuck it"
Err(err) => panic!("Failed to create high_score: {:?}:", err),
}
},
};
// Grab high_score contents if found
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => contents.parse().unwrap(),
// ReCURSEDively attempt to open file if it fails
// primarily due to the "bad file descriptor" error
Err(err) => {
println!("Couldn't read high_score: {}. Trying again...", err);
get_hi_score()
},
}
}
/// Write the high score to the high_score file
fn set_hi_score(score: usize) {
fn create(score: usize) -> Result<(), Error> {
let mut new_file = fs::OpenOptions::new().write(true).truncate(true).open("high_score")?;
new_file.write_fmt(format_args!("{}", score))?;
Ok(())
}
match create(score) {
Ok(_) => (),
Err(err) => println!("Failed to write file: {:?}:", err),
}
}
/// Run the snake game loop
fn game(rate: u64, bounce: bool, vert_slow: bool) {
// Set game update rate
let mut rate_div = 1.0;
// Get high score, create a file for it if there isn't one
let hi_score = get_hi_score();
// Initialize window
let window = initscr();
window.refresh();
window.keypad(true);
window.nodelay(true);
noecho();
let window_size = Vec2::new(
window.get_max_x(),
window.get_max_y()
);
// Initialize game
// Initialize and place snake sections
let mut snake: Vec<Vec2> = vec![];
let mut snake_len: usize = 5;
let center_x = window_size.x / 2;
let center_y = window_size.y / 2;
for x in -2..3 {
snake.push(Vec2::new(center_x + x, center_y));
}
// Get the snake moving
let mut direction = Vec2::new(1, 0);
// Place food
let mut food = Vec2::new(0, 0);
place_food(&mut food, &window_size, &snake);
let game_end;
let mut paused: bool = false;
// Start game loop
'game: loop {
let tick = time::Duration::from_millis(
(rate as f32 * rate_div) as u64
);
let snake_last = match snake.last() {
Some(_vec) => _vec,
None => panic!("Game crash: Snake has no sections. How did this happen?"),
};
// Check if snake is eating food
if snake_last.equal_xy(&food) {
place_food(&mut food, &window_size, &snake);
snake_len += 1;
}
// Check if snake is eating itself
for s in 0..snake.len()-1 {
if snake_last.equal_xy(&snake[s]) {
game_end = GameEnd::AteSelf;
break 'game;
}
}
// Check if snake is hitting wall
if
(snake_last.x < 1 || snake_last.y < 1
|| snake_last.x >= window_size.x-1
|| snake_last.y >= window_size.y-1)
&& !bounce
{
game_end = GameEnd::HitWall;
break 'game;
}
// Bounce the head off the wall if bounce is enabled
// FIXME: Snake stops when hitting center, causing game to end
// FIXME: When the snake heads towards the wall, this statement will forcibly
// steer the snake towards the center, even if the player turns away from it
if bounce {
// Vertical wall bounce
if snake_last.x < 2 || snake_last.x >= window_size.x-2 {
set_direction(&mut direction, Vec2::new(0, i32::signum(center_y - snake_last.y)));
// println!("{:?}", direction);
}
// Horizontal wall bounce
if snake_last.y < 2 || snake_last.y >= window_size.y-2 {
set_direction(&mut direction, Vec2::new(i32::signum(center_x - snake_last.x), 0));
// println!("{:?}", direction);
}
}
// Check user input
// TODO: Find a way to clear input queue
// TODO: so input does not lag from holding controls down
match window.getch() {
Some(Input::KeyUp) |
Some(Input::Character('w')) |
Some(Input::Character('k')) => {
set_direction(&mut direction, Vec2::new(0, -1));
if vert_slow { rate_div = 1.5; }
},
Some(Input::KeyLeft) |
Some(Input::Character('a')) |
Some(Input::Character('h')) => {
set_direction(&mut direction, Vec2::new(-1, 0));
rate_div = 1.0;
},
Some(Input::KeyDown) |
Some(Input::Character('s')) |
Some(Input::Character('j')) => {
set_direction(&mut direction, Vec2::new(0, 1));
if vert_slow { rate_div = 1.5; }
},
Some(Input::KeyRight) |
Some(Input::Character('d')) |
Some(Input::Character('l')) => {
set_direction(&mut direction, Vec2::new(1, 0));
rate_div = 1.0;
},
Some(Input::Character('p')) |
Some(Input::KeyExit) |
Some(Input::KeyBreak) => {
paused = !paused;
window.nodelay(!paused);
},
Some(Input::Character('q')) => {
game_end = GameEnd::PlayerQuit;
break 'game;
},
Some(_) => (),
None => (),
}
// Unintended behavior, doesn't allow for two movements within a single tick
// flushinp();
// Move snake
snake.push(Vec2::new(snake_last.x + direction.x, snake_last.y + direction.y));
if snake.len() > snake_len { snake.remove(0); }
// Clear screen, draw food
window.erase();
window.mvaddch(food.y, food.x, '$');
// Draw snake
for s in &snake { window.mvaddch(s.y, s.x, '@'); }
// Draw wall
window.draw_box('#','#');
// Draw score and high score
let score = format!(" Score {:4} ", snake_len);
window.mvaddstr(0, 2, score);
let score = format!(" Hi Score {:4} ", hi_score);
window.mvaddstr(0, 16, score);
// Draw paused if paused
if paused {
window.mvaddstr(window_size.y-1, 2, String::from(" [PAUSED] "));
}
thread::sleep(tick);
}
// Stop game
// Display end
let end_msg = format!("## GAME OVER ##\nYou {}!", game_end.tell());
let congrats = format!("[ X ] New high score! Previous: {}", hi_score);
let score_msg = format!("[ X ] Your score: {:4}", snake_len);
/* // Should this even be a feature?
window.clear();
window.mvaddstr(0, 3, end_msg.clone());
window.mvaddstr(3, 3, score_msg);
if snake_len > hi_score {
window.mvaddstr(6, 3, &*congrats);
}
window.refresh();
thread::sleep(time::Duration::from_millis(2000));
*/
// Exit game
endwin();
println!("{}", end_msg);
println!("{}", score_msg);
if snake_len > hi_score {
println!("{}", congrats);
set_hi_score(snake_len);
}
}
/// Print help information when -h flag is used
fn help() {
println!();
println!(
"CONTROLS:
To move the snake, either use:
- Arrow keys
- WASD keys
- HJKL keys
To pause or unpause, press p
OPTIONS
--bounce, -b [BROKEN]
Allow the snake's head to bounce off the wall, instead of
ending the game when hit. By default, this is disabled.
--help, -h
Shows this screen.
--rate, -r
Modifies rate of the game in milliseconds. (Default 150)
--no-vertical-slow, -v
By default, the game slows the snake when moving vertically
due to most terminal fonts being taller than wide.
If your font for whatever reason is equal aspect ratio,
or you don't like the effect, this option may be a good idea.
");
}
fn main() {
let args: Vec<String> = env::args().collect();
let start_time = time::Instant::now();
// Defaults
let mut rate: u64 = 150;
let mut bounce: bool = false;
let mut vert_slow: bool = true;
// Arguments
print!("Starting snake with: ");
for arg in 0..args.len() {
match Some(&*args[arg].to_string()) {
Some("--bounce") | Some("-b") => {
print!("{} ", args[arg]);
bounce = true
}
Some("--help") | Some("-h") => {
print!("{} ", args[arg]);
help();
exit(0);
},
Some("--rate") | Some("-r") => {
print!("{} ", args[arg]);
print!("{} ", args[arg+1]);
rate = args[arg+1].parse().unwrap();
},
Some("--no-vertical-slow") | Some("-v") => {
print!("{} ", args[arg]);
vert_slow = false;
}
_ => (),
}
}
println!();
// Start game
game(rate, bounce, vert_slow);
let elapsed = start_time.elapsed().as_millis()/* - 2000*/;
println!("Game lasted {}m {:.1}s",
elapsed / 60000,
(elapsed as f32 / 1000.0) % 60.0
);
}