summaryrefslogtreecommitdiff
path: root/src/minesweeper.rs
diff options
context:
space:
mode:
authorstale <redkugelblitzin@gmail.com>2022-05-29 03:14:44 -0300
committerstale <redkugelblitzin@gmail.com>2022-05-29 03:14:44 -0300
commit2826d78bbfab50eab90e5a1611576f33e752b7d8 (patch)
treed97f56ae29ea7ddbcafd68d5bc233102a236e284 /src/minesweeper.rs
parentfae0fca7aabb81325a296a1d6202239c3db44b60 (diff)
constrained some fields, first move grace config, bugfixes
Diffstat (limited to 'src/minesweeper.rs')
-rw-r--r--src/minesweeper.rs260
1 files changed, 174 insertions, 86 deletions
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 }
}