I have completed a Minesweeper program in Python using Tkinter.
A few questions:
- I feel I should convert this to Object-Oriented Programming. Any starting tips?
- Expanding on the above, I have way too many global variables. What methods to reduce these?
- Any basic styling and convention issues?
All feedback is accepted.
WARNING: this code may contain references to Dababy or Fortnite. (and UwUs)
Side note: highscores are saved on my website hosted with pythonanywhere
Now, for the code:
import numpy as np
import random
import sys
import requests
import tkinter as tk
import tkinter.messagebox as messagebox
from tkinter import Widget
from tkinter import simpledialog
import applescript
def printArr():
for row in arr:
print(" ".join(str(cell) for cell in row))
print("")
def createBoard(length, width):
global arr
arr = [[0 for _ in range(length)] for _ in range(width)]
def placeMine(x, y):
ydim, xdim = len(arr), len(arr[0])
arr[y][x] = 'π£'
if x != (xdim - 1):
if arr[y][x + 1] != 'π£':
arr[y][x + 1] += 1 # center right
if x != 0:
if arr[y][x - 1] != 'π£':
arr[y][x - 1] += 1 # center left
if (x != 0) and (y != 0):
if arr[y - 1][x - 1] != 'π£':
arr[y - 1][x - 1] += 1 # top left
if (y != 0) and (x != (xdim - 1)):
if arr[y - 1][x + 1] != 'π£':
arr[y - 1][x + 1] += 1 # top right
if y != 0:
if arr[y - 1][x] != 'π£':
arr[y - 1][x] += 1 # top center
if (x != (xdim - 1)) and (y != (ydim - 1)):
if arr[y + 1][x + 1] != 'π£':
arr[y + 1][x + 1] += 1 # bottom right
if (x != 0) and (y != (ydim - 1)):
if arr[y + 1][x - 1] != 'π£':
arr[y + 1][x - 1] += 1 # bottom left
if y != (ydim - 1):
if arr[y + 1][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 openZeroNeighbors(first_widget, keep_going=True):
openNeighbors(first_widget)
global cellsopened
info = first_widget.grid_info()
row, col = info['row'], info['column']
colorcodes = {1: 'blue',
2: 'green',
3: 'red',
4: 'purple',
5: 'maroon',
6: 'teal',
7: 'black',
8: 'white',
0: 'gray'}
if keep_going is False:
print('j')
widget: Widget
for widget in game.winfo_children():
info = widget.grid_info()
row1, col1 = info['row'], info['column']
arritem = arr[row1][col1]
neighbor = False
if (row1 == row) and (abs(col1 - col) == 1):
neighbor = True
if (col1 == col) and (abs(row1 - row) == 1):
neighbor = True
if (abs(col1 - col) == 1) and (abs(row1 - row) == 1):
neighbor = True
if arritem != 0: # and neighbor == False:
continue
elif arritem == 0 and neighbor is True:
openNeighbors(first_widget)
if neighbor is True and widget not in stack:
if widget.cget('text') != str(arritem):
cellsopened += 1
widget.config(text=str(arritem), fg=colorcodes[arritem])
stack.add(widget)
openZeroNeighbors(widget, keep_going=True)
def openNeighbors(widget):
global cellsopened
colorcodes = {1: 'blue',
2: 'green',
3: 'red',
4: 'purple',
5: 'maroon',
6: 'teal',
7: 'black',
8: 'white',
0: 'gray'}
info = widget.grid_info()
row1, col1 = info['row'], info['column']
arritem = arr[row1][col1]
if widget.cget('text') != str(arritem):
widget.config(text=str(arritem), fg=colorcodes[arritem])
cellsopened += 1
for widget in getNeighborsA(widget):
info = widget.grid_info()
row1, col1 = info['row'], info['column']
arritem = arr[row1][col1]
if widget.cget('text') != str(arritem):
widget.config(text=str(arritem), fg=colorcodes[arritem])
cellsopened += 1
def getNeighbors(widget):
global stack
info = widget.grid_info()
row, col = info['row'], info['column']
neighbors = set([])
for widget in game.winfo_children():
info = widget.grid_info()
row1, col1 = info['row'], info['column']
neighbor = False
if (row1 == row) and (abs(col1 - col) == 1):
neighbor = True
if (col1 == col) and (abs(row1 - row) == 1):
neighbor = True
if (abs(col1 - col) == 1) and (abs(row1 - row) == 1):
neighbor = True
if neighbor:
neighbors.add(widget)
return neighbors
def getNeighborsA(widget):
ydim, xdim = len(arr), len(arr[0])
info = widget.grid_info()
row, col = info['row'], info['column']
neighbors = []
x = row
y = col
try:
if x != (xdim - 1):
if widgetlist[y][x + 1] != 'π£':
neighbors.append(widgetlist[y][x + 1]) # center right
if x != 0:
if widgetlist[y][x - 1] != 'π£':
neighbors.append(widgetlist[y][x - 1]) # center left
if (x != 0) and (y != 0):
if widgetlist[y - 1][x - 1] != 'π£':
neighbors.append(widgetlist[y - 1][x - 1]) # top left
if (y != 0) and (x != (xdim - 1)):
if widgetlist[y - 1][x + 1] != 'π£':
neighbors.append(widgetlist[y - 1][x + 1]) # top right
if y != 0:
if widgetlist[y - 1][x] != 'π£':
neighbors.append(widgetlist[y - 1][x]) # top center
if (x != (xdim - 1)) and (y != (ydim - 1)):
if widgetlist[y + 1][x + 1] != 'π£':
neighbors.append(widgetlist[y + 1][x + 1]) # bottom right
if (x != 0) and (y != (ydim - 1)):
if widgetlist[y + 1][x - 1] != 'π£':
neighbors.append(widgetlist[y + 1][x - 1]) # bottom left
if y != (ydim - 1):
if widgetlist[y + 1][x] != 'π£':
neighbors.append(widgetlist[y + 1][x]) # bottom center
except IndexError:
pass
return neighbors
def lclick(event):
global cellsopened, first_click
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 not first_click:
global stack
stack.add(widget)
first_click = True
while arritem != 0:
createMap(mines=mines, x=dimensions[0], y=dimensions[1])
arritem = arr[row][col]
openZeroNeighbors(widget)
if current != ' ':
return
if arritem == 'π£':
showMines()
window.after(100, gameOver, False)
elif arritem == 0:
openZeroNeighbors(widget)
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')
current_amount = flags.get()
current_amount = int(current_amount.replace(('\u2691' + ' '), ''))
if current == ' ' and current_amount > 0:
widget.configure(text='\u2691', fg='red')
subtractFlags()
elif current == '\u2691':
widget.configure(text=' ')
addFlags()
def showAll():
colorcodes = {1: 'blue',
2: 'green',
3: 'red',
4: 'purple',
5: 'maroon',
6: 'teal',
7: 'black',
8: 'white',
0: 'gray'}
for button in game.winfo_children():
button: Widget
info = button.grid_info()
row, col = info['row'], info['column']
current = button.cget('text')
arritem = arr[row][col]
if current == '\u2691':
if arritem == 'π£':
button.configure(text=str(arritem), bg='dark green')
else:
button.configure(text=str(arritem),
fg=colorcodes[arritem], bg='dark red')
if arritem != 'π£':
button.configure(text=str(arritem), fg=colorcodes[arritem])
else:
button.configure(text=str(arritem))
def showMines():
colorcodes = {1: 'blue',
2: 'green',
3: 'red',
4: 'purple',
5: 'maroon',
6: 'teal',
7: 'black',
8: 'white',
0: 'gray'}
for button in game.winfo_children():
button: Widget
info = button.grid_info()
row, col = info['row'], info['column']
current = button.cget('text')
arritem = arr[row][col]
if current == '\u2691':
if arritem == 'π£':
button.configure(text=str(arritem), bg='dark green')
else:
button.configure(text=' ',
fg=colorcodes[arritem], bg='dark red')
if current != ' ':
if arritem != 'π£':
button.configure(text=str(arritem), fg=colorcodes[arritem])
if arritem == 'π£':
button.configure(text='π£')
def changeGameButtons(gamearr):
colorcodes = {1: 'blue',
2: 'green',
3: 'red',
4: 'purple',
5: 'maroon',
6: 'teal',
7: 'black',
8: 'white',
0: 'gray',
' ': 'black',
'\u2691': 'red'}
for button in game.winfo_children():
button: Widget
info = button.grid_info()
row, col = info['row'], info['column']
arritem = gamearr[row][col]
current = button.cget('text')
if current == '\u2691':
if arritem == 'π£':
button.configure(text=str(arritem), bg = 'light gray')
else:
button.configure(text=str(arritem),
fg=colorcodes[int(arritem)], bg = 'light gray')
if arritem != 'π£':
if arritem == ' ' or arritem == '\u2691':
button.configure(text=str(arritem), fg=colorcodes[(arritem)], bg = 'light gray')
else:
button.configure(text=str(arritem), fg=colorcodes[int(arritem)], bg = 'light gray')
else:
button.configure(text=str(arritem))
def toggleCheat():
global cheat
if cheat_activated:
if not cheat:
global gamearr
gamearr = np.reshape(np.array([widget.cget('text') for widget in game.winfo_children()]), (dimensions[0], dimensions[1]))
showAll()
cheat = True
else:
changeGameButtons(gamearr)
cheat = False
def turnOnCheat():
global cheat_activated
cheat_activated = True
def f5Clicked():
global f5_clicked
if cheat_on_time:
f5_clicked = True
if cheat_on_time and f6_clicked:
turnOnCheat()
def f6Clicked():
global f6_clicked
if cheat_on_time and f5_clicked:
f6_clicked = True
if cheat_on_time and f5_clicked:
turnOnCheat()
def addGameButtons():
global zeros, widgetlist
zeros = 0
cols = dimensions[1]
rows = dimensions[0]
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')
widgetlist[col][row] = button
button.grid(column=col, row=row, padx=2, pady=2)
button.bind("<Button-1>", lclick)
button.bind('<Button-2>', rclick)
def resetCheatOn():
global cheat_on_time
cheat_on_time = False
def timerUpdate():
if first_click:
current = timer.get()
current = current.replace('β± ', '')
timer.set(f'β± {round(eval(current) + 1)}')
current = timer.get()
current = current.replace('β± ', '')
if current == '3':
global cheat_on_time
cheat_on_time = True
window.after(1500, resetCheatOn)
if not won:
window.after(1000, timerUpdate)
def wonIn(time):
if dimensions == (16, 30):
diff = 'hard'
elif dimensions == (13, 15):
diff = 'med gium'
elif dimensions == (9, 9):
diff = 'easy'
url = 'https://colding10.pythonanywhere.com/minesweeper'
current = int(requests.post(url, data = {'command':'gethighscore', 'diff':diff}).text)
if time < current:
message = f'You beat the high score by {current-time} seconds!'
else:
message = f'You\'re {time-current} seconds behind the highscore of {current}!'
return message
def gameOver(won):
if not won:
ans = messagebox.askyesno(
message='You lost!\n\nDo you wish to play again?')
else:
current = timer.get()
time = current.replace('β± ', '')
msg = wonIn(int(time))
ans = messagebox.askyesno(
message=f'You won in {time} seconds!\n{msg}\n\nDo you wish to play again?')
if ans:
restart()
else:
window.destroy()
sys.exit()
def checkWon():
global won
if cellsopened == (dimensions[0] * dimensions[1]) - mines - zeros:
won = True
gameOver(True)
def getCustomSize():
dialog_text = 'set dimensions to display dialog "Enter dimensions (XxY)" default answer "2x2" with icon note ' \
'buttons {"Cancel", "Continue"} default button "Continue"\nset mines to display dialog "Enter ' \
'mines" default answer 0 with icon note buttons {"Cancel", "Continue"} default button ' \
'"Continue"\n\nset mine to text returned of mines\nset dimes to text returned of ' \
'dimensions\n\nreturn {mine, dimes} '
out = applescript.run(dialog_text)
if out.err == '63:219: execution error: User canceled. (-128)':
sys.exit()
returned = out.out.split(', ')
mines, dimes = returned[0], returned[1]
try:
mines = int(mines)
if mines <= 0:
raise ValueError
except ValueError:
messagebox.showinfo(message='Mines must be a positive integer!')
sys.exit()
dimes = dimes.split('x')
err = 'no err les go DABABY uwu fortnite'
if 'x'.join(dimes) == dimes:
err = 'wrong dimensions format'
else:
try:
dimensions = (int(dimes[0]), int(dimes[1]))
except ValueError:
err = 'not integers'
if err == 'no err les go DABABY uwu fortnite':
return dimensions, mines
else:
if err == 'wrong dimensions format':
messagebox.showinfo(
message='Wrong dimensions format! Please try again.')
sys.exit()
else:
messagebox.showinfo(
message='Dimensions not integers! Please try again.')
sys.exit()
def getResp(buttons=[], text='', title='', default=0, cancel=0):
d = simpledialog.SimpleDialog(window,
text=text,
buttons=buttons,
default=default,
cancel=cancel,
title=title)
return d.go()
def chooseDifficulty():
dialog_text = 'set theDialogText to "Please select a difficulty:"\nset resp to display dialog theDialogText ' \
'buttons {"Cancel", "Custom", "Preset (Easy, Medium, Hard)"} cancel button "Cancel" default button ' \
'"Preset (Easy, Medium, Hard)"\nif button returned of resp = "Preset (Easy, Medium, ' \
'Hard)" then\n\treturn "preset"\nend if\nif button returned of resp = "Custom" then\n\treturn ' \
'"custom"\nend if '
out = applescript.run(dialog_text)
if out.err == '63:219: execution error: User canceled. (-128)':
sys.exit()
returned = out.out
if returned == 'custom':
dimes, mines = getCustomSize()
if returned == 'preset':
dialog_text = 'set theDialogText to "Please select a difficulty:"\nset resp to display dialog theDialogText ' \
'buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (16x30, 99 mines)"} ' \
'default button "Expert (16x30, 99 mines)"\nreturn button returned of resp '
out = applescript.run(dialog_text)
returned = out.out
if returned == 'Expert (16x30, 99 mines)':
dimes = (16, 30)
mines = 99
elif returned == 'Medium (13x15, 40 mines)':
dimes = (13, 15)
mines = 40
elif returned == 'Easy (9x9, 10 mines)':
dimes = (9, 9)
mines = 10
outres = {'dimensions': dimes, 'mines': mines}
return outres
def main():
global window, widgetlist, stack, game, flags, first_click, mines, flagcounter, infobar, timer, cellsopened, mines, dimensions, won, cheat, cheat_on_time, cheat_activated, f5_clicked, f6_clicked
cheat_on_time = False
cheat_activated = False
cheat = False
f5_clicked = False
f6_clicked = False
stack = set([])
won = False
first_click = False
cellsopened = 0
window = tk.Tk()
window.geometry('+1090+400')
window.withdraw()
window.bind('<F5>', lambda x: f5Clicked())
window.bind('<F6>', lambda x: f6Clicked())
# ========== CON0FIG SECTION ==========
# mines = 99
# dimensions = (16, 30)
# ========== CONFIG SECTION ==========
# ==============================
difchoice = chooseDifficulty()
mines = difchoice['mines']
dimensions = difchoice['dimensions']
# ==============================
widgetlist = [[None for _ in range(dimensions[0])]
for _ in range(dimensions[1])]
createMap(mines=mines, x=dimensions[0], y=dimensions[1])
# center(window)
# window.eval('tk::PlaceWindow . middle')
#iconimage = tk.PhotoImage(file = 'icon_png.png')
#window.iconphoto(True, iconimage)
window.deiconify()
window.config(bg='gray')
window.title('Minesweeper')
window.resizable(False, False)
window.geometry('+1090+400')
window.bind('<F19>', lambda x: toggleCheat())
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, cheat, cheat_on_time, cheat_activated, f5_clicked, f6_clicked
window.destroy()
del window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won, cheat, cheat_on_time, cheat_activated, f5_clicked, f6_clicked
main()
if __name__ == '__main__':
global window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won, cheat, cheat_on_time, cheat_activated, f5_clicked, f6_clicked
main()
1 Answer 1
DRY
The mine icon is used in many places throughout the code; set it to a
constant such as MINE_ICON
.
The following dictionary is used multiple times throughout the code:
colorcodes = {1: 'blue',
2: 'green',
3: 'red',
4: 'purple',
5: 'maroon',
6: 'teal',
7: 'black',
8: 'white',
0: 'gray'}
It should be factored out as common code.
Globals
I have way too many global variables. What methods to reduce these?
The key to minimizing global variables used by functions is to pass values into functions and return values from functions. You are already doing that to some degree; you just need to do more of it.
The side benefit will be reducing long lines like this, which are hard to read and maintain:
global window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won, cheat, cheat_on_time, cheat_activated, f5_clicked, f6_clicked
Unused code
This function is not called:
def printArr():
You can delete it to reduce clutter.
Documentation
The PEP 8 style guide recommends adding doctrings to describe your functions. It would be nice to add a doctrings to summarize the purpose of the code as well.
Naming
PEP-8 also recommends using snake_case for function and variable names. For example:
createBoard
becomes create_board
openZeroNeighbors
becomes open_zero_neighbors
arr
is not a very descriptive variable name. If it represents
the game board, perhaps board
or game_board
would be better.
Comments
Remove all commented-out code to reduce clutter:
# center(window)
# window.eval('tk::PlaceWindow . middle')
#iconimage = tk.PhotoImage(file = 'icon_png.png')
#window.iconphoto(True, iconimage)