So, 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, thanks in advance.
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]: https://pythonanywhere.com