From 2826d78bbfab50eab90e5a1611576f33e752b7d8 Mon Sep 17 00:00:00 2001 From: stale Date: Sun, 29 May 2022 03:14:44 -0300 Subject: constrained some fields, first move grace config, bugfixes --- src/main.rs | 19 ++-- src/minesweeper.rs | 260 +++++++++++++++++++++++++++++++++++------------------ src/types.rs | 16 +--- 3 files changed, 185 insertions(+), 110 deletions(-) (limited to 'src') 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> { 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::().ok()), - rinfo.get("rheight").and_then(|ht| ht.parse::().ok()), + if let (Some(w),Some(h),Some(num),Some(denom),access,asfm,limit) = ( + rinfo.get("rwidth").and_then(|wt| wt.parse::().ok()), + rinfo.get("rheight").and_then(|ht| ht.parse::().ok()), rinfo.get("rration").and_then(|nt| nt.parse::().ok()), - rinfo.get("rratiod").and_then(|dt| dt.parse::().ok()), + rinfo.get("rratiod").and_then(|dt| dt.parse::().ok()), rinfo.get("raccess"), + rinfo.get("ralwayssafe1move"), rinfo.get("rlimit").and_then(|l| l.parse::().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> { } // If a move is made, broadcast new board, else just send current board -async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver, players: PlayerMapData, bconf: BoardConf) { +async fn gameloop(mut move_rx: tokio::sync::mpsc::UnboundedReceiver, 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, 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, - 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(&self, pos: (T,T)) -> Option> + where T: TryInto + { + 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(&mut self, pos: (T,T), mut f: impl FnMut(u8) -> u8) where T: TryInto { + if let Some(neighs) = self.neighs(pos) { + let npos = neighs.iter().filter_map(|pos| self.pos_to_off(*pos)).collect::>(); + npos.iter().for_each(|o| { + self.data[*o] = f(self.data[*o]); + }); + } + } + + pub fn pos_to_off(&self, pos: (usize,usize)) -> Option + { + 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(&self, pos: (T,T)) -> Option<(usize, usize)> + where T: TryInto + { + 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 { 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" "), _ 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