DEV Community

Chig Beef
Chig Beef

Posted on • Edited on

Make In A Day: Minesweeper

One of the most important things I was forced to create in school was minesweeper. This is because it brings up recursion, which every programmer seems to struggle with. Heck, in university, there are many students that still don't grasp it, halfway through the degree. So when people ask how they can learn recursion, my favorite task is minesweeper. And, of course, you get a cool game out of it!

So, here's the challenge, can we make it in one day? Don't start the timer yet, we must design first, list out what we have to do. That's the good part of games like these, they are well-defined, and can be described in a page. I'll describe the game in long form, from the perspective of interactions from the user. I'll then list out notes on these features. In concept, if our application adheres to this list, we've made minesweeper!

Before we begin, we have to write this code in something, obviously. For me, personally, I'll be using Golang as my language, and Ebitengine for graphics. I also have extra code I've written over the years to help me here. This code has nothing to do with minesweeper, but is code that deals with UI logic, and images. The sort of features you'd get from other application engines, but my own crude version.

From the User's Perspective

Minesweeper is a very simple game, either you hit a mine and lose, or you clear the board and win. In this way, there are really only 3 phases in the game, those being win, lose, and play. The game can be reset at any time, when you press the little face at the top. There is a timer, to tell you how long you've taken to clear the mines. Opposite this timer, is another number. This is the number of flags you have left to place. This allows the player to see how many mines are left to win. The board will be a 9x9 grid of tiles. There will be 10 mines in the grid. Never when you play minesweeper, is the first revealed tile a mine. You do not need to worry about the logic of changing the face. Using right click should use a flag on a tile. Flagged tiles shouldn't be revealable. Left clicking a tile should reveal it. If the tile is a mine, it should push the game into the win state. If the tile has no surrounding mines, it should reveal all surrounding tiles. If all tiles that aren't mines are revealed, the game moves to the win state. Revealed tiles should show how many surrounding tiles are mines, or nothing if 0. In win and lose state, the only pressable button should be the reset button. Since lose state happens when a mine is revealed, a revealed mine should show that it has blown. Lose state should also show the remaining mines.

Shortlist

  1. There are 3 phases, play, win, lose
  2. The reset button can be pressed at any time, and resets the timer, grid, and flags
  3. There is a flag counter
  4. There is a 9x9 grid of tiles
  5. There will be 10 mines, which are placed to avoid the player's first click
  6. Right clicking will flag a tile
  7. Left clicking a non-flagged tile reveals it
  8. Revealing a mine switches to lose state
  9. Revealing all non-mine tiles switches to win state
  10. Revealed tiles, if not 0, should show the number of surrounding mines
  11. The exploded mine should be noticable
  12. Lose state should show remaining mines
  13. Revealing a tile with no surrounding mines should reveal all surrounding tiles

First Steps

Now we've got a well defined problem, let's follow along implementing them. For yourself, you may want to stop here and program by yourself, or follow my logic. I would encourage you to take the list above, and create minesweeper yourself, to see how easy solving a well-defined problem really is.

To start off with, I got a blank window with the title "Minesweeper".
Now we can easily tick off the first step, and create 3 phases.

type Phase byte

const (
    PLAY Phase = iota
    WIN
    LOSE
)
Enter fullscreen mode Exit fullscreen mode

The next step is a reset button, so let's add that in.

stagui.Button{
    X:       92.5,
    Y:       0.5,
    W:       7,
    H:       7,
    Name:    "reset",
    BgColor: buttonColor,
    BgImg:   g.vis.GetImage("face"),
}
Enter fullscreen mode Exit fullscreen mode

I've hooked this button up to a reset method, which is empty right now.
We'll fill it in when we have something to reset.

Now we need a flag counter from step 3.

Simple UI

Look at that great UI!
Now we need to start working on tiles.

Deep in the Code

There's a bunch of information we have about tiles, listed here.

  1. They need to know how many surrounding tiles are mines
  2. They need to know if they're a bomb
  3. They have a revealed and hidden state
  4. They have a flagged and unflagged state

To me this sounds pretty simple, 3 bools and an int.
Now, you could have all this in one int, to describe this.
Both options are equally valid, just a matter of opinion.
One may save more memory, the other may reduce chance for invalid state.

type Tile struct {
    surrounds               int
    mine, flagged, revealed bool
}

func (t *Tile) reveal(g *Game) {

}

func (t *Tile) draw(g *Game, x, y float64) {
    // TODO: Replace placeholder
    g.vis.DrawRect(x, y, TILE_SIZE, TILE_SIZE, color.RGBA{128, 128, 128, 255})
}
Enter fullscreen mode Exit fullscreen mode

Drawing simple tiles

That big blob on the left are our tiles.
Let's skip mines for later, and do flagging.
When we right click a tile, we should flag it.

func (g *Game) rightClick() {
    x := int(g.mousePos[0] / TILE_SIZE)
    y := int(g.mousePos[1] / TILE_SIZE)

    if x < 0 || x >= GRID_SIZE || y < 0 || y >= GRID_SIZE {
        return
    }

    // Can only flag hidden tiles
    if g.grid[y][x].revealed {
        return
    }

    // Taking back a flag
    if g.grid[y][x].flagged {
        g.grid[y][x].flagged = false
        g.flags++
        g.updateFlagCounter()
        return
    }

    // Trying to place a flag
    if !g.grid[y][x].flagged {
        // No more flags
        if g.flags == 0 {
            return
        }

        // Place a flag
        g.grid[y][x].flagged = true
        g.flags--
        g.updateFlagCounter()
        return
    }
}
Enter fullscreen mode Exit fullscreen mode

Hopefully the code above makes sense, and we get the below result.
We only call the above method when we right click for the first frame.

A few placed flags

Looks like it's working well, now for revealing.

func (t *Tile) reveal(g *Game) {
    // Can't double reveal
    if t.revealed {
        return
    }

    // Don't accidentally reveal mines
    if t.flagged {
        return
    }

    t.revealed = true

    if t.mine {
        g.phase = LOSE
        return
    }

    // TODO: Check win state

    // TODO: Reveal surrounding tiles
}
Enter fullscreen mode Exit fullscreen mode

Revealed tiles

Looks like we're getting somewhere.
You'll notice the TODOs in this method, I put them there because it's extremely important we don't forget.
Imagine if we forget the win state!

func (g *Game) checkWin() {
    // Check for a non-mined tile without a mine
    for r := range GRID_SIZE {
        for c := range GRID_SIZE {
            if !g.grid[r][c].revealed && !g.grid[r][c].mine {
                return
            }
        }
    }

    // We won!
    g.phase = WIN
}
Enter fullscreen mode Exit fullscreen mode

The win logic is quite easy, one every non-mine tile is revealed, we've won!

Now let's do mine placing logic, which is one of the harder parts of this game.
What we want, is a random position that will avoid the first clicking place.
The placement of a mine must also avoid previously placed mines.
Now, we could do the simple solution, which is randomly pick numbers until we find a valid one.
This solution, however, has a case where it stalls infinitely.
In this way, we must create a correct solution.
The first thing I want to do is simplify the problem.
Rather than getting a position, we can get a single random number.
We can then use mod and div to convert this to a position.
Every tile position can be described by a single number, that being.

i = GRID_SIZE*y+x
Enter fullscreen mode Exit fullscreen mode

Now, we've simplified the problem to a single random number.
We just want that random number to not contact the other given random numbers.

// Add this new position to the list of tiles to avoid
avoids = append(avoids, mineY*GRID_SIZE+mineX)
Enter fullscreen mode Exit fullscreen mode

So now we have an easy way to keep track of tiles to avoid.
At the first tile, there are GRID_SIZE*GRID_SIZE-1 valid positions.
The -1 is the first click position.
So now we know we need a random number from 0 to GRID_SIZE*GRID_SIZE-len(avoids).
And now we have the random number, we need to get the correct position.
So we go through the list of position to avoid, and if our position is greater or equal to that position, we increase our position by 1.
The last important point is that we must have a sorted list, otherwise our solution fails.

func (g *Game) placeMines(x, y int) {
    // List of tiles to avoid
    avoids := []int{y*GRID_SIZE + x}

    for range START_MINES {
        inlinePos := rand.Intn(GRID_SIZE*GRID_SIZE - len(avoids))

        // Avoid positions
        for _, avoidPos := range avoids {
            if inlinePos >= avoidPos {
                inlinePos++
            }
        }

        // Convert to a 2D position
        mineX := inlinePos % GRID_SIZE
        mineY := inlinePos / GRID_SIZE

        // Turn it into a mine
        g.grid[mineY][mineX].mine = true

        // Add this new position to the list of tiles to avoid (sorted)
        insertPos := 0
        for insertPos = 0; insertPos < len(avoids); insertPos++ {
            if avoids[insertPos] > inlinePos {
                break
            }
        }
        avoids = slices.Insert(avoids, insertPos, mineY*GRID_SIZE+mineX)
    }
}
Enter fullscreen mode Exit fullscreen mode

This method is everything we just explained as code.
Try to read through it as well as you can if you didn't understand my explanation.
Is this the best way to get a random position?
Probably not, but it works, and is simple enought to reason with.
Let's take a second to see what we've ticked off the list.

  • [x] There are 3 phases, play, win, lose
  • [ ] The reset button can be pressed at any time, and resets the timer, grid, and flags
  • [x] There is a flag counter
  • [x] There is a 9x9 grid of tiles
  • [x] There will be 10 mines, which are placed to avoid the player's first click
  • [x] Right clicking will flag a tile
  • [x] Left clicking a non-flagged tile reveals it
  • [x] Revealing a mine switches to lose state
  • [x] Revealing all non-mine tiles switches to win state
  • [ ] Revealed tiles, if not 0, should show the number of surrounding mines
  • [ ] The exploded mine should be noticable
  • [ ] Lose state should show remaining mines
  • [ ] Revealing a tile with no surrounding mines should reveal all surrounding tiles

Looks like we've already done quite a bit here.
Let's tick of an easy one, numbering tiles.
First though, we have to count this number up first.
Then we can display this number relatively simply.

func (g *Game) countMines() {
    for r := range GRID_SIZE {
        for c := range GRID_SIZE {
            // Each tile

            for nr := r - 1; nr <= r+1; nr++ {
                for nc := c - 1; nc <= c+1; nc++ {
                    // Each surrounding position

                    // Don't check self
                    if nr == r && nc == c {
                        continue
                    }

                    // Out of bounds check
                    if nr < 0 || nc < 0 || nr >= GRID_SIZE || nc >= GRID_SIZE {
                        continue
                    }

                    if g.grid[nr][nc].mine {
                        g.grid[r][c].surrounds++
                    }
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Numbered tiles

Numbered tiles without 0s

Looks good!
Now let's check that exploded mine when we lose.
Let's add an exploded property to tiles.

func (t *Tile) draw(g *Game, x, y float64) {
    if t.flagged {
        g.vis.DrawRect(x, y, TILE_SIZE, TILE_SIZE, color.RGBA{128, 128, 128, 255})
        g.vis.DrawImage(g.vis.GetImage("flag"), x, y, TILE_SIZE, TILE_SIZE, &ebiten.DrawImageOptions{})
        return
    }

    if t.exploded {
        g.vis.DrawRect(x, y, TILE_SIZE, TILE_SIZE, color.RGBA{255, 0, 0, 255})
        g.vis.DrawImage(g.vis.GetImage("mine"), x, y, TILE_SIZE, TILE_SIZE, &ebiten.DrawImageOptions{})
        return
    }

    if t.revealed {
        g.vis.DrawRect(x, y, TILE_SIZE, TILE_SIZE, color.RGBA{64, 64, 64, 255})
        if t.surrounds != 0 {
            g.vis.DrawText(strconv.Itoa(t.surrounds), TILE_SIZE*0.8, x, y, g.vis.GetFont("default"), &text.DrawOptions{})
        }
        return
    }

    if t.mine && g.phase == LOSE {
        g.vis.DrawRect(x, y, TILE_SIZE, TILE_SIZE, color.RGBA{64, 64, 64, 255})
        g.vis.DrawImage(g.vis.GetImage("mine"), x, y, TILE_SIZE, TILE_SIZE, &ebiten.DrawImageOptions{})
        return
    }

    g.vis.DrawRect(x, y, TILE_SIZE, TILE_SIZE, color.RGBA{128, 128, 128, 255})
}
Enter fullscreen mode Exit fullscreen mode

Mines shown when lose state

Here is my drawing logic.
As you can see, we get to see all mines, and the exploded mine.

Now there's only 2 things left, the timer, and the recursive revealing.
Let's do the recursive revealing.
Now we've really set ourself up quite easily here, except one thing.
Tiles don't know what their neighbours are!
This is a simple fix though, all we need to do is tell each tile its position.
Then the tile can simply ask its neighbours to be revealed.

func (t *Tile) revealNeighbours(g *Game) {
    for nr := t.r - 1; nr <= t.r+1; nr++ {
        for nc := t.c - 1; nc <= t.c+1; nc++ {
            // Each surrounding position

            // Don't check self
            if nr == t.r && nc == t.c {
                continue
            }

            // Out of bounds check
            if nr < 0 || nc < 0 || nr >= GRID_SIZE || nc >= GRID_SIZE {
                continue
            }

            g.grid[nr][nc].reveal(g)
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

That's very simple, isn't it.
Now how is this code called?
At the end of the reveal method, I added this.

if t.surrounds == 0 {
    t.revealNeighbours(g)
}
Enter fullscreen mode Exit fullscreen mode

And this works just fine!
The reveal method has safe-guards to make sure that you don't double-reveal a tile.
Then, it only reveals its neighbours if it has 0 surrounding mines.
There are no gaps in our logic, and it works well.

So now we've only got one task left for us, that being the timer.
All we need is the number to go up every second.
Now, my own simple brain wants to simple have a counter to increments every frame.
Then, dividing by the ticks per second, we get a timer.
However, if you were to slow down the game, you could effectively cheat.
Instead, I need a solution that only updates a counter when there has definitely been an increase in time.
For me, Golang gives me a reasonable solution.
I can save the time that I started this run.
Then, every frame, I can take the difference between the current real time, and that time.
I then write that time to the screen.
Real simple.

g.page.Text[1].Text = strconv.Itoa(int(time.Now().Sub(g.startTime).Seconds()))
Enter fullscreen mode Exit fullscreen mode

Timer counting 3 seconds

I know that's an ugly line but it does exactly as I said.
Take the start time, and get the number of seconds we've take from then to now.
Then display it.

Done

Not too bad, was it! If you want my full code, I've put it all here.

Your Next Steps

Now that you have minesweeper, you probably have a few ideas for extending it. Maybe you want to make a massive board, in which case I would suggest adding an easy, medium, and hard difficulty. A cool idea that I had never thought of before would be to allow tiles to hold multiple mines. Maybe allow players to save their times, and make a leaderboard mechanic. You could make it so that mines can move between hidden tiles. If the player wins, why not let them keep going, and then the score is how many boards they can clear?

My Next Steps

Now, this is definitely a fun challenge, and if it's your first time, very educational. It would be great to have more applications like this to make, so if you can think of any, tell me in the comments.

Top comments (6)

Collapse
 
mcondon profile image
Micah Condon

looks like a fun project and a great way to learn / refresh! Games like this are simple enough to not be overwhelming as a learning project, but complex enough to challenge your thinking a bit. They're also great for practicing a bit of unit testing or TDD

Collapse
 
chigbeef_77 profile image
Chig Beef

Exactly, it's all practice, we just can't be scared to actually sit down and write some code (:

Collapse
 
nevodavid profile image
Nevo David

Pretty cool seeing the whole breakdown like this. I always get stuck on little bugs so it helps seeing all the steps actually written out.

Collapse
 
chigbeef_77 profile image
Chig Beef

Yeah, when you have it broken down to a simple list like that, it's hard to mess up. Of course on more complex systems, or when the end goal isn't entirely known, it's not 100% viable, but regardless, a little pre-thought seems to go a long way here (:

Collapse
 
dotallio profile image
Dotallio

Love how you broke this down - recursion finally feels less scary. Have you thought about doing something similar with Conway’s Game of Life or maybe Tetris next?

Collapse
 
chigbeef_77 profile image
Chig Beef

Exactly, that's why I love minesweeper, it really helped me learn recursion when I was in school. I think I have Tetris written down to do at some point, but I'll add Conway to the list, great idea (: