parent
3b72b7f45c
commit
7f17c14012
8 changed files with 614 additions and 3 deletions
@ -0,0 +1,2 @@ |
||||
/target |
||||
high_score |
||||
@ -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", |
||||
] |
||||
@ -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" |
||||
@ -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 |
||||
``` |
||||
@ -1,3 +0,0 @@ |
||||
# snek-game |
||||
|
||||
A game of Snake written in Rust designed to be played in the terminal |
||||
@ -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<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 |
||||
); |
||||
} |
||||
Loading…
Reference in new issue