├── .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 | Screenshot on OSX 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 | --------------------------------------------------------------------------------