use rand::{self, Rng}; use std::io; use std::str::FromStr; use std::sync::RwLock; static GLOBAL_STATE: RwLock = RwLock::new(GameState::new()); #[derive(Clone, Copy)] struct GameState { picked_tile: bool, game_ended: bool, game_won: bool, game_ending_tile: Option<(usize, usize)>, } impl GameState { const fn new() -> Self { return Self { picked_tile: false, game_ended: false, game_won: false, game_ending_tile: None, }; } fn restart(&mut self) { *self = Self::new(); } } #[derive(Clone, Debug)] struct Board { rows: usize, cols: usize, mines_count: usize, tiles: Vec, } impl Board { fn new(rows: usize, cols: usize, mines: usize) -> Self { assert!((rows * cols) >= mines); let mut tiles = Vec::with_capacity(rows * cols); unsafe { tiles.set_len(tiles.capacity()); }; tiles.fill(Tile::new()); Self { rows, cols, mines_count: mines, tiles, } } // reveals mines and adjacent mines if the revealed mine is not surrounded by any mines pub fn reveal(&mut self, x: usize, y: usize) { let tile_idx = (y * self.cols) + x; if !GLOBAL_STATE.read().unwrap().picked_tile { if self.tiles[tile_idx].has_mine { println!( "mine_count: {}", self.tiles.iter().filter(|tile| tile.has_mine).count() ); // Todo: move mine let (mut new_x, mut new_y) = self.get_new_pos(); while new_x == x && new_y == y { (new_x, new_y) = self.get_new_pos(); } let tile_a = self.tiles[(new_y * self.cols) + new_x]; let tile_b = self.tiles[tile_idx]; self.tiles[tile_idx] = tile_a; self.tiles[(new_y * self.cols) + new_x] = tile_b; return self.reveal(x, y); } GLOBAL_STATE.write().unwrap().picked_tile = true; } let tile = &mut self.tiles[tile_idx]; if tile.revealed { return; } tile.reveal(); if GLOBAL_STATE.read().unwrap().game_ended { GLOBAL_STATE.write().unwrap().game_ending_tile = Some((x, y)); return; } let neighbors_pos = self.get_neighboring_tiles(x, y); // if any neighboring tiles have a mine, stop the recursive reveal if neighbors_pos .iter() .filter(|(tile_x, tile_y)| { let tile = &self.tiles[(tile_y * self.cols) + tile_x]; tile.has_mine }) .count() > 0 { return; } for (tile_x, tile_y) in neighbors_pos { self.reveal(tile_x, tile_y); } } pub fn flag(&mut self, x: usize, y: usize) { let tile_idx = (y * self.cols) + x; let tile = &mut self.tiles[tile_idx]; tile.flag(); } // TODO: gave save us all from these horrible names fn get_neighboring_tiles(&self, x: usize, y: usize) -> Vec<(usize, usize)> { let mut neighbors: Vec<(usize, usize)> = Vec::new(); // dumb solution, yes? Do I care, not really. let signed_x = x as isize; let signed_y = y as isize; let tl_x = signed_x - 1; let tl_y = signed_y - 1; for tile_x in 0..3 { for tile_y in 0..3 { let check_x = tl_x + tile_x; let check_y = tl_y + tile_y; if (check_x < 0 || check_y < 0) || (check_x > self.cols as isize - 1 || check_y > self.rows as isize - 1) { continue; } if check_x as usize == x && check_y as usize == y { // Don't include outselfs in the check obv continue; } neighbors.push((check_x as usize, check_y as usize)); } } neighbors } pub fn draw(&self) { println!(" ┌{}┐", "─".repeat(self.cols)); for row in 0..self.rows { for col in 0..self.cols { if col == 0 { print!("{row:<02} │"); } let tile = self.tiles[(row * self.cols) + col]; if GLOBAL_STATE.read().unwrap().game_ended && tile.has_mine { // Highlight the game ending mine in red, and the flagged mines in green print!( "{}*\x1B[0m", if tile.has_flag { "\x1B[92m" } else if { let game_ending_tile = GLOBAL_STATE.read().unwrap().game_ending_tile.unwrap(); game_ending_tile.0 == col && game_ending_tile.1 == row } { "\x1B[91m" } else { "" } ); if col == self.cols - 1 { print!("│"); } continue; } if tile.revealed { let nearby_mines = self .get_neighboring_tiles(col, row) .iter() .filter(|(tile_x, tile_y)| { let tile = &self.tiles[(tile_y * self.cols) + tile_x]; tile.has_mine }) .count(); if nearby_mines == 0 { print!(" "); } else { print!( "{}{nearby_mines}\x1B[0m", match nearby_mines { 1 => "\x1B[32m", 2 => "\x1B[37m", 3 => "\x1B[96m", 4 => "\x1B[33m", 5 => "\x1B[34m", 6 => "\x1B[35m", 7 => "\x1B[31m", 8 => "\x1B[97m", _ => panic!("More than 8 nearby mines!"), } ); } } else if tile.has_flag { print!("^"); } else { print!("#"); } if col == self.cols - 1 { print!("│"); } } println!(""); } println!(" └{}┘", "─".repeat(self.cols)); } pub fn generate_mines(&mut self) { for _ in 0..self.mines_count { let pos = self.get_new_pos(); let idx = (pos.1 * self.cols) + pos.0; self.tiles[idx].has_mine = true; } } fn get_new_pos(&self) -> (usize, usize) { let mut y = rng(self.rows); let mut x = rng(self.cols); let mut idx = (y * self.cols) + x; while self.tiles[idx].has_mine != false { y = rng(self.rows); x = rng(self.cols); idx = (y * self.cols) + x; } return (x, y); } } #[derive(Clone, Copy, Debug)] struct Tile { pub revealed: bool, pub has_mine: bool, pub has_flag: bool, } impl Tile { fn new() -> Self { Self { revealed: false, has_mine: false, has_flag: false, } } fn reveal(&mut self) { if self.has_mine { GLOBAL_STATE.write().unwrap().game_ended = true; return; } self.has_flag = false; self.revealed = true; } fn flag(&mut self) { println!("{}", self.has_flag); self.has_flag = !self.has_flag; println!("{}", self.has_flag); } } fn main() -> io::Result<()> { print!("\x1B[2J\x1B[1;1H"); println!("Starting RustySweep... (bad name ik)"); loop { GLOBAL_STATE.write().unwrap().restart(); game_loop()?; } } fn game_loop() -> io::Result<()> { let mut stdin = String::new(); println!("Please select board size"); let boards = [ Board::new(9, 9, 10), Board::new(16, 16, 40), Board::new(16, 30, 99), ]; for (i, board) in boards.iter().enumerate() { println!( "[{}]: {:<02}x{:<02} {:<02}", i + 1, board.rows, board.cols, board.mines_count ); } println!("[{}]: Custom board", boards.len() + 1); io::stdin().read_line(&mut stdin)?; // easier if we want to make custom sized boards a thing later let mut board = match usize::from_str(&stdin.trim()) { Err(_) => { println!("Selected board is invalid!"); return Ok(()); } Ok(idx) => { if idx == boards.len() + 1 { // custom board let custom_board = make_custom_board(); let board = match custom_board { Ok(board) => board, Err(board_err) => match board_err { CustomBoardError::Cancel => return Ok(()), CustomBoardError::Error => { println!("Invalid custom board!"); return Ok(()); } }, }; board } else { if idx > boards.len() { println!("Selected board is invalid!"); return Ok(()); } boards[idx - 1].clone() } } }; drop(stdin); board.generate_mines(); loop { print!("\x1B[2J\x1B[1;1H"); if board .tiles .iter() .filter(|tile| !tile.revealed && !tile.has_flag) .count() == 0 { GLOBAL_STATE.write().unwrap().game_won = true; } board.draw(); if GLOBAL_STATE.read().unwrap().game_won { println!("You won!"); enter_to_continue(); break; } if GLOBAL_STATE.read().unwrap().game_ended { println!("Game over!"); enter_to_continue(); break; } let x = get_input_num("Select an X position", None); if x.is_cancel() || x.is_invalid() { continue; } if x.get_num() >= board.cols { println!("X position is invalid!"); enter_to_continue(); continue; } let y = get_input_num("Select a Y position", None); if y.is_cancel() || x.is_invalid() { continue; } if y.get_num() >= board.rows { println!("Y position is invalid!"); enter_to_continue(); continue; } let action = get_input_num( "What would you like to do", Some(&["Reveal the tile", "Place or remove a flag"]), ); if action.is_cancel() { continue; } let action: Action = ((action.get_num() as u8) - 1).into(); println!("User action: {action:?}ing {} {}", x.get_num(), y.get_num()); match action { Action::Reveal => board.reveal(x.get_num(), y.get_num()), Action::Flag => board.flag(x.get_num(), y.get_num()), } println!( "Tile at {} {} is now: {:?}", x.get_num(), y.get_num(), board.tiles[(y.get_num() * board.cols) + x.get_num()] ); } Ok(()) } fn enter_to_continue() { println!("Press Return to continue!"); let _ = io::stdin().read_line(&mut String::new()); } enum CustomBoardError { Cancel, Error, } fn make_custom_board() -> Result { let mut cols = get_input_num("Board width", None); if cols.is_cancel() { return Err(CustomBoardError::Cancel); } if cols.is_invalid() { return Err(CustomBoardError::Error); } let mut rows = get_input_num("Board height", None); if rows.is_cancel() { return Err(CustomBoardError::Cancel); } if rows.is_invalid() { return Err(CustomBoardError::Error); } let mut mines = get_input_num("Number of mines", None); if mines.is_cancel() { return Err(CustomBoardError::Cancel); } if mines.is_invalid() { return Err(CustomBoardError::Error); } // winmine minimums at least in the winmine from archive.org if cols.get_num() < 8 { cols = Input::Num(8) } if rows.get_num() < 8 { rows = Input::Num(8); } if mines.get_num() < 10 { mines = Input::Num(10); } if mines.get_num() > (rows.get_num() - 1) * (cols.get_num() - 1) { mines = Input::Num((rows.get_num() - 1) * (cols.get_num() - 1)); } return Ok(Board::new(rows.get_num(), cols.get_num(), mines.get_num())); } #[derive(Clone, Copy, Debug)] #[repr(u8)] enum Action { Reveal, Flag, } impl From for Action { fn from(other: u8) -> Self { match other { 0 => Self::Reveal, 1 => Self::Flag, _ => panic!("Invalid Action {other}!"), } } } enum Input { Num(usize), Cancel, Invalid, } impl Input { fn is_cancel(&self) -> bool { match self { Input::Cancel => return true, _ => return false, } } fn get_num(&self) -> usize { match self { Input::Num(num) => return num.clone(), _ => panic!("tried to unwrap a non-Num value!"), } } fn is_invalid(&self) -> bool { match self { Input::Invalid => return true, _ => return false, } } } fn get_input_num(message: &str, options: Option<&[&str]>) -> Input { println!("{message} (c to cancel):"); if options.is_some() { for (i, option) in options.unwrap().iter().enumerate() { println!("[{}]: {option}", i + 1); } } let mut stdin = String::new(); io::stdin().read_line(&mut stdin).unwrap(); if stdin.as_bytes()[0] == b'c' { return Input::Cancel; } let num = match usize::from_str(&stdin.trim()) { Err(_) => { return Input::Invalid; } Ok(idx) => { if options.is_some() { if idx > options.unwrap().len() { return Input::Invalid; } } idx } }; return Input::Num(num); } fn rng(max: usize) -> usize { rand::thread_rng().gen_range(0..max) }