3
\$\begingroup\$

I have completed a Minesweeper program in Python using Tkinter.
A few questions:

  1. I feel I should convert this to Object-Oriented Programming. Any starting tips?
  2. Expanding on the above, I have way too many global variables. What methods to reduce these?
  3. 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()
toolic
14.4k5 gold badges29 silver badges201 bronze badges
asked Jun 28, 2022 at 15:06
\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

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)
answered Jan 7 at 11:40
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.