Skip to content

Commit aadbf3d

Browse files
committed
more refactoring, option to restart game on finish, timer
1 parent a2b4862 commit aadbf3d

File tree

2 files changed

+161
-133
lines changed

2 files changed

+161
-133
lines changed

README.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,5 @@ Contents:
1313
To Do:
1414
----------
1515
- Have specific number of mines, rather than random
16-
- Time counter
1716
- Highscore table
1817
- Adjustable grid and mine count via UI

minesweeper.py

Lines changed: 161 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import random
88
import platform
99
import time
10+
from datetime import time, date, datetime
1011

1112
SIZE_X = 10
1213
SIZE_Y = 10
@@ -18,216 +19,244 @@
1819
BTN_CLICK = "<Button-1>"
1920
BTN_FLAG = "<Button-2>" if platform.system() == 'Darwin' else "<Button-3>"
2021

22+
window = None
23+
2124
class Minesweeper:
2225

23-
def __init__(self, master):
26+
def __init__(self, tk):
2427

2528
# import images
26-
self.tiles = {
29+
self.images = {
2730
"plain": PhotoImage(file = "images/tile_plain.gif"),
2831
"clicked": PhotoImage(file = "images/tile_clicked.gif"),
2932
"mine": PhotoImage(file = "images/tile_mine.gif"),
3033
"flag": PhotoImage(file = "images/tile_flag.gif"),
3134
"wrong": PhotoImage(file = "images/tile_wrong.gif"),
3235
"numbers": []
3336
}
34-
for x in range(1, 9):
35-
self.tiles["numbers"].append(PhotoImage(file = "images/tile_"+str(x)+".gif"))
37+
for i in range(1, 9):
38+
self.images["numbers"].append(PhotoImage(file = "images/tile_"+str(i)+".gif"))
3639

3740
# set up frame
38-
frame = Frame(master)
39-
frame.pack()
41+
self.tk = tk
42+
self.frame = Frame(self.tk)
43+
self.frame.pack()
44+
45+
# set up labels/UI
46+
self.labels = {
47+
"time": Label(self.frame, text = "00:00:00"),
48+
"mines": Label(self.frame, text = "Mines: 0"),
49+
"flags": Label(self.frame, text = "Flags: 0")
50+
}
51+
self.labels["time"].grid(row = 0, column = 0, columnspan = SIZE_Y) # top full width
52+
self.labels["mines"].grid(row = SIZE_X+1, column = 0, columnspan = SIZE_Y/2) # bottom left
53+
self.labels["flags"].grid(row = SIZE_X+1, column = SIZE_Y/2-1, columnspan = SIZE_Y/2) # bottom right
4054

41-
# show "Minesweeper" at the top
42-
self.label1 = Label(frame, text="Minesweeper")
43-
self.label1.grid(row = 0, column = 0, columnspan = 10)
55+
self.restart() # start game
56+
self.updateTimer() # init timer
4457

58+
def setup(self):
4559
# create flag and clicked tile variables
46-
self.flags = 0
47-
self.correct_flags = 0
48-
self.clicked = 0
60+
self.flagCount = 0
61+
self.correctFlagCount = 0
62+
self.clickedCount = 0
63+
self.startTime = None
4964

5065
# create buttons
51-
self.buttons = dict({})
66+
self.tiles = dict({})
5267
self.mines = 0
5368
for x in range(0, SIZE_X):
5469
for y in range(0, SIZE_Y):
5570
if y == 0:
56-
self.buttons[x] = {}
71+
self.tiles[x] = {}
5772

5873
id = str(x) + "_" + str(y)
5974
isMine = False
6075

6176
# tile image changeable for debug reasons:
62-
gfx = self.tiles["plain"]
77+
gfx = self.images["plain"]
6378

6479
# currently random amount of mines
6580
if random.uniform(0.0, 1.0) < 0.1:
6681
isMine = True
6782
self.mines += 1
6883

69-
self.buttons[x][y] = {
84+
self.tiles[x][y] = {
7085
"id": id,
7186
"isMine": isMine,
7287
"state": STATE_DEFAULT,
7388
"coords": {
7489
"x": x,
7590
"y": y
7691
},
77-
"widget": Button(frame, image = gfx),
92+
"button": Button(self.frame, image = gfx),
7893
"mines": 0 # calculated after grid is built
7994
}
8095

81-
self.buttons[x][y]["widget"].bind(BTN_CLICK, self.lclicked_wrapper(x, y))
82-
self.buttons[x][y]["widget"].bind(BTN_FLAG, self.rclicked_wrapper(x, y))
96+
self.tiles[x][y]["button"].bind(BTN_CLICK, self.onClickWrapper(x, y))
97+
self.tiles[x][y]["button"].bind(BTN_FLAG, self.onRightClickWrapper(x, y))
8398

84-
# lay buttons in grid
85-
self.buttons[x][y]["widget"].grid( row = x, column = y )
99+
# lay buttons in grid, offset by 1 row for timer
100+
self.tiles[x][y]["button"].grid( row = x+1, column = y )
86101

87102
# loop again to find nearby mines and display number on tile
88103
for x in range(0, SIZE_X):
89104
for y in range(0, SIZE_Y):
90-
nearby_mines = 0
91-
nearby_mines += 1 if self.check_for_mines(x-1, y-1) else 0 #top right
92-
nearby_mines += 1 if self.check_for_mines(x-1, y) else 0 #top middle
93-
nearby_mines += 1 if self.check_for_mines(x-1, y+1) else 0 #top left
94-
nearby_mines += 1 if self.check_for_mines(x, y-1) else 0 #left
95-
nearby_mines += 1 if self.check_for_mines(x, y+1) else 0 #right
96-
nearby_mines += 1 if self.check_for_mines(x+1, y-1) else 0 #bottom right
97-
nearby_mines += 1 if self.check_for_mines(x+1, y) else 0 #bottom middle
98-
nearby_mines += 1 if self.check_for_mines(x+1, y+1) else 0 #bottom left
105+
mc = 0
106+
for n in self.getNeighbors(x, y):
107+
mc += 1 if n["isMine"] else 0
108+
self.tiles[x][y]["mines"] = mc
99109

100-
self.buttons[x][y]["mines"] = nearby_mines
110+
def restart(self):
111+
self.setup()
112+
self.refreshLabels()
101113

102-
#add mine and count at the end
103-
self.label2 = Label(frame, text = "Mines: "+str(self.mines))
104-
self.label2.grid(row = SIZE_X+1, column = 0, columnspan = SIZE_Y/2)
114+
def refreshLabels(self):
115+
self.labels["flags"].config(text = "Flags: "+str(self.flagCount))
116+
self.labels["mines"].config(text = "Mines: "+str(self.mines))
105117

106-
self.label3 = Label(frame, text = "Flags: "+str(self.flags))
107-
self.label3.grid(row = SIZE_X+1, column = SIZE_Y/2-1, columnspan = SIZE_Y/2)
118+
def gameOver(self, won):
119+
for x in range(0, SIZE_X):
120+
for y in range(0, SIZE_Y):
121+
if self.tiles[x][y]["isMine"] == False and self.tiles[x][y]["state"] == STATE_FLAGGED:
122+
self.tiles[x][y]["button"].config(image = self.images["wrong"])
123+
if self.tiles[x][y]["isMine"] == True and self.tiles[x][y]["state"] != STATE_FLAGGED:
124+
self.tiles[x][y]["button"].config(image = self.images["mine"])
108125

109-
## End of __init__
126+
self.tk.update()
110127

111-
def check_for_mines(self, x, y):
128+
msg = "You Win! Play again?" if won else "You Lose! Play again?"
129+
res = tkMessageBox.askyesno("Game Over", msg)
130+
if res:
131+
self.restart()
132+
else:
133+
self.tk.quit()
134+
135+
def updateTimer(self):
136+
ts = "00:00:00"
137+
if self.startTime != None:
138+
delta = datetime.now() - self.startTime
139+
ts = str(delta).split('.')[0] # drop ms
140+
if delta.total_seconds() < 36000:
141+
ts = "0" + ts # zero-pad
142+
self.labels["time"].config(text = ts)
143+
self.frame.after(100, self.updateTimer)
144+
145+
def getNeighbors(self, x, y):
146+
neighbors = []
147+
coords = [
148+
{"x": x-1, "y": y-1}, #top right
149+
{"x": x-1, "y": y}, #top middle
150+
{"x": x-1, "y": y+1}, #top left
151+
{"x": x, "y": y-1}, #left
152+
{"x": x, "y": y+1}, #right
153+
{"x": x+1, "y": y-1}, #bottom right
154+
{"x": x+1, "y": y}, #bottom middle
155+
{"x": x+1, "y": y+1}, #bottom left
156+
]
157+
for n in coords:
158+
try:
159+
neighbors.append(self.tiles[n["x"]][n["y"]])
160+
except KeyError:
161+
pass
162+
return neighbors
163+
164+
def isMineAt(self, x, y):
112165
try:
113-
return self.buttons[x][y]["isMine"]
166+
return self.tiles[x][y]["isMine"]
114167
except KeyError:
115168
pass
169+
return False
170+
171+
def onClickWrapper(self, x, y):
172+
return lambda Button: self.onClick(self.tiles[x][y])
116173

117-
def lclicked_wrapper(self, x, y):
118-
return lambda Button: self.lclicked(self.buttons[x][y])
174+
def onRightClickWrapper(self, x, y):
175+
return lambda Button: self.onRightClick(self.tiles[x][y])
119176

120-
def rclicked_wrapper(self, x, y):
121-
return lambda Button: self.rclicked(self.buttons[x][y])
177+
def onClick(self, tile):
178+
if self.startTime == None:
179+
self.startTime = datetime.now()
122180

123-
def lclicked(self, button_data):
124-
if button_data["isMine"] == True:
181+
if tile["isMine"] == True:
125182
# end game
126-
self.gameover()
183+
self.gameOver(False)
184+
return
185+
186+
# change image
187+
if tile["mines"] == 0:
188+
tile["button"].config(image = self.images["clicked"])
189+
self.clearSurroundingTiles(tile["id"])
127190
else:
128-
# change image
129-
if button_data["mines"] == 0:
130-
button_data["widget"].config(image = self.tiles["clicked"])
131-
self.clear_empty_tiles(button_data["id"])
132-
else:
133-
button_data["widget"].config(image = self.tiles["numbers"][button_data["mines"]-1])
134-
# if not already set as clicked, change state and count
135-
if button_data["state"] != STATE_CLICKED:
136-
button_data["state"] = STATE_CLICKED
137-
self.clicked += 1
138-
if self.clicked == 100 - self.mines:
139-
self.victory()
140-
141-
def rclicked(self, button_data):
191+
tile["button"].config(image = self.images["numbers"][tile["mines"]-1])
192+
# if not already set as clicked, change state and count
193+
if tile["state"] != STATE_CLICKED:
194+
tile["state"] = STATE_CLICKED
195+
self.clickedCount += 1
196+
if self.clickedCount == (SIZE_X * SIZE_Y) - self.mines:
197+
self.gameOver(True)
198+
199+
def onRightClick(self, tile):
200+
if self.startTime == None:
201+
self.startTime = datetime.now()
202+
142203
# if not clicked
143-
if button_data["state"] == STATE_DEFAULT:
144-
button_data["widget"].config(image = self.tiles["flag"])
145-
button_data["state"] = STATE_FLAGGED
146-
button_data["widget"].unbind(BTN_CLICK)
204+
if tile["state"] == STATE_DEFAULT:
205+
tile["button"].config(image = self.images["flag"])
206+
tile["state"] = STATE_FLAGGED
207+
tile["button"].unbind(BTN_CLICK)
147208
# if a mine
148-
if button_data["isMine"] == True:
149-
self.correct_flags += 1
150-
self.flags += 1
151-
self.update_flags()
209+
if tile["isMine"] == True:
210+
self.correctFlagCount += 1
211+
self.flagCount += 1
212+
self.refreshLabels()
152213
# if flagged, unflag
153-
elif button_data["state"] == 2:
154-
button_data["widget"].config(image = self.tiles["plain"])
155-
button_data["state"] = 0
156-
button_data["widget"].bind(BTN_CLICK, self.lclicked_wrapper(button_data["coords"]["x"], button_data["coords"]["y"]))
214+
elif tile["state"] == 2:
215+
tile["button"].config(image = self.images["plain"])
216+
tile["state"] = 0
217+
tile["button"].bind(BTN_CLICK, self.onClickWrapper(tile["coords"]["x"], tile["coords"]["y"]))
157218
# if a mine
158-
if button_data["isMine"] == True:
159-
self.correct_flags -= 1
160-
self.flags -= 1
161-
self.update_flags()
219+
if tile["isMine"] == True:
220+
self.correctFlagCount -= 1
221+
self.flagCount -= 1
222+
self.refreshLabels()
162223

163-
def check_tile(self, x, y, queue):
164-
try:
165-
if self.buttons[x][y]["state"] == STATE_DEFAULT:
166-
if self.buttons[x][y]["mines"] == 0:
167-
self.buttons[x][y]["widget"].config(image = self.tiles["clicked"])
168-
queue.append(self.buttons[x][y]["id"])
169-
else:
170-
self.buttons[x][y]["widget"].config(image = self.tiles["numbers"][self.buttons[x][y]["mines"]-1])
171-
self.buttons[x][y]["state"] = STATE_CLICKED
172-
self.clicked += 1
173-
except KeyError:
174-
pass
175-
176-
def clear_empty_tiles(self, id):
224+
def clearSurroundingTiles(self, id):
177225
queue = deque([id])
178226

179227
while len(queue) != 0:
180228
key = queue.popleft()
181229
parts = key.split("_")
182-
source_x = int(parts[0])
183-
source_y = int(parts[1])
184-
185-
self.check_tile(source_x-1, source_y-1, queue) #top right
186-
self.check_tile(source_x-1, source_y, queue) #top middle
187-
self.check_tile(source_x-1, source_y+1, queue) #top left
188-
self.check_tile(source_x, source_y-1, queue) #left
189-
self.check_tile(source_x, source_y+1, queue) #right
190-
self.check_tile(source_x+1, source_y-1, queue) #bottom right
191-
self.check_tile(source_x+1, source_y, queue) #bottom middle
192-
self.check_tile(source_x+1, source_y+1, queue) #bottom left
193-
194-
def reveal(self):
195-
for x in range(0, SIZE_X):
196-
for y in range(0, SIZE_Y):
197-
if self.buttons[x][y]["isMine"] == False and self.buttons[x][y]["state"] == STATE_FLAGGED:
198-
self.buttons[x][y]["widget"].config(image = self.tiles["wrong"])
199-
if self.buttons[x][y]["isMine"] == True and self.buttons[x][y]["state"] != STATE_FLAGGED:
200-
self.buttons[x][y]["widget"].config(image = self.tiles["mine"])
201-
global root
202-
root.update()
203-
204-
def gameover(self):
205-
self.reveal()
206-
tkMessageBox.showinfo("Game Over", "You Lose!")
207-
global root
208-
root.destroy()
209-
210-
def victory(self):
211-
self.reveal()
212-
tkMessageBox.showinfo("Game Over", "You Win!")
213-
global root
214-
root.destroy()
215-
216-
def update_flags(self):
217-
self.label3.config(text = "Flags: "+str(self.flags))
230+
x = int(parts[0])
231+
y = int(parts[1])
232+
233+
for n in self.getNeighbors(x, y):
234+
self.clearTile(n["coords"]["x"], n["coords"]["y"], queue)
235+
236+
def clearTile(self, x, y, queue):
237+
try:
238+
if self.tiles[x][y]["state"] == STATE_DEFAULT:
239+
if self.tiles[x][y]["mines"] == 0:
240+
self.tiles[x][y]["button"].config(image = self.images["clicked"])
241+
queue.append(self.tiles[x][y]["id"])
242+
else:
243+
self.tiles[x][y]["button"].config(image = self.images["numbers"][self.tiles[x][y]["mines"]-1])
244+
self.tiles[x][y]["state"] = STATE_CLICKED
245+
self.clickedCount += 1
246+
except KeyError:
247+
pass
218248

219249
### END OF CLASSES ###
220250

221251
def main():
222-
global root
223-
# create Tk widget
224-
root = Tk()
252+
# create Tk instance
253+
window = Tk()
225254
# set program title
226-
root.title("Minesweeper")
255+
window.title("Minesweeper")
227256
# create game instance
228-
minesweeper = Minesweeper(root)
257+
minesweeper = Minesweeper(window)
229258
# run event loop
230-
root.mainloop()
259+
window.mainloop()
231260

232261
if __name__ == "__main__":
233262
main()

0 commit comments

Comments
 (0)