The Developer’s Cry

Yet another blog by a hobbyist programmer

A Rusty Minesweeper

Haven’t been coding in a while, so it’s time for something simple; let’s code a Minesweeper game in Rust. It is said that back in the day when Windows was first conceived at Microsoft, Bill Gates got so addicted to the game, he would barely do his job anymore. Apparently he even managed to solve the field in only a matter of seconds on beginner level.

Minesweeper is a simple but fun game, and it’s deceptively easy to code. My implementation in Rust is about a thousand lines, but half of that is dedicated to graphics.

What is minesweeper? You have a field of cells, typically 9x9 on beginner level, that you can click to open. When you click a bomb, you ‘die’ and it’s game over. A number may appear, which indicates how many bombs there are in adjacent cells. Or, empty cells may appear. By examining the numbers, you can deduce where the bombs are. Those cells can be flagged with a red flag. You win the game when all safe fields have been opened (and thus no bombs have been tripped). On expert level the game has a 30x16 field with 99 bombs in it.

Implementation

It makes sense to use an array for the game field. An empty cell can be easily depicted by a zero, a number can be just the numeric value, and a bomb can be a special value. We also need to keep the width and height of the field, and it’s convenient to keep counts of how many mines there are, and how many cells already have been opened.

struct MineSweeper {
    width: usize,
    height: usize,
    board: Vec<u8>,
    mines: usize,
    opened: usize,
    level: Level,
    state: GameState,
}

where Level and GameState are enums like so:

#[derive(Debug, PartialEq)]
pub enum Level {
    Beginner,
    Intermediate,
    Expert,
}

#[derive(Debug, PartialEq)]
pub enum GameState {
    GameStart,
    GamePlaying,
    GameWin,
    GameLose,
}

Initializing a game board then becomes:

fn new() -> Self {
    MineSweeper {
        width: 9,
        height: 9,
        board: vec![0; 9 * 9],
        mines: 10,
        opened: 0,
        level: Level::Beginner,
        state: GameState::GameStart,
    }
}

Mind that cells can be opened or closed, and cells may be flagged. I solved this by using bit flags (but you might also use vecs of booleans or so).

Opening a cell at position (x,y) is a matter of checking whether there is a bomb, or else open the cell. Opening an empty cell results in opening up the entire area. Opening an area is basically a flood-fill algorithm.

You may also double click a number, which opens the surrounding cells, which is called “chording”. When chording opens an empty cell, it will flood-fill-open the area again.

Flood-fill can be implemented using recursion, or you can use a local stack. Actually, it’s convenient to use a VecDeque of cell positions, where you push_back adjacent cells, and pop_front the cell to open. The code is just like this:

// "flood fill" open area around (x, y)
// cell (x, y) equals zero by definition
fn open_area(&mut self, x: usize, y: usize) {
    let mut stack = VecDeque::<(usize, usize)>::new();

    stack.push_back((x, y));

    while !stack.is_empty() {
        let (xp, yp) = stack.pop_front().unwrap();

        if self.is_visible(xp, yp) {
            continue;
        }

        self.open_cell(xp, yp);

        if self.is_empty(xp, yp) {
            if xp > 0 {
                stack.push_back((xp - 1, yp));
            }
            if xp < self.width - 1 {
                stack.push_back((xp + 1, yp));
            }

            // ... and so on for adjacent cells

The if-statements are there for bounds checking; we can not have x,y positions that are outside the game board.

That’s mostly all there is concerning the gameplay. The win condition is somewhat interesting: the game is not won by flagging all bombs; you have to open all safe cells. The win condition is thus:

fn win_condition(&self) -> bool {
    if self.playing() {
        self.opened + self.mines == self.width * self.height
    } else {
        false
    }
}

Graphics code

Remember that last year I had tons of trouble with SDL2 for Rust? This time I used Raylib, and it’s absolutely glorious. Raylib is easy. There are a few things to be aware of though, for example, my macbook does not have a right mouse button (for planting a flag on a cell) and raylib does not automagically map Apple’s Option-click as a right-click. I also encountered a problem on macOS where centering the window would place it halfway off-screen. Some kind of display scaling bug? I reckon it’s a raylib-on-macOS bug, there was no such issue on Linux.

Rust for games

Finally, the question that’s on everybody’s mind is, is Rust good for making games? Well, yes and no. Rust is a systems language, so you can code anything you like in it. Minesweeper is such a simple game that it was also very easy to code in Rust. Games with more complex data structures will be more difficult to make; Rust demands a clean design, and you can’t hack around in it easily. This already begins with lack of mutable globals, and there’s of course the borrow checker. The uptake of memory safety is that the game is not likely to crash (how nice is that!). That said, I’m betting that most game programmers are sticking with C++. When will the Jai revolution finally happen?