feat: first version of playable minesweeper in browser
This commit is contained in:
parent
0c6d61ac63
commit
038c2d8257
@ -3,7 +3,10 @@ name = "rust-wasm-minesweeper"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
[lib]
|
||||||
|
crate-type=["cdylib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
rand = "0.8.5"
|
rand = "0.8.5"
|
||||||
|
wasm-bindgen = "0.2.80"
|
||||||
|
getrandom = { version = "0.2.6", features = ["js"] }
|
||||||
|
62
index.html
Normal file
62
index.html
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<title>Minesweeper</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<style>
|
||||||
|
html {
|
||||||
|
font-size: 200%;
|
||||||
|
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
text-decoration: none;
|
||||||
|
text-align: center;
|
||||||
|
width: 1.2rem;
|
||||||
|
height: 1.2rem;
|
||||||
|
line-height: 1.2rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root">
|
||||||
|
<script type="module">
|
||||||
|
import init, { getState, openField } from "./pkg/rust_wasm_minesweeper.js";
|
||||||
|
|
||||||
|
async function main() {
|
||||||
|
await init();
|
||||||
|
render();
|
||||||
|
}
|
||||||
|
|
||||||
|
function render() {
|
||||||
|
let state = getState();
|
||||||
|
let data = state.split("\n").map(row => row.trim().split(/\s+/));
|
||||||
|
|
||||||
|
let root = document.getElementById("root");
|
||||||
|
root.innerHTML = "";
|
||||||
|
root.style.display = "inline-grid"
|
||||||
|
root.style.gridTemplate = `repeat(${data.length}, auto) / repeat(${data[0].length}, auto)`
|
||||||
|
|
||||||
|
for (let y = 0; y < data.length; y++) {
|
||||||
|
for (let x = 0; x < data[y].length; x++) {
|
||||||
|
let elem = document.createElement("a");
|
||||||
|
elem.classList.add("field");
|
||||||
|
elem.href = "#";
|
||||||
|
elem.innerHTML = data[y][x];
|
||||||
|
elem.addEventListener("click", e => {
|
||||||
|
e.preventDefault();
|
||||||
|
openField(x, y);
|
||||||
|
render();
|
||||||
|
});
|
||||||
|
root.appendChild(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
|
</script>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
127
src/lib.rs
127
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)]
|
thread_local! {
|
||||||
pub struct Minesweeper {
|
static MINESWEEPER: RefCell<Minesweeper> = RefCell::new(Minesweeper::new(10, 10, 10));
|
||||||
width: usize,
|
|
||||||
height: usize,
|
|
||||||
open_fields: HashSet<Position>,
|
|
||||||
mines: HashSet<Position>,
|
|
||||||
flags: HashSet<Position>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Display for Minesweeper {
|
#[wasm_bindgen(js_name = getState)]
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
pub fn get_state() -> String {
|
||||||
for y in 0..self.height {
|
MINESWEEPER.with(move |ms| ms.borrow().to_string())
|
||||||
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(())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub enum OpenResult {
|
#[wasm_bindgen(js_name = openField)]
|
||||||
Mine,
|
pub fn open_field(x: usize, y: usize) {
|
||||||
NoMine(u8)
|
MINESWEEPER.with(move |ms| ms.borrow_mut().open((x, y)));
|
||||||
}
|
|
||||||
|
|
||||||
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<Item = Position> {
|
|
||||||
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<OpenResult> {
|
|
||||||
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<Position> {
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,3 +0,0 @@
|
|||||||
fn main() {
|
|
||||||
println!("Hello, world!");
|
|
||||||
}
|
|
138
src/minesweeper.rs
Normal file
138
src/minesweeper.rs
Normal file
@ -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<Position>,
|
||||||
|
mines: HashSet<Position>,
|
||||||
|
flags: HashSet<Position>,
|
||||||
|
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<Item = Position> {
|
||||||
|
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<OpenResult> {
|
||||||
|
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<Position> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user