Minesweeper Solver in Python
Legal Hexadecimal Code and Eng DK30 Fall 2023 1 1
Description
I’d like to brush up on my Python programming skills in preparation for a game I’m going to develop in the near future. To do this, I’d like to revisit a personal project I first did in college, now that I have more professional coding experience and some ideas on how to do it better than I did the first time: a clone of Microsoft’s Minesweeper, along with an automatic solver.
Recent Updates
Final summary:
!!! NOTE: You should only download and run executables from sources you trust !!!
If you’d like to try out the solver yourself, you can download it from here:
https://drive.google.com/file/d/11HigtM10eU_5zQJaNyilLd9ZmwXil1lg/view?usp=sharing
This program was built on Windows, and so may not run properly on other operating systems. I was able to run it without Python installed on my machine, so hopefully having Python installed isn’t a requirement. It is provided as-is without guarantee or warranty.
Start/stop automatic solver:
- Keyboard: space bar
- Dropdown menu: Solver -> Solve
Have the solver take a single step:
- Keyboard: period key
- Dropdown menu: Solver -> Step
Besides that, the interface is identical to Microsoft Minesweeper:
- Expose square: left click
- Flag/unflag square: right click
- Expose-surrounding: left+right click on an exposed square whose value equals the number of flags surrounding it
- Start a new game:
- . . . with the same board size:
- Click the smiley button
- Dropdown menu: Game -> New
- . . . with a different board size:
- Dropdown menu: Game -> Beginner/Intermediate/Expert
- . . . with the same board size:
The goals of this project were to refresh my knowledge of Python and to gain experience with developing game mechanics in Python. In that, I feel it was a great success.
This project helped me feel confident that my last decade of professional software development really improved my ability to write high-quality (and bug-free) code. It helped me realize that I am actually quite thorough when it comes to unit testing, but that unit testing is often not very fun and can kill my enthusiasm for development and slow me down. This realization caused me to re-evaluate my priorities partway through this project to de-prioritize unit testing the solver in order to get it done faster. I also decided to de-prioritize some planned features (the cheating solver and moving mines) when I felt there wouldn’t be enough time to finish them and they would be difficult to demo in such a way as to show that they were different from the existing functionality. So overall, a great learning experience, and invaluable for my hopeful future projects in game development.
Task 2.7: Create a standalone executable of the program for sharing.
This took a few hours and was moderately painful. It required learning how to add image files to pyinstaller which was trickier than I anticipated. The easy part involved changing the folder structure of my program so that the images folder was in the same folder as the source code, and basing image paths off the temporary folder where they would be placed during execution. The more difficult part was figuring out how to bundle all images into the executable at once without having to specify each one individually. (The secret ended up being I didn’t need to use a wildcard: I just needed to specify the source folder “images\” with a trailing backslash and the destination folder “images” without a backslash. But it was frustrating how much searching I needed to do to find this minor, simple thing.)
After all that, the program takes a second or two to load. Some of the things I read online said that this slowness was inherent to pyinstaller, so I looked around for alternatives. I looked into PyOxidizer, but couldn’t find any simple tutorials for creating an executable, and my brain was fried from the struggle with pyinstaller. I also looked into py2exe, but there was no version of it that supported my current version of Python (3.12.0), and I wasn’t willing to downgrade to try it, so I settled for pyinstaller.
Task 2.6: Just a couple quick-and-dirty last-minute improvements that can’t be easily demoed:
- Solver will add flags and use expose-surrounding if this takes fewer steps than exposing each square individually.
- If the number of arrangements of mines within the remaining unexposed squares is small enough, the solver tries every possibility to see if it can deduce the positions of the remaining mines.
Task 2.5: Add the option to have the solver take a single action or “step”. This can be done via the “Solve” dropdown menu or by pressing the period key.
This was a feature requested by someone I talked to about this project early on, so I designed my code with it in mind. If the solver is running, “step” is called once each frame until the solver is stopped or the solver signals that it has taken an action (if only a single step was requested). To keep the program responsive, the solver tries to only ever do at most one full pass of the board on each step, meaning it either analyzes the board and attempts to make a deduction, or it takes an action, but not both.
Task 2.4: Add solver speed options.
I added speed options to the “Solve” dropdown: Normal (solve instantly), Slow (0.2 second interval) and Slowest (1 second interval). These control how often the solver takes an action and can be changed at any time. It makes it much easier to follow what the solver is doing, moment-to-moment.
Task 2.3: Solver removes flags placed by the user on non-mined squares.
The Minesweeper solver I wrote in college did not track the deduced positions of mines. Instead, it flagged any square it determined was a mine and used the positions of flags as the positions of known mines in its logic. The flaw with this approach is that a user could flag or expose any number of squares before starting the solver, and the logic was too brittle to account for this and would subsequently fail. As a workaround, I prevented the solver from starting if the user had made any changes to the board, including adding flags.
For this newer version of the solver, I planned to keep its logic versatile enough to be able to start from any board state. While this is currently the case, the solver does not yet have logic to remove flags placed by the user on non-mined squares. If you place just one flag on the board and then run the solver, as long as you’re not lucky enough to randomly choose a location that ends up being a mine, the solver will either expose a mine or get stuck in a loop trying to expose one of the flagged squares. This task will fix this minor bug.
. . .
I was hoping it would be possible to see the solver unflagging squares before solving the board, but it runs so quickly the board appears to get solved (or a mine exposed) the instant the solver is started. This might be a reason to add a “slow” checkbox option to the “solve” dropdown to make it clear what the solver is doing, step-by-step.
Task 2.2: Solver logic deduces the locations of mines and non-mined squares.
This is the core of the solver logic, and probably the one thing in this whole project that is the most fun to see working. When I wrote a Minesweeper solver back in college, I basically hard-coded several rules like this:
- If the number of unexposed, unflagged squares around an exposed square equals the number in that square minus the number of surrounding flags, then flag all remaining surrounding unflagged squares.
- Consider two adjacent exposed squares. Get the sets of squares around each and compare these sets. If the number of squares around the exposed square with the larger number minus the number of squares in both sets equals the difference in the numbers in the exposed squares, then every square around the exposed square with the larger number but not the smaller number must contain a mine.
As you can see, this logic was highly specific, limited, full of qualifications, and brittle. It was difficult to implement correctly, there were lots of cases it didn’t cover, and I either couldn’t think of every possible case or lost interest in specifying every possibility. Making a more generic logical system was one of my goals for this project, and was the thing I was most looking forward to seeing work.
My new logic covers a lot more cases and goes like this:
- Pick an exposed square that is not yet known to be solved (that is, it has surrounding unexposed squares that may or may not contain mines)
- Let U be a list of the unexposed squares around this exposed square that are not already known to be mined or non-mined.
- Let M be the number of mines that still need to be located around the exposed square. (This is the number in the square minus the number of unexposed surrounding squares that are known to be mines.)
- Iterate through every possible subset of U with size M, and consider the consequences of mines being in the positions in the current subset (and all squares that are in U but not the current subset are considered non-mined). For each position in U, examine each exposed square around it. The current arrangement of mines is invalid if, for any such exposed adjacent square: 1) the number of mines is too high, or 2) the non-mined spaces would prevent the exposed number from being correct. (Example: the exposed number is 2 and has four surrounding unexposed squares. There is one known mine location but the remaining three squares are assumed non-mined for this mine arrangement. This is an invalid arrangement of mines, since there is no way for the 2 to be correct.)
- Count the number of valid arrangements of mines, as well as the number of times each position in U is mined in a valid arrangement. If a position can never logically have a mine, then that position must be non-mined. If a position always contains a mine in every valid configuration, then that position must contain a mine.
I originally thought I would implement some logic to determine the minimal set of actions needed to expose the known-non-mined squares (ex. making selective use of flagging and expose-surrounding), but the solver usually wins or loses in under a second, so I feel this isn’t strictly necessary. (Or, at least, the effects won’t be visible.)
Note that since the game is won as soon as all non-mined squares are exposed, the solver doesn’t bother with flagging, and only exposes individual squares via logical deduction or random choice.
Task 2.1: If the solver can’t logically deduce that any squares are safe to expose, it exposes a square at random.
Exposing a square at random is often necessary to keep the game going. It’s obviously needed at the very start when no squares are exposed, but it also comes up fairly often during normal play when all possible deductions have been made from the available information but there are still squares left to expose.
As long as the logical part of the solver algorithm works correctly, the only time when the solver is at risk of exposing a mine is when it chooses a square to expose at random. For this reason, the square to expose must be chosen carefully to minimize the risk of hitting a mine.
The naive implementation is to just choose a square randomly from all unexposed squares on the board. While this is easy to implement, it is also needlessly risky, as there is a chance that the chosen square will be in a region with a high known density of mines. Excluding such regions will reduce the chances of hitting a mine, so the algorithm should take this into account.
For example, imagine we are playing on a beginner board (9-by-9 with 10 mines), we expose a single (center) square at random, and that square contains the number 6. From this information alone, we cannot deduce which of the 8 squares around the exposed square contain mines, but we know that, if we were to choose to expose one of those squares at random, we would expose a mine with probability 6/8 = 75% of the time. If we were to expose a random square from all 9 * 9 - 1 = 80 unexposed squares, we would expose a mine 10/80 = 12.5% of the time. This is obviously safer than choosing from the squares around the exposed 6, but this probability still includes those squares, so there’s still a chance we’d pick one of them. If we were to exclude the squares from around the exposed 6, that would reduce the number of known mines in the remaining squares by 6, and reduce the number of squares that can be chosen by 8, which would make the likelihood of exposing a mine (10 - 6) / (80 - 8) = 4/72 = 5.555…%. This is the lowest likelihood possible given this situation, so we should choose from the set of all exposed squares that are not around the exposed square.
More generally, we want to find the set of squares with known mine counts that can be excluded from the set of all unexposed squares to minimize the likelihood of exposing a mine. This should be done only if each smaller set of squares is a proper subset of the larger set of squares and has a higher mine density than the larger set (mine density = number of mines / number of unexposed squares).
. . .
This task ended up taking me four iterations to get the algorithm right. My initial implementation was needlessly complicated (involving doing several pairwise comparisons of increasingly complicated sets of squares). Even though I was eventually able to get it to work, I had to sleep on it to realize I could simplify it.
My second approach involved choosing combinations of exposed squares and removing their surrounding unexposed squares from the set of all unexposed squares. But on partially solved boards, the number of combinations of sets of squares to exclude is larger than can be checked exhaustively (while keeping the program responsive), so I had to make the algorithm exit early if it detected it was taking too long. After getting this second algorithm to work, I realized this wasn’t prioritizing sets of squares with higher mine densities. Also, I realized the more sets of squares that can be removed, the lower the mine density of the remaining set, so I should try to remove as many sets of squares as possible.
My next approach involved sorting all square sets by mine density then removing each one in order, if possible. This worked fine and was fast enough to never need to exit early, but I wasn’t convinced it was optimal. After thinking it over a little, I decided the only way to be certain the algorithm found the set of squares with the truly minimal mine density would be to check every possible permutation of squares, which would take too long to be feasible.
In the end, I settled on a heuristic wherein I started with each square set in the sorted list, then removed each set of squares when possible from the set of all unexposed squares, looping around to the beginning of the list until all sets of squares were considered. Most of the time, the previous version of the algorithm found the best solution, but sometimes this new approach would find a better one. I’m not sure whether I can do better than this while keeping the algorithm performant, so I’m calling it here for now.
Interestingly, this much logic alone is enough to solve a beginner board some of the time. It will still expose mines whose locations should have been logically deduced (because the algorithm failed to exclude them because they overlapped with an set of squares with equal mine density), but this issue will be resolved once the deduction logic is in place.
Solving intermediate boards is unlikely to succeed at this point.
Task 2.0: Add the ability to toggle the solver on and off.
When the solver is running, it will analyze the board on each iteration of the game loop and take actions it determines are correct, with the goal of winning the game. This may involve removing flags placed by the user before the solver began, exposing spaces that are determined to be safe, flagging spaces where there are mines, or exposing a random space when no logical deduction can be made.
The solver can be toggled on and off by pressing the space bar or by choosing “Solve” from a drop-down menu. While the solver is running, the user cannot interact with the board, squares will not show as pressed, and the smiley button will not change to its surprised variant.
. . .
PyGame was not receiving key press events, so I had to do some research into why this might be. As it turns out, tkinter (the library I’m using for the dropdown menus) does not pass keyboard events to embedded windows, so I had to tell tkinter to listen for key press events and then add appropriate events to the PyGame event queue. This was a minor hassle, but not insurmountable.
Besides that, the only tricky part of this feature was implementing the event handling. If any events for enabling or disabling the solver come in, then all user events that might change the board should be ignored, otherwise there will be a chance for a race condition. For example: given the events [left click, space] (which will be processed sequentially, left to right), the naive implementation of event handling would first expose a space in response to the left-click then start the solver in response to the space bar being pressed. But if the events were [space, left click], then the solver would get started and the left click would be ignored. This is a race condition: a significant difference in behavior as the result of a slight difference in timing (i.e. the space bar event being added first or second). The fix for this is to process all solver-related events first. Then, if there were none and the solver isn’t running, also process user events on the board.
Task 1.10: Add game over and game won.
A game of Minesweeper is lost as soon as one or more mines are exposed. When a game is lost, the smiley changes to have X marks in its eyes, all user input on the board is ignored (i.e. it’s no longer possible to expose or mark squares), and the timer stops. The board also shows the locations of all mines, which mines were exposed, and which locations were flagged but did not have a mine.
A game of Minesweeper is won once all non-mined spaces are exposed. On a win, the smiley changes to its sunglasses version, the timer stops, further user input on the board is ignored, all mine locations are shown as flagged (even if the user didn’t flag them), and the mine counter changes to 0.
. . .
I had originally intended to implement these two features separately, but forgot once I began coding and accidentally implemented them simultaneously. Adding unit tests took a little longer than usual since these new behaviors broke some existing tests. But on the whole, the new functionality is fairly simple and easy to understand, so it wasn’t too difficult to implement or test (just time consuming).
This first stage of the project ended up taking me four weeks when I thought it would only take one. Admittedly, having Thanksgiving in the middle slowed me down a lot, as did my insistence on adding unit tests as I went. In the interest of trying to get the remaining work done before the end of the DK30, I will plan to spend less time unit-testing my solver except in cases where it is easy to do or I feel unit testing is necessary to confirm that the behavior is correct.
Task 1.9: Implement expose-surrounding (left + right click).
If a square is exposed and the number in that square equals the number of flags in surrounding squares, you can press left and right click simultaneously on the square to expose all non-flagged, unexposed spaces around that square. I call this action “expose-surrounding”. If the square isn’t exposed, or if the number of surrounding flags is too high or too low, then nothing happens.
Task 1.8: Implement expose (left-click). Exposing a space that contains no surrounding mines exposes all spaces around it automatically. (Note that game-over and game-won will be implemented later, so exposing a mine at this point won’t end the game.)
Task 1.7: On the first (successful) expose action, build the board. The built board is guaranteed to not have a mine in the first exposed space.
(Note that the clicked space will not actually be exposed as part of this task.)
Since the user starts with no information on where mines might be, the first expose needs to be guaranteed safe to keep the game fair. I believe the earliest version of Minesweeper didn’t have this constraint, but it’s easy enough to implement. I will do this by only placing mines after the user has selected the first space to expose, and that exposed space will not be allowed as a mine location.
Task 1.6: Start the timer on the first expose action.
This behavior differs from how Microsoft’s Minesweeper does it: in the official version, the timer starts the first time you release the left mouse button over the board (or right mouse button while the left mouse button is still pressed). This happens even if no spaces are exposed by the action, like if the click happens on a square that’s been flagged.
This seems like a bug to me, since no valid game action was taken by the user and no information about the board has been revealed. I’m going to make what I feel is a slightly better choice for my project: the timer only starts once the user takes an action that will actually expose a space.
Task 1.5: Add the ability to start a new game with the smiley button.
Task 1.4: Show pressed versions of the smiley button, a square during expose (left click), or a 9-square region during expose-surrounding (left + right click).
One peculiarity of this feature is that, in Microsoft’s Minesweeper, if you press the smiley button then drag the mouse down to the board, the spaces don’t show as pressed. Similarly, if you click anywhere in the window other than over the smiley face, the smiley face will change to its shocked version and will not become pressed even if you move the mouse over it, but squares will show as pressed if you move over them.
This seems like a minor bug to me. I don’t think it makes sense to constrain what a left-click can do just because of where it started. The behavior in my version will work as I believe it should: either the smiley or squares can become pressed if the mouse moves over them, regardless of where the click began. I’m doing this less from the perspective of what is easy to implement and more how I think it should work (i.e. a minor improvement over the base game).
Task 1.3: Add flags and question marks.
Question marks aren’t really necessary for experienced players, but I’m adding them anyway since they’re in the original Minesweeper, are easy enough to implement, and add an additional case to consider when checking if a square is unexposed.
Since it’s very closely related, I also implemented the remaining-mines counter in the upper left corner of the window. Note that this number reflects the number of mines minus the number of flags on the board, and so can go into the negatives.
Writing unit tests for all this is really starting to feel like a chore. I feel like I’m just exercising skills I already have, not learning new ones or shaking off rust, so it’s getting difficult to force myself to keep doing it.
I had to add a class whose sole responsibility is to act as a wrapper to pygame methods, so that user input could be correctly mocked in tests. (And I had to learn how mocking in Python works.)
Task 1.2: Add a drop-down menu.
The drop-down menu in Minesweeper is used to start a new game, change difficulty (beginner, intermediate, or expert), enable/disable question marks, and exit. The drop-down menu in the official Microsoft Minesweeper also has options for enabling/disabling color and sound, as well as viewing high scores. I’m not interested in implementing these features (I always play with colors on and sounds off and don’t care about preserving high scores), so I’m declaring them out-of-scope for this project.
First, I’ll need to find an appropriate Python library for adding dropdown menus.
. . .
My initial search turned up a library called tkinter, but Eclipse isn’t recognizing the package name when I try to import it. It’s supposed to be included by default when you install Python, so I’m not sure what’s going wrong.
. . .
I spent an hour jumping around to various websites looking for solutions to this problem. I tried downloading tkinter using pip, updating my system path, repairing my Python installation, uninstalling and reinstalling Python, confirming that tkinter is installed (it is), and importing tkinter from console. None of these caused Eclipse to recognize the import.
As it turns out, for some reason I had multiple Python installations on my machine. Eclipse was pointing to a folder containing Python files that wasn’t the location where the Python installer copied its files (and where my system path was pointing, and where tkinter was being added by pip). After deleting this extraneous Python folder and pointing Eclipse to the proper folder, the import works just fine.
Phew. That’s one headache down. Now it’s time to actually use the library.
. . .
I can get a window to show up with a drop-down, but pygame shows its content in a different window. I’ll have to look into how to get tkinter and pygame to work together.
. . .
Some of the Stack Overflow answers I read weren’t encouraging. Some posters are saying that tkinter fundamentally won’t work with pygame, but they’re not the highest-rated answers, so I’m hoping they’re wrong.
After copying and modifying some example code that should get tkinter and pygame to work together, I am getting a menu bar and my pygame content is being rendered underneath it in the same window, but it’s constantly flashing black. I’m hoping this isn’t a symptom of the fundamental incompatibility mentioned by some posters, but it might be.
. . .
After being unable to fix the flashing window for a while, I tried looking into other drop-down menu solutions. One that I found that sounds like it’s specifically designed to work with PyGame is called Albow. I’ll give it a try now.
. . .
Compared to tkinter, Albow is much, much less well-documented. There weren’t any online examples I could find for how to set up a drop-down menu. Luckily, the code comes with a demo that shows off other features and I was able to modify it to suit my needs.
In doing this, however, I became convinced that this library isn’t refined enough for use by the general public. The demo itself was crashing until I changed a line of code. The library also seems to have a built-in requirement (not documented anywhere online that I could see, I might add) that your source folder must contain a “Resources” folder with subfolders for “fonts”, “images”, and “text”. This all seems highly restrictive and not well-suited for general use.
Then, after all that, the menu doesn’t even look good. In fact, I’d even go so far as to say it looks downright amateur:
I’m going to try to go back to tkinter and see if I can get it to work. It has the look and feel I’m going for and seems more professionally-written.
. . .
As it turns out, I was calling pygame.display.set_mode() each time my window was getting drawn, which was causing the flashing. It looks fine now, so I’ll proceed in the hopes that I won’t encounter any hard-blocking issues down the line. (I also had to figure out how to auto-size the tkinter frame to the visible region of the pygame window without causing pygame to launch in a separate window, but that’s perhaps too technical to be interesting.)
I went ahead and set the window title, added an icon, made the “Marks (?)” option a check button, and the difficulty options into radio buttons.
Doing all of this in Java was trivially easy, by the way. But it’s becoming blatantly obvious to me that Python isn’t intended for this kind of use case. (That is, replicating the look and feel of Windows applications.) This step took me multiple days to finish and was the source of a fair amount of frustration. Please, let the worst of the pain points be behind me now.
Task 1.1: Display the board & add unit tests.
Rendering the board went by without a hitch. There’s a chance I could do things a little cleaner than I did, but I feel the way I wrote it is pretty straightforward and easy to understand.
I was a bit unsure about when to load all the images of the parts of the board. My understanding is that constructors shouldn’t do work aside from assigning variables, and loading images is a kind of work that should be reserved for a dedicated “load” method. But having such a method requires that callers be made aware of the need to call “load_images” before attempting to access any of the images, which seems like it’s making callers deal with the internal sequencing logic of the image-storage class. I had the thought of loading the images dynamically the first time each is accessed, but this seemed like over-engineering the solution. So I fell back to loading all the images in a constructor. It works just fine, but I feel if I were in a professional team, there might be better solutions that more experienced members would propose.
I also wrote some unit tests for this step to ensure that loaded images are the right size (ex. all squares are 16x16), and conduct some pixel-diffs of full boards. I couldn’t figure out how to run all unit tests within Eclipse despite trying a few different solutions. I’ll revisit this issue later. For now, I’ll just have to run each test file individually.
Task 1.0: Screenshot the various parts of Microsoft’s Minesweeper.
My previous Minesweeper solver used screenshotted images of Microsoft’s Minesweeper to replicate its appearance. I originally intended to reuse these images, but looking at them now, I’m starting to think I cropped them incorrectly. My old project stored the board squares as 17x17-pixel images, which I suppose I did so that each square included its full 1-pixel border on all sides. However, when drawing the grid of squares, having these dimensions means that each square overlaps each of its neighbors on their 1-pixel border. It now seems to me that it would make more sense to have each square be 16x16, and to omit the bottom and right borders from each square’s image.
This approach actually makes more sense if you look closely at how the squares are displayed in the game: the gray border isn’t drawn along the bottom row or right column. I’m guessing I fixed this issue in my previous version by drawing the outer border last to overwrite the bottom and right borders of the squares. In my new version, it shouldn’t be necessary to draw the borders last as the bounds of no two squares will overlap.
Similarly, in my old version, it seems I screenshotted the full length of each border using a beginner-sized board. While this means I had to draw each section of border fewer times (as each copy covered many rows), it also meant that I had to draw the corners after the edges to correct the places where the border extended into the corner. I realize now that I can make sections of each border have the same width or height as the board squares and then draw each border section the exact number of times it is needed, without having to extend the borders into the corners or draw the corners last. The bounds of the top left and top right corners can also be extended slightly to align with the edge of a square to make this work.
Also, I noticed that the images in my old project were stored as .png and am unsure if the colors got changed during compression. I think I’ll take the screenshots over again, this time saving each image as a .bmp to avoid possible lossy compression.
. . .
While taking these screenshots, I noticed another problem: the window of my current version of Microsoft Minesweeper appears to be cutting off the bottom and rightmost edges of the border. As a perfectionist, I would like to show the full border in my version but can’t resize the window of Microsoft’s Minesweeper to screenshot it. It looks like I’ll need to search online for some examples of the proper Minesweeper border. (While taking the screenshots, I also noticed some inconsistencies between my computer’s version of Minesweeper and the screenshot I found online. These were relatively minor, but I had to make a note to prefer the online screenshot’s design and dimensions when programmatically building the board.)
When creating files to store these screenshotted images, I’m going to try to organize them better than I did last time. Before, I placed all images in the same “images” folder and gave them all-lowercase names without delimiters. This time, I’m thinking I’ll divide the images into four separate folders for better organization: border, digits (for the mine count & timer), smiley, and squares. I’ll also delimit words with underscores for readability (ex. digit_9.bmp, smiley_gameover.bmp, exposed_6.bmp, top_left.bmp).
Screenshotting these images went relatively quickly. Here are some tips and tricks I figured out for doing this:
- Getting the smiley, borders, and most square states was easy.
- The fastest way to get squares with higher numbers (5 and up) is to make a custom board with a lot of mines and then click around. I was able to get images of squares with the numbers 5-8 this way pretty fast.
- The fastest way to get all the different kinds of digits is to make custom boards with different numbers of mines: 012, -9 (Did you know the mine counter can show a minus sign? Just create a Beginner board then start flagging more spaces than there are mines!), 345, and 78. (The maximum number of mines on a custom board is 667, so 678 and 789 aren’t possible.)
Even though I didn’t end up reusing the images from my old project, the mere act of having it on hand has already proven useful: I almost forgot one image I needed to capture, but was able to find it by comparing the number of image files between my old and new folders. (The image I almost forgot was the one of a mismarked mine, which is shown if you flag a space without a mine and then lose the game.)
Task 0.4: Choose an application for version control.
Part of the purpose of this project is to practice game development in Python in preparation for a game I plan to make in the future. Some of the advice I found online for first-time game developers was to follow professional software development best-practices, including using version control and writing unit tests. I plan to do this, but I need to decide which version control solution I’ll use.
I’ve been using version control for my entire professional career, but have never had the chance to go through the process of setting up a completely new repository, so this should be a useful learning experience.
. . .
After a brief search online, I think that SVN should work for me. I’m confident I’ll only ever work as a solo developer and so don’t need any complicated file server infrastructure like would be needed to use Git.
I’ve used TortoiseSVN before and greatly prefer it to the console, so I read the first few chapters of its documentation and installed it. (I’ve also used SVN before and greatly prefer it to Git.)
Task 0.3: Copy & run an example PyGame loop.
Now that I have a Python project in Eclipse, I can copy an example PyGame loop I found online to get a simple window up and running:
Success! Seems simple enough. I should now be ready to start on the actual work of the project.
Task 0.2: Install PyDev for Eclipse.
I haven’t used Eclipse in years, so first I’ll need to update it and maybe reorganize the folder where my workspace files live.
. . .
This took a little time, but it feels good to have done, and should hopefully prevent compatibility issues when installing PyDev.
. . .
It ended up taking me 3 hours to get PyDev installed and working. The documentation on the PyDev website is pretty bad. The PyDev tutorial says to take all these steps that aren’t strictly necessary for setting up a simple project. Trying to follow all these steps for the first time just confused me, and the later steps caused errors that prevented me from finishing setting up my project. In frustration, I searched online and found a decade-old website from an introductory computer science college course. The instructions on this website were much easier to understand than the PyDev manual, actually laid out the minimal set of steps necessary to set up a new Python project in Eclipse, and, crucially, worked without errors.
This part of the process was more painful, frustrating, and draining than I anticipated, but it needed to be done before I could do any of the actual work.
Task 0.1: Decide on an IDE that can run Python.
I already have Eclipse installed on my computer, so for simplicity’s sake, I’d like to stick with that. However, Eclipse doesn’t support Python by default, so I’ll need to look into how to run Python in Eclipse.
. . .
After conducting a brief search, it seems there are a few options for running Python in Eclipse. I’m going with PyDev as it’s just a plugin which I hope makes it relatively simple to use.
Task 0.0: Dredge up my old Minesweeper solver project, and notes on Python syntax.
I’m not planning on copying any of the code from my old project (as it is written in Java), but having it around as a reference (and sanity check) might be useful. It would also be nice not to have to re-make the images of the parts of the board, if possible. Plus, it’s nice to be able to look back on the code I wrote over a decade ago and see how much I’ve learned and improved since then.
I haven’t programmed in Python since college, so I’ll need to brush up on the syntax and primitives (ex. tuples). I know I have a file with notes on programming in Python somewhere, but will need to track it down.
. . .
My long-term storage (which contains all my old files from high school and college) is reasonably well-organized, so it didn’t end up taking too long to find these files, but it did require some time and effort.
Estimated Timeframe
Nov 1st - Dec 14th
Week 1 Goal
Implement & unit test base game
Week 2 Goal
Implement & unit test solver
Week 3 Goal
Implement & unit test cheating solver (that uses board knowledge to beat the game in the fewest moves)
Week 4 Goal
Stretch goal: menu setting to move mines when possible (only relevant for normal play & normal solver) Prepare images, animations, and possible video for final summary