NLinker icon

Console minesweeper in Rust

NLinker | PRO | 07/10/19 05:53:53 PM UTC | 0 ⭐ | 1271 👁️ | Never ⏰ | []
Rust |

19.51 KB

|

None

|

0 👍

/

0 👎

use termion::{clear, cursor, color, style};
use termion::raw::IntoRawMode;
use termion::input::TermRead;
use termion::event::Key;
 
use std::env;
use std::io::{self, Read, Write};
use std::process;
 
pub struct Randomizer {
    state: u64,
}
 
impl Randomizer {
    /// Create a new randomizer from a seed.
    pub fn new(seed: u64) -> Randomizer {
        Randomizer {
            state: seed.wrapping_add(0xDEADBEEFDEADBEEF),
        }
    }
 
    /// Write a buffer into the randomizer (as entropy).
    pub fn write(&mut self, buf: &[u8]) {
        for &i in buf {
            self.write_u8(i);
        }
    }
 
    /// Read random bytes to a buffer.
    pub fn read(&mut self, buf: &mut [u8]) {
        for i in buf {
            *i = self.read_u8();
        }
    }
 
    /// Read a byte from the randomizer.
    pub fn read_u8(&mut self) -> u8 {
        self.state = self.state.wrapping_mul(6364136223846793005).wrapping_add(1);
        (self.state.wrapping_mul(1152921504735157271).rotate_right(2) ^ 0xFAB00105C0DE) as u8
    }
 
    /// Write a byte into the randomizer.
    ///
    /// This is used for collecting entropy to the randomizer.
    pub fn write_u8(&mut self, b: u8) {
        self.state ^= b as u64;
        self.read_u8();
    }
}
 
/// A cell in the grid.
#[derive(Copy, Clone, PartialEq, Eq, Debug, Hash)]
struct Cell {
    /// Does it contain a mine?
    mine: bool,
    /// Is it revealed?
    ///
    /// That is, is it showed or chosen previously by the player?
    revealed: bool,
    /// Is this cell observed?
    ///
    /// That is, is the state of this cell determined, or is it pending for randomization.
    observed: bool,
    /// Does this flag contain a flag?
    flagged: bool,
}
 
/// The string printed for flagged cells.
const FLAGGED: &'static str = "F";
/// The string printed for mines in the game over revealing.
const MINE: &'static str = "*";
/// The string printed for concealed cells.
const CONCEALED: &'static str = "▒";
 
/// The game over screen.
const GAME_OVER: &'static str = "╔═════════════════╗\n\r\
                                 ║───┬Game over────║\n\r\
                                 ║ r ┆ replay      ║\n\r\
                                 ║ q ┆ quit        ║\n\r\
                                 ╚═══╧═════════════╝";
 
/// The upper and lower boundary char.
const HORZ_BOUNDARY: &'static str = "─";
/// The left and right boundary char.
const VERT_BOUNDARY: &'static str = "│";
 
/// The top-left corner
const TOP_LEFT_CORNER: &'static str = "┌";
/// The top-right corner
const TOP_RIGHT_CORNER: &'static str = "┐";
/// The bottom-left corner
const BOTTOM_LEFT_CORNER: &'static str = "└";
/// The bottom-right corner
const BOTTOM_RIGHT_CORNER: &'static str = "┘";
 
/// The help page.
const HELP: &'static str = r#"
minesweeper ~ a simple minesweeper implementation.
rules:
    Select a cell to reveal, printing the number of adjacent cells holding a mine.
    If no adjacent cells hold a mine, the cell is called free. Free cell will recursively
    reveal their neighboring cells. If a mine is revealed, you loose. The grid wraps.
flags:
    -r | --height N ~ set the height of the grid.
    -c | --width N  ~ set the width of the grid.
    -h | --help     ~ this help page.
    -b              ~ beginner mode.
    -i              ~ intermediate mode.
    -a              ~ advanced mode.
    -g              ~ god mode.
controls:
    ---selection--------------------
    space ~ reveal the current cell.
    ---movement---------------------
    h | a ~ move left.
    j | s ~ move down.
    k | w ~ move up.
    l | d ~ move right.
    ---flags------------------------
    f     ~ set flag.
    F     ~ remove flag.
    ---control----------------------
    q     ~ quit game.
    r     ~ restart game.
author:
    ticki.
"#;
 
/// The game state.
struct Game<R, W: Write> {
    /// Width of the grid.
    width: u16,
    /// The grid.
    ///
    /// The cells are enumerated like you would read a book. Left to right, until you reach the
    /// line ending.
    grid: Box<[Cell]>,
    /// The difficulty of the game.
    ///
    /// The lower, the easier.
    difficulty: u8,
    /// The x coordinate.
    x: u16,
    /// The y coordinate.
    y: u16,
    /// The randomizer.
    rand: Randomizer,
    /// Points.
    ///
    /// That is, revealed fields.
    points: u16,
    /// Standard output.
    stdout: W,
    /// Standard input.
    stdin: R,
}
 
/// Initialize the game.
fn init<W: Write, R: Read>(mut stdout: W, stdin: R, difficulty: u8, w: u16, h: u16) {
    write!(stdout, "{}", clear::All).unwrap();
 
    // Set the initial game state.
    let mut game = Game {
        x: 0,
        y: 0,
        rand: Randomizer::new(0),
        width: w,
        grid: vec![Cell {
            mine: false,
            revealed: false,
            observed: false,
            flagged: false,
        }; w as usize * h as usize].into_boxed_slice(),
        points: 0,
        stdin: stdin.keys(),
        stdout: stdout,
        difficulty: difficulty,
    };
 
    // Reset that game.
    game.reset();
 
    // Start the event loop.
    game.start();
}
 
impl<R, W: Write> Drop for Game<R, W> {
    fn drop(&mut self) {
        // When done, restore the defaults to avoid messing with the terminal.
        write!(self.stdout, "{}{}{}", clear::All, style::Reset, cursor::Goto(1, 1)).unwrap();
    }
}
 
impl<R: Iterator<Item=Result<Key, std::io::Error>>, W: Write> Game<R, W> {
    /// Get the grid position of a given coordinate.
    fn pos(&self, x: u16, y: u16) -> usize {
        y as usize * self.width as usize + x as usize
    }
 
    /// Read cell, randomizing it if it is unobserved.
    fn read_cell(&mut self, c: usize) {
        if !self.grid[c].observed {
            self.grid[c].mine = self.rand.read_u8() % self.difficulty == 0;
            self.grid[c].observed = true;
        }
    }
 
    /// Get the cell at (x, y).
    fn get(&mut self, x: u16, y: u16) -> Cell {
        let pos = self.pos(x, y);
 
        self.read_cell(pos);
        self.grid[pos]
    }
 
    /// Get a mutable reference to the cell at (x, y).
    fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
        let pos = self.pos(x, y);
 
        self.read_cell(pos);
        &mut self.grid[pos]
    }
 
    /// Start the game loop.
    ///
    /// This will listen to events and do the appropriate actions.
    fn start(&mut self) {
        let mut first_click = true;
        loop {
            // Read a single byte from stdin.
            let b = self.stdin.next().unwrap().unwrap();
            use termion::event::Key::*;
            if let Char(c) = b {
                // Collect it as entropy.
                self.rand.write_u8(c as u8);
            }
            match b {
                Char('h') | Char('a') | Left  => self.x = self.left(self.x),
                Char('j') | Char('s') | Down  => self.y = self.down(self.y),
                Char('k') | Char('w') | Up    => self.y = self.up(self.y),
                Char('l') | Char('d') | Right => self.x = self.right(self.x),
                Char(' ') => {
                    // Check if it was a mine.
                    let (x, y) = (self.x, self.y);
 
                    if first_click {
                        // This is the player's first turn; clear all cells of
                        // mines around the cursor.
                        for &(x, y) in self.adjacent(x, y).iter() {
                            self.get_mut(x, y).mine = false;
                        }
                        self.get_mut(x, y).mine = false;
                        first_click = false;
                    }
 
                    if self.get(x, y).mine {
                        self.reveal_all();
                        // Make the background colour of the mine we just
                        // landed on red, and the foreground black.
                        write!(self.stdout, "{}{}{}{}{}",
                               cursor::Goto(x + 2, y + 2),
                               color::Bg(color::Red), color::Fg(color::Black),
                               MINE,
                               style::Reset).unwrap();
                        self.game_over();
                        return;
                    }
 
                    if !self.get(x, y).revealed {
                        self.points += 1;
                    }
 
                    // Reveal the cell.
                    self.reveal(x, y);
 
                    self.print_points();
                },
                Char('f') => {
                    let (x, y) = (self.x, self.y);
                    self.toggle_flag(x, y);
                }
                Char('r') => {
                    self.restart();
                    return;
                }
                Char('q') => return,
                _ => {},
            }
 
            // Make sure the cursor is placed on the current position.
            write!(self.stdout, "{}", cursor::Goto(self.x + 2, self.y + 2)).unwrap();
            self.stdout.flush().unwrap();
        }
    }
 
    /// Set a flag on cell.
    fn set_flag(&mut self, x: u16, y: u16) {
        if !self.get(x, y).revealed {
            self.stdout.write(FLAGGED.as_bytes()).unwrap();
            self.get_mut(x, y).flagged = true;
        }
    }
    /// Remove a flag on cell.
    fn remove_flag(&mut self, x: u16, y: u16) {
        self.stdout.write(CONCEALED.as_bytes()).unwrap();
        self.get_mut(x, y).flagged = false;
    }
    /// Place a flag on cell if unflagged, or remove it if present.
    fn toggle_flag(&mut self, x: u16, y: u16) {
        if !self.get(x, y).flagged {
            self.set_flag(x, y);
        } else {
            self.remove_flag(x, y);
        }
    }
 
    /// Reset the game.
    ///
    /// This will display the starting grid, and fill the old grid with random mines.
    fn reset(&mut self) {
        // Reset the cursor.
        write!(self.stdout, "{}", cursor::Goto(1, 1)).unwrap();
 
        // Write the upper part of the frame.
        self.stdout.write(TOP_LEFT_CORNER.as_bytes()).unwrap();
        for _ in 0..self.width {
            self.stdout.write(HORZ_BOUNDARY.as_bytes()).unwrap();
        }
        self.stdout.write(TOP_RIGHT_CORNER.as_bytes()).unwrap();
        self.stdout.write(b"\n\r").unwrap();
 
        // Conceal all the cells.
        for _ in 0..self.height() {
            // The left part of the frame
            self.stdout.write(VERT_BOUNDARY.as_bytes()).unwrap();
 
            for _ in 0..self.width {
                self.stdout.write_all(CONCEALED.as_bytes()).unwrap();
            }
 
            // The right part of the frame.
            self.stdout.write(VERT_BOUNDARY.as_bytes()).unwrap();
            self.stdout.write(b"\n\r").unwrap();
        }
 
        // Write the lower part of the frame.
        self.stdout.write(BOTTOM_LEFT_CORNER.as_bytes()).unwrap();
        for _ in 0..self.width {
            self.stdout.write(HORZ_BOUNDARY.as_bytes()).unwrap();
        }
        self.stdout.write(BOTTOM_RIGHT_CORNER.as_bytes()).unwrap();
 
        write!(self.stdout, "{}", cursor::Goto(self.x + 2, self.y + 2)).unwrap();
        self.stdout.flush().unwrap();
 
        // Reset the grid.
        for i in 0..self.grid.len() {
            // Fill it with random, concealed fields.
            self.grid[i] = Cell {
                mine: false,
                revealed: false,
                observed: false,
                flagged: false,
            };
 
            self.points = 0;
        }
    }
 
    /// Get the value of a cell.
    ///
    /// The value represent the sum of adjacent cells containing mines. A cell of value, 0, is
    /// called "free".
    fn val(&mut self, x: u16, y: u16) -> u8 {
        // To avoid nightly version, we manually sum the adjacent mines.
        let mut res = 0;
        for &(x, y) in self.adjacent(x, y).iter() {
            res += self.get(x, y).mine as u8;
        }
        res
    }
 
    /// Reveal the cell, _c_.
    ///
    /// This will recursively reveal free cells, until non-free cell is reached, terminating the
    /// current recursion descendant.
    fn reveal(&mut self, x: u16, y: u16) {
        let v = self.val(x, y);
 
        self.get_mut(x, y).revealed = true;
 
        write!(self.stdout, "{}", cursor::Goto(x + 2, y + 2)).unwrap();
 
        if v == 0 {
            // If the cell is free, simply put a space on the position.
            self.stdout.write(b" ").unwrap();
 
            // Recursively reveal adjacent cells until a non-free cel is reached.
            for &(x, y) in self.adjacent(x, y).iter() {
                if !self.get(x, y).revealed && !self.get(x, y).mine {
                    self.reveal(x, y);
                }
            }
        } else {
            // Aww. The cell was not free. Print the value instead.
            self.stdout.write(&[b'0' + v]).unwrap();
        }
    }
 
    /// Print the point count.
    fn print_points(&mut self) {
        let height = self.height();
        write!(self.stdout, "{}", cursor::Goto(3, height + 2)).unwrap();
        self.stdout.write(self.points.to_string().as_bytes()).unwrap();
    }
 
    /// Reveal all the fields, printing where the mines were.
    fn reveal_all(&mut self) {
        write!(self.stdout, "{}", cursor::Goto(1, 1)).unwrap();
 
        for y in 0..self.height() {
            for x in 0..self.width {
                write!(self.stdout, "{}", cursor::Goto(x + 2, y + 2)).unwrap();
                if self.get(x, y).mine {
                    self.stdout.write(MINE.as_bytes()).unwrap();
                }
            }
        }
    }
 
    /// Game over!
    fn game_over(&mut self) {
        //Goto top left corner
        write!(self.stdout, "{}", cursor::Goto(1, 1)).unwrap();
 
        self.stdout.write(GAME_OVER.as_bytes()).unwrap();
        self.stdout.flush().unwrap();
 
        loop {
            // Repeatedly read a single byte.
            match self.stdin.next().unwrap().unwrap() {
                Key::Char('r') => {
                    // Replay!
                    self.restart();
                    return;
                },
                Key::Char('q') => return,
                _ => {},
            }
        }
    }
 
    /// Restart (replay) the game.
    fn restart(&mut self) {
        self.reset();
        self.start();
    }
 
    /// Calculate the adjacent cells.
    fn adjacent(&self, x: u16, y: u16) -> [(u16, u16); 8] {
        let left = self.left(x);
        let right = self.right(x);
        let up = self.up(y);
        let down = self.down(y);
 
        [
            // Left-up
            (left, up),
            // Up
            (x, up),
            // Right-up
            (right, up),
            // Left
            (left, y),
            // Right
            (right, y),
            // Left-down
            (left, down),
            // Down
            (x, down),
            // Right-down
            (right, down)
        ]
    }
 
    /// Calculate the height (number of rows) of the grid.
    fn height(&self) -> u16 {
        (self.grid.len() / self.width as usize) as u16
    }
 
    /// Calculate the y coordinate of the cell "above" a given y coordinate.
    ///
    /// This wraps when _y = 0_.
    fn up(&self, y: u16) -> u16 {
        if y == 0 {
            // Upper bound reached. Wrap around.
            self.height() - 1
        } else {
            y - 1
        }
    }
    /// Calculate the y coordinate of the cell "below" a given y coordinate.
    ///
    /// This wraps when _y = h - 1_.
    fn down(&self, y: u16) -> u16 {
        if y + 1 == self.height() {
            // Lower bound reached. Wrap around.
            0
        } else {
            y + 1
        }
    }
    /// Calculate the x coordinate of the cell "left to" a given x coordinate.
    ///
    /// This wraps when _x = 0_.
    fn left(&self, x: u16) -> u16 {
        if x == 0 {
            // Lower bound reached. Wrap around.
            self.width - 1
        } else {
            x - 1
        }
    }
    /// Calculate the x coordinate of the cell "left to" a given x coordinate.
    ///
    /// This wraps when _x = w - 1_.
    fn right(&self, x: u16) -> u16 {
        if x + 1 == self.width {
            // Upper bound reached. Wrap around.
            0
        } else {
            x + 1
        }
    }
}
 
fn main() {
    let mut args = env::args().skip(1);
    let mut width = None;
    let mut height = None;
    let mut diff = 6;
 
    // Get and lock the stdios.
    let stdout = io::stdout();
    let mut stdout = stdout.lock();
    let stdin = io::stdin();
    let stdin = stdin.lock();
    let stderr = io::stderr();
    let mut stderr = stderr.lock();
 
    loop {
        // Read the arguments.
        // Does not use a for loop because each argument may have second parameter.
 
        let arg = if let Some(x) = args.next() {
            x
        } else {
            break;
        };
 
        match arg.as_str() {
            "-r" | "--height" => if height.is_none() {
                height = Some(args.next().unwrap_or_else(|| {
                    stderr.write(b"no height given.\n").unwrap();
                    stderr.flush().unwrap();
                    process::exit(1);
                }).parse().unwrap_or_else(|_| {
                    stderr.write(b"invalid integer given.\n").unwrap();
                    stderr.flush().unwrap();
                    process::exit(1);
                }));
            } else {
                stderr.write(b"you may only input one height.\n").unwrap();
                stderr.flush().unwrap();
                process::exit(1);
            },
            "-c" | "--width" => if width.is_none() {
                width = Some(args.next().unwrap_or_else(|| {
                    stderr.write(b"no width given.\n").unwrap();
                    stderr.flush().unwrap();
                    process::exit(1);
                }).parse().unwrap_or_else(|_| {
                    stderr.write(b"invalid integer given.\n").unwrap();
                    stderr.flush().unwrap();
                    process::exit(1);
                }));
            } else {
                stderr.write(b"you may only input one width.\n").unwrap();
                stderr.flush().unwrap();
                process::exit(1);
            },
            "-h" | "--help" => {
                // Print the help page.
                stdout.write(HELP.as_bytes()).unwrap();
                stdout.flush().unwrap();
                process::exit(0);
            },
            "-g" => diff = 2,
            "-a" => diff = 4,
            "-i" => diff = 6,
            "-b" => diff = 10,
            _ => {
                stderr.write(b"Unknown argument.\n").unwrap();
                stderr.flush().unwrap();
                process::exit(1);
            }
        }
    }
 
    // We go to raw mode to make the control over the terminal more fine-grained.
    let stdout = stdout.into_raw_mode().unwrap();
 
    let termsize = termion::terminal_size().ok();
    let termwidth = termsize.map(|(w,_)| w - 2);
    let termheight = termsize.map(|(_,h)| h - 2);
    // Initialize the game!
    init(stdout, stdin, diff, width.or(termwidth).unwrap_or(70),
         height.or(termheight).unwrap_or(40));
}

Comments