diff --git a/Cargo.toml b/Cargo.toml index 09ccdf3..b9f445d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,10 @@ name = "rust-wasm-minesweeper" version = "0.1.0" edition = "2021" -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[lib] +crate-type=["cdylib"] [dependencies] rand = "0.8.5" +wasm-bindgen = "0.2.80" +getrandom = { version = "0.2.6", features = ["js"] } diff --git a/index.html b/index.html new file mode 100644 index 0000000..ca3094a --- /dev/null +++ b/index.html @@ -0,0 +1,62 @@ + + + + + + Minesweeper + + + + +
+ +
+ + diff --git a/src/lib.rs b/src/lib.rs index 0c9db93..f6fc4ae 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,122 +1,21 @@ -use std::{collections::HashSet, fmt::{Display, Write}}; +#![allow(unused_unsafe)] +mod minesweeper; -use rand::Rng; +use std::cell::RefCell; -pub type Position = (usize, usize); +use minesweeper::*; +use wasm_bindgen::prelude::*; -#[derive(Debug)] -pub struct Minesweeper { - width: usize, - height: usize, - open_fields: HashSet, - mines: HashSet, - flags: HashSet, +thread_local! { + static MINESWEEPER: RefCell = RefCell::new(Minesweeper::new(10, 10, 10)); } -impl Display for Minesweeper { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - for y in 0..self.height { - for x in 0..self.height { - let pos = (x, y); - if self.flags.contains(&pos) { - f.write_str("F ")?; - } else if !self.open_fields.contains(&pos) { - f.write_str("# ")?; - } else if self.mines.contains(&pos) { - f.write_str("B ")?; - } else { - write!(f, "{} ", self.count_mines(pos))?; - } - } - f.write_char('\n')?; - } - - Ok(()) - } +#[wasm_bindgen(js_name = getState)] +pub fn get_state() -> String { + MINESWEEPER.with(move |ms| ms.borrow().to_string()) } -pub enum OpenResult { - Mine, - NoMine(u8) -} - -impl Minesweeper { - pub fn new(width: usize, height: usize, mine_count: usize) -> Minesweeper { - Minesweeper { - width, - height, - open_fields: HashSet::new(), - mines: generate_mines(width, height, mine_count), - flags: HashSet::new(), - } - } - - pub fn iter_neighbours(&self, (x, y): Position) -> impl Iterator { - let width = self.width; - let height = self.height; - - (x.max(1) - 1 ..= (x + 1).min(width - 1)).flat_map( - move |i| (y.max(1) - 1 ..= (y + 1).min(height - 1)).map(move |j| (i, j)) - ).filter(move |&pos| pos != (x, y)) - } - - pub fn count_mines(&self, position: Position) -> u8 { - self.iter_neighbours(position) - .filter(|pos| self.mines.contains(pos)) - .count() as u8 - } - - pub fn open(&mut self, position: Position) -> Option { - if self.flags.contains(&position) { - return None; - } - - self.open_fields.insert(position); - let is_mine = self.mines.contains(&position); - if is_mine { - Some(OpenResult::Mine) - } else { - Some(OpenResult::NoMine(0)) - } - } - - pub fn toggle_flag(&mut self, position: Position) { - if self.open_fields.contains(&position) { - return; - } - - if self.flags.contains(&position) { - self.flags.remove(&position); - } else { - self.flags.insert(position); - } - } -} - -fn generate_mines(field_width: usize, field_height: usize, mine_count: usize) -> HashSet { - let mut mines = HashSet::new(); - - let mut rng = rand::thread_rng(); - while mines.len() < mine_count { - let x = rng.gen_range(0..field_width); - let y = rng.gen_range(0..field_height); - mines.insert((x, y)); - } - - mines -} - -#[cfg(test)] -mod tests { - use crate::Minesweeper; - - #[test] - fn test() { - let mut ms = Minesweeper::new(5, 5, 5); - - ms.open((2, 2)); - ms.toggle_flag((3, 3)); - - println!("{}", ms); - } +#[wasm_bindgen(js_name = openField)] +pub fn open_field(x: usize, y: usize) { + MINESWEEPER.with(move |ms| ms.borrow_mut().open((x, y))); } diff --git a/src/main.rs b/src/main.rs deleted file mode 100644 index a30eb95..0000000 --- a/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/src/minesweeper.rs b/src/minesweeper.rs new file mode 100644 index 0000000..81426c4 --- /dev/null +++ b/src/minesweeper.rs @@ -0,0 +1,138 @@ +use std::{collections::HashSet, fmt::{Display, Write}}; + +use rand::Rng; + +pub type Position = (usize, usize); + +#[derive(Debug)] +pub struct Minesweeper { + width: usize, + height: usize, + open_fields: HashSet, + mines: HashSet, + flags: HashSet, + game_over: bool +} + +impl Display for Minesweeper { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + for y in 0..self.height { + for x in 0..self.height { + let pos = (x, y); + if self.game_over && self.mines.contains(&pos) { + f.write_str("B ")?; + } else if self.flags.contains(&pos) { + f.write_str("F ")?; + } else if !self.open_fields.contains(&pos) { + f.write_str("# ")?; + } else if self.mines.contains(&pos) { + f.write_str("B ")?; + } else { + let mines = self.count_mines(pos); + if mines == 0 { + write!(f, "☐ ")?; + } else { + write!(f, "{} ", mines)?; + } + } + } + f.write_char('\n')?; + } + + Ok(()) + } +} + +pub enum OpenResult { + Mine, + NoMine(u8) +} + +impl Minesweeper { + pub fn new(width: usize, height: usize, mine_count: usize) -> Minesweeper { + Minesweeper { + width, + height, + open_fields: HashSet::new(), + mines: generate_mines(width, height, mine_count), + flags: HashSet::new(), + game_over: false + } + } + + pub fn iter_neighbours(&self, (x, y): Position) -> impl Iterator { + let width = self.width; + let height = self.height; + + (x.max(1) - 1 ..= (x + 1).min(width - 1)).flat_map( + move |i| (y.max(1) - 1 ..= (y + 1).min(height - 1)).map(move |j| (i, j)) + ).filter(move |&pos| pos != (x, y)) + } + + pub fn count_mines(&self, position: Position) -> u8 { + self.iter_neighbours(position) + .filter(|pos| self.mines.contains(pos)) + .count() as u8 + } + + pub fn open(&mut self, position: Position) -> Option { + if self.game_over || self.open_fields.contains(&position) || self.flags.contains(&position) { + return None; + } + + self.open_fields.insert(position); + let is_mine = self.mines.contains(&position); + if is_mine { + self.game_over = true; + Some(OpenResult::Mine) + } else { + let mines = self.count_mines(position); + if mines == 0 { + for neighbour in self.iter_neighbours(position) { + self.open(neighbour); + } + } + Some(OpenResult::NoMine(mines)) + } + } + + pub fn toggle_flag(&mut self, position: Position) { + if self.game_over || self.open_fields.contains(&position) { + return; + } + + if self.flags.contains(&position) { + self.flags.remove(&position); + } else { + self.flags.insert(position); + } + } +} + +fn generate_mines(field_width: usize, field_height: usize, mine_count: usize) -> HashSet { + let mut mines = HashSet::new(); + + let mut rng = rand::thread_rng(); + while mines.len() < mine_count { + let x = rng.gen_range(0..field_width); + let y = rng.gen_range(0..field_height); + mines.insert((x, y)); + } + + mines +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test() { + let mut ms = Minesweeper::new(5, 5, 5); + + ms.open((2, 2)); + ms.toggle_flag((3, 3)); + + println!("{}", ms); + } +}