diff --git a/src/main.rs b/src/main.rs index c9089fd..b8705c7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,7 @@ use rand::Rng; -use std::fmt; +use std::collections::HashSet; +use std::io::Write; +use std::{fmt, io, process}; #[derive(Clone, Copy)] struct Cell { @@ -40,65 +42,205 @@ 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(()) } } -fn mark_neighbors(grid: &mut Grid, x: usize, y: 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 { +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 row = rng.gen_range(0..self.0.len()); + let col = rng.gen_range(0..self.0[0].len()); + let cell: &mut Cell = &mut self.0[row][col]; + if cell.mine { continue; } - let Some(cell) = grid.0.get_mut(row).and_then(|row| row.get_mut(col)) else { - continue; - }; + mines_left -= 1; + cell.mine = true; + self.on_neighbors(col, row, |cell, _| cell.neighbors += 1); + } + } - cell.neighbors += 1; + 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, + win: 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() { + "flag" => Ok(GridCommand::FLAG(coord)), + "reveal" => Ok(GridCommand::REVEAL(coord)), + _ => Err("Unknown command"), + } +} + +fn reveal_at(game_state: &mut GameState, coord: (usize, usize)) { + let mut seen = HashSet::new(); + let mut stack = Vec::<(usize, usize)>::new(); + stack.push(coord); + + while let Some((y, x)) = stack.pop() { + 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; + }; + if cell.neighbors > 0 || cell.mine { + continue; + } + + cell.revealed = true; + + 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; + } + + stack.push((row, col)); + } } } } -fn plant_mines(grid: &mut Grid, mines: u8) { - let mut rng = rand::thread_rng(); - - let mut mines_left = mines; - while mines_left > 0 { - let row = rng.gen_range(0..grid.0.len()); - let col = rng.gen_range(0..grid.0[0].len()); - let cell: &mut Cell = &mut grid.0[row][col]; - - if !cell.mine { - mines_left -= 1; - cell.mine = true; - - mark_neighbors(grid, col, row); - } +fn play_game(game_state: &mut GameState) { + if game_state.win { + return; } + + println!("{}\n", game_state); + print!("> "); + let _ = io::stdout().flush(); + + let mut buffer = String::new(); + let stdin = io::stdin(); + stdin.read_line(&mut buffer); + + let cmd = match parse_command(buffer.as_str()) { + Ok(command) => command, + Err(e) => { + println!("error: {}", e); + return play_game(game_state); + } + }; + + game_state.turn += 1; + play_game(game_state); } fn main() { - let mut grid = Grid(vec![ - vec![ - Cell { - neighbors: 0, - flagged: false, - mine: false, - revealed: false, - }; - 9 - ]; - 9 - ]); + let Some(grid) = Grid::new(9, 9, 10) else { + eprintln!("failed to initialize grid"); + process::exit(1); + }; - plant_mines(&mut grid, 10); - - println!("{}", grid); + let mut game_state = GameState { + grid, + win: false, + turn: 1, + }; + play_game(&mut game_state); }