1
\$\begingroup\$

So, 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, 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
\$\endgroup\$

0

Your Answer

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

Browse other questions tagged or ask your own question.