diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f5f6e38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +high_score diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..e4441b3 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,162 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "cc" +version = "1.0.79" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50d30906286121d95be3d479533b458f87493b30a4b5f79a607db8f5d11aa91f" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "ncurses" +version = "5.101.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e2c5d34d72657dc4b638a1c25d40aae81e4f1c699062f72f467237920752032" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + +[[package]] +name = "pancurses" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0352975c36cbacb9ee99bfb709b9db818bed43af57751797f8633649759d13db" +dependencies = [ + "libc", + "log", + "ncurses", + "pdcurses-sys", + "winreg", +] + +[[package]] +name = "pdcurses-sys" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084dd22796ff60f1225d4eb6329f33afaf4c85419d51d440ab6b8c6f4529166b" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "pkg-config" +version = "0.3.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ac9a59f73473f1b8d852421e59e64809f025994837ef743615c6d0c5b305160" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "snek-game" +version = "1.0.0" +dependencies = [ + "pancurses", + "rand", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "winreg" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a27a759395c1195c4cc5cda607ef6f8f6498f64e78f7900f5de0a127a424704a" +dependencies = [ + "winapi", +] diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..f6a7d34 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "snek-game" +version = "1.0.0" +authors = ["voluminum"] +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[profile.release] +opt-level = 3 + +[dependencies] +pancurses = "0.17.0" +rand = "0.8.5" diff --git a/LICENSE b/LICENSE index 7a3094a..bde2ec9 100644 --- a/LICENSE +++ b/LICENSE @@ -1,3 +1,4 @@ +<<<<<<< HEAD DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE Version 2, December 2004 @@ -9,3 +10,18 @@ DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 0. You just DO WHAT THE FUCK YOU WANT TO. +======= + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + Version 2, December 2004 + +Copyright (C) 2004 Sam Hocevar + +Everyone is permitted to copy and distribute verbatim or modified +copies of this license document, and changing it is allowed as long +as the name is changed. + + DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. You just DO WHAT THE FUCK YOU WANT TO. +>>>>>>> c2f2f11 (snek game) diff --git a/README b/README new file mode 100644 index 0000000..6493bd7 --- /dev/null +++ b/README @@ -0,0 +1,14 @@ +# Snake +This is a game of Snake that is designed to be played in the terminal. It is written in Rust with zero unsafe code and uses the pancurses library. The game tracks your high score, and has variable speed and difficulty. + +## Building and running +This game can be built and ran with + +``` sh +./run +``` +Additionally, more options to the game can be found by typing + +``` sh +./run -h +``` diff --git a/README.md b/README.md deleted file mode 100644 index 44ebe2e..0000000 --- a/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# snek-game - -A game of Snake written in Rust designed to be played in the terminal \ No newline at end of file diff --git a/run b/run new file mode 100755 index 0000000..69813f4 --- /dev/null +++ b/run @@ -0,0 +1,3 @@ +#!/usr/bin/env bash + +cargo run --release -- $@ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..2f9d871 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,404 @@ +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) { + // 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 = 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 = 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 + ); +}