I have completed my minesweeper game using Tkinter and would like to know ways to improve. I also have a question for extra features: does anybody know a way to have an opening start, like Google minesweeper does where it breaks open a bunch of squares?
I'm accepting all comments.
Please criticize: readability comments and performance comments accepted.
import tkinter as tk
import random
import sys
import applescript
import tkinter.messagebox as messagebox
def printArr():
for row in arr:
print(" ".join(str(cell) for cell in row))
print("")
def createBoard(length, width):
global arr
arr = [[0 for row in range(length)] for column in range(width)]
def placeMine(x, y):
ydim, xdim = len(arr), len(arr[0])
arr[y][x] = 'X'
if (x != (xdim - 1)):
if arr[y][x+1] != 'X':
arr[y][x+1] += 1 # center right
if (x != 0):
if arr[y][x-1] != 'X':
arr[y][x-1] += 1 # center left
if (x != 0) and (y != 0):
if arr[y-1][x-1] != 'X':
arr[y-1][x-1] += 1 # top left
if (y != 0) and (x != (xdim - 1)):
if arr[y-1][x+1] != 'X':
arr[y-1][x+1] += 1 # top right
if (y != 0):
if arr[y-1][x] != 'X':
arr[y-1][x] += 1 # top center
if (x != (xdim - 1)) and (y != (ydim - 1)):
if arr[y+1][x+1] != 'X':
arr[y+1][x+1] += 1 # bottom right
if (x != 0) and (y != (ydim - 1)):
if arr[y+1][x-1] != 'X':
arr[y+1][x-1] += 1 # bottom left
if (y != (ydim - 1)):
if arr[y+1][x] != 'X':
arr[y+1][x] += 1 # bottom center
def createMap(mines, x, y):
global arr
createBoard(y, x)
xlist = list(range(0, x))
ylist = list(range(0, y))
choiceslist = []
for xchoice in xlist:
for ychoice in ylist:
choiceslist.append((xchoice, ychoice))
if mines >= len(choiceslist):
print('bro thats too many mines')
sys.exit()
for _ in range(mines):
choice = random.choice(choiceslist)
choiceslist.remove(choice)
placeMine(choice[1], choice[0])
def subtractFlags():
current_amount = flags.get()
current_amount = current_amount.replace(('\u2691' + ' '), '')
flags.set(f'\u2691 {int(current_amount) - 1}')
def addFlags():
current_amount = flags.get()
current_amount = current_amount.replace(('\u2691' + ' '), '')
flags.set(f'\u2691 {int(current_amount) + 1}')
def lclick(event):
global cellsopened
colorcodes = {1 : 'blue',
2 : 'green',
3 : 'red',
4 : 'purple',
5 : 'maroon',
6 : 'teal',
7 : 'black',
8 : 'white',
0 : 'gray'}
widget = event.widget
info = widget.grid_info()
row, col = info['row'], info['column']
current = widget.cget('text')
arritem = arr[row][col]
if current != ' ':
return
if arritem == 'X':
gameOver(False)
else:
widget.configure(text = str(arritem), fg = colorcodes[arritem])
cellsopened += 1
window.after(69, checkWon)
def rclick(event):
global widget
widget = event.widget
info = widget.grid_info()
row, col = info['row'], info['column']
current = widget.cget('text')
if current == ' ':
widget.configure(text = '\u2691', fg = 'red')
subtractFlags()
elif current == '\u2691':
widget.configure(text = ' ')
addFlags()
def addGameButtons():
global zeros
zeros = 0
cols = dimensions[1]
rows = dimensions[0]
mines = 99
for row in range(rows):
for col in range(cols):
button = tk.Label(game, width = 1, text = ' ', bg = 'light gray')
if arr[row][col] == 0:
zeros += 1
button.config(text = '0', fg = 'gray')
button.grid(column = col, row = row, padx = 2, pady = 2)
button.bind("<Button-1>", lclick)
button.bind('<Button-2>', rclick)
def timerUpdate():
current = timer.get()
current = current.replace('⏱ ', '')
timer.set(f'⏱ {round(eval(current) + 1)}')
if won == False:
window.after(1000, timerUpdate)
def gameOver(won):
if won == False:
ans = messagebox.askyesno(message = 'You lost!\n\nDo you wish to play again?')
else:
current = timer.get()
time = current.replace('⏱ ', '')
ans = messagebox.askyesno(message = f'You won in {time} seconds!\n\nDo you wish to play again?')
if ans == True:
restart()
else:
window.destroy()
sys.exit()
def checkWon():
global won
if cellsopened == (dimensions[0] * dimensions[1]) - mines - zeros:
won = True
gameOver(True)
def chooseDifficulty():
dialog_text = 'set theDialogText to "Please select a difficulty:"\ndisplay dialog theDialogText buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (30x16, 99 mines)"} default button "Expert (30x16, 99 mines)"'
out = applescript.run(dialog_text)
returned = (out.out).replace('button returned:', '')
if returned == 'Expert (30x16, 99 mines)':
dimensions = (30, 16)
mines = 99
elif returned == 'Medium (13x15, 40 mines)':
dimensions = (13, 15)
mines = 40
elif returned == 'Easy (9x9, 10 mines)':
dimensions = (9, 9)
mines = 10
outres = {'dimensions' : dimensions, 'mines' : mines}
return outres
def main():
global window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won
won = False
cellsopened = 0
#========== CON0FIG SECTION ==========
mines = 99
dimensions = (16, 30)
#========== CONFIG SECTION ==========
#==============================
difchoice = chooseDifficulty()
mines = difchoice['mines']
dimensions = difchoice['dimensions']
#==============================
createMap(mines = mines, x = dimensions[0], y = dimensions[1])
window = tk.Tk()
#center(window)
#window.eval('tk::PlaceWindow . middle')
window.config(bg = 'gray')
window.title('Minesweeper')
window.resizable(False, False)
infobar = tk.Frame(bg = 'red')
infobar.pack(side = 'top')
game = tk.Frame()
game.pack(side = 'bottom')
flags = tk.StringVar()
flags.set('\u2691' + f' {mines}')
timer = tk.StringVar()
timer.set('⏱' + ' 0')
timecounter = tk.Label(infobar, bg = 'gray', textvariable = timer, width = 5)
timecounter.propagate(False)
timecounter.grid(row = 0, column = 0, sticky = 'w')
flagcounter = tk.Label(infobar, bg = 'gray', textvariable = flags)
flagcounter.grid(row = 0, column = 1, sticky = 'e')
addGameButtons()
window.after(10, timerUpdate)
window.mainloop()
def restart():
global window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won
window.destroy()
del window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won
main()
if __name__ == '__main__':
main()
3 Answers 3
A rose by any other name...
row
, column
, length
, width
, xdim
, ydim
, row
, col
, x
, y
...
My, oh my! How many different names are used for variables for each of two dimensions...
Because people are accustomed to co-ordinates being (x, y)
, it's second nature to think in terms of "horizontal, vertical". UNLESS, of course, you're dealing with geographic co-ordinates, in which case the convention is "Latitude, then Longitude"...
Unfortunately, it appears that the library authors approach to the ordering of parameters is 'swapped' (perhaps based on conventions of efficient array notation: "row major; column minor")
Consider:
def createBoard(length, width):
...
def placeMine(x, y):
...
There we go... App code that uses, not only different names, but two different orderings of parameters to two functions that are only a few lines apart in the same source code.
And, again:
def placeMine(x, y):
...
if (x != 0) and (y != 0): # col and row in bounds?
...
if (y != 0) and (x != (xdim - 1)): # row and col in bounds?
...
# ordering that will be used next is up for grabs!
Recommend that you follow the unchangeable library ordering for parameters. Accept being consistent with what's available that is also out of your control.
Recommend sticking to ONLY height
and width
, and, perhaps simply h
and w
for very locally scoped loop counters. Do NOT start using i
and j
... Yes, be creative, but stick to h-ish for height and w-ish for width.
Common beginner problem is to create and use new variables to get around a problem. This ultimately leads to costly and often error-prone maintenance issues. Spend the time doing it right the first time so you don't find yourself spending 10x the effort cleaning up messes created in haste to achieve some dopamine rush of "getting across the finish line".
More words...
...
if arr[y][x+1] != 'X':
arr[y][x+1] += 1 # center right
...
It is my belief that there are a few coders who can, at a glance, 'grok' larger 'chunks' of code than most of us plebeians. I believe most of us trundle slowly forward reading code, mentally shifting modes as we parse-and-comprehend bite-sized nibbles one-by-one.
As no more than a suggestion, please consider revising the relevant statements - making up this sequence of operations - in this fashion:
...
if arr[y+0][x+1] != 'X':
arr[y+0][x+1] += 1 # center right
...
The unnecessary +0
will be like tissue paper to the interpreter with (I presume) no detectable difference in speed of execution. However, because every index value "incorporates an operator", the human reader's mental hardware does not need to shift 'mode' from line-to-line, chunk-to-chunk. I cannot speak to Python, but in any other modern compiled language, there would be no trace of that +0
in the emitted code of the compiler.
Uniformity can aid in preventing bugs, or detecting and squashing them quickly.
a way to have an opening start, ... breaks open a bunch of squares?
The program 'cheats'. The first tile clicked causes a few mines to be relocated so that the user gets an initial 'window' that they can expand. Ever notice that no Minesweeper game ever terminates immediately? I don't pretend to know how much slight of hand is executed before the first 'reveal' is revealed. (Perhaps ALL the mines are distributed in response to that first click, carefully excluding the tile where that click occurred.)
Update: Just occurred to me that the OP means "left-click on a '0' tile reveals as many neighbouring tiles as possible in a cascade." Obviously, lclick()
needs elaboration. Perhaps push "automatic left-clicks" of tiles onto a stack in some sort of recursive or iterative reveal of neighbours until no more can be done.
The 'feature' you're likely asking about is called Flood Fill.
"Minesweeper" appears, literally, in the first paragraph of that Wikipedia page.
-
1\$\begingroup\$ Row,column ordering is quite common in character-cell screen addressing, whereas x,y is more common in pixel addressing (OpenCV being an exception, IIRC). I agree that good naming can help us remember which convention we're using when we're in the unenviable position of having to mix the two. \$\endgroup\$Toby Speight– Toby Speight2025年06月06日 06:35:11 +00:00Commented Jun 6 at 6:35
-
1\$\begingroup\$ I don't know the Google Minesweeper mentioned in the question, but Ace of Penguins implementation does frequently terminate on the first exposure. That said, a simple strategy to avoid that could be to defer placing the mines until after the interaction has begun. \$\endgroup\$Toby Speight– Toby Speight2025年06月06日 06:38:15 +00:00Commented Jun 6 at 6:38
-
1\$\begingroup\$ @TobySpeight "row <-> col" ... increasing display and BMP row indices in a downward direction (usually), increasing bit id's going to the left, not right... "(x, y)" vs "arr[row][col]" ... It's all a bit of a jumble that's only more difficult to keep straight with each dying neuron... (sigh...) \$\endgroup\$Fe2O3– Fe2O32025年06月06日 07:17:21 +00:00Commented Jun 6 at 7:17
-
1\$\begingroup\$ Even in very strongly-typed languages, I've yet to find any library that uses different types for the different dimensions, to help avoid mixups. That might be an interesting project to undertake. \$\endgroup\$Toby Speight– Toby Speight2025年06月06日 07:21:21 +00:00Commented Jun 6 at 7:21
-
\$\begingroup\$ Re: use different types... "When one door closes another opens" Something tells me this would only shift the locations of those coding hazards to darker, more 'invisible' corners of coding... A lot like playing Minesweeper!!
:-)
Cheers! \$\endgroup\$Fe2O3– Fe2O32025年06月06日 07:27:56 +00:00Commented Jun 6 at 7:27
Simpler
Code like this:
if ans == True:
is simpler as:
if ans:
Similarly:
if won == False:
is simpler as:
if not won:
These 2 lines:
outres = {'dimensions' : dimensions, 'mines' : mines}
return outres
can be combined as one:
return {'dimensions' : dimensions, 'mines' : mines}
This eliminates a vaguely named variable (outres
).
Comments
Delete all comment-out code to reduce clutter:
#center(window)
#window.eval('tk::PlaceWindow . middle')
Comments like these:
#========== CON0FIG SECTION ==========
mines = 99
dimensions = (16, 30)
#========== CONFIG SECTION ==========
should be indented with the code:
#========== CON0FIG SECTION ==========
mines = 99
dimensions = (16, 30)
#========== CONFIG SECTION ==========
Documentation
The PEP 8 style guide recommends adding docstrings for functions. You should also add a docstring at the top of the code to summarize its purpose:
"""
Minesweeper game
"""
Naming
PEP 8 recommends snake_case for function and variable names.
For example, createBoard
would be create_board
.
This constant value is used many times in the code: '\u2691'
.
You should set it to a named constant that describes what it means.
The variable named arr
is not very descriptive. Choose a name that
conveys more meaning.
Portability
I'm not a big fan of fancy Unicode characters in source code, like the symbol that looks like either a clock or a timer. Sometimes they don't render well in editors, and other times they don't render well in output generated by the code.
Control flow tools
As of Python 3.10, released in October of 2021, we have the match statement. Using this we might rewrite the following:
def chooseDifficulty(): dialog_text = 'set theDialogText to "Please select a difficulty:"\ndisplay dialog theDialogText buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (30x16, 99 mines)"} default button "Expert (30x16, 99 mines)"' out = applescript.run(dialog_text) returned = (out.out).replace('button returned:', '') if returned == 'Expert (30x16, 99 mines)': dimensions = (30, 16) mines = 99 elif returned == 'Medium (13x15, 40 mines)': dimensions = (13, 15) mines = 40 elif returned == 'Easy (9x9, 10 mines)': dimensions = (9, 9) mines = 10 outres = {'dimensions' : dimensions, 'mines' : mines} return outres
def chooseDifficulty():
dialog_text = 'set theDialogText to "Please select a difficulty:"\ndisplay dialog theDialogText buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (30x16, 99 mines)"} default button "Expert (30x16, 99 mines)"'
out = applescript.run(dialog_text)
returned = (out.out).replace('button returned:', '')
match returned:
case 'Expert (30x16, 99 mines)':
dimensions = (30, 16)
mines = 99
case 'Medium (13x15, 40 mines)':
dimensions = (13, 15)
mines = 40
case 'Easy (9x9, 10 mines)':
dimensions = (9, 9)
mines = 10
return {'dimensions' : dimensions, 'mines' : mines}
Globals
Your code makes extensive use of global variables. This means keeping track of what's going on in your program carries a huge mental load. I would strongly suggest you break this habit. You can either pass state via arguments and return values from functions, or use classes to encapsulate state.
Explore related questions
See similar questions with these tags.