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
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
|
|
);
|
|
}
|
|
|