Developing Minesweeper like it's 1999 (or fun with VB6)
Minesweeper was one of the first games I played on a computer (after Solitaire ). I wouldn’t say that I’m very good at it, but that doesn’t discourage me from wanting to implement it. For some added nostalgia, let’s make it with tooling from more than 20 years ago!
Minesweeper for dummies
In Minesweeper, the mines are scattered throughout a minefield, which is divided into squares, which may or may not contain a mine. According to Wikipedia article on Minesweeper calls them board and cells. The player left-clicks a square to uncover it.
If it contained a mine, the game is over. If not, the cell will now contain a number with a number of adjacent mines (on the sides and diagonally) or it will stay blank, if there are no adjacent mines. Also, if the cell turned out blank, all neighboring tiles will be automatically uncovered.
The player is supposed to flag mines with right click (displayed as X in my version), deducing whether the square will contain a mine from the neighboring numbers.
Tech stack
I’ve chosen Visual Basic 6 on Windows XP. I’ve never done a game in VB6, but I did work on a Visual Basic .NET project for about a year and C# + Windows Forms for about eight years before that, so I hoped it would feel familiar.
VB.NET with Windows Forms was a replacement stack for the VB6 developers, and it was fun transitioning in the opposite direction, where some things work exactly as they do now, and others need a clever workaround.
On to programming
Separating the concerns a little bit, I’ve structured the project around visual controls and modules.
The modules contain Configuration that holds part of the game state and ModuleMinefield , that contain the core game logic that the visual controls delegate to, such as populating the minefield.
The visual controls are UserControlField
, which is the grid of UserControlSquare
controls. It’s all hosted within a Form
that also handles window resizing, timer and redrawing the game counters.
However, there’s a liberal mixing of responsibilities, so the Field and Square handles some bits of logic, as I tried to avoid having callbacks to re-trigger visual stuff if the core logic changed state.
VB6 crash course
Visual Basic for Python/JavaScript/C developers:
Dim i As Integer
declares a variable called i
.
If
blocks are terminated with End If
.
For
blocks are terminated with Next
and the bounds are inclusive.
For i = 0 to 3
MsgBox(i)
End For
The code block above will show 4 message boxes when ran, not 3, as in most other languages
Arrays are indexed with parentheses, such as MyArray(3)
.
Arrays can be resized with ReDim
, if used as ReDim Preserve
, it will copy the contents of the old array to the resized one.
Procedures can be invoked with the Call
keyword, then the argument list needs to be enclosed in parentheses, such as Call MineLookup.Add(key, Str(key))
. If there are no arguments, the list can be omitted, for example Call PopulateNeighbors
.
However, the procedures can also be invoked without the Call
keyword, such as MineLookup.Add key, Str(key)
.
Procedures can be exited early by the Exit Sub
statement
Returning value 9 from a function MyFunction is done with a statement MyFunction = 9
.
Functions can be exited early using the Exit Function
statement, provided you set the return value.
Public MineLookup As New Collection
creates a public Collection
that can be used as a look up table with a string key.
Colors can be expressed with a hexadecimal constants in the format of &HRRGGBB&
, for example &HC6C3C6&
.
Overall, I didn’t fell terribly limited by Visual Basic, it was reasonably productive to use for a GUI-based app. ❤️
Generating the minefield
Internally the field is represented by an array of Integers, which states the presence of a mine (-1), or the amount of adjacent mines (neighbors).
The minefield array gets configured at the game start in terms of its size (rows x columns) and the amount of mines contained. Populating it happens in a simple Do While .. Loop
loop that picks a random coordinate and attempts. After the mines are placed, the adjacent mine count gets calculated, as we’ll refer to those numbers later.
Then the UserControlField
generates individual UserControlSquare
controls into a Control Array, which is a VB6 way to handle multiple controls of the same type under the same identifier.
Note: After multiple vain attempts I figured out how to remove controls from the item array (after resizing the game board): First the control needs to be set to invisible first, then remove it with
Unload
call.
It was also an opportunity to use VB6 With
scoping statement on the newly created control:
Load Squares(idx)
With Squares(idx)
.Move col * 405, Row * 405, 405, 405
If Minefield.HasMine(Row, col) Then
.HasMine = True
End If
.MineNeighborCount = Minefield.MinesWithCount(Minefield.GetIndex(Row, col))
...
Call .ShowState
End With
Given a 4x4 minefield with two mines at (2,2) and (2,3)
0111
02X2
02X2
0111
This was the early output, when the square controls just displayed the amount of neighbors / mine presence by default.
Indexing mines
It was impractical to work with two-dimensional arrays or maps, so I’m using one-dimensional data structures everywhere and generate the index with a simple function:
Public Function GetIndex(Row As Integer, Column As Integer) As Integer
GetIndex = Row * Configuration.columns + Column
End Function
Handling input and output
All of the user interaction is done by clicking some kind of buttons or capturing mouse click events on the labels / control surface.
This is done by creating an event handler:
Sub Btn_MouseUp(Button As Integer, Shift As Integer, x As Single, y As Single)
and that either marks it as a potential mine or uncovers the square, maybe exploding it in the process!
We change the text on the controls (mine square, score board or the timer) by changing the text via the Caption
property, there’s no data binding of any kind going on.
Compared to modern frameworks this is a bit tedious, but you as the programmer know exactly when the UI is going to get updated.
GUI programming
As described earlier, designing a fixed-size window is a breeze with the VB6 Form designer, as you just need to drag and drop controls from a “palette” to the working area. If you prefer separation of concerns, you can design your own controls and reuse them just like the prebuilt ones.
Note: I had to close the control designer instance for ControlXYZ to make it available in another form/control designer instance.
Visual Basic form designer with some code
Adding menu and menu handlers
To edit the window menu (File->…), we can invoke the standalone menu editor with Ctrl+E, then edit the menu visually, and add separators where necessary. After the menu items are in place, they appear in the designer view,
Debugging
There’s the usual array of debugging tools, such as breakpoints, watches and the Immediate Window, where we can evaluate expressions and execute statements, just like in in newer Visual Studios:
? MineLookup.Count
3
? TypeName (MineLookup)
Collection
Revealing adjacent blank squares with a flood fill
When the player clicks a tile that doesn’t have a bomb and has 0 adjacent mines, all neighboring tiles should be automatically uncovered. This is implemented by a textbook flood fill algorithm.
I have to admit that this bit took me much more time than I expected. I’ve read the Flood fill
Wikipedia page and re-wrote the algorithm in VB6. It took a while to figure out the boundaries of the function, as it turned out to be better called from the UserControlField
control, which can reach both the individual Square
controls and the ModuleMinefield
non-GUI module.
The 8-way flood fill pseudocode is:
# implicitly stack-based, recursive, eight-way flood-fill implementation goes as follows:
Flood-fill (node):
1. If node is already visited, return.
2. Reveal the node
3-10. Perform Flood-fill one step to the neighbors of node (all 8 directions)
11. Return.
My VB6 implementation
has an added extra simplification, that the Visited
array could be persisted across flood fills in the same game, as it made no sense to uncover already uncovered fields.
Things I wished I knew about Visual Basic 6.0 before
Renaming a control doesn’t rename all references in the code, which is nasty compared to more modern IDEs.
I could not figure out a way to trigger flood fill from the child control, the syntax is actually:
Call ParentControls(0).Floodfill
Definitely set “Save changes” option in VB6 settings->Environment->When program starts, as VB6 IDE doesn’t save your code changes by default, and you’re bound to lose some work if you manage to cause a crash.
Using variant data type in collections is tricky, as collection.Item(1) also returned the first item in the collection, not only the one added with the key 1
.
File->Make tries to compile the code to native code and also stops and notifies the developer on a compile errror, so this is useful for discovering syntax errors before your runtime code encounters them.
Conclusion
This was a fun exercise, I will do something similar again soon!
Repository is available on GitHub: https://github.com/jborza/mines
And this time, you can download the binary from the GitHub release page .