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