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()
\$\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)
\$\endgroup\$

You must log in to answer this question.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.