From 2826d78bbfab50eab90e5a1611576f33e752b7d8 Mon Sep 17 00:00:00 2001
From: stale <redkugelblitzin@gmail.com>
Date: Sun, 29 May 2022 03:14:44 -0300
Subject: constrained some fields, first move grace config, bugfixes

---
 assets/index.html  |   5 +-
 src/main.rs        |  19 ++--
 src/minesweeper.rs | 260 +++++++++++++++++++++++++++++++++++------------------
 src/types.rs       |  16 +---
 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)]
-- 
cgit v1.2.3