Out of boredom I decided to make simple minesweeper in python. I decided to do it using only libraries which are included in standard installation on Windows.
I have overall been coding in Python for a while now, but decided to give my code for review to see whatever my coding skills can be improved.
import tkinter, configparser, random, os, tkinter.messagebox, tkinter.simpledialog
window = tkinter.Tk()
window.title("Minesweeper")
#prepare default values
rows = 10
cols = 10
mines = 10
field = []
buttons = []
colors = ['#FFFFFF', '#0000FF', '#008200', '#FF0000', '#000084', '#840000', '#008284', '#840084', '#000000']
gameover = False
customsizes = []
def createMenu():
menubar = tkinter.Menu(window)
menusize = tkinter.Menu(window, tearoff=0)
menusize.add_command(label="small (10x10 with 10 mines)", command=lambda: setSize(10, 10, 10))
menusize.add_command(label="medium (20x20 with 40 mines)", command=lambda: setSize(20, 20, 40))
menusize.add_command(label="big (35x35 with 120 mines)", command=lambda: setSize(35, 35, 120))
menusize.add_command(label="custom", command=setCustomSize)
menusize.add_separator()
for x in range(0, len(customsizes)):
menusize.add_command(label=str(customsizes[x][0])+"x"+str(customsizes[x][1])+" with "+str(customsizes[x][2])+" mines", command=lambda customsizes=customsizes: setSize(customsizes[x][0], customsizes[x][1], customsizes[x][2]))
menubar.add_cascade(label="size", menu=menusize)
menubar.add_command(label="exit", command=lambda: window.destroy())
window.config(menu=menubar)
def setCustomSize():
global customsizes
r = tkinter.simpledialog.askinteger("Custom size", "Enter amount of rows")
c = tkinter.simpledialog.askinteger("Custom size", "Enter amount of columns")
m = tkinter.simpledialog.askinteger("Custom size", "Enter amount of mines")
while m > r*c:
m = tkinter.simpledialog.askinteger("Custom size", "Maximum mines for this dimension is: " + str(r*c) + "\nEnter amount of mines")
customsizes.insert(0, (r,c,m))
customsizes = customsizes[0:5]
setSize(r,c,m)
createMenu()
def setSize(r,c,m):
global rows, cols, mines
rows = r
cols = c
mines = m
saveConfig()
restartGame()
def saveConfig():
global rows, cols, mines
#configuration
config = configparser.SafeConfigParser()
config.add_section("game")
config.set("game", "rows", str(rows))
config.set("game", "cols", str(cols))
config.set("game", "mines", str(mines))
config.add_section("sizes")
config.set("sizes", "amount", str(min(5,len(customsizes))))
for x in range(0,min(5,len(customsizes))):
config.set("sizes", "row"+str(x), str(customsizes[x][0]))
config.set("sizes", "cols"+str(x), str(customsizes[x][1]))
config.set("sizes", "mines"+str(x), str(customsizes[x][2]))
with open("config.ini", "w") as file:
config.write(file)
def loadConfig():
global rows, cols, mines, customsizes
config = configparser.SafeConfigParser()
config.read("config.ini")
rows = config.getint("game", "rows")
cols = config.getint("game", "cols")
mines = config.getint("game", "mines")
amountofsizes = config.getint("sizes", "amount")
for x in range(0, amountofsizes):
customsizes.append((config.getint("sizes", "row"+str(x)), config.getint("sizes", "cols"+str(x)), config.getint("sizes", "mines"+str(x))))
def prepareGame():
global rows, cols, mines, field
field = []
for x in range(0, rows):
field.append([])
for y in range(0, cols):
#add button and init value for game
field[x].append(0)
#generate mines
for _ in range(0, mines):
x = random.randint(0, rows-1)
y = random.randint(0, cols-1)
#prevent spawning mine on top of each other
while field[x][y] == -1:
x = random.randint(0, rows-1)
y = random.randint(0, cols-1)
field[x][y] = -1
if x != 0:
if y != 0:
if field[x-1][y-1] != -1:
field[x-1][y-1] = int(field[x-1][y-1]) + 1
if field[x-1][y] != -1:
field[x-1][y] = int(field[x-1][y]) + 1
if y != cols-1:
if field[x-1][y+1] != -1:
field[x-1][y+1] = int(field[x-1][y+1]) + 1
if y != 0:
if field[x][y-1] != -1:
field[x][y-1] = int(field[x][y-1]) + 1
if y != cols-1:
if field[x][y+1] != -1:
field[x][y+1] = int(field[x][y+1]) + 1
if x != rows-1:
if y != 0:
if field[x+1][y-1] != -1:
field[x+1][y-1] = int(field[x+1][y-1]) + 1
if field[x+1][y] != -1:
field[x+1][y] = int(field[x+1][y]) + 1
if y != cols-1:
if field[x+1][y+1] != -1:
field[x+1][y+1] = int(field[x+1][y+1]) + 1
def prepareWindow():
global rows, cols, buttons
tkinter.Button(window, text="Restart", command=restartGame).grid(row=0, column=0, columnspan=cols, sticky=tkinter.N+tkinter.W+tkinter.S+tkinter.E)
buttons = []
for x in range(0, rows):
buttons.append([])
for y in range(0, cols):
b = tkinter.Button(window, text=" ", width=2, command=lambda x=x,y=y: clickOn(x,y))
b.bind("<Button-3>", lambda e, x=x, y=y:onRightClick(x, y))
b.grid(row=x+1, column=y, sticky=tkinter.N+tkinter.W+tkinter.S+tkinter.E)
buttons[x].append(b)
def restartGame():
global gameover
gameover = False
#destroy all - prevent memory leak
for x in window.winfo_children():
if type(x) != tkinter.Menu:
x.destroy()
prepareWindow()
prepareGame()
def clickOn(x,y):
global field, buttons, colors, gameover, rows, cols
if gameover:
return
buttons[x][y]["text"] = str(field[x][y])
if field[x][y] == -1:
buttons[x][y]["text"] = "*"
buttons[x][y].config(background='red', disabledforeground='black')
gameover = True
tkinter.messagebox.showinfo("Game Over", "You have lost.")
#now show all other mines
for _x in range(0, rows):
for _y in range(cols):
if field[_x][_y] == -1:
buttons[_x][_y]["text"] = "*"
else:
buttons[x][y].config(disabledforeground=colors[field[x][y]])
if field[x][y] == 0:
buttons[x][y]["text"] = " "
#now repeat for all buttons nearby which are 0... kek
autoClickOn(x,y)
buttons[x][y]['state'] = 'disabled'
buttons[x][y].config(relief=tkinter.SUNKEN)
checkWin()
def autoClickOn(x,y):
global field, buttons, colors, rows, cols
if buttons[x][y]["state"] == "disabled":
return
if field[x][y] != 0:
buttons[x][y]["text"] = str(field[x][y])
else:
buttons[x][y]["text"] = " "
buttons[x][y].config(disabledforeground=colors[field[x][y]])
buttons[x][y].config(relief=tkinter.SUNKEN)
buttons[x][y]['state'] = 'disabled'
if field[x][y] == 0:
if x != 0 and y != 0:
autoClickOn(x-1,y-1)
if x != 0:
autoClickOn(x-1,y)
if x != 0 and y != cols-1:
autoClickOn(x-1,y+1)
if y != 0:
autoClickOn(x,y-1)
if y != cols-1:
autoClickOn(x,y+1)
if x != rows-1 and y != 0:
autoClickOn(x+1,y-1)
if x != rows-1:
autoClickOn(x+1,y)
if x != rows-1 and y != cols-1:
autoClickOn(x+1,y+1)
def onRightClick(x,y):
global buttons
if gameover:
return
if buttons[x][y]["text"] == "?":
buttons[x][y]["text"] = " "
buttons[x][y]["state"] = "normal"
elif buttons[x][y]["text"] == " " and buttons[x][y]["state"] == "normal":
buttons[x][y]["text"] = "?"
buttons[x][y]["state"] = "disabled"
def checkWin():
global buttons, field, rows, cols
win = True
for x in range(0, rows):
for y in range(0, cols):
if field[x][y] != -1 and buttons[x][y]["state"] == "normal":
win = False
if win:
tkinter.messagebox.showinfo("Gave Over", "You have won.")
if os.path.exists("config.ini"):
loadConfig()
else:
saveConfig()
createMenu()
prepareWindow()
prepareGame()
window.mainloop()
This minesweeper creates settings.ini
in the same location where from script was run.
3 Answers 3
A few superficial things:
- Games like this are perfect for object oriented code. Some obvious classes for a Minesweeper game would include for example
Game
,Board
andTile
. - Avoid
global
s. These helpfully often disappear naturally when using OO. - Pass the code through
pycodestyle
and correct everything it reports. Other Pythonistas will thank you, even though it may feel unfamiliar and even arbitrary some times. - Avoid single letter names such as
b
. Evenx
andy
can be misleading in your specific case - a mathematically inclined person would think of them as offsets from the bottom left, but it looks like in your case it's actually an offset from the top left since they are used asrow
andcolumn
inprepareWindow
. - Use constants for magic values such as configuration variables and the minimum number of mines (5).
- Add spacing to your
longvariablenames
to make them easier to read.
-
\$\begingroup\$ Just to note: doing point 1 (going OO) should fix point 2 (use of globals) \$\endgroup\$pjz– pjz2018年04月10日 03:52:06 +00:00Commented Apr 10, 2018 at 3:52
Also, you can use random.sample(num_tiles, num_mines)
to avoid repetitive mine spawning.
-
3\$\begingroup\$ While this point is correct, it's not quite obvious why the current code for spawning mines isn't actually "properly random". This answer would be a lot better if you explained why using
random.sample
is better than usingrandom.randint
twice with checking for duplicates :) \$\endgroup\$Vogel612– Vogel6122020年05月24日 15:17:25 +00:00Commented May 24, 2020 at 15:17 -
\$\begingroup\$ thanks for the feedback, but i don't see how it's not properly random there. \$\endgroup\$sideshow123– sideshow1232020年05月24日 16:44:33 +00:00Commented May 24, 2020 at 16:44
-
\$\begingroup\$ The key here is realizing that the two calls to radom.randint in succession are not as independent as one would want for a fully random distribution in 2d space. With sufficiently many trials, you'd see the properties of the underlying PRNG, Since python 3.2 the distribution is better, but looking at the relevant documentation suggests that subsequent calls to
random.randint
are still not perfectly independent enough to replacerandom.sample
. TBQH in practice that property doesn't matter too much for non-cryptographic uses. \$\endgroup\$Vogel612– Vogel6122020年05月24日 17:43:01 +00:00Commented May 24, 2020 at 17:43 -
\$\begingroup\$ so it's something like a stream cipher with entropy limited to key length? \$\endgroup\$sideshow123– sideshow1232020年05月25日 01:17:27 +00:00Commented May 25, 2020 at 1:17
-
\$\begingroup\$ that's basically how it works, yes \$\endgroup\$Vogel612– Vogel6122020年05月25日 10:32:40 +00:00Commented May 25, 2020 at 10:32
In the original minesweeper game, it was impossible to lose on the first try because the mine (if you were unlucky enough to click it) moved to another tile. I think this would be another improvement.
Explore related questions
See similar questions with these tags.