I wrote a version of MineSweeper in batch. It’s not the most efficient or the most user-friendly, but it works. It’s a fun little project that I thought I’d share.
Where to get it?
thedaveCA/stupid-batch-tricks. Just grab everything, it requires some stuff from the _helpers
directory as well, so either clone the whole repo or download the .zip.
It has only been tested on Windows 11, under both Windows Terminal and the Command Prompt. An early prototype worked fine on Windows 10 as well, but I have not tested the final release. It should work on Windows 10 >= 1909, but I make no guarantees.
Controls
Command | Action | Command | Action |
---|---|---|---|
w s |
Move around the board | a d |
Move around the board |
x |
Flag as safe/mine | z |
Reveal if we found a mine! |
r |
Draw the board | R |
Clear and refresh the screen |
0 |
New game with: No mines | 1 |
New game with: ALL THE MINES! 100% mines |
2 |
New game with: A lot of mines | 3 |
New game with: A few mines |
c |
Clear the board | q |
Quit |
shift-C |
DEBUG: Enable cheat mode | shift-V |
DEBUG: Display variables |
The controls are… A bit janky, but they work. I can’t trivially use arrow keys batch, so I had to use w
, a
, s
, and d
for movement.
- Cheat mode reveals three separate boards: The mines, the flags, the pre-calculated numbers.
- Display variables shows the gamestate varibles, excluding the actual board.
Weird/fun implementation details
- It mainly runs internally in
cmd.exe
, but it does requirechoice.exe
andfindstr.exe
, both of which are included in Windows. All other dependencies are just other.cmd
batch files. -
Batch extensions are heavily used, so it will not work in MS-DOS or other non-Windows environments. Sorry, FreeDOS user.
- Game state was a unique challenge.
- Implemented 100% in memory, no temporary files are used for tracking game state.
- MineSweeper is a textbook 2D array project, but…
- Batch doesn’t have arrays.
- Please, I cannot stress this enough, do not tell my batch files that it doesn’t have arrays, because it just pretends that arrays are real and runs with it.
- While not used here, my
Helpers\CleanEnvironmentVariables.cmd
script can be used to selectively clear varibles matching patterns. It could be used to clear a one dimensional not-really-an-array, or the first dimension of a two-dimensional not-really-an-array, but I wouldn’t recommend it so that version of the script is not published anywhere.
- Batch has no concept of objects or classes.
- I store three versions of each board, and determine the game state by comparing them.
game_board_mines
: The mines are prepresented by0
or1
.game_board_state
: The location of the flags are stored using the the visible flag characters to speed up rendering.game_board_count
: The number of nearby cells with mines is pre-calculated into a table.
- The way the boards are stored has some interesting side effects.
game_board_state
is directly displayed to the user, to speed up rendering.- To check if a cell has been flagged, check if
game_board_state[x][y] == %game_visual_flag%
. - To determine if a cell has been revealed, check if
game_board_state[x][y] == game_board_count[x][y]
. - Checking if a cell has a mine is easy,
game_board_mines[x][y] == 1
. - To reveal a cell, check
game_board_mines[x][y]
to see if it is a mine, if not, setgame_board_state[x][y] = game_board_count[x][y]
. - The visible flags are all variables, and can be emojis or other characters, but alignment on the console is hard and emoji support cannot be reliably detected.
- I said you don’t have arrays, so you can’t actually do
game_board_mines[x][y]
, it actually looks likeif "!game_board_state[%game_position_x%][%game_position_y%]!" == "%game_visual_flag%"
.
- The board is variable size, with no fixed limits. That doesn’t mean no limits, just no fixed limits. It is slow, because batch.
- I did implement a flood-fill algorthim that will automatically reveal nearby safe cells as a timesaver. I guess it should be optional, it’s a pig on large boards. Since I don’t have proper arrays, I just keep scanning the board over and over until it doesn’t find any new cells to reveal, there’s probably a better way, but the complexity goes up, fast.
- There is a live display of the time and game timer which updates every second.
- Except batch has no timers, so it’s just a loop that sleeps for a second.
- Batch has no sleep command either.
- The “timer” ends early when the user presses a key.
- The timer can actually trigger more than once a second if the user is fast enough.
- The timer is therefore limited to incrementing no more than once per second, but it is not guaranteed to count every second.
- This is fine, we time the amount of time we are waiting for the user, not the time the game is processing.
- It would be possible to store the start time and calculate the elapsed time, but that would make it impossible to implement a pause button.
- I didn’t actually implement a pause button, but it is enough that I could. If you don’t care what it looks like, literally just
pause
would be fine, this would stop the timer, and the game would redraw the screen without further action.
- The whole board is actually re-drawn every second, but you won’t see the screen flicker.
- … and not because it draws to the screen amazingly fast, either.
- Turn on the two debug options and you’ll see the variables flicker, but not the board or menu.
- I didn’t bother to implement the anti-flicker technique for the debug stuff, because it’s debug stuff.
- Also the debug stuff flickering is actually like a heatbeat so I can see when it crashes. It’s a feature.
- Calling
echo
is expensive, so when possible I assemble as much as I can into memory first and dump it at once. - Extra large boards will probably break the way the board is echoed long before you encounter any other limits.
- Technically the entire screen including the menu is redrawn, which allows for variable sized elements and popover menus and messages.
- I didn’t implement variable sized elements or popover menus.
- Board size is hardcoded, the menu does let you start a new game with a selected number of mines. Both are set as variables near the top of the script if you want to tweak. I set
y=x
by default, but the game should play with non-square boards. - Game messages are displayed briefly at the top, and if you miss them, you miss them.
- It will handle console resizes, and resize the display accordingly. The main game board will not resize, but the clock and timer will as a proof of concept.
- When possible, it will restore your scrollback buffer, if you exit cleanly.
- If you decide to do any coding, run
cmd /c minesweeper.cmd
rather than calling it directly, this is the only way to guarantee that variables do not persist after the game crashes.setlocal
usually works, but it is not foolproof.
Should you play this game?
No.
Should you look at the code?
Also no.
Why does it exist?
I guess I didn’t have a headache one day and said to myself, wouldn’t I like to have a headache?
And I was doing some other stuff in batch that would benefit from some column alignment and this seemed like one way to play around. I have some basic functions in _helpers
and _snippets
that will pad strings, align columns, and other goodies.
Colours and other ANSI-escape codes are used extensively, and require a _helper
script to load. This is entirely essential, there is no way to display the board without it. Okay, you could rewrite it, but it would mean clearing the screen which causes an unacceptable amount of flicker.
It is “batch” but not MS-DOS compatible batch, it uses various Windows-specific features that were added to more modern versions of Windows.
What’s next?
Hopefully not batch.
Okay, but what else could be added?
- You could make the first-click always safe. I didn’t bother, but it would be easy enough to just destroy the mine. But right now the cell counts are pre-computed so that would need to be deferred.
- Some MineSweeper games apparently make the edges somewhat safer for beginners. I didn’t bother.
- Timed games, high scores, etc. I didn’t bother. But there is a timer, so it wouldn’t be hard to add.
- A pause button. I didn’t bother, but it would be easy enough. It probably should intentionally cover the board.
- I started writing a version that would play itself visibly, but it wasn’t worth the time. If you want a challenge, why not give it a shot?
- I didn’t bother with a win/lose condition, but it would be easy enough to check if all non-mine cells are revealed.