├── .editorconfig
├── .github
└── FUNDING.yml
├── LICENSE.txt
├── README.md
├── images
├── original
│ ├── tile_1.png
│ ├── tile_2.png
│ ├── tile_3.png
│ ├── tile_4.png
│ ├── tile_5.png
│ ├── tile_6.png
│ ├── tile_7.png
│ ├── tile_8.png
│ ├── tile_clicked.png
│ ├── tile_flag.png
│ ├── tile_mine.png
│ ├── tile_plain.png
│ └── tile_wrong.png
├── tile_1.gif
├── tile_2.gif
├── tile_3.gif
├── tile_4.gif
├── tile_5.gif
├── tile_6.gif
├── tile_7.gif
├── tile_8.gif
├── tile_clicked.gif
├── tile_flag.gif
├── tile_mine.gif
├── tile_plain.gif
└── tile_wrong.gif
└── minesweeper.py
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://EditorConfig.org
2 |
3 | root = true
4 |
5 | [*]
6 | charset = utf-8
7 | indent_style = space
8 | indent_size = 4
9 | end_of_line = lf
10 | insert_final_newline = true
11 | trim_trailing_whitespace = true
12 |
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | github: [ripexz]
4 | patreon: ripexz
5 | #open_collective: # Replace with a single Open Collective username
6 | ko_fi: ripexz
7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
9 | #liberapay: # Replace with a single Liberapay username
10 | #issuehunt: # Replace with a single IssueHunt username
11 | #otechie: # Replace with a single Otechie username
12 | #custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
13 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Paulius J.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Python Tkinter Minesweeper
2 | ===========================
3 |
4 | Minesweeper game written in Python using Tkinter GUI library.
5 |
6 |
7 |
8 | Contents:
9 | ----------
10 |
11 | - */minesweeper.py* - The actual python program
12 | - */images/* - GIF Images ready for usage with Tkinter
13 | - */images/original* - Original PNG images made with GraphicsGale
14 |
15 | To Do:
16 | ----------
17 | - Have specific number of mines, rather than random
18 | - Highscore table
19 | - Adjustable grid and mine count via UI
20 |
--------------------------------------------------------------------------------
/images/original/tile_1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_1.png
--------------------------------------------------------------------------------
/images/original/tile_2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_2.png
--------------------------------------------------------------------------------
/images/original/tile_3.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_3.png
--------------------------------------------------------------------------------
/images/original/tile_4.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_4.png
--------------------------------------------------------------------------------
/images/original/tile_5.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_5.png
--------------------------------------------------------------------------------
/images/original/tile_6.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_6.png
--------------------------------------------------------------------------------
/images/original/tile_7.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_7.png
--------------------------------------------------------------------------------
/images/original/tile_8.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_8.png
--------------------------------------------------------------------------------
/images/original/tile_clicked.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_clicked.png
--------------------------------------------------------------------------------
/images/original/tile_flag.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_flag.png
--------------------------------------------------------------------------------
/images/original/tile_mine.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_mine.png
--------------------------------------------------------------------------------
/images/original/tile_plain.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_plain.png
--------------------------------------------------------------------------------
/images/original/tile_wrong.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/original/tile_wrong.png
--------------------------------------------------------------------------------
/images/tile_1.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_1.gif
--------------------------------------------------------------------------------
/images/tile_2.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_2.gif
--------------------------------------------------------------------------------
/images/tile_3.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_3.gif
--------------------------------------------------------------------------------
/images/tile_4.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_4.gif
--------------------------------------------------------------------------------
/images/tile_5.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_5.gif
--------------------------------------------------------------------------------
/images/tile_6.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_6.gif
--------------------------------------------------------------------------------
/images/tile_7.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_7.gif
--------------------------------------------------------------------------------
/images/tile_8.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_8.gif
--------------------------------------------------------------------------------
/images/tile_clicked.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_clicked.gif
--------------------------------------------------------------------------------
/images/tile_flag.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_flag.gif
--------------------------------------------------------------------------------
/images/tile_mine.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_mine.gif
--------------------------------------------------------------------------------
/images/tile_plain.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_plain.gif
--------------------------------------------------------------------------------
/images/tile_wrong.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ripexz/python-tkinter-minesweeper/fc281a07ffdac8975ddaa7ed49a027c563c5f7a1/images/tile_wrong.gif
--------------------------------------------------------------------------------
/minesweeper.py:
--------------------------------------------------------------------------------
1 | # Python Version 2.7.3
2 | # File: minesweeper.py
3 |
4 | from tkinter import *
5 | from tkinter import messagebox as tkMessageBox
6 | from collections import deque
7 | import random
8 | import platform
9 | import time
10 | from datetime import time, date, datetime
11 |
12 | SIZE_X = 10
13 | SIZE_Y = 10
14 |
15 | STATE_DEFAULT = 0
16 | STATE_CLICKED = 1
17 | STATE_FLAGGED = 2
18 |
19 | BTN_CLICK = ""
20 | BTN_FLAG = "" if platform.system() == 'Darwin' else ""
21 |
22 | window = None
23 |
24 | class Minesweeper:
25 |
26 | def __init__(self, tk):
27 |
28 | # import images
29 | self.images = {
30 | "plain": PhotoImage(file = "images/tile_plain.gif"),
31 | "clicked": PhotoImage(file = "images/tile_clicked.gif"),
32 | "mine": PhotoImage(file = "images/tile_mine.gif"),
33 | "flag": PhotoImage(file = "images/tile_flag.gif"),
34 | "wrong": PhotoImage(file = "images/tile_wrong.gif"),
35 | "numbers": []
36 | }
37 | for i in range(1, 9):
38 | self.images["numbers"].append(PhotoImage(file = "images/tile_"+str(i)+".gif"))
39 |
40 | # set up frame
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 = int(SIZE_Y/2)) # bottom left
53 | self.labels["flags"].grid(row = SIZE_X+1, column = int(SIZE_Y/2)-1, columnspan = int(SIZE_Y/2)) # bottom right
54 |
55 | self.restart() # start game
56 | self.updateTimer() # init timer
57 |
58 | def setup(self):
59 | # create flag and clicked tile variables
60 | self.flagCount = 0
61 | self.correctFlagCount = 0
62 | self.clickedCount = 0
63 | self.startTime = None
64 |
65 | # create buttons
66 | self.tiles = dict({})
67 | self.mines = 0
68 | for x in range(0, SIZE_X):
69 | for y in range(0, SIZE_Y):
70 | if y == 0:
71 | self.tiles[x] = {}
72 |
73 | id = str(x) + "_" + str(y)
74 | isMine = False
75 |
76 | # tile image changeable for debug reasons:
77 | gfx = self.images["plain"]
78 |
79 | # currently random amount of mines
80 | if random.uniform(0.0, 1.0) < 0.1:
81 | isMine = True
82 | self.mines += 1
83 |
84 | tile = {
85 | "id": id,
86 | "isMine": isMine,
87 | "state": STATE_DEFAULT,
88 | "coords": {
89 | "x": x,
90 | "y": y
91 | },
92 | "button": Button(self.frame, image = gfx),
93 | "mines": 0 # calculated after grid is built
94 | }
95 |
96 | tile["button"].bind(BTN_CLICK, self.onClickWrapper(x, y))
97 | tile["button"].bind(BTN_FLAG, self.onRightClickWrapper(x, y))
98 | tile["button"].grid( row = x+1, column = y ) # offset by 1 row for timer
99 |
100 | self.tiles[x][y] = tile
101 |
102 | # loop again to find nearby mines and display number on tile
103 | for x in range(0, SIZE_X):
104 | for y in range(0, SIZE_Y):
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
109 |
110 | def restart(self):
111 | self.setup()
112 | self.refreshLabels()
113 |
114 | def refreshLabels(self):
115 | self.labels["flags"].config(text = "Flags: "+str(self.flagCount))
116 | self.labels["mines"].config(text = "Mines: "+str(self.mines))
117 |
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"])
125 |
126 | self.tk.update()
127 |
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 onClickWrapper(self, x, y):
165 | return lambda Button: self.onClick(self.tiles[x][y])
166 |
167 | def onRightClickWrapper(self, x, y):
168 | return lambda Button: self.onRightClick(self.tiles[x][y])
169 |
170 | def onClick(self, tile):
171 | if self.startTime == None:
172 | self.startTime = datetime.now()
173 |
174 | if tile["isMine"] == True:
175 | # end game
176 | self.gameOver(False)
177 | return
178 |
179 | # change image
180 | if tile["mines"] == 0:
181 | tile["button"].config(image = self.images["clicked"])
182 | self.clearSurroundingTiles(tile["id"])
183 | else:
184 | tile["button"].config(image = self.images["numbers"][tile["mines"]-1])
185 | # if not already set as clicked, change state and count
186 | if tile["state"] != STATE_CLICKED:
187 | tile["state"] = STATE_CLICKED
188 | self.clickedCount += 1
189 | if self.clickedCount == (SIZE_X * SIZE_Y) - self.mines:
190 | self.gameOver(True)
191 |
192 | def onRightClick(self, tile):
193 | if self.startTime == None:
194 | self.startTime = datetime.now()
195 |
196 | # if not clicked
197 | if tile["state"] == STATE_DEFAULT:
198 | tile["button"].config(image = self.images["flag"])
199 | tile["state"] = STATE_FLAGGED
200 | tile["button"].unbind(BTN_CLICK)
201 | # if a mine
202 | if tile["isMine"] == True:
203 | self.correctFlagCount += 1
204 | self.flagCount += 1
205 | self.refreshLabels()
206 | # if flagged, unflag
207 | elif tile["state"] == 2:
208 | tile["button"].config(image = self.images["plain"])
209 | tile["state"] = 0
210 | tile["button"].bind(BTN_CLICK, self.onClickWrapper(tile["coords"]["x"], tile["coords"]["y"]))
211 | # if a mine
212 | if tile["isMine"] == True:
213 | self.correctFlagCount -= 1
214 | self.flagCount -= 1
215 | self.refreshLabels()
216 |
217 | def clearSurroundingTiles(self, id):
218 | queue = deque([id])
219 |
220 | while len(queue) != 0:
221 | key = queue.popleft()
222 | parts = key.split("_")
223 | x = int(parts[0])
224 | y = int(parts[1])
225 |
226 | for tile in self.getNeighbors(x, y):
227 | self.clearTile(tile, queue)
228 |
229 | def clearTile(self, tile, queue):
230 | if tile["state"] != STATE_DEFAULT:
231 | return
232 |
233 | if tile["mines"] == 0:
234 | tile["button"].config(image = self.images["clicked"])
235 | queue.append(tile["id"])
236 | else:
237 | tile["button"].config(image = self.images["numbers"][tile["mines"]-1])
238 |
239 | tile["state"] = STATE_CLICKED
240 | self.clickedCount += 1
241 |
242 | ### END OF CLASSES ###
243 |
244 | def main():
245 | # create Tk instance
246 | window = Tk()
247 | # set program title
248 | window.title("Minesweeper")
249 | # create game instance
250 | minesweeper = Minesweeper(window)
251 | # run event loop
252 | window.mainloop()
253 |
254 | if __name__ == "__main__":
255 | main()
256 |
--------------------------------------------------------------------------------