pub fn board_to_str(
board: &[impl Into<Option<usize>> + Clone],
width: usize,
mines: Option<&[bool]>,
) -> String {
assert!(
board.len().is_multiple_of(width),
"Board length not divisible by width"
);
board
.chunks_exact(width)
.enumerate()
.map(|(i, row)| {
row.iter()
.enumerate()
.map(|(j, cell)| match cell.clone().into() {
None if mines.is_some_and(|mines_arr| mines_arr[i * width + j]) => {
"X".to_string()
}
None => "#".to_string(),
Some(num) => num.to_string(),
})
.collect::<Vec<_>>()
.join(" ")
})
.collect::<Vec<_>>()
.join("\n")
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BoardFromStrCell {
Known(usize),
Mine,
Unknown,
}
#[derive(Clone, Debug)]
pub struct BoardFromStrResult {
pub cells: Vec<BoardFromStrCell>,
pub w: usize,
pub h: usize,
}
impl BoardFromStrResult {
pub fn to_known(&self) -> impl Iterator<Item = Option<usize>> {
self.cells.iter().map(|&cell| match cell {
BoardFromStrCell::Known(x) => Some(x),
_ => None,
})
}
pub fn to_mines(&self) -> impl Iterator<Item = bool> {
self.cells
.iter()
.map(|&cell| cell == BoardFromStrCell::Mine)
}
}
pub fn board_from_str(data: &str) -> Result<BoardFromStrResult, &'static str> {
let line_vecs = data
.lines()
.map(|line| {
line.split_ascii_whitespace()
.map(|x| -> Result<BoardFromStrCell, &'static str> {
if x == "X" {
Ok(BoardFromStrCell::Mine)
} else if x == "#" {
Ok(BoardFromStrCell::Unknown)
} else {
x.parse::<usize>()
.map(BoardFromStrCell::Known)
.map_err(|_| "Invalid integer")
}
})
.collect::<Result<Vec<_>, &'static str>>()
})
.collect::<Result<Vec<_>, &'static str>>()?;
if line_vecs.is_empty() {
Err("Empty data")
} else if line_vecs[1..]
.iter()
.any(|line_vec| line_vec.len() != line_vecs[0].len())
{
Err("Inconsistent row lengths in board")
} else {
let width = line_vecs[0].len();
Ok(BoardFromStrResult {
h: line_vecs.len(),
cells: line_vecs.into_iter().flatten().collect(),
w: width,
})
}
}
pub fn iter_neighbors(
row: usize,
col: usize,
w: usize,
h: usize,
) -> impl Iterator<Item = (usize, usize)> {
let i_lo = row.saturating_sub(1);
let j_lo = col.saturating_sub(1);
let i_hi = (row + 1).min(h - 1);
let j_hi = (col + 1).min(w - 1);
(i_lo..=i_hi).flat_map(move |ni| {
(j_lo..=j_hi).filter_map(move |nj| {
if ni == row && nj == col {
None
} else {
Some((ni, nj))
}
})
})
}
pub fn is_adj(a_index: usize, b_index: usize, board_width: usize) -> bool {
(a_index / board_width).abs_diff(b_index / board_width) <= 1
&& (a_index % board_width).abs_diff(b_index % board_width) <= 1
}
pub fn position_to_row_col(pos: usize, width: usize) -> (usize, usize) {
(pos / width, pos % width)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn board_roundtrips_known_and_unknown_cells() {
let cells = vec![Some(0), Some(1), None, Some(2), None, Some(3)];
let rendered = board_to_str(&cells, 3, None);
assert_eq!(rendered, "0 1 #\n2 # 3");
let parsed = board_from_str(&rendered).unwrap();
assert_eq!(parsed.w, 3);
assert_eq!(parsed.h, 2);
assert_eq!(parsed.to_known().collect::<Vec<_>>(), cells);
assert_eq!(parsed.to_mines().collect::<Vec<_>>(), vec![false; 6]);
}
#[test]
fn board_roundtrips_with_mine_overlay() {
let known = vec![Some(1), None, Some(2), None];
let mines = vec![false, true, false, false];
let rendered = board_to_str(&known, 2, Some(&mines));
assert_eq!(rendered, "1 X\n2 #");
let parsed = board_from_str(&rendered).unwrap();
assert_eq!(
parsed.cells,
vec![
BoardFromStrCell::Known(1),
BoardFromStrCell::Mine,
BoardFromStrCell::Known(2),
BoardFromStrCell::Unknown,
]
);
}
#[test]
fn board_from_str_rejects_inconsistent_rows() {
assert!(board_from_str("0 1\n2").is_err());
}
#[test]
fn board_from_str_rejects_unknown_tokens() {
assert!(board_from_str("0 nope 1").is_err());
}
#[test]
fn iter_neighbors_matches_expected_cells() {
let corner = iter_neighbors(0, 0, 4, 3).collect::<Vec<_>>();
assert_eq!(corner, vec![(0, 1), (1, 0), (1, 1)]);
let edge = iter_neighbors(0, 2, 4, 3).collect::<Vec<_>>();
assert_eq!(edge, vec![(0, 1), (0, 3), (1, 1), (1, 2), (1, 3)]);
let center = iter_neighbors(1, 1, 4, 3).collect::<Vec<_>>();
assert_eq!(
center,
vec![
(0, 0),
(0, 1),
(0, 2),
(1, 0),
(1, 2),
(2, 0),
(2, 1),
(2, 2),
]
);
}
#[test]
fn is_adj_only_for_touching_cells() {
assert!(is_adj(0, 1, 3));
assert!(is_adj(0, 3, 3));
assert!(is_adj(0, 4, 3));
assert!(is_adj(4, 0, 3));
assert!(is_adj(4, 4, 3));
assert!(!is_adj(0, 2, 3));
assert!(!is_adj(0, 8, 3));
}
#[test]
fn position_to_row_col_maps_linear_indices() {
assert_eq!(position_to_row_col(0, 4), (0, 0));
assert_eq!(position_to_row_col(3, 4), (0, 3));
assert_eq!(position_to_row_col(4, 4), (1, 0));
assert_eq!(position_to_row_col(11, 4), (2, 3));
}
}