4
\$\begingroup\$

I have completed my minesweeper game using Tkinter and would like to know ways to improve. I also have a question for extra features: does anybody know a way to have an opening start, like Google minesweeper does where it breaks open a bunch of squares?

I'm accepting all comments.

Please criticize: readability comments and performance comments accepted.

import tkinter as tk
import random
import sys
import applescript
import tkinter.messagebox as messagebox

def printArr():
    for row in arr:
        print(" ".join(str(cell) for cell in row))
        print("")

def createBoard(length, width):
    global arr

    arr = [[0 for row in range(length)] for column in range(width)]

def placeMine(x, y):
    ydim, xdim = len(arr), len(arr[0])

    arr[y][x] = 'X'

    if (x != (xdim - 1)):
        if arr[y][x+1] != 'X':
            arr[y][x+1] += 1 # center right

    if (x != 0):
        if arr[y][x-1] != 'X':
            arr[y][x-1] += 1 # center left

    if (x != 0) and (y != 0):
        if arr[y-1][x-1] != 'X':
            arr[y-1][x-1] += 1 # top left

    if (y != 0) and (x != (xdim - 1)):
        if arr[y-1][x+1] != 'X':
            arr[y-1][x+1] += 1 # top right

    if (y != 0):
        if arr[y-1][x] != 'X':
            arr[y-1][x] += 1 # top center

    if (x != (xdim - 1)) and (y != (ydim - 1)):
        if arr[y+1][x+1] != 'X':
            arr[y+1][x+1] += 1 # bottom right

    if (x != 0) and (y != (ydim - 1)):
        if arr[y+1][x-1] != 'X':
            arr[y+1][x-1] += 1 # bottom left

    if (y != (ydim - 1)):
        if arr[y+1][x] != '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 lclick(event):
    global cellsopened

    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 current != ' ':
        return

    if arritem == 'X':
        gameOver(False)
    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')

    if current == ' ':
        widget.configure(text = '\u2691', fg = 'red')
        subtractFlags()
    elif current == '\u2691':
        widget.configure(text = ' ')
        addFlags()

def addGameButtons():
    global zeros

    zeros = 0

    cols = dimensions[1]
    rows = dimensions[0]
    mines = 99

    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')

            button.grid(column = col, row = row, padx = 2, pady = 2)
            
            button.bind("<Button-1>", lclick)
            button.bind('<Button-2>', rclick)

def timerUpdate():
    current = timer.get()
    current = current.replace('⏱ ', '')
    
    timer.set(f'⏱ {round(eval(current) + 1)}')

    if won == False:
        window.after(1000, timerUpdate)

def gameOver(won):
    if won == False:
        ans = messagebox.askyesno(message = 'You lost!\n\nDo you wish to play again?')
    else:
        current = timer.get()
        time = current.replace('⏱ ', '')
        ans = messagebox.askyesno(message = f'You won in {time} seconds!\n\nDo you wish to play again?')

    if ans == True:
        restart()

    else:
        window.destroy()
        sys.exit()
        
def checkWon():
    global won
    
    if cellsopened == (dimensions[0] * dimensions[1]) - mines - zeros:
        won = True
        gameOver(True)

def chooseDifficulty():
    dialog_text = 'set theDialogText to "Please select a difficulty:"\ndisplay dialog theDialogText buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (30x16, 99 mines)"} default button "Expert (30x16, 99 mines)"'

    out = applescript.run(dialog_text)

    returned = (out.out).replace('button returned:', '')

    if returned == 'Expert (30x16, 99 mines)':
        dimensions = (30, 16)
        mines = 99

    elif returned == 'Medium (13x15, 40 mines)':
        dimensions = (13, 15)
        mines = 40

    elif returned == 'Easy (9x9, 10 mines)':
        dimensions = (9, 9)
        mines = 10

    outres = {'dimensions' : dimensions, 'mines' : mines}

    return outres
    
def main():
    global window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won

    won = False

    cellsopened = 0

#========== CON0FIG SECTION ==========
    mines = 99
    dimensions = (16, 30)
#========== CONFIG SECTION ==========

#==============================
    difchoice = chooseDifficulty()

    mines = difchoice['mines']
    dimensions = difchoice['dimensions']
#==============================
    
    createMap(mines = mines, x = dimensions[0], y = dimensions[1])
    
    window = tk.Tk()
    #center(window)
    #window.eval('tk::PlaceWindow . middle')
    
    window.config(bg = 'gray')
    window.title('Minesweeper')
    window.resizable(False, False)

    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
    
    window.destroy()
    
    del window, game, flags, flagcounter, infobar, timer, cellsopened, mines, dimensions, won

    main()
    
if __name__ == '__main__':
    main()
\$\endgroup\$

3 Answers 3

5
\$\begingroup\$

A rose by any other name...

row, column, length, width, xdim, ydim, row, col, x, y...
My, oh my! How many different names are used for variables for each of two dimensions...

Because people are accustomed to co-ordinates being (x, y), it's second nature to think in terms of "horizontal, vertical". UNLESS, of course, you're dealing with geographic co-ordinates, in which case the convention is "Latitude, then Longitude"...

Unfortunately, it appears that the library authors approach to the ordering of parameters is 'swapped' (perhaps based on conventions of efficient array notation: "row major; column minor")

Consider:

def createBoard(length, width):
    ...
def placeMine(x, y):
    ...

There we go... App code that uses, not only different names, but two different orderings of parameters to two functions that are only a few lines apart in the same source code.

And, again:

def placeMine(x, y):
    ...
    if (x != 0) and (y != 0): # col and row in bounds?
        ...
    if (y != 0) and (x != (xdim - 1)): # row and col in bounds?
        ...
    # ordering that will be used next is up for grabs!

Recommend that you follow the unchangeable library ordering for parameters. Accept being consistent with what's available that is also out of your control.

Recommend sticking to ONLY height and width, and, perhaps simply h and w for very locally scoped loop counters. Do NOT start using i and j... Yes, be creative, but stick to h-ish for height and w-ish for width.

Common beginner problem is to create and use new variables to get around a problem. This ultimately leads to costly and often error-prone maintenance issues. Spend the time doing it right the first time so you don't find yourself spending 10x the effort cleaning up messes created in haste to achieve some dopamine rush of "getting across the finish line".


More words...

        ...
        if arr[y][x+1] != 'X':
            arr[y][x+1] += 1 # center right
        ...

It is my belief that there are a few coders who can, at a glance, 'grok' larger 'chunks' of code than most of us plebeians. I believe most of us trundle slowly forward reading code, mentally shifting modes as we parse-and-comprehend bite-sized nibbles one-by-one.

As no more than a suggestion, please consider revising the relevant statements - making up this sequence of operations - in this fashion:

        ...
        if arr[y+0][x+1] != 'X':
            arr[y+0][x+1] += 1 # center right
        ...

The unnecessary +0 will be like tissue paper to the interpreter with (I presume) no detectable difference in speed of execution. However, because every index value "incorporates an operator", the human reader's mental hardware does not need to shift 'mode' from line-to-line, chunk-to-chunk. I cannot speak to Python, but in any other modern compiled language, there would be no trace of that +0 in the emitted code of the compiler.

Uniformity can aid in preventing bugs, or detecting and squashing them quickly.


a way to have an opening start, ... breaks open a bunch of squares?

The program 'cheats'. The first tile clicked causes a few mines to be relocated so that the user gets an initial 'window' that they can expand. Ever notice that no Minesweeper game ever terminates immediately? I don't pretend to know how much slight of hand is executed before the first 'reveal' is revealed. (Perhaps ALL the mines are distributed in response to that first click, carefully excluding the tile where that click occurred.)

Update: Just occurred to me that the OP means "left-click on a '0' tile reveals as many neighbouring tiles as possible in a cascade." Obviously, lclick() needs elaboration. Perhaps push "automatic left-clicks" of tiles onto a stack in some sort of recursive or iterative reveal of neighbours until no more can be done.

The 'feature' you're likely asking about is called Flood Fill.
"Minesweeper" appears, literally, in the first paragraph of that Wikipedia page.

\$\endgroup\$
5
  • 1
    \$\begingroup\$ Row,column ordering is quite common in character-cell screen addressing, whereas x,y is more common in pixel addressing (OpenCV being an exception, IIRC). I agree that good naming can help us remember which convention we're using when we're in the unenviable position of having to mix the two. \$\endgroup\$ Commented Jun 6 at 6:35
  • 1
    \$\begingroup\$ I don't know the Google Minesweeper mentioned in the question, but Ace of Penguins implementation does frequently terminate on the first exposure. That said, a simple strategy to avoid that could be to defer placing the mines until after the interaction has begun. \$\endgroup\$ Commented Jun 6 at 6:38
  • 1
    \$\begingroup\$ @TobySpeight "row <-> col" ... increasing display and BMP row indices in a downward direction (usually), increasing bit id's going to the left, not right... "(x, y)" vs "arr[row][col]" ... It's all a bit of a jumble that's only more difficult to keep straight with each dying neuron... (sigh...) \$\endgroup\$
    – Fe2O3
    Commented Jun 6 at 7:17
  • 1
    \$\begingroup\$ Even in very strongly-typed languages, I've yet to find any library that uses different types for the different dimensions, to help avoid mixups. That might be an interesting project to undertake. \$\endgroup\$ Commented Jun 6 at 7:21
  • \$\begingroup\$ Re: use different types... "When one door closes another opens" Something tells me this would only shift the locations of those coding hazards to darker, more 'invisible' corners of coding... A lot like playing Minesweeper!! :-) Cheers! \$\endgroup\$
    – Fe2O3
    Commented Jun 6 at 7:27
3
\$\begingroup\$

Simpler

Code like this:

if ans == True:

is simpler as:

if ans:

Similarly:

if won == False:

is simpler as:

if not won:

These 2 lines:

outres = {'dimensions' : dimensions, 'mines' : mines}

return outres

can be combined as one:

return {'dimensions' : dimensions, 'mines' : mines}

This eliminates a vaguely named variable (outres).

Comments

Delete all comment-out code to reduce clutter:

#center(window)
#window.eval('tk::PlaceWindow . middle')

Comments like these:

#========== CON0FIG SECTION ==========
    mines = 99
    dimensions = (16, 30)
#========== CONFIG SECTION ==========

should be indented with the code:

    #========== CON0FIG SECTION ==========
    mines = 99
    dimensions = (16, 30)
    #========== CONFIG SECTION ==========

Documentation

The PEP 8 style guide recommends adding docstrings for functions. You should also add a docstring at the top of the code to summarize its purpose:

"""
Minesweeper game
"""

Naming

PEP 8 recommends snake_case for function and variable names. For example, createBoard would be create_board.

This constant value is used many times in the code: '\u2691'. You should set it to a named constant that describes what it means.

The variable named arr is not very descriptive. Choose a name that conveys more meaning.

Portability

I'm not a big fan of fancy Unicode characters in source code, like the symbol that looks like either a clock or a timer. Sometimes they don't render well in editors, and other times they don't render well in output generated by the code.

\$\endgroup\$
3
\$\begingroup\$

Control flow tools

As of Python 3.10, released in October of 2021, we have the match statement. Using this we might rewrite the following:

def chooseDifficulty():
    dialog_text = 'set theDialogText to "Please select a difficulty:"\ndisplay dialog theDialogText buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (30x16, 99 mines)"} default button "Expert (30x16, 99 mines)"'

    out = applescript.run(dialog_text)

    returned = (out.out).replace('button returned:', '')

    if returned == 'Expert (30x16, 99 mines)':
        dimensions = (30, 16)
        mines = 99

    elif returned == 'Medium (13x15, 40 mines)':
        dimensions = (13, 15)
        mines = 40

    elif returned == 'Easy (9x9, 10 mines)':
        dimensions = (9, 9)
        mines = 10

    outres = {'dimensions' : dimensions, 'mines' : mines}

    return outres
def chooseDifficulty():
    dialog_text = 'set theDialogText to "Please select a difficulty:"\ndisplay dialog theDialogText buttons {"Easy (9x9, 10 mines)", "Medium (13x15, 40 mines)", "Expert (30x16, 99 mines)"} default button "Expert (30x16, 99 mines)"'
    out = applescript.run(dialog_text)
    returned = (out.out).replace('button returned:', '')

    match returned:
        case 'Expert (30x16, 99 mines)':
            dimensions = (30, 16)
            mines = 99
        case 'Medium (13x15, 40 mines)':
            dimensions = (13, 15)
            mines = 40
        case 'Easy (9x9, 10 mines)':
            dimensions = (9, 9)
            mines = 10

    return {'dimensions' : dimensions, 'mines' : mines}

Globals

Your code makes extensive use of global variables. This means keeping track of what's going on in your program carries a huge mental load. I would strongly suggest you break this habit. You can either pass state via arguments and return values from functions, or use classes to encapsulate state.

\$\endgroup\$

Your Answer

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.