How to program MineSweeper in Python
Difficulty: fairly easy
Welcome to my tutorial on how to program the classic game, MineSweeper, in Python! Before you begin, I would highly recommend playing a few games to get the hang of the rules. You can do this either online or in my program.
We will be building our game in the Python terminal, which, as you probably know, has its limitations. Instead of clicking on the square in the grid, as you do in the original game, the player will type its coordinates. But what we will create is a fully functional 9x9 MineSweeper game, that will entertain you and your friends for hours on end (sorry, that sounded very cheesy and predictable).
Note: I would encourage you, especially if you are a beginner, to write out the code rather than just copy it, as this will help you to understand it.
Part 1: Planning
You should now understand the game mechanics of MineSweeper, but before we can get started we need to think about what the computer does. This will help us when writing our code later. First, just to avoid confusion later, a few definitions/variables I will use:
- Bomb: I will use this word instead of 'mine', to avoid confusion with the possessive pronoun! In our game, bombs will be represented by asterixes,
- Marker: a flag which the player can place to help them remember any locations of bombs they have deduced. In our game, these will be represented by the unicode flag symbol,
- Solution grid: this contains the locations of all the bombs and numbered cells.
- Known grid: this contains the squares that the player knows about.
- To open: to move data from the solution grid to the known grid, when the player makes their move.
Now we've got that cleared up, here's a quick outline of what the program will do.
- Display menu to player.
- If they ask for instructions, print them.
- Generate random locations of 10 bombs and place them in the solution grid.
- Update the numbers around them in the solution grid.
- Until the player wins/loses, loop:
- Display the known grid to the player and ask for their move.
- If they chose to open a square, open it.
- If that square is a bomb, they lose.
- Offer to play again.
- If the number in that square is a 0, open up all of the squares around it automatically, as there could not be any bombs there, and do the same if any of those squares are 0, etc etc.
- If all squares in the grid except the 10 bomb squares are open, they win!
- If that square is a bomb, they lose.
- If they chose to place a marker in a square, place that marker.
- If they chose to open a square, open it.
- Display the known grid to the player and ask for their move.
Part 2: Setting up our program structure.
Because this is not going to be a very long program, (about 250 lines - don't worry if you do think this is quite long!) we will write almost all of it in one file. The only exception will be the instruction text, which we will put in a seperate .txt (text) file to declutter our code a bit. So, if you're going to write the program as we go along, now is the time to create your repl, call it something, and create the new file.
In the panel on the left, click 'files' and then 'new file'. Call it
Paste into it the following text:
INSTRUCTIONS ============ The aim of MineSweeper is to determine the locations of 10 bombs, randomly placed in a 9x9 grid. On each go, you type in the coordinates of a square, e.g. E4. If there is a bomb in that square, you lose. Otherwise, the number of bombs directly surrounding that square, including diagonally, will appear in that square. If that number is a 0, the squares around it will be 'opened' automatically, as there cannot be any bombs there, to save you time. If you think you know the position of a bomb, type 'M' followed by the coordinates of that square, e.g. ME4. You win by 'opening' all of the squares except those with bombs in. NB: It is luck on the first move, and you may get to a stage where it is luck later in the game too. Good luck!
In Part 5 we will write the code that gets the text from the file and prints it so the user can read it.
Part 3: Beginning our code.
Now move back to
main.py, and write one of the following.
If you're writing your code on repl.it:
import random, time, copy, replit from termcolor import cprint
import random, time, copy from termcolor import cprint
This imports four/five packages,
copy, and certain parts of
termcolor, which we will use later in the program. If you are on repl.it, it also imports the module
replit which allows us to clear the terminal. If you are on a different platform and know how to clear the terminal, that's fine - just use your method whenever I refer to
replit.clear(). If you don't, that's ok too - sometimes I provide an alternative, but if I don't, just leave it out and it should be fine.
But it doesn't do anything yet. Let's change that.
First things first: we need to write an introduction for the player. Here's one I made earlier, but feel free to edit it / write your own.
#Introduction print() cprint('Welcome to MineSweeper v.3.0!', 'red') cprint('=============================', 'red') print() print('Excited to declare version 3.0 of MineSweeper as almost fully functional!')
'cprint()' is a function we imported just a minute ago. It allows us to print colour in the terminal, with the syntax
cprint('text', 'colour'). I've set it up to print the title in red, but you can choose any of the following colours:
grey red green yellow blue magenta cyan white
Termcolor has lots of other cool features such as text highlights - see the package website.
You can now run your program and it will, for the first time, do something! Oh and, if it hasn't already it will first chuck out some rubbish about importing
termcolor. Ignore it - it only does it once.
Part 4: A Python Sandwich
By the end of this tutorial, you will be sick of functions (
def example():). Apart from one line, which initiates all of the functions, the rest of the program... will be entirely functions. Why? Because MineSweeper is a repetitive game where a 'go' always leads to one of a few outcomes. This means that it is much simpler and faster to write the rest of the code in functions.
Functions allow us to do one of two things. In the mathematical sense, they can be used as a quick way of writing one equation which would otherwise take up several lines (a mathematical function is a term you have probably heard of/used). But they can also be used for more substantial pieces of code that will be used several times, or as a substitute for a loop. Functions can do both, and quite often do. This may all sound a bit wierd, but hopefully it will become clear as we continue.
Let's write the last line that's not part of a function. It's pretty simple, and it goes right at the end of our program. Leave some space for the rest of the code, and type:
The code we have written so far are the slices of bread, and what we will write now is the filling. There you go: a Python Sandwich!
Side note: this will be a very well filled sandwich.
If you try to run your code now, it will give you an error that looks something like this...
NameError: name 'reset' is not defined
... because we haven't defined
reset(). That is what we need to do next.
reset() will be the function that runs every time we want a new game. It resets all the variables, as well as printing the menu and starting the timer.
In the space we left in Part 4, write this:
#Sets up the game. def reset(): print(''' MAIN MENU ========= -> For instructions on how to play, type 'I' -> To play immediately, type 'P' ''') choice = input('Type here: ').upper() if choice == 'I': replit.clear() #Prints instructions. print(open('instructions.txt', 'r').read()) input('Press [enter] when ready to play. ') elif choice != 'P': replit.clear() reset()
This, when called upon by
reset(), prints the main menu and asks the player for your choice - to play immediately or to see the instructions.
If the player typed 'I', it clears the terminal, then gets the text from the file
instructions.txt that we made earlier, reads it, and prints it (
print(open('instructions.txt', 'r').read())). It then waits until the player presses enter before continuing.
If the player didn't type 'I' or 'P', it clears the terminal and runs
reset() again, as they didn't enter one of the given options.
If the player typed 'P', it continues straight on.
Part 6: Generating the Solution Grid
The next thing we need to think about is generating the solution grid for each particular game. It must have 10 bombs in random places, and every cell must be marked with how many bombs surround it
First, we need to set up the array (two-dimensional list) for the solution grid. We'll call it
b, for bombs. Copy this code inside
reset(), and make sure they are both tabulated the correct distance.
#The solution grid. b = [[0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0]]
But what is this mess of zeros? As you can see,
b is a list, but it also contains some lists - 9 to be exact. Each of these lists contain 9 objects, which are all zeros at the moment. This essentially makes it a two-dimensional list, which is called an array, to define the solution grid.
The next step is generating the random locations of the 10 bombs, and placing them in the solution grid. For each bomb, we need to do three things:
- Generate a random location.
- Check if there's already a bomb there; if there is, go back to the first step.
- Place the bomb in the solution grid.
We're going to do this through another function, and you'll see why later. To call it, add this to the end of
for n in range (0, 10): placeBomb(b)
That should be pretty self-explanatory - it simply calls upon
placeBomb(b) 10 times. In case you don't know, that
(b) is there because a function can't access external variables without being passed ('given') them. Now we need to define
placeBomb(b). Write this after
#Places a bomb in a random location. def placeBomb(b): r = random.randint(0, 8) c = random.randint(0, 8) #Checks if there's a bomb in the randomly generated location. If not, it puts one there. If there is, it requests a new location to try. currentRow = b[r] if not currentRow[c] == '*': currentRow[c] = '*' else: placeBomb(b)
This does all three things that we mentioned earlier:
First, it generates a random location in the grid through the variables
r (row) and
c (column). Note that they are a random number from 0 to 8, because indexing in Python starts at 0 not 1.
Next, it checks if there is already a bomb at that location. It gets the row (one of the lists in
b) and then the column (one of the values in that row).
If it's not already a bomb, it puts one there. If it is, it runs the function again.
Once it has succesfully placed a bomb, it returns to the line in
reset() which it was called from. Since it runs
placeBomb(b) 10 times, we end up with 10 randomly placed bombs in the grid!
The next thing we need to do is make sure that all the 0s in the grid are changed to the number of bombs surrounding that square. By far the easiest and quickest way of doing this is to add 1 to the numbers in all of the squares surrounding each bomb. This will lead to all of the numbers correctly reflecting the number of bombs surrounding them. We are going to do that with this code. Write it at the end of
for r in range (0, 9): for c in range (0, 9): value = l(r, c, b) if value == '*': updateValues(r, c, b)
for loops go through each square in the grid, by cycling through the x and y coordinates,
c. In the next line, we have another function which we are yet to build,
l(r, c, b), which gets the value at the given coordinates - we'll write that in a minute. If that value is a bomb, it updates the numbers around it through yet another function, which we will write now.
updateValues(r, c, b) is an annoying bit of code which is too boring, repetitive and overcomplicated for the code to be worth explaining now (if you want a challenge, feel free to read through it!). I will, however, explain what it does.
The variables that you pass to it are the row and column of the bomb, as well as
b, our solution grid. It goes through all 8 squares directly surrounding the given square, and adds 1 to the value there in the solution grid, unless that square is also a bomb. Don't worry if that's unclear, I'll explain it with an example in a moment. The full function is here (copy it anywhere after the
#Adds 1 to all of the squares around a bomb. def updateValues(rn, c, b): #Row above. if rn-1 > -1: r = b[rn-1] if c-1 > -1: if not r[c-1] == '*': r[c-1] += 1 if not r[c] == '*': r[c] += 1 if 9 > c+1: if not r[c+1] == '*': r[c+1] += 1 #Same row. r = b[rn] if c-1 > -1: if not r[c-1] == '*': r[c-1] += 1 if 9 > c+1: if not r[c+1] == '*': r[c+1] += 1 #Row below. if 9 > rn+1: r = b[rn+1] if c-1 > -1: if not r[c-1] == '*': r[c-1] += 1 if not r[c] == '*': r[c] += 1 if 9 > c+1: if not r[c+1] == '*': r[c+1] += 1
The other function that we need is
l(r, c, b), which, as I mentioned earlier, gets the value in the solution grid at the given coordinates (
c). It's a very simple bit of code, but we are going to be using it a lot and we will want to be able to write it as shorthand as possible (
l stands for location). Here's the code (copy it after
#Gets the value of a coordinate on the grid. def l(r, c, b): row = b[r] c = row[c] return c
It's one of the more mathematical-type functions, as it only does one thing and then returns the output. Whenever we call this function, it will simply return the value at the given location - it doesn't change anything, it just gives us information. We'll see why this is so useful in Part 7!
The first line gets the correct row from our solution grid, and the second gets the correct column from (value in) that row. The third line simply returns that value.
We can actually shorten it to just two lines...
#Gets the value of a coordinate on the grid. def l(r, c, b): c = b[r][c] return c
... because previously
row in the second line was just standing for
And even to just one line!
#Gets the value of a coordinate on the grid. def l(r, c, b): return b[r][c]
Your program should now look something like this. There shouldn't be any errors, but if you try to play a game it will just stop abruptly, because although we have the gird generation going on in the background, we haven't asked the program to print anything yet. That's all going to change in Part 7!
Review of Part 6
Some of that was probably quite confusing, so I'll run through an example (with a smaller grid). If you feel comfortable so far, skip to Part 7.
We started by setting up the variable for the solution grid, a two-dimensional list of zeros. So at the moment, our solution grid looks like this:
╔═══╦═══╦═══╦═══╦═══╗ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝
Next, we added 10 random bombs (I'll just do 4 here as it's a smaller grid):
╔═══╦═══╦═══╦═══╦═══╗ ║ 0 ║ * ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ * ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ * ║ * ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝
Then we went through each bomb and added one to each of the numbers around it, to get this:
╔═══╦═══╦═══╦═══╦═══╗ ║ 1 ║ * ║ 1 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 1 ║ 1 ║ 1 ║ 0 ║ * ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ * ║ * ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝ ↓ ╔═══╦═══╦═══╦═══╦═══╗ ║ 1 ║ * ║ 1 ║ 1 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 1 ║ 1 ║ 1 ║ 1 ║ * ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ * ║ * ║ 1 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝ ↓ ╔═══╦═══╦═══╦═══╦═══╗ ║ 1 ║ * ║ 1 ║ 1 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 2 ║ 2 ║ 2 ║ 1 ║ * ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 2 ║ * ║ * ║ 1 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 1 ║ 1 ║ 1 ║ 0 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝ ↓ ╔═══╦═══╦═══╦═══╦═══╗ ║ 1 ║ * ║ 1 ║ 1 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 2 ║ 3 ║ 3 ║ 2 ║ * ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 2 ║ * ║ * ║ 2 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 1 ║ 2 ║ 2 ║ 1 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝
Part 7: Printing the Grid
Part 6 was by far the longest and most complicated stage we've done, so well done for getting this far. Part 7 should be a bit easier! In this stage, we're going to write a function which prints the grid so our player can see it. Of course in the real game we'll print a blank grid at first, but so that we can see that the grid generation worked we'll print the solution grid for now.
The function we're going to write will be called
printBoard(b). Eventually, it should output something like this (using our smaller example from earlier):
A B C D E ╔═══╦═══╦═══╦═══╦═══╗ 0 ║ 1 ║ * ║ 1 ║ 1 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ 1 ║ 2 ║ 3 ║ 3 ║ 2 ║ * ║ ╠═══╬═══╬═══╬═══╬═══╣ 2 ║ 2 ║ * ║ * ║ 2 ║ 1 ║ ╠═══╬═══╬═══╬═══╬═══╣ 3 ║ 1 ║ 2 ║ 2 ║ 1 ║ 0 ║ ╠═══╬═══╬═══╬═══╬═══╣ 4 ║ 0 ║ 0 ║ 0 ║ 0 ║ 0 ║ ╚═══╩═══╩═══╩═══╩═══╝
The letters at the top and the numbers to the left will be used as coordinates when the player types their chosen move. To create the grid shape, we'll use some of the Unicode box-drawing characters:
To start our function, type this:
#Prints the given board. def printBoard(b):
The first thing we need to do in our function is to move the grid from the player's previous go off the visible screen. We can do this in a couple of different ways:
- Print 40 or so empty lines
- Use the repl.it
This will trick the player into thinking that we are updating the grid each time they make a move rather than reprinting it. To do this, type one of these inside the new function:
If writing your code on repl.it:
for n in range (0, 40): print()
Next, we need to print the letters and then the 'top' of the grid/board/box. These two lines will do that:
print(' A B C D E F G H I') print(' ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗')
Now for the slightly trickier bit. Until now, what we've printed will always be the same, whatever the board (
b) that was passed to the function. But now we need to write the code that prints the grid with the correct values in it. It needs to get the data at each location in the grid. How do we do that? With
l(r, c, b) of course! This is the code we're going to use - I'll explain it in just a second. Add it to our new function:
for r in range (0, 9): print(r,'║',l(r,0,b),'║',l(r,1,b),'║',l(r,2,b),'║',l(r,3,b),'║',l(r,4,b),'║',l(r,5,b),'║',l(r,6,b),'║',l(r,7,b),'║',l(r,8,b),'║') if not r == 8: print(' ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣')
Don't panic - this is a lot less complicated than it looks.
Because we need to print a 9x9 grid, the
for loop runs the above code 9 times, changing
r (the row number) each time.
Let's break down the
print() line that follows:
As you can see, it is essentially made up of three things - the line number, the box-drawing characters, and the
l(r, c, b) functions. Not as complicated as it might have looked at first, hopefully!
The next two lines are an
if statement, which prints the line that runs between each row on the board...
... unless the row number is 8 (the 9th and last row). After the last row, we need to print the closing line - to do this, add the following after the loop:
And that's the
printBoard() function finished! The complete function should look like this:
#Prints the given board. def printBoard(b): replit.clear() print(' A B C D E F G H I') print(' ╔═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╦═══╗') for r in range (0, 9): print(r,'║',l(r,0,b),'║',l(r,1,b),'║',l(r,2,b),'║',l(r,3,b),'║',l(r,4,b),'║',l(r,5,b),'║',l(r,6,b),'║',l(r,7,b),'║',l(r,8,b),'║') if not r == 8: print(' ╠═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╬═══╣') print(' ╚═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╩═══╝')
Now all we need to do is check that it - and all the code we've done so far - work! To do this, we just need to call the function,
printBoard(b), at the end of
reset(). Just type
printBoard(b), and run the program. When the main menu comes up, type 'I' to check that the instructions are working, and then press enter to see your randomly generated grid!
Working? Great - proceed to Part 8.
Somthing wrong? Your entire program should now look like this.
Part 8: So nearly ready to start the gameplay!
We are so nearly ready to start programming the actual gameplay - there are just a few more things we need to set up, primarily the known grid.
Way back in Part 1 I mentioned the known grid. It contains the squares that the player knows about, and, unlike the solution grid, it changes when the player makes their move. At first, of course, it needs to be blank, as the player doesnt know anything about the grid. In
reset(), before the line where we call on
printBoard(b), add the following:
#Sets the variable k to a grid of blank spaces, because nothing is yet known about the grid. k = [[' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '], [' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ']]
k, the known grid, is another array like our solution grid, but it contains just blanks spaces because at the beginning of the game the player knows nothing about the solution grid. Data will be copied into it from the solution grid as the player makes their moves.
We still have the line
printBoard(b) at the end of the
reset() function, but we don't want the player to be given the solution grid at the beginning! Instead, we want them to see a blank grid. To do this, we just need to pass the known grid to
printBoard rather than the solution grid. So just change it to
If you run your code now, when you start the game you should just see a blank grid; if not, check your code looks like this.
Part 9: Ready Player One
We've now set up pretty much everything we need, and can (finally, I hear you sigh) get started on the gameplay!
Minesweeper isn't just about locating all of the bombs in the grid, it's also about doing that in the fastest time possible. So let's set up a timer so players can see how well they did. The timer will work by getting the exact time at the start of the game, and then the same when the player wins/loses. It will then find the difference between them, and that's our time! So at the end of
reset( add the following lines:
#Start timer startTime = time.time()
We'll then initiate gameplay by calling on our yet-to-be-created
#The game begins! play(b, k, startTime)
Now, of course, we need to write the
play() function. It's the longest function in the program, at, excluding comments, about 40 lines long. Each time we run through the
play() function, one 'go' happens - i.e, the player places their move and we update the known grid / end the game accordingly. Here's a rough breakdown of what the function will do:
- Let the player choose a square (which will be verified as a valid square though another function,
- If the player asked to place a marker:
- Change the value in the known grid at the given location to the marker symbol,
- Change the value in the known grid at the given location to the marker symbol,
- Get the value at the given location.
- If it is a bomb:
- Tell the player they lose, and offer them the chance to play again.
- Change the value in the known grid at the given location to the real value.
- If it is a zero, run
checkZeros()(another function - we will make this later).
- If there are only 10 'unopened' squares left in the grid:
- Tell the player that they win, and offer them the chance to play again.
So now let's start writing the function. As usual, we need to write
def play(), but since our function needs the solution and known grids, as well as the time the game started (
startTime), we'll write the following:
def play(b, k, startTime):
I appreciate that this is probably really annoying, but we are going to start our function by calling another funtion,
choose(), which we'll write later. We will use it to get the coordinates of the square that our player wants to 'open' / place a marker in, which
play() will use - for this reason, rather than just calling
choose(), we want to set a variable to the output from that function. In fact, since
choose() gives us two variables (the coordinates of the square that the player chose), we'll write this:
#Player chooses square. c, r = choose(b, k, startTime)
We now want to find out what the value at that square is, so we'll add the following, using our
l(r, c, b) function from earlier:
#Gets the value at that location. v = l(r, c, b)
If there's a bomb in that location, we need to end the game. We also need to show the player the solution grid, tell them their time, and offer them the chance to play again. Write this in
play() - it should be pretty self explanatory:
#If you hit a bomb, it ends the game. if v == '*': printBoard(b) print('You Lose!') #Print timer result. print('Time: ' + str(round(time.time() - startTime)) + 's') #Offer to play again. playAgain = input('Play again? (Y/N): ').lower() if playAgain == 'y': replit.clear() reset() else: quit()
The fourth line in the if statement just calculates the time the game took, by subtracting the start time from the current time, and prints it.
Now, assuming that the game hasn't just ended, we need to put the value that the player 'found' into the known grid. To do that, we just write the following:
#Puts that value into the known grid (k). k[r][c] = v
Now, if that value is a zero, we need to run
checkZeros(), a function that we'll write later. It will open up any unopened squares around that zero. Just add this:
#Runs checkZeros() if that value is a 0. if v == 0: checkZeros(k, b, r, c) printBoard(k)
The last thing to do in this function is to work out if the player has won. We'll do this by counting up how many unopened squares there are left. If there are 10 left, we know that they have won (the ten squares remaining must be the ten bombs, or else they would have died). Here's the code, which goes through each row and counts how many unopened/flagged squares there are, and keeps a running total:
#Checks to see if you have won. squaresLeft = 0 for x in range (0, 9): row = k[x] squaresLeft += row.count(' ') squaresLeft += row.count('⚐')
Now we need to write what it does if the player has won. This is exactly the same as the code we wrote earlier for if the player loses (except of course it tells you that you've won, not lost). So add this to
if squaresLeft == 10: printBoard(b) print('You win!') #Print timer result. print('Time: ' + str(round(time.time() - startTime)) + 's') #Offer to play again. playAgain = input('Play again? (Y/N): ') playAgain = playAgain.lower() if playAgain == 'y': replit.clear() reset() else: quit()
Now all that's left to do is create a loop of gameplay by calling
#Repeats! play(b, k, startTime)
play() function is complete. Now we just need to write all the functions it called...
Part 10: We face a choice
In this part, we will write the
choose() function that we called in
play(). Above the
play() function, begin the function by writing this:
#The player chooses a location. def choose(b, k, startTime): #Variables 'n stuff. letters = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ,'i'] numbers = ['0', '1', '2', '3', '4', '5', '6', '7', '8'] #Loop in case of invalid entry. while True: chosen = input('Choose a square (eg. E4) or place a marker (eg. mE4): ').lower()
We just set up a few lists which we'll use later to check if the player has entered a valid coordinate, and begun an infinite loop which will only be broken when the player enters a valid coordinate. As you can see from the final line, there are two types of input:
- If the player wants to open a square and see what's there, then they can just type its coordinate (e.g E4)
- If they want to place a marker in that square, if they think it's a bomb, then they can type 'm', followed by the coordinate (e.g mE4)
So now to write the code that detects those two types of input:
#Checks for valid square. if len(chosen) == 3 and chosen == 'm' and chosen in letters and chosen in numbers: c, r = (ord(chosen))-97, int(chosen) marker(r, c, k) play(b, k, startTime) break elif len(chosen) == 2 and chosen in letters and chosen in numbers: return (ord(chosen))-97, int(chosen) else: choose(b, k, startTime)
The first part of the if statement runs if the player tries to place a marker (e.g. mE4). We will write the
marker() function next. The next bit returns the chosen coordinate to
play() if the player tries to 'open' a square. The
else: statement just runs
choose() again if the player hasn't entered a valid square. Pretty neat, huh?
Oh, I should probably explain what python's
ord() function does: it stands for ordinal, and returns the ASCII value of whatever character you give it, and from it we can get the numerical equivalent of a letter without the need for an alphabet variable. We need this because computers much prefer dealing with numbers rather than letters.
Part 11: The
The last couple of Parts have been pretty long so I thought we'd do something shorter and (hopefully) easier for a change. The
marker(r, c, k) is literally three lines long. Just add it somewhere above the
#Places a marker in the given location. def marker(r, c, k): k[r][c] = '⚐' printBoard(k)
Woah. Mind blown.
It's really simple: it just changes the value in the known grid to a flag symbol, and prints the board. The code then returns to
choose() where the player can enter another location.
Part 12: Ha
See, I told you part 11 would be short. If you run your code now, it should give you the main menu, and then when you begin playing print the board. But it doesn't yet ask you what square you want to open. This is because we haven't called
play() at the end of
reset(). So now at the end of the
reset() add the following code:
#Start timer startTime = time.time() #The game begins! play(b, k, startTime)
As you can see, this just starts the timer by setting a variable to the current time, and begins the game! Your code should now look something like this. If you run it, it will now let you play a game! It should all work until you hit a 0. This is because we haven't yet written the
checkZeros() function, which will open up all the squares around a 0 automativally (because we know there can't be any bombs there). That's coming next, and then we're pretty much done!
checkZeros() - we're so nearly there!
checkZeros() function looks like this:
#Checks known grid for 0s. def checkZeros(k, b, r, c): oldGrid = copy.deepcopy(k) zeroProcedure(r, c, k, b) if oldGrid == k: return while True: oldGrid = copy.deepcopy(k) for x in range (9): for y in range (9): if l(x, y, k) == 0: zeroProcedure(x, y, k, b) if oldGrid == k: return
It essentially goes through the whole grid, and checks that the squares around every zero are open. If they aren't, it opens them. It keeps doing this until nothing changes when it goes through the whole grid. Here's the function in more detail, bit by bit (feel free to skip ahead if you don't want this much detail):
oldGrid = copy.deepcopy(k)- this makes a copy of the know grid. We don't just say
oldGrid = kbecause of the weird (but also often helpful) way that python deals with lists - if we did, it would create two lists that are linked, and any changes to one would affect the other. The
copy.deepcopy()function ensures this doesn't happen.
zeroProcedure(r, c, k, b)- runs the
zeroProcedure()functon that we will copy in in a minute. It does the actual opening of the squares around the given location where we found a zero (
if oldGrid == k: return- this detects if anything has changed in the grid - if it hasn't, then we can return to
play()and do the next go. If it has, then we go to the next bit which deals with any 'chain reactions' - when more 0s are revealed from opening the squares around a previous one.
while True: ... return- this loop continually goes through the each square in the known grid, opening up the squares around any zeros if finds (bear in mind that they may already have been opened), until nothing changes, and then we know we are done and can return to
Now copy it into the code, somewhere above our
play() function. Then copy the following above that - it is the
zeroProcedure() function that I mentioned earlier. It's pretty boring and repetitive so I'm not going to explain it, but feel free to read it through if you want:
#When a zero is found, all the squares around it are opened. def zeroProcedure(r, c, k, b): #Row above if r-1 > -1: row = k[r-1] if c-1 > -1: row[c-1] = l(r-1, c-1, b) row[c] = l(r-1, c, b) if 9 > c+1: row[c+1] = l(r-1, c+1, b) #Same row row = k[r] if c-1 > -1: row[c-1] = l(r, c-1, b) if 9 > c+1: row[c+1] = l(r, c+1, b) #Row below if 9 > r+1: row = k[r+1] if c-1 > -1: row[c-1] = l(r+1, c-1, b) row[c] = l(r+1, c, b) if 9 > c+1: row[c+1] = l(r+1, c+1, b)
And that's it!!! You're done! It should now be fully functional, try running it. If you hit a zero, it should set of a chain reaction and open at least a few more squares - maybe half the grid if you're lucky! Not working? Make sure your code looks something like this. Still stuck? Send me a message in the comments below - I'll try to reply as soon as possible.
I really hope you enjoyed the tutorial, I've spent a looong time making it. An upvote and some feedback in the comments would be really appreciated :)