summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--assets/index.html5
-rw-r--r--src/main.rs19
-rw-r--r--src/minesweeper.rs260
-rw-r--r--src/types.rs16
4 files changed, 188 insertions, 112 deletions
diff --git a/assets/index.html b/assets/index.html
index bb18d1c..4571428 100644
--- a/assets/index.html
+++ b/assets/index.html
@@ -19,8 +19,9 @@
in every<input name="rratiod" type="number" value="8" required>
tiles are mines
</label><br>
- <label>public, ie. shown in the lobby? <input name="raccess" type="checkbox"></label><br>
- <label>player limit (0 for none)<input name="rlimit" type="number"></label><br>
+ <label>public, ie. shown in the lobby <input name="raccess" type="checkbox"></label><br>
+ <label>always safe first move <input name="ralwayssafe1move" type="checkbox"></label><br>
+ <label>player limit<input name="rlimit" type="number" value="32"></label><br>
<button>create</button>
</fieldset>
<form>
diff --git a/src/main.rs b/src/main.rs
index 6c2f375..2b5f9b1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3,6 +3,7 @@ use std::{
net::SocketAddr,
sync::Arc,
collections::HashMap,
+ num::NonZeroUsize,
};
mod types;
@@ -48,15 +49,16 @@ async fn tokio_main(conf: Config) -> Result<(), Box<dyn Error>> {
let rooms = rooms.clone();
let pubs = pubs.clone();
async move {
- if let (Some(w),Some(h),Some(num),Some(denom),access,limit) = (
- rinfo.get("rwidth").and_then(|wt| wt.parse::<usize>().ok()),
- rinfo.get("rheight").and_then(|ht| ht.parse::<usize>().ok()),
+ if let (Some(w),Some(h),Some(num),Some(denom),access,asfm,limit) = (
+ rinfo.get("rwidth").and_then(|wt| wt.parse::<NonZeroUsize>().ok()),
+ rinfo.get("rheight").and_then(|ht| ht.parse::<NonZeroUsize>().ok()),
rinfo.get("rration").and_then(|nt| nt.parse::<usize>().ok()),
- rinfo.get("rratiod").and_then(|dt| dt.parse::<usize>().ok()),
+ rinfo.get("rratiod").and_then(|dt| dt.parse::<NonZeroUsize>().ok()),
rinfo.get("raccess"),
+ rinfo.get("ralwayssafe1move"),
rinfo.get("rlimit").and_then(|l| l.parse::<usize>().ok()),
) {
- let board_conf = BoardConf { w, h, mine_ratio: (num,denom) };
+ let board_conf = minesweeper::BoardConf { w, h, mine_ratio: (num,denom), always_safe_first_move: asfm.is_some() };
let name = rinfo.get("rname").map(|r| r.to_owned()).unwrap_or(format!("{w}x{h} room"));
let mut rooms = rooms.write().await;
@@ -151,10 +153,9 @@ async fn tokio_main(conf: Config) -> Result<(), Box<dyn Error>> {
}
// If a move is made, broadcast new board, else just send current board
-async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver<MetaMove>, players: PlayerMapData, bconf: BoardConf) {
+async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver<MetaMove>, players: PlayerMapData, bconf: minesweeper::BoardConf) {
use minesweeper::*;
- let mine_cnt = (bconf.w * bconf.h * bconf.mine_ratio.0)/(bconf.mine_ratio.1);
- let mut game = Game::new(Board::new(bconf.w, bconf.h), mine_cnt);
+ let mut game = Game::new(bconf);
let mut latest_player_name = None;
while let Some(req) = move_rx.recv().await {
let done = game.phase == Phase::Die || game.phase == Phase::Win;
@@ -167,7 +168,7 @@ async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver<MetaMove>, p
latest_player_name = players.read().await.get(&o).map(|p| p.name.clone());
},
MetaMove::Dump => (),
- MetaMove::Reset => { game = Game::new(Board::new(bconf.w, bconf.h), mine_cnt); },
+ MetaMove::Reset => { game = Game::new(bconf); },
}
use warp::ws::Message;
let mut reply = vec![Message::binary(game.board.render())];
diff --git a/src/minesweeper.rs b/src/minesweeper.rs
index 6f5b3b6..c81aa09 100644
--- a/src/minesweeper.rs
+++ b/src/minesweeper.rs
@@ -1,9 +1,14 @@
-use std::convert::TryFrom;
+use std::{
+ convert::TryInto,
+ num::NonZeroUsize,
+};
use rand::{ thread_rng, Rng, distributions::Uniform };
const HIDDEN_BIT: u8 = 1 << 7;
pub const FLAGGED_BIT: u8 = 1 << 6;
const CORRECT_BIT: u8 = 1 << 5; // grading for a rightly flagged mine
-const MINE_VAL: u8 = !(HIDDEN_BIT | FLAGGED_BIT | CORRECT_BIT);
+// all the bits that aren't flags
+const TILE_NUMBITS: u8 = !(HIDDEN_BIT | FLAGGED_BIT | CORRECT_BIT);
+const MINED: u8 = HIDDEN_BIT | TILE_NUMBITS;
const NEIGH_OFFS: &[(isize,isize)] = &[
(-1,-1),(0,-1),(1,-1),
(-1, 0), (1, 0),
@@ -16,18 +21,35 @@ pub enum Phase {
Run,
Die,
Win,
- Leave,
+// Leave,
}
pub struct Game {
pub phase: Phase,
pub board: Board,
- pub mine_count: usize,
+ pub board_conf: BoardConf,
+}
+
+#[derive(Debug, Clone, Copy)]
+pub struct BoardConf {
+ pub w: NonZeroUsize,
+ pub h: NonZeroUsize,
+ /// mines/tiles, expressed as (numerator, denominator)
+ pub mine_ratio: (usize,NonZeroUsize),
+ pub always_safe_first_move: bool,
+}
+
+impl std::fmt::Display for BoardConf {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(f, "{}x{} {}/{}", self.w, self.h, self.mine_ratio.0, self.mine_ratio.1)
+ }
}
+
pub struct Board {
pub data: Vec<u8>,
- pub width: usize,
- pub height: usize,
+ pub width: NonZeroUsize,
+ pub height: NonZeroUsize,
pub hidden_tiles: usize,
+ pub mine_count: usize,
}
#[derive(Debug)]
pub enum MoveType {
@@ -39,12 +61,16 @@ pub struct Move {
pub t: MoveType,
pub pos: (usize,usize),
}
+
pub struct MoveResult(pub Board, pub bool);
impl Game {
- pub fn new(board: Board, mine_count: usize) -> Self {
- let mut g = Game { phase: Phase::SafeFirstMove, board, mine_count };
- g.board = g.board.spread_mines(mine_count);
- g
+ pub fn new(conf: BoardConf) -> Self {
+ let board = Board::new(conf);
+ Game {
+ phase: if conf.always_safe_first_move { Phase::SafeFirstMove } else { Phase::Run },
+ board,
+ board_conf: conf
+ }
}
pub fn act(mut self, m: Move) -> Self {
let lost_phase = | phase | {
@@ -69,120 +95,145 @@ impl Game {
MoveType::ToggleFlag => self.board = self.board.flag(m.pos).0,
};
- if self.board.hidden_tiles == self.mine_count {
- self.phase = Phase::Win;
- }
if self.phase == Phase::FirstMoveFail {
- self.board = Board::new(self.board.width, self.board.height);
- self.mine_count -= 1;
- self.board = self.board.spread_mines(self.mine_count);
- self.phase = Phase::SafeFirstMove;
- self = self.act(m);
+ let winnable = self.board.mine_count < (self.board.width.get() * self.board.height.get());
+ if winnable {
+ self.board.move_mine_elsewhere(m.pos);
+ self.phase = Phase::Run;
+ self = self.act(m);
+ } else {
+ self.phase = Phase::Die;
+ }
+ } else if self.phase != Phase::Die && self.board.hidden_tiles == self.board.mine_count {
+ self.phase = Phase::Win;
}
self
}
}
impl Board {
- pub fn new(w: usize, h: usize) -> Self {
- Board {
- data: [HIDDEN_BIT].repeat(w*h),
+ pub fn new(conf: BoardConf) -> Self {
+ let (w,h) = (conf.w,conf.h);
+ let area = w.get()*h.get();
+ let mine_count = ((conf.mine_ratio.0 * area) / conf.mine_ratio.1.get()).clamp(0, area);
+ let b = Board {
+ data: [HIDDEN_BIT].repeat(area),
width: w,
height: h,
- hidden_tiles: w*h,
- }
+ hidden_tiles: area,
+ mine_count: mine_count.clone(),
+ };
+ b.spread_mines(mine_count)
}
- pub fn spread_mines(mut self, count: usize) -> Self {
+ pub fn spread_mines(mut self, mut count: usize) -> Self {
let mut rng = thread_rng();
- let mut c = count;
- let w = self.width;
- let h = self.height;
- while c > 0 {
- let randpos: (usize, usize) = (rng.sample(Uniform::new(0,w-1)), rng.sample(Uniform::new(0,h-1)));
- let o = self.pos_to_off(randpos);
- if self.data[o] == MINE_VAL | HIDDEN_BIT { continue }
+ let w = self.width.get();
+ let h = self.height.get();
+ while count > 0 {
+ let randpos: (usize, usize) = (rng.sample(Uniform::new(0,w)), rng.sample(Uniform::new(0,h)));
+ let o = self.pos_to_off_unchecked(randpos);
+ if self.data[o] == MINED { continue }
else {
- self.data[o] |= MINE_VAL;
- c -= 1;
- for (nx,ny) in NEIGH_OFFS {
- let x = randpos.0;
- let y = randpos.1;
- let nxc = usize::try_from(isize::try_from(x).unwrap() + nx);
- let nyc = usize::try_from(isize::try_from(y).unwrap() + ny);
- let _ = nxc.and_then(|nx: usize| { nyc.and_then(|ny: usize| {
- if nx > w - 1 || ny > h - 1 { return usize::try_from(-1) };
- let off = self.pos_to_off((nx,ny));
- let c = &mut self.data[off];
- if *c != HIDDEN_BIT | MINE_VAL {
- *c += 1;
- }
- Ok(0)
- })
- });
- }
+ self.data[o] = MINED;
+ count -= 1;
+ let minepos = pos_u2i(randpos).unwrap();
+ self.map_neighs(minepos, |neigh| {
+ if neigh != MINED {
+ neigh + 1
+ } else { neigh }
+ });
}
}
self
}
- pub fn pos_to_off(&self, pos: (usize, usize)) -> usize {
- pos.0 + pos.1 * self.width
- }
- pub fn flood_reveal(&mut self, pos: (usize, usize)) {
- if pos.0 > self.width || pos.1 > self.height { return; }
- let off = self.pos_to_off(pos);
- let c = &mut self.data[off];
- if *c & HIDDEN_BIT > 0 {
- *c &= !(HIDDEN_BIT | FLAGGED_BIT);
- self.hidden_tiles -= 1;
- if *c > 0 { return }
- drop(c);
- for (ox,oy) in NEIGH_OFFS {
- let nxr = usize::try_from(isize::try_from(pos.0).unwrap() + ox);
- let nyr = usize::try_from(isize::try_from(pos.1).unwrap() + oy);
- let _ = nxr.and_then(|nx: usize| { nyr.and_then(|ny: usize| {
- if nx > self.width - 1 || ny > self.height - 1 { return usize::try_from(-1) };
- self.flood_reveal((nx, ny));
- Ok(0)
- })
- });
+
+ fn neighs<T>(&self, pos: (T,T)) -> Option<Vec<(usize,usize)>>
+ where T: TryInto<isize>
+ {
+ if let (Ok(ox),Ok(oy)) = (pos.0.try_into(),pos.1.try_into()) {
+ Some(NEIGH_OFFS
+ .iter()
+ .map(|(x,y)| (*x + ox, *y + oy)).filter_map(|p| self.bounded(p))
+ .collect())
+ } else {
+ None
+ }
+ }
+ fn map_neighs<T>(&mut self, pos: (T,T), mut f: impl FnMut(u8) -> u8) where T: TryInto<isize> {
+ if let Some(neighs) = self.neighs(pos) {
+ let npos = neighs.iter().filter_map(|pos| self.pos_to_off(*pos)).collect::<Vec<usize>>();
+ npos.iter().for_each(|o| {
+ self.data[*o] = f(self.data[*o]);
+ });
+ }
+ }
+
+ pub fn pos_to_off(&self, pos: (usize,usize)) -> Option<usize>
+ {
+ self.bounded(pos).map(|x| self.pos_to_off_unchecked(x))
+ }
+ pub fn pos_to_off_unchecked(&self, pos: (usize, usize)) -> usize {
+ pos.0 + pos.1 * self.width.get()
+ }
+ pub fn bounded<T>(&self, pos: (T,T)) -> Option<(usize, usize)>
+ where T: TryInto<usize>
+ {
+ if let (Ok(x),Ok(y)) = (
+ pos.0.try_into(),
+ pos.1.try_into(),
+ ) {
+ (x < self.width.get() && y < self.height.get()).then(|| (x,y))
+ } else { None }
+ }
+ pub fn flood_reveal(&mut self, pos: (usize,usize)) {
+ if let Some(off) = self.pos_to_off(pos) {
+ let c = &mut self.data[off];
+ if *c & HIDDEN_BIT > 0 {
+ *c &= !(HIDDEN_BIT | FLAGGED_BIT);
+ self.hidden_tiles -= 1;
+ if *c > 0 { return }
+ drop(c);
+ self.neighs(pos).map(|n| n.iter().for_each(|pos| {
+ self.flood_reveal(*pos);
+ }));
}
}
}
- pub fn reveal(mut self, pos: (usize, usize)) -> MoveResult {
- if pos.0 > self.width - 1 || pos.1 > self.height - 1 {
- println!("attempted OOB reveal @ {:?}", pos);
- return MoveResult { 0: self, 1: false };
+ pub fn reveal(mut self, pos: (usize,usize)) -> MoveResult {
+ if let Some(off) = self.pos_to_off(pos) {
+ self.flood_reveal(pos);
+ let c = self.data[off];
+ MoveResult { 0: self, 1: (c & !(FLAGGED_BIT | CORRECT_BIT)) == TILE_NUMBITS }
+ } else {
+ MoveResult { 0: self, 1: false }
}
- let off = self.pos_to_off(pos);
- self.flood_reveal(pos);
- let c = self.data[off];
- MoveResult { 0: self, 1: (c & !(FLAGGED_BIT | CORRECT_BIT)) == MINE_VAL }
}
pub fn grade(mut self) -> Board {
for i in &mut self.data {
- if *i == MINE_VAL | FLAGGED_BIT | HIDDEN_BIT {
+ if *i == TILE_NUMBITS | FLAGGED_BIT | HIDDEN_BIT {
*i |= CORRECT_BIT;
}
}
self
}
- pub fn flag(mut self, pos: (usize, usize)) -> MoveResult {
- let off = self.pos_to_off(pos);
- self.data[off] ^= FLAGGED_BIT;
+ pub fn flag(mut self, pos: (usize,usize)) -> MoveResult {
+ if let Some(off) = self.pos_to_off(pos) {
+ self.data[off] ^= FLAGGED_BIT;
+ }
MoveResult { 0: self, 1: false }
}
pub fn render(&self) -> Vec<u8> {
let mut ret = vec![];
- for y in 0..self.height {
- for x in 0..self.width {
- let c = &self.data[self.pos_to_off((x,y))];
+ for y in 0..self.height.get() {
+ for x in 0..self.width.get() {
+ let c = &self.data[self.pos_to_off_unchecked((x,y))];
match *c {
0 => ret.extend_from_slice(b"&nbsp"),
_ if *c <= 8 => ret.push(b'0' + c),
_ if (*c & CORRECT_BIT) > 0 => ret.push(b'C'),
_ if (*c & FLAGGED_BIT) > 0 => ret.push(b'F'),
_ if (*c & HIDDEN_BIT) > 0 => ret.push(b'#'),
- _ if *c == MINE_VAL => ret.push(b'O'),
+ _ if *c == TILE_NUMBITS => ret.push(b'O'),
_ => ret.push(b'?'),
}
}
@@ -190,5 +241,42 @@ impl Board {
}
ret
}
+
+ pub fn move_mine_elsewhere(&mut self, pos: (usize, usize)) {
+ let mut surround_count = 0;
+ self.map_neighs(pos, |val| {
+ if (val & !FLAGGED_BIT) == MINED {
+ surround_count += 1;
+ val
+ } else {
+ val - 1
+ }});
+ let off = self.pos_to_off(pos).unwrap();
+ let vacant_pos = {
+ let v = self.data.iter()
+ .enumerate()
+ .filter(|(_,val)| (*val & TILE_NUMBITS) != TILE_NUMBITS)
+ .map(|(p,_)| p)
+ .next()
+ .unwrap(); // there must be at least one
+ (v%self.width.get(), v/self.width.get())
+ };
+ let voff = self.pos_to_off_unchecked(vacant_pos);
+ debug_assert!(voff != off, "swapped mine to the same position in a FirstMoveFail/grace'd first move (???)");
+
+ { // swap 'em (keep these together, pls kthnx (bugs were had))
+ self.data[voff] |= MINED;
+ self.data[off] = surround_count;
+ }
+
+ self.map_neighs(vacant_pos, |val| {
+ if (val & !FLAGGED_BIT) == MINED { val } else { val + 1 }
+ });
+ }
+}
+
+fn pos_u2i(pos: (usize, usize)) -> Option<(isize, isize)> {
+ if let (Ok(x),Ok(y)) = (pos.0.try_into(), pos.1.try_into())
+ { Some((x,y)) } else { None }
}
diff --git a/src/types.rs b/src/types.rs
index df9e168..1c9ae89 100644
--- a/src/types.rs
+++ b/src/types.rs
@@ -23,20 +23,6 @@ pub struct Config {
pub socket_addr: SocketAddr,
}
-#[derive(Debug, Clone, Copy)]
-pub struct BoardConf {
- pub w: usize,
- pub h: usize,
- /// tiles to mines, expressed as (numerator, denominator)
- pub mine_ratio: (usize,usize),
-}
-
-impl Display for BoardConf {
- fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- write!(f, "{}x{} {}/{}", self.w, self.h, self.mine_ratio.0, self.mine_ratio.1)
- }
-}
-
#[derive(Debug)]
pub struct Room {
pub name: String,
@@ -45,7 +31,7 @@ pub struct Room {
pub public: bool,
pub driver: tokio::task::JoinHandle<()>,
pub cmd_stream: CmdTx,
- pub board_conf: BoardConf,
+ pub board_conf: minesweeper::BoardConf,
}
#[derive(Debug)]