use rand::Rng; use std::collections::HashSet; use std::io::Write; use std::{fmt, io, process}; #[derive(Clone, Copy)] struct Cell { neighbors: u8, revealed: bool, flagged: bool, mine: bool, } impl fmt::Display for Cell { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { if self.flagged { write!(f, "F")?; return Ok(()); } if !self.revealed { write!(f, "+")?; return Ok(()); } if self.mine { write!(f, "M")?; return Ok(()); } if self.neighbors > 0 { write!(f, "{}", self.neighbors)?; } else { write!(f, "_")?; } Ok(()) } } #[derive(Clone)] struct Grid(Vec>); impl fmt::Display for Grid { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let mut y = 'a' as u8; for row in self.0.iter() { write!(f, "{:2} ", y as char)?; y += 1; for cell in row.iter() { write!(f, " {} ", cell)?; } write!(f, "\n")?; } write!(f, "{:2} ", "")?; let mut col = 0; for _ in self.0[0].iter() { write!(f, "{:2} ", col)?; col += 1; } Ok(()) } } impl Grid { fn plant_mines_and_set_neighbors(&mut self, mines: u8) { let mut rng = rand::thread_rng(); let mut mines_left = mines; while mines_left > 0 { let height = self.0.len(); let width = self.0[0].len(); let row = rng.gen_range(0..height); let col = rng.gen_range(0..width); let cell: &mut Cell = &mut self.0[row][col]; if cell.mine { continue; } mines_left -= 1; cell.mine = true; self.on_neighbors(col, row, |cell, _| cell.neighbors += 1); } } fn on_neighbors(&mut self, x: usize, y: usize, mut f: F) where F: FnMut(&mut Cell, (usize, usize)), { for row in y.saturating_sub(1)..=y.saturating_add(1) { for col in x.saturating_sub(1)..=x.saturating_add(1) { if x == col && y == row { continue; } let Some(cell) = self.0.get_mut(row).and_then(|row| row.get_mut(col)) else { continue; }; f(cell, (row, col)); } } } pub fn new(rows: usize, cols: usize, mines: u8) -> Option { if rows < 1 || cols < 1 { return None; } let mut grid: Grid = Grid(vec![ vec![ Cell { neighbors: 0, flagged: false, mine: false, revealed: false, }; rows ]; cols ]); grid.plant_mines_and_set_neighbors(mines); return Some(grid); } } enum GridCommand { REVEAL((usize, usize)), FLAG((usize, usize)), } struct GameState { grid: Grid, play: bool, turn: u32, } impl fmt::Display for GameState { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "turn({})\n", self.turn)?; write!(f, "{}", self.grid)?; Ok(()) } } fn parse_coord(coord: &str) -> Result<(usize, usize), &'static str> { let mut parts = coord.chars(); if coord.len() < 2 { return Err("expected at least two characters when parsing coordinate"); } let mut y: usize = 0; let mut x: usize = 0; loop { let Some(next) = parts.next() else { break; }; if next >= 'a' && next <= 'z' { y = ((next as u8) - 'a' as u8) as usize; } if next >= '0' && next <= '9' { x = ((next as u8) - '0' as u8) as usize; } } return Ok((y, x)); } fn parse_command(cmd: &str) -> Result { let mut parts = cmd.split_whitespace(); if parts.clone().count() == 1 { if let Some(coord_str) = parts.next() { match parse_coord(coord_str) { Ok(coord) => return Ok(GridCommand::REVEAL(coord)), Err(e) => return Err(e), } } else { return Err("Can't get coordinate part of command"); } } let command_part = parts.next().ok_or("Can't get command part of input")?; let coord_str = parts.next().ok_or("Can't get coordinate part of command")?; let coord = parse_coord(coord_str).or(Err("Invalid coordinates"))?; match command_part.to_lowercase().as_str() { "f" | "flag" => Ok(GridCommand::FLAG(coord)), "r" | "reveal" => Ok(GridCommand::REVEAL(coord)), _ => Err("Unknown command"), } } fn reveal_at(game_state: &mut GameState, coord: (usize, usize)) -> Result { let Some(cell) = game_state .grid .0 .get_mut(coord.0) .and_then(|row| row.get_mut(coord.1)) else { return Err("invalid coordinates"); }; let mut seen = HashSet::new(); let mut stack = Vec::<(usize, usize)>::new(); stack.push(coord); if cell.mine { println!("that's a mine."); return Ok(false); } while let Some((y, x)) = stack.pop() { let coord = (y, x); if seen.contains(&coord) { continue; } seen.insert(coord); let Some(cell) = game_state.grid.0.get_mut(y).and_then(|row| row.get_mut(x)) else { continue; }; cell.revealed = true; if cell.neighbors > 0 || cell.mine { continue; } game_state .grid .on_neighbors(x, y, |_cell, coord| stack.push(coord)); } return Ok(true); } fn flag_at(game_state: &mut GameState, coord: (usize, usize)) -> Result { let Some(cell) = game_state .grid .0 .get_mut(coord.0) .and_then(|row| row.get_mut(coord.1)) else { return Err("invalid coordinates"); }; cell.flagged = !cell.flagged; let won = game_state.grid.0.iter().fold(true, |won, row| { won && row .iter() .fold(won, |won, cell| won && cell.flagged == cell.mine) }); return Ok(!won); } fn play_game(game_state: &mut GameState) { if !game_state.play { return; } println!("{}\n", game_state); print!("> "); let _ = io::stdout().flush(); let mut buffer = String::new(); let stdin = io::stdin(); match stdin.read_line(&mut buffer) { Ok(_) => (), Err(e) => { eprintln!("error: {}", e); return play_game(game_state); } } let cmd = match parse_command(buffer.as_str()) { Ok(command) => command, Err(e) => { eprintln!("error: {}", e); return play_game(game_state); } }; match cmd { GridCommand::REVEAL(coord) => { game_state.play = match reveal_at(game_state, coord) { Ok(continue_play) => continue_play, Err(e) => { eprintln!("error: {}", e); return play_game(game_state); } } } GridCommand::FLAG(coord) => { game_state.play = match flag_at(game_state, coord) { Ok(continue_play) => continue_play, Err(e) => { eprintln!("error: {}", e); return play_game(game_state); } } } } game_state.turn += 1; play_game(game_state); } fn main() { let Some(grid) = Grid::new(9, 9, 10) else { eprintln!("failed to initialize grid"); process::exit(1); }; let mut game_state = GameState { grid, play: true, turn: 1, }; play_game(&mut game_state); main(); }