use std::{ io, io::Read }; mod minesweeper { use std::convert::TryFrom; use std::fmt::Display; 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); const NEIGH_OFFS: &[(i8,i8)] = &[ (-1,-1),(0,-1),(1,-1), (-1, 0), (1, 0), (-1, 1),(0, 1),(1, 1), ]; #[derive(PartialEq)] pub enum Phase { SafeFirstMove, Run, Die, Win, Leave, } pub struct Game { pub phase: Phase, pub board: Board, pub mine_count: usize, } pub struct Board { pub data: Vec, pub width: usize, pub height: usize, pub hidden_tiles: usize, } pub enum MoveType { Reveal, ToggleFlag, } 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 act(mut self, m: Move) -> Self { let lost_phase = | phase | { match phase { Phase::SafeFirstMove => unimplemented!(), Phase::Run => Phase::Die, _ => unreachable!(), } }; match m.t { MoveType::Reveal => { let kaboom: bool; self.board = { let mr = self.board.reveal(m.pos); kaboom = mr.1; mr.0 }; if kaboom { self.phase = lost_phase(self.phase) } if self.phase == Phase::SafeFirstMove { self.phase = Phase::Run } }, MoveType::ToggleFlag => self.board = self.board.flag(m.pos).0, }; if self.board.hidden_tiles == self.mine_count { self.phase = Phase::Win; } self } } impl Board { pub fn new(w: usize, h: usize) -> Self { Board { data: [HIDDEN_BIT].repeat(w*h), width: w, height: h, hidden_tiles: w*h, } } pub fn spread_mines(mut self, 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 } 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(x as i8 + nx); let nyc = usize::try_from(y as i8 + 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 } 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(pos.0 as i8 + ox); let nyr = usize::try_from(pos.1 as i8 + 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) }) }); } } } pub fn reveal(mut self, pos: (usize, usize)) -> MoveResult { if pos.0 > self.width - 1 || pos.1 > self.height - 1 { panic!("OOB reveal"); } 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 } // Kaboom } pub fn grade(mut self) -> Board { for i in &mut self.data { if *i == MINE_VAL | 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; MoveResult { 0: self, 1: false } } pub fn render(&self, cursor: Option<(usize,usize)>) -> Vec { const CYAN: &[u8] = &[27,b'[',b'3',b'6',b'm']; const YELLOW: &[u8] = &[27,b'[',b'3',b'3',b'm']; const GREEN: &[u8] = &[27,b'[',b'3',b'2',b'm']; const RED: &[u8] = &[27,b'[',b'3',b'1',b'm']; const FG: &[u8] = &[27,b'[',b'3',b'9',b'm']; const ULINE: &[u8] = &[27,b'[',b'4',b'm']; const REGULAR: &[u8] = &[27,b'[',b'0',b'm']; let mut ret = vec![27,b'[',b'2',b'J',27,b'[',b'0',b'm',b'\r']; let mut cur_clr = FG; for y in 0..self.height { for x in 0..self.width { let c = &self.data[self.pos_to_off((x,y))]; let is_cursor = if let Some(cursor) = cursor { cursor.0 == x && cursor.1 == y } else { false }; if is_cursor { ret.extend_from_slice(ULINE); } match *c { 0 => ret.push(b' '), _ if *c <= 8 => { if cur_clr != FG { ret.extend_from_slice(FG); cur_clr = FG; } ret.push(b'0' + c); }, _ if (*c & CORRECT_BIT) > 0 => { if cur_clr != GREEN { ret.extend_from_slice(GREEN); cur_clr = GREEN; } ret.push(b'F') }, _ if (*c & FLAGGED_BIT) > 0 => { if cur_clr != YELLOW { ret.extend_from_slice(YELLOW); cur_clr = YELLOW; } ret.push(b'F'); }, _ if (*c & HIDDEN_BIT) > 0 => { if cur_clr != CYAN { ret.extend_from_slice(CYAN); cur_clr = CYAN; } ret.push(b'-'); }, _ if *c == MINE_VAL => { if cur_clr != RED { ret.extend_from_slice(RED); cur_clr = RED; } ret.push(b'O'); }, _ => ret.push(b'?'), } if is_cursor { ret.extend_from_slice(REGULAR); ret.extend_from_slice(cur_clr); } } ret.push(b'\r'); ret.push(b'\n'); } ret } } } use minesweeper::*; use std::convert::{ TryFrom, TryInto }; use std::io::Write; fn main() -> Result<(), io::Error> { let board = Board::new(10,10); let mut game = Game::new(board, 10); let mut cursor: (usize, usize) = (0,0); let stdout = io::stdout(); let mut lstdout = stdout.lock(); while game.phase == Phase::Run || game.phase == Phase::SafeFirstMove { let screen = game.board.render(Some(cursor)); lstdout.write_all(&screen)?; lstdout.flush()?; let mm = playerctrl( (cursor.0.try_into().unwrap(), cursor.1.try_into().unwrap()), (game.board.width.try_into().unwrap(), game.board.height.try_into().unwrap()))?; match mm { MetaMove::Move(m) => game = game.act(m), MetaMove::CursorMove(newc) => cursor = newc, MetaMove::Quit => game.phase = Phase::Leave, MetaMove::Noop => (), } } game.board = game.board.grade(); lstdout.write_all(&game.board.render(None))?; Ok(()) } enum MetaMove { Move(Move), CursorMove((usize, usize)), Noop, Quit, } fn playerctrl(mut cursor: (isize, isize), bounds: (isize, isize)) -> io::Result { const ERR_STR: &str = "bad/no input"; let mut mov_dir = | dir: (isize,isize) | { cursor.0 += dir.0; cursor.1 += dir.1; MetaMove::CursorMove(cursor_bounds(cursor, bounds)) }; let stdin = io::stdin(); let lstdin = stdin.lock(); let mut input = lstdin.bytes(); Ok(match input.next().expect(ERR_STR)? { b'w' => mov_dir((0,-1)), b'a' => mov_dir((-1,0)), b's' => mov_dir((0,1)), b'd' => mov_dir((1,0)), 13 => MetaMove::Move(Move { t: MoveType::Reveal, pos: cursor_bounds(cursor, bounds) }), b'f' => MetaMove::Move(Move { t: MoveType::ToggleFlag, pos: cursor_bounds(cursor, bounds) }), b'q' => MetaMove::Quit, 27 => { if input.next().expect(ERR_STR)? == b'[' { match input.next().expect(ERR_STR)? { b'A' => mov_dir((0,-1)), b'B' => mov_dir((0,1)), b'C' => mov_dir((1,0)), b'D' => mov_dir((-1,0)), _ => MetaMove::Noop, } } else { MetaMove::Noop } } v => { println!("{:?}", v); MetaMove::Noop }, }) } fn cursor_bounds(mut c: (isize, isize), b: (isize, isize)) -> (usize, usize) { if c.0 > b.0 - 1 { c.0 = 0; } else if c.0 < 0 { c.0 = b.0 - 1; } if c.1 > b.1 - 1 { c.1 = 0; } else if c.1 < 0 { c.1 = b.1 - 1; } ( usize::try_from(c.0).unwrap(), usize::try_from(c.1).unwrap() ) }