Python 3 simple Minesweeper game using tkinter

The name of the pictureThe name of the pictureThe name of the pictureClash Royale CLAN TAG#URR8PPP





.everyoneloves__top-leaderboard:empty,.everyoneloves__mid-leaderboard:empty margin-bottom:0;







up vote
11
down vote

favorite
2












I am relatively new to programming, and I wish to use this simple minesweeper game in a portfolio. A few questions:



  1. Currently, game setup gets progressively slower with each reset button call, and the window height slightly increases downward. This is very apparent on medium and hard difficulties. What changes can speed up this code? I want to reuse the same window with each reset if possible.

  2. The code uses a Model-View-Controller approach. Does this make sense for a project using tkinter? Is there a better approach?

  3. Some parts such as the additions variable are repeated, but I cannot find a way to make them instance variables.

Any suggestions/constructive feedback is appreciated.



"""
Minesweeper

Implements a basic minesweeper game using tkinter.
Uses Model-View-Controller architecture.
"""

import tkinter as tk
import random


class Model(object):
"""Crates a board and adds mines to it"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.create_grid()
self.add_mines()

def create_grid(self):
"""Create a self.width by self.height grid of elements with value 0"""
self.grid = [[0]*self.width for i in range(self.height)]

def add_mines(self):
"""Randomly adds the amount of self.num_mines to grid"""
def get_coords():
row = random.randint(0, self.height - 1)
col = random.randint(0, self.width - 1)
return row, col
for i in range(self.num_mines):
row, col = get_coords()
while self.grid[row][col] == "b":
row, col = get_coords()
self.grid[row][col] = "b"
for i in self.grid:
print (i)


class View(tk.Frame):
"""Creates a main window and grid of button cells"""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.width = width
self.height = height
self.num_mines = num_mines
self.master.title("Minesweeper")
self.grid()
self.top_panel = TopPanel(self.master, self.height,
self.width, self.num_mines)
self.create_widgets()

def create_widgets(self):
"""Create cell button widgets"""
self.buttons =
for i in range(self.height):
for j in range(self.width):
self.buttons[str(i) + "," + str(j)] = tk.Button(
self.master, width=5, bg="grey")
self.buttons[str(i) + "," + str(j)].grid(row=i+1, column=j+1)

def disp_loss(self):
"""Display the loss label when loss condition is reached"""
self.top_panel.loss_label.grid(row=0, columnspan=5)

def disp_win(self):
"""Display the win label when win condition is reached"""
self.top_panel.win_label.grid(row=0, columnspan=5)

def hide_labels(self, condition=None):
"""Hides labels based on condition argument"""
if condition:
self.top_panel.mines_left.grid_remove()
else:
self.top_panel.loss_label.grid_remove()
self.top_panel.win_label.grid_remove()


class TopPanel(tk.Frame):
"""Create top panel which houses reset button and win/loss and
mines left labels."""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.height = height
self.width = width
self.num_mines = num_mines
self.grid()
self.create_widgets()

def create_widgets(self):
self.reset_button = tk.Button(self.master, width = 7, text="Reset")
self.reset_button.grid(row=0, columnspan=int((self.width*7)/2))
# Create win and loss labels
self.loss_label = tk.Label(text="You Lose!", bg="red")
self.win_label = tk.Label(text="You Win!", bg="green")
# Create number of mines remaining label
self.mine_count = tk.StringVar()
self.mine_count.set("Mines remaining: " + str(self.num_mines))
self.mines_left = tk.Label(textvariable=self.mine_count)
self.mines_left.grid(row=0, columnspan=5)


class Controller(object):
"""Sets up button bindings and minsweeper game logic.

The act of revealing cells is delegated to the methods: give_val(),
reveal_cell(), reveal_adj(), and reveal_cont(). End conditions are handled
by the loss() and win() methods.
"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.model = Model(self.width, self.height, self.num_mines)
self.root = tk.Tk()
self.view = View(self.root, self.width, self.height, self.num_mines)
# self.color_dict is used to assign colors to cells
self.color_dict =
0: "white", 1:"blue", 2:"green",
3:"red", 4:"orange", 5:"purple",
6: "grey", 7:"grey", 8: "grey"

# Self.count keeps track of cells with value of 0 so that they
# get revealed with self.reveal_cont call only once
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.bindings()
self.root.mainloop()

def bindings(self):
"""Set up reveal cell and flag cell key bindings"""
for i in range(self.height):
for j in range(self.width):
# Right click bind to reveal decision method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-1>",
lambda event, index=[i, j]:self.reveal(event, index))
# Left click bind to flag method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-3>",
lambda event, index=[i, j]:self.flag(event, index))
# Set up reset button
self.view.top_panel.reset_button.bind("<Button>", self.reset)

def reset(self, event):
"""Resets game. Currently, game setup gets slower with each reset call,
and window height slightly increases"""
self.view.hide_labels()
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.model = Model(self.width, self.height, self.num_mines)
self.view = View(self.root, self.width,
self.height, self.num_mines)
self.bindings()

def reveal(self, event, index):
"""Main decision method determining how to reveal cell"""
i = index[0]
j = index[1]
val = self.give_val(index)
if val in [x for x in range(1, 9)]:
self.reveal_cell(val, index)
self.count.append(index)
if (val == "b" and self.game_state != "win" and
self.view.buttons[str(i) + "," + str(j)]["text"] != "FLAG"):
self.game_state = "Loss"
self.loss()
# Begin the revealing recursive method when cell value is 0
if val == 0:
self.reveal_cont(index)

def give_val(self, index):
"""Returns the number of adjacent mine. Returns "b" if cell is mine"""
i = index[0]
j = index[1]
num_mines = 0
try:
if self.model.grid[i][j] == "b":
return "b"
except IndexError:
pass
def increment():
try:
if self.model.grid[pos[0]][pos[1]] == "b":
return 1
except IndexError:
pass
return 0
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
#Adds 1 to num_mines if cell is adjacent to a mine
for pos in additions:
if 0 <= pos[0] <= self.height -1 and 0 <= pos[1] <= self.width - 1:
num_mines += increment()
return num_mines

def reveal_cell(self, value, index):
"""Reveals cell value and assigns an associated color for that value"""
i = index[0]
j = index[1]
cells_unrev = self.height * self.width - len(self.cells_revealed) - 1
button_key = str(i) + "," + str(j)
if self.view.buttons[button_key]["text"] == "FLAG":
pass
elif value == "b":
self.view.buttons[button_key].configure(bg="black")
else:
# Checks if cell is in the board limits
if (0 <= i <= self.height - 1 and
0 <= j <= self.width - 1 and
[button_key] not in self.cells_revealed):
self.view.buttons[button_key].configure(
text=value, bg=self.color_dict[value])
self.count.append(button_key)
self.cells_revealed.append([button_key])
# Removes cell from flagged list when the cell gets revealed
if button_key in self.cells_flagged:
self.cells_flagged.remove(button_key)
self.update_mines()
# Check for win condition
if (cells_unrev == self.num_mines and not self.game_state):
self.win()

def reveal_adj(self, index):
"""Reveals the 8 adjacent cells to the input cell index"""
org_val = self.give_val(index)
self.reveal_cell(org_val, index)
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width - 1):
new_val = self.give_val(pos)
self.reveal_cell(new_val, pos)

def reveal_cont(self, index):
"""Recursive formula that reveals all adjacent cells only if the
selected cell has no adjacent mines.
(meaning self.give_val(index) == 0)"""
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
val = self.give_val(index)
self.reveal_adj(index)
if val != 0:
return None
else:
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width -1 and
self.give_val(pos) == 0 and pos not in self.count):
self.count.append(pos)
self.reveal_cont(pos)

def win(self):
"""Display win"""
self.view.hide_labels("mine")
self.view.disp_win()
self.game_state = "win"

def loss(self):
"""Display loss. Reveal all cells when a mine is clicked"""
self.view.hide_labels("mine")
for i in range(self.height):
for j in range(self.width):
val = self.give_val([i, j])
self.reveal_cell(val, [i, j])
self.view.disp_loss()

def flag(self, event, index):
"""Allows player to flag cells for possible mines.
Does not reveal cell."""
i = index[0]
j = index[1]
button_key = str(i) + "," + str(j)
button_val = self.view.buttons[button_key]
if button_val["bg"] == "grey":
button_val.configure(bg="yellow", text="FLAG")
self.cells_flagged.append(button_key)
elif button_val["text"] == "FLAG":
button_val.configure(bg="grey", text="")
self.cells_flagged.remove(button_key)
self.update_mines()

def update_mines(self):
"""Update mine counter"""
mines_left = self.num_mines - len(self.cells_flagged)
if mines_left >= 0:
self.view.top_panel.mine_count.set(
"Mines remaining: " + str(mines_left))

def main():
n = input("Pick a difficulty: Easy, Medium, or Hard. ")
if n[0] == "E" or n[0] == "e":
return Controller(9, 9, 10)
elif n[0] == "M" or n[0] == "m":
return Controller(16, 16, 40)
elif n[0] == "H" or n[0] == "h":
return Controller(30, 16, 99)


if __name__ == "__main__":
main()






share|improve this question





















  • I'm working on it, you can see what I currently have here.
    – Solomon Ucko
    Mar 26 at 2:59

















up vote
11
down vote

favorite
2












I am relatively new to programming, and I wish to use this simple minesweeper game in a portfolio. A few questions:



  1. Currently, game setup gets progressively slower with each reset button call, and the window height slightly increases downward. This is very apparent on medium and hard difficulties. What changes can speed up this code? I want to reuse the same window with each reset if possible.

  2. The code uses a Model-View-Controller approach. Does this make sense for a project using tkinter? Is there a better approach?

  3. Some parts such as the additions variable are repeated, but I cannot find a way to make them instance variables.

Any suggestions/constructive feedback is appreciated.



"""
Minesweeper

Implements a basic minesweeper game using tkinter.
Uses Model-View-Controller architecture.
"""

import tkinter as tk
import random


class Model(object):
"""Crates a board and adds mines to it"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.create_grid()
self.add_mines()

def create_grid(self):
"""Create a self.width by self.height grid of elements with value 0"""
self.grid = [[0]*self.width for i in range(self.height)]

def add_mines(self):
"""Randomly adds the amount of self.num_mines to grid"""
def get_coords():
row = random.randint(0, self.height - 1)
col = random.randint(0, self.width - 1)
return row, col
for i in range(self.num_mines):
row, col = get_coords()
while self.grid[row][col] == "b":
row, col = get_coords()
self.grid[row][col] = "b"
for i in self.grid:
print (i)


class View(tk.Frame):
"""Creates a main window and grid of button cells"""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.width = width
self.height = height
self.num_mines = num_mines
self.master.title("Minesweeper")
self.grid()
self.top_panel = TopPanel(self.master, self.height,
self.width, self.num_mines)
self.create_widgets()

def create_widgets(self):
"""Create cell button widgets"""
self.buttons =
for i in range(self.height):
for j in range(self.width):
self.buttons[str(i) + "," + str(j)] = tk.Button(
self.master, width=5, bg="grey")
self.buttons[str(i) + "," + str(j)].grid(row=i+1, column=j+1)

def disp_loss(self):
"""Display the loss label when loss condition is reached"""
self.top_panel.loss_label.grid(row=0, columnspan=5)

def disp_win(self):
"""Display the win label when win condition is reached"""
self.top_panel.win_label.grid(row=0, columnspan=5)

def hide_labels(self, condition=None):
"""Hides labels based on condition argument"""
if condition:
self.top_panel.mines_left.grid_remove()
else:
self.top_panel.loss_label.grid_remove()
self.top_panel.win_label.grid_remove()


class TopPanel(tk.Frame):
"""Create top panel which houses reset button and win/loss and
mines left labels."""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.height = height
self.width = width
self.num_mines = num_mines
self.grid()
self.create_widgets()

def create_widgets(self):
self.reset_button = tk.Button(self.master, width = 7, text="Reset")
self.reset_button.grid(row=0, columnspan=int((self.width*7)/2))
# Create win and loss labels
self.loss_label = tk.Label(text="You Lose!", bg="red")
self.win_label = tk.Label(text="You Win!", bg="green")
# Create number of mines remaining label
self.mine_count = tk.StringVar()
self.mine_count.set("Mines remaining: " + str(self.num_mines))
self.mines_left = tk.Label(textvariable=self.mine_count)
self.mines_left.grid(row=0, columnspan=5)


class Controller(object):
"""Sets up button bindings and minsweeper game logic.

The act of revealing cells is delegated to the methods: give_val(),
reveal_cell(), reveal_adj(), and reveal_cont(). End conditions are handled
by the loss() and win() methods.
"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.model = Model(self.width, self.height, self.num_mines)
self.root = tk.Tk()
self.view = View(self.root, self.width, self.height, self.num_mines)
# self.color_dict is used to assign colors to cells
self.color_dict =
0: "white", 1:"blue", 2:"green",
3:"red", 4:"orange", 5:"purple",
6: "grey", 7:"grey", 8: "grey"

# Self.count keeps track of cells with value of 0 so that they
# get revealed with self.reveal_cont call only once
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.bindings()
self.root.mainloop()

def bindings(self):
"""Set up reveal cell and flag cell key bindings"""
for i in range(self.height):
for j in range(self.width):
# Right click bind to reveal decision method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-1>",
lambda event, index=[i, j]:self.reveal(event, index))
# Left click bind to flag method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-3>",
lambda event, index=[i, j]:self.flag(event, index))
# Set up reset button
self.view.top_panel.reset_button.bind("<Button>", self.reset)

def reset(self, event):
"""Resets game. Currently, game setup gets slower with each reset call,
and window height slightly increases"""
self.view.hide_labels()
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.model = Model(self.width, self.height, self.num_mines)
self.view = View(self.root, self.width,
self.height, self.num_mines)
self.bindings()

def reveal(self, event, index):
"""Main decision method determining how to reveal cell"""
i = index[0]
j = index[1]
val = self.give_val(index)
if val in [x for x in range(1, 9)]:
self.reveal_cell(val, index)
self.count.append(index)
if (val == "b" and self.game_state != "win" and
self.view.buttons[str(i) + "," + str(j)]["text"] != "FLAG"):
self.game_state = "Loss"
self.loss()
# Begin the revealing recursive method when cell value is 0
if val == 0:
self.reveal_cont(index)

def give_val(self, index):
"""Returns the number of adjacent mine. Returns "b" if cell is mine"""
i = index[0]
j = index[1]
num_mines = 0
try:
if self.model.grid[i][j] == "b":
return "b"
except IndexError:
pass
def increment():
try:
if self.model.grid[pos[0]][pos[1]] == "b":
return 1
except IndexError:
pass
return 0
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
#Adds 1 to num_mines if cell is adjacent to a mine
for pos in additions:
if 0 <= pos[0] <= self.height -1 and 0 <= pos[1] <= self.width - 1:
num_mines += increment()
return num_mines

def reveal_cell(self, value, index):
"""Reveals cell value and assigns an associated color for that value"""
i = index[0]
j = index[1]
cells_unrev = self.height * self.width - len(self.cells_revealed) - 1
button_key = str(i) + "," + str(j)
if self.view.buttons[button_key]["text"] == "FLAG":
pass
elif value == "b":
self.view.buttons[button_key].configure(bg="black")
else:
# Checks if cell is in the board limits
if (0 <= i <= self.height - 1 and
0 <= j <= self.width - 1 and
[button_key] not in self.cells_revealed):
self.view.buttons[button_key].configure(
text=value, bg=self.color_dict[value])
self.count.append(button_key)
self.cells_revealed.append([button_key])
# Removes cell from flagged list when the cell gets revealed
if button_key in self.cells_flagged:
self.cells_flagged.remove(button_key)
self.update_mines()
# Check for win condition
if (cells_unrev == self.num_mines and not self.game_state):
self.win()

def reveal_adj(self, index):
"""Reveals the 8 adjacent cells to the input cell index"""
org_val = self.give_val(index)
self.reveal_cell(org_val, index)
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width - 1):
new_val = self.give_val(pos)
self.reveal_cell(new_val, pos)

def reveal_cont(self, index):
"""Recursive formula that reveals all adjacent cells only if the
selected cell has no adjacent mines.
(meaning self.give_val(index) == 0)"""
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
val = self.give_val(index)
self.reveal_adj(index)
if val != 0:
return None
else:
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width -1 and
self.give_val(pos) == 0 and pos not in self.count):
self.count.append(pos)
self.reveal_cont(pos)

def win(self):
"""Display win"""
self.view.hide_labels("mine")
self.view.disp_win()
self.game_state = "win"

def loss(self):
"""Display loss. Reveal all cells when a mine is clicked"""
self.view.hide_labels("mine")
for i in range(self.height):
for j in range(self.width):
val = self.give_val([i, j])
self.reveal_cell(val, [i, j])
self.view.disp_loss()

def flag(self, event, index):
"""Allows player to flag cells for possible mines.
Does not reveal cell."""
i = index[0]
j = index[1]
button_key = str(i) + "," + str(j)
button_val = self.view.buttons[button_key]
if button_val["bg"] == "grey":
button_val.configure(bg="yellow", text="FLAG")
self.cells_flagged.append(button_key)
elif button_val["text"] == "FLAG":
button_val.configure(bg="grey", text="")
self.cells_flagged.remove(button_key)
self.update_mines()

def update_mines(self):
"""Update mine counter"""
mines_left = self.num_mines - len(self.cells_flagged)
if mines_left >= 0:
self.view.top_panel.mine_count.set(
"Mines remaining: " + str(mines_left))

def main():
n = input("Pick a difficulty: Easy, Medium, or Hard. ")
if n[0] == "E" or n[0] == "e":
return Controller(9, 9, 10)
elif n[0] == "M" or n[0] == "m":
return Controller(16, 16, 40)
elif n[0] == "H" or n[0] == "h":
return Controller(30, 16, 99)


if __name__ == "__main__":
main()






share|improve this question





















  • I'm working on it, you can see what I currently have here.
    – Solomon Ucko
    Mar 26 at 2:59













up vote
11
down vote

favorite
2









up vote
11
down vote

favorite
2






2





I am relatively new to programming, and I wish to use this simple minesweeper game in a portfolio. A few questions:



  1. Currently, game setup gets progressively slower with each reset button call, and the window height slightly increases downward. This is very apparent on medium and hard difficulties. What changes can speed up this code? I want to reuse the same window with each reset if possible.

  2. The code uses a Model-View-Controller approach. Does this make sense for a project using tkinter? Is there a better approach?

  3. Some parts such as the additions variable are repeated, but I cannot find a way to make them instance variables.

Any suggestions/constructive feedback is appreciated.



"""
Minesweeper

Implements a basic minesweeper game using tkinter.
Uses Model-View-Controller architecture.
"""

import tkinter as tk
import random


class Model(object):
"""Crates a board and adds mines to it"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.create_grid()
self.add_mines()

def create_grid(self):
"""Create a self.width by self.height grid of elements with value 0"""
self.grid = [[0]*self.width for i in range(self.height)]

def add_mines(self):
"""Randomly adds the amount of self.num_mines to grid"""
def get_coords():
row = random.randint(0, self.height - 1)
col = random.randint(0, self.width - 1)
return row, col
for i in range(self.num_mines):
row, col = get_coords()
while self.grid[row][col] == "b":
row, col = get_coords()
self.grid[row][col] = "b"
for i in self.grid:
print (i)


class View(tk.Frame):
"""Creates a main window and grid of button cells"""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.width = width
self.height = height
self.num_mines = num_mines
self.master.title("Minesweeper")
self.grid()
self.top_panel = TopPanel(self.master, self.height,
self.width, self.num_mines)
self.create_widgets()

def create_widgets(self):
"""Create cell button widgets"""
self.buttons =
for i in range(self.height):
for j in range(self.width):
self.buttons[str(i) + "," + str(j)] = tk.Button(
self.master, width=5, bg="grey")
self.buttons[str(i) + "," + str(j)].grid(row=i+1, column=j+1)

def disp_loss(self):
"""Display the loss label when loss condition is reached"""
self.top_panel.loss_label.grid(row=0, columnspan=5)

def disp_win(self):
"""Display the win label when win condition is reached"""
self.top_panel.win_label.grid(row=0, columnspan=5)

def hide_labels(self, condition=None):
"""Hides labels based on condition argument"""
if condition:
self.top_panel.mines_left.grid_remove()
else:
self.top_panel.loss_label.grid_remove()
self.top_panel.win_label.grid_remove()


class TopPanel(tk.Frame):
"""Create top panel which houses reset button and win/loss and
mines left labels."""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.height = height
self.width = width
self.num_mines = num_mines
self.grid()
self.create_widgets()

def create_widgets(self):
self.reset_button = tk.Button(self.master, width = 7, text="Reset")
self.reset_button.grid(row=0, columnspan=int((self.width*7)/2))
# Create win and loss labels
self.loss_label = tk.Label(text="You Lose!", bg="red")
self.win_label = tk.Label(text="You Win!", bg="green")
# Create number of mines remaining label
self.mine_count = tk.StringVar()
self.mine_count.set("Mines remaining: " + str(self.num_mines))
self.mines_left = tk.Label(textvariable=self.mine_count)
self.mines_left.grid(row=0, columnspan=5)


class Controller(object):
"""Sets up button bindings and minsweeper game logic.

The act of revealing cells is delegated to the methods: give_val(),
reveal_cell(), reveal_adj(), and reveal_cont(). End conditions are handled
by the loss() and win() methods.
"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.model = Model(self.width, self.height, self.num_mines)
self.root = tk.Tk()
self.view = View(self.root, self.width, self.height, self.num_mines)
# self.color_dict is used to assign colors to cells
self.color_dict =
0: "white", 1:"blue", 2:"green",
3:"red", 4:"orange", 5:"purple",
6: "grey", 7:"grey", 8: "grey"

# Self.count keeps track of cells with value of 0 so that they
# get revealed with self.reveal_cont call only once
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.bindings()
self.root.mainloop()

def bindings(self):
"""Set up reveal cell and flag cell key bindings"""
for i in range(self.height):
for j in range(self.width):
# Right click bind to reveal decision method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-1>",
lambda event, index=[i, j]:self.reveal(event, index))
# Left click bind to flag method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-3>",
lambda event, index=[i, j]:self.flag(event, index))
# Set up reset button
self.view.top_panel.reset_button.bind("<Button>", self.reset)

def reset(self, event):
"""Resets game. Currently, game setup gets slower with each reset call,
and window height slightly increases"""
self.view.hide_labels()
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.model = Model(self.width, self.height, self.num_mines)
self.view = View(self.root, self.width,
self.height, self.num_mines)
self.bindings()

def reveal(self, event, index):
"""Main decision method determining how to reveal cell"""
i = index[0]
j = index[1]
val = self.give_val(index)
if val in [x for x in range(1, 9)]:
self.reveal_cell(val, index)
self.count.append(index)
if (val == "b" and self.game_state != "win" and
self.view.buttons[str(i) + "," + str(j)]["text"] != "FLAG"):
self.game_state = "Loss"
self.loss()
# Begin the revealing recursive method when cell value is 0
if val == 0:
self.reveal_cont(index)

def give_val(self, index):
"""Returns the number of adjacent mine. Returns "b" if cell is mine"""
i = index[0]
j = index[1]
num_mines = 0
try:
if self.model.grid[i][j] == "b":
return "b"
except IndexError:
pass
def increment():
try:
if self.model.grid[pos[0]][pos[1]] == "b":
return 1
except IndexError:
pass
return 0
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
#Adds 1 to num_mines if cell is adjacent to a mine
for pos in additions:
if 0 <= pos[0] <= self.height -1 and 0 <= pos[1] <= self.width - 1:
num_mines += increment()
return num_mines

def reveal_cell(self, value, index):
"""Reveals cell value and assigns an associated color for that value"""
i = index[0]
j = index[1]
cells_unrev = self.height * self.width - len(self.cells_revealed) - 1
button_key = str(i) + "," + str(j)
if self.view.buttons[button_key]["text"] == "FLAG":
pass
elif value == "b":
self.view.buttons[button_key].configure(bg="black")
else:
# Checks if cell is in the board limits
if (0 <= i <= self.height - 1 and
0 <= j <= self.width - 1 and
[button_key] not in self.cells_revealed):
self.view.buttons[button_key].configure(
text=value, bg=self.color_dict[value])
self.count.append(button_key)
self.cells_revealed.append([button_key])
# Removes cell from flagged list when the cell gets revealed
if button_key in self.cells_flagged:
self.cells_flagged.remove(button_key)
self.update_mines()
# Check for win condition
if (cells_unrev == self.num_mines and not self.game_state):
self.win()

def reveal_adj(self, index):
"""Reveals the 8 adjacent cells to the input cell index"""
org_val = self.give_val(index)
self.reveal_cell(org_val, index)
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width - 1):
new_val = self.give_val(pos)
self.reveal_cell(new_val, pos)

def reveal_cont(self, index):
"""Recursive formula that reveals all adjacent cells only if the
selected cell has no adjacent mines.
(meaning self.give_val(index) == 0)"""
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
val = self.give_val(index)
self.reveal_adj(index)
if val != 0:
return None
else:
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width -1 and
self.give_val(pos) == 0 and pos not in self.count):
self.count.append(pos)
self.reveal_cont(pos)

def win(self):
"""Display win"""
self.view.hide_labels("mine")
self.view.disp_win()
self.game_state = "win"

def loss(self):
"""Display loss. Reveal all cells when a mine is clicked"""
self.view.hide_labels("mine")
for i in range(self.height):
for j in range(self.width):
val = self.give_val([i, j])
self.reveal_cell(val, [i, j])
self.view.disp_loss()

def flag(self, event, index):
"""Allows player to flag cells for possible mines.
Does not reveal cell."""
i = index[0]
j = index[1]
button_key = str(i) + "," + str(j)
button_val = self.view.buttons[button_key]
if button_val["bg"] == "grey":
button_val.configure(bg="yellow", text="FLAG")
self.cells_flagged.append(button_key)
elif button_val["text"] == "FLAG":
button_val.configure(bg="grey", text="")
self.cells_flagged.remove(button_key)
self.update_mines()

def update_mines(self):
"""Update mine counter"""
mines_left = self.num_mines - len(self.cells_flagged)
if mines_left >= 0:
self.view.top_panel.mine_count.set(
"Mines remaining: " + str(mines_left))

def main():
n = input("Pick a difficulty: Easy, Medium, or Hard. ")
if n[0] == "E" or n[0] == "e":
return Controller(9, 9, 10)
elif n[0] == "M" or n[0] == "m":
return Controller(16, 16, 40)
elif n[0] == "H" or n[0] == "h":
return Controller(30, 16, 99)


if __name__ == "__main__":
main()






share|improve this question













I am relatively new to programming, and I wish to use this simple minesweeper game in a portfolio. A few questions:



  1. Currently, game setup gets progressively slower with each reset button call, and the window height slightly increases downward. This is very apparent on medium and hard difficulties. What changes can speed up this code? I want to reuse the same window with each reset if possible.

  2. The code uses a Model-View-Controller approach. Does this make sense for a project using tkinter? Is there a better approach?

  3. Some parts such as the additions variable are repeated, but I cannot find a way to make them instance variables.

Any suggestions/constructive feedback is appreciated.



"""
Minesweeper

Implements a basic minesweeper game using tkinter.
Uses Model-View-Controller architecture.
"""

import tkinter as tk
import random


class Model(object):
"""Crates a board and adds mines to it"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.create_grid()
self.add_mines()

def create_grid(self):
"""Create a self.width by self.height grid of elements with value 0"""
self.grid = [[0]*self.width for i in range(self.height)]

def add_mines(self):
"""Randomly adds the amount of self.num_mines to grid"""
def get_coords():
row = random.randint(0, self.height - 1)
col = random.randint(0, self.width - 1)
return row, col
for i in range(self.num_mines):
row, col = get_coords()
while self.grid[row][col] == "b":
row, col = get_coords()
self.grid[row][col] = "b"
for i in self.grid:
print (i)


class View(tk.Frame):
"""Creates a main window and grid of button cells"""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.width = width
self.height = height
self.num_mines = num_mines
self.master.title("Minesweeper")
self.grid()
self.top_panel = TopPanel(self.master, self.height,
self.width, self.num_mines)
self.create_widgets()

def create_widgets(self):
"""Create cell button widgets"""
self.buttons =
for i in range(self.height):
for j in range(self.width):
self.buttons[str(i) + "," + str(j)] = tk.Button(
self.master, width=5, bg="grey")
self.buttons[str(i) + "," + str(j)].grid(row=i+1, column=j+1)

def disp_loss(self):
"""Display the loss label when loss condition is reached"""
self.top_panel.loss_label.grid(row=0, columnspan=5)

def disp_win(self):
"""Display the win label when win condition is reached"""
self.top_panel.win_label.grid(row=0, columnspan=5)

def hide_labels(self, condition=None):
"""Hides labels based on condition argument"""
if condition:
self.top_panel.mines_left.grid_remove()
else:
self.top_panel.loss_label.grid_remove()
self.top_panel.win_label.grid_remove()


class TopPanel(tk.Frame):
"""Create top panel which houses reset button and win/loss and
mines left labels."""
def __init__(self, master, width, height, num_mines):
tk.Frame.__init__(self, master)
self.master = master
self.height = height
self.width = width
self.num_mines = num_mines
self.grid()
self.create_widgets()

def create_widgets(self):
self.reset_button = tk.Button(self.master, width = 7, text="Reset")
self.reset_button.grid(row=0, columnspan=int((self.width*7)/2))
# Create win and loss labels
self.loss_label = tk.Label(text="You Lose!", bg="red")
self.win_label = tk.Label(text="You Win!", bg="green")
# Create number of mines remaining label
self.mine_count = tk.StringVar()
self.mine_count.set("Mines remaining: " + str(self.num_mines))
self.mines_left = tk.Label(textvariable=self.mine_count)
self.mines_left.grid(row=0, columnspan=5)


class Controller(object):
"""Sets up button bindings and minsweeper game logic.

The act of revealing cells is delegated to the methods: give_val(),
reveal_cell(), reveal_adj(), and reveal_cont(). End conditions are handled
by the loss() and win() methods.
"""
def __init__(self, width, height, num_mines):
self.width = width
self.height = height
self.num_mines = num_mines
self.model = Model(self.width, self.height, self.num_mines)
self.root = tk.Tk()
self.view = View(self.root, self.width, self.height, self.num_mines)
# self.color_dict is used to assign colors to cells
self.color_dict =
0: "white", 1:"blue", 2:"green",
3:"red", 4:"orange", 5:"purple",
6: "grey", 7:"grey", 8: "grey"

# Self.count keeps track of cells with value of 0 so that they
# get revealed with self.reveal_cont call only once
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.bindings()
self.root.mainloop()

def bindings(self):
"""Set up reveal cell and flag cell key bindings"""
for i in range(self.height):
for j in range(self.width):
# Right click bind to reveal decision method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-1>",
lambda event, index=[i, j]:self.reveal(event, index))
# Left click bind to flag method
self.view.buttons[str(i) + "," + str(j)].bind(
"<Button-3>",
lambda event, index=[i, j]:self.flag(event, index))
# Set up reset button
self.view.top_panel.reset_button.bind("<Button>", self.reset)

def reset(self, event):
"""Resets game. Currently, game setup gets slower with each reset call,
and window height slightly increases"""
self.view.hide_labels()
self.count =
self.cells_revealed =
self.cells_flagged =
self.game_state = None
self.model = Model(self.width, self.height, self.num_mines)
self.view = View(self.root, self.width,
self.height, self.num_mines)
self.bindings()

def reveal(self, event, index):
"""Main decision method determining how to reveal cell"""
i = index[0]
j = index[1]
val = self.give_val(index)
if val in [x for x in range(1, 9)]:
self.reveal_cell(val, index)
self.count.append(index)
if (val == "b" and self.game_state != "win" and
self.view.buttons[str(i) + "," + str(j)]["text"] != "FLAG"):
self.game_state = "Loss"
self.loss()
# Begin the revealing recursive method when cell value is 0
if val == 0:
self.reveal_cont(index)

def give_val(self, index):
"""Returns the number of adjacent mine. Returns "b" if cell is mine"""
i = index[0]
j = index[1]
num_mines = 0
try:
if self.model.grid[i][j] == "b":
return "b"
except IndexError:
pass
def increment():
try:
if self.model.grid[pos[0]][pos[1]] == "b":
return 1
except IndexError:
pass
return 0
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
#Adds 1 to num_mines if cell is adjacent to a mine
for pos in additions:
if 0 <= pos[0] <= self.height -1 and 0 <= pos[1] <= self.width - 1:
num_mines += increment()
return num_mines

def reveal_cell(self, value, index):
"""Reveals cell value and assigns an associated color for that value"""
i = index[0]
j = index[1]
cells_unrev = self.height * self.width - len(self.cells_revealed) - 1
button_key = str(i) + "," + str(j)
if self.view.buttons[button_key]["text"] == "FLAG":
pass
elif value == "b":
self.view.buttons[button_key].configure(bg="black")
else:
# Checks if cell is in the board limits
if (0 <= i <= self.height - 1 and
0 <= j <= self.width - 1 and
[button_key] not in self.cells_revealed):
self.view.buttons[button_key].configure(
text=value, bg=self.color_dict[value])
self.count.append(button_key)
self.cells_revealed.append([button_key])
# Removes cell from flagged list when the cell gets revealed
if button_key in self.cells_flagged:
self.cells_flagged.remove(button_key)
self.update_mines()
# Check for win condition
if (cells_unrev == self.num_mines and not self.game_state):
self.win()

def reveal_adj(self, index):
"""Reveals the 8 adjacent cells to the input cell index"""
org_val = self.give_val(index)
self.reveal_cell(org_val, index)
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width - 1):
new_val = self.give_val(pos)
self.reveal_cell(new_val, pos)

def reveal_cont(self, index):
"""Recursive formula that reveals all adjacent cells only if the
selected cell has no adjacent mines.
(meaning self.give_val(index) == 0)"""
i = index[0]
j = index[1]
additions = [
[i,j+1], [i+1,j], [i+1,j+1], [i,j-1],
[i+1,j-1], [i-1,j], [i-1,j+1], [i-1,j-1]
]
val = self.give_val(index)
self.reveal_adj(index)
if val != 0:
return None
else:
for pos in additions:
if (0 <= pos[0] <= self.height - 1 and
0 <= pos[1] <= self.width -1 and
self.give_val(pos) == 0 and pos not in self.count):
self.count.append(pos)
self.reveal_cont(pos)

def win(self):
"""Display win"""
self.view.hide_labels("mine")
self.view.disp_win()
self.game_state = "win"

def loss(self):
"""Display loss. Reveal all cells when a mine is clicked"""
self.view.hide_labels("mine")
for i in range(self.height):
for j in range(self.width):
val = self.give_val([i, j])
self.reveal_cell(val, [i, j])
self.view.disp_loss()

def flag(self, event, index):
"""Allows player to flag cells for possible mines.
Does not reveal cell."""
i = index[0]
j = index[1]
button_key = str(i) + "," + str(j)
button_val = self.view.buttons[button_key]
if button_val["bg"] == "grey":
button_val.configure(bg="yellow", text="FLAG")
self.cells_flagged.append(button_key)
elif button_val["text"] == "FLAG":
button_val.configure(bg="grey", text="")
self.cells_flagged.remove(button_key)
self.update_mines()

def update_mines(self):
"""Update mine counter"""
mines_left = self.num_mines - len(self.cells_flagged)
if mines_left >= 0:
self.view.top_panel.mine_count.set(
"Mines remaining: " + str(mines_left))

def main():
n = input("Pick a difficulty: Easy, Medium, or Hard. ")
if n[0] == "E" or n[0] == "e":
return Controller(9, 9, 10)
elif n[0] == "M" or n[0] == "m":
return Controller(16, 16, 40)
elif n[0] == "H" or n[0] == "h":
return Controller(30, 16, 99)


if __name__ == "__main__":
main()








share|improve this question












share|improve this question




share|improve this question








edited Mar 25 at 23:16









200_success

123k14142399




123k14142399









asked Mar 25 at 21:26









EndreoT

564




564











  • I'm working on it, you can see what I currently have here.
    – Solomon Ucko
    Mar 26 at 2:59

















  • I'm working on it, you can see what I currently have here.
    – Solomon Ucko
    Mar 26 at 2:59
















I'm working on it, you can see what I currently have here.
– Solomon Ucko
Mar 26 at 2:59





I'm working on it, you can see what I currently have here.
– Solomon Ucko
Mar 26 at 2:59











3 Answers
3






active

oldest

votes

















up vote
5
down vote













It's a good idea to use the Model–View–Controller pattern. But the implementation needs some work.



In MVC, the model should contain the complete description of the data being manipulated, together with the operations on that data. In the case of a minesweeper game, the model should consist of the following data:



  1. the size of the playing area;

  2. the locations of the mines;

  3. which squares have been uncovered so far;

  4. the locations of the flags;

  5. the state of the game (win/loss/still playing);

together with the operations:



  1. set or clear a flag;

  2. uncover a square;

  3. start a new game.

The idea is that you should be able to port the program to a different kind of interface by swapping out the view and controller, and leaving the model unchanged. But in the implementation in the post, most of the data, and all the operations, have gone into the controller instead. This makes it inconvenient to swap out the controller as all of this would have to be reimplemented in the new controller.






share|improve this answer




























    up vote
    4
    down vote













    The game looks great! The code looks pretty good as well!



    I definitely agree with Gareth Rees about actually separating the parts of the MVC.



    What I changed



    1. I added type hints to all the functions.

    2. I fixed a few typos, as well as adding periods to the ends of the comments.

    3. I changed most of the initializer function calls to be functional-ish (changed from assigning within the function to returning the value from the function and doing the assignment within the initializer).

    4. I changed a lot of the for loops to iterator 'math'.

    5. I extracted out the list of adjacent cells to its own function, as it was repeated a lot.

    6. I changed some of the data types from lists (or strings) to sets or tuples.

    7. I replaced tuple indexing with tuple unpacking.

    8. I attempted to reduce the repetition within the main function.

    9. Maybe a few other smaller things as well.

    What still needs to be done



    1. Separation of the MVC components.

    2. It would be nice if the prompt for the difficulty was GUI-based and it was displayed after each reset.

    The code



    """
    Minesweeper

    Implements a basic minesweeper game using tkinter.
    Uses Model-View-Controller architecture.
    """

    from functools import reduce
    from itertools import product
    from operator import add
    from random import sample
    from tkinter import Button, Frame, Label, StringVar, Tk
    from typing import Set, Tuple


    class Model(object):
    """Creates a board and adds mines to it."""

    def __init__(self, width: int, height: int, num_mines: int):
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.grid = self.create_grid()
    self.add_mines()

    def create_grid(self):
    """Create a self.width by self.height grid of elements with value 0."""

    return [[0] * self.width for _ in range(self.height)]

    def add_mines(self):
    """Randomly adds the amount of self.num_mines to grid."""

    for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
    self.grid[x][y] = 'm'


    class View(Frame):
    """Creates a main window and grid of button cells."""

    def __init__(self, master: Tk, width: int, height: int, num_mines: int):
    Frame.__init__(self, master)
    self.master = master
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.master.title('Minesweeper')
    self.grid()
    self.top_panel = TopPanel(self.master, self.height, self.width, self.num_mines)
    self.buttons = self.create_buttons()

    def create_buttons(self):
    """Create cell button widgets."""

    def create_button(x, y):
    button = Button(self.master, width=5, bg='grey')
    button.grid(row=x + 1, column=y + 1)
    return button

    return [[create_button(x, y) for y in range(self.height)] for x in range(self.width)]

    def display_lose(self):
    """Display the lose label when lose condition is reached."""

    self.top_panel.loss_label.grid(row=0, columnspan=5)

    def display_win(self):
    """Display the win label when win condition is reached."""

    self.top_panel.win_label.grid(row=0, columnspan=5)

    def hide_labels(self, condition=None):
    """Hides labels based on condition argument."""

    if condition:
    self.top_panel.mines_left.grid_remove()
    else:
    self.top_panel.loss_label.grid_remove()
    self.top_panel.win_label.grid_remove()


    class TopPanel(Frame):
    """Create top panel which houses reset button and win/lose and mines left labels."""

    def __init__(self, master: Tk, width: int, height: int, num_mines: int):
    Frame.__init__(self, master)
    self.master = master
    self.num_mines = num_mines
    self.grid()

    self.reset_button = Button(self.master, width=7, text='Reset')
    self.reset_button.grid(row=0, columnspan=int((width * 7) / 2))

    self.loss_label = Label(text='You Lose!', bg='red')
    self.win_label = Label(text='You Win!', bg='green')

    self.mine_count = StringVar()
    self.mine_count.set('Mines remaining: ' + str(self.num_mines))
    self.mines_left = Label(textvariable=self.mine_count)
    self.mines_left.grid(row=0, columnspan=5)


    def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
    x, y = index

    return
    (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
    (x - 1, y), (x + 1, y),
    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



    class Controller(object):
    """Sets up button bindings and minesweeper game logic.

    The act of revealing cells is delegated to the methods: adjacent_mine_count(), reveal_cell(), reveal_adjacent(), and reveal_cont(). End conditions are handled by the lose() and win() methods.
    """

    def __init__(self, width: int, height: int, num_mines: int):
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.model = Model(self.width, self.height, self.num_mines)
    self.root = Tk()
    self.view = View(self.root, self.width, self.height, self.num_mines)
    # self.color_dict is used to assign colors to cells
    self.color_dict =
    0: 'white', 1: 'blue', 2: 'green',
    3: 'red', 4: 'orange', 5: 'purple',
    6: 'grey', 7: 'grey', 8: 'grey'

    # self.count keeps track of cells with value of 0 so that they get revealed with self.reveal_cont call only once.
    self.count = set()
    self.cells_revealed = set()
    self.cells_flagged = set()
    self.game_state = None
    self.initialize_bindings()
    self.root.mainloop()

    def initialize_bindings(self):
    """Set up reveal cell and flag cell key bindings."""

    for x in range(self.height):
    for y in range(self.width):
    def closure_helper(f, index):
    def g(_): f(index)

    return g

    # Right click bind to reveal decision method
    self.view.buttons[x][y].bind('<Button-1>', closure_helper(self.reveal, (x, y)))

    # Left click bind to flag method
    self.view.buttons[x][y].bind('<Button-3>', closure_helper(self.flag, (x, y)))

    # Set up reset button
    self.view.top_panel.reset_button.bind('<Button>', lambda event: self.reset())

    def reset(self):
    """Resets game. Currently, game setup gets slower with each reset call, and window height slightly increases."""

    self.view.hide_labels()
    self.count = set()
    self.cells_revealed = set()
    self.cells_flagged = set()
    self.game_state = None
    self.model = Model(self.width, self.height, self.num_mines)
    self.view = View(self.root, self.width, self.height, self.num_mines)
    self.initialize_bindings()

    def reveal(self, index: Tuple[int, int]):
    """Main decision method determining how to reveal cell."""

    x, y = index
    val = self.adjacent_mine_count(index)

    if val in range(1, 9):
    self.reveal_cell(index)
    self.count.add(index)

    if self.model.grid[x][y] == 'm' and self.game_state != 'win' and self.view.buttons[x][y]['text'] != 'FLAG':
    self.game_state = 'Loss'
    self.lose()

    # Begin the revealing recursive method when cell value is 0
    if val == 0:
    self.reveal_cont(index)

    def adjacent_mine_count(self, index: Tuple[int, int]) -> int:
    """Returns the number of adjacent mines."""

    def is_mine(pos):
    try:
    return self.model.grid[pos[0]][pos[1]] == 'm'
    except IndexError:
    return False

    return reduce(add, map(is_mine, get_adjacent(index)))

    def reveal_cell(self, index: Tuple[int, int]):
    """Reveals cell value and assigns an associated color for that value."""

    x, y = index

    cells_unrevealed = self.height * self.width - len(self.cells_revealed) - 1

    if self.view.buttons[x][y]['text'] == 'FLAG':
    pass
    elif self.model.grid[x][y] == 'm':
    self.view.buttons[x][y].configure(bg='black')
    else:
    # Checks if cell is in the board limits
    if 0 <= x <= self.height - 1 and 0 <= y <= self.width - 1 and index not in self.cells_revealed:
    value = self.adjacent_mine_count(index)

    self.view.buttons[x][y].configure(text=value, bg=self.color_dict[value])
    self.count.add(index)
    self.cells_revealed.add(index)

    # Removes cell from flagged list when the cell gets revealed
    if index in self.cells_flagged:
    self.cells_flagged.remove(index)
    self.update_mines()

    # Check for win condition
    if cells_unrevealed == self.num_mines and not self.game_state:
    self.win()

    def reveal_adjacent(self, index: Tuple[int, int]):
    """Reveals the 8 adjacent cells to the input cell index."""

    for pos in get_adjacent(index) | index:
    if 0 <= pos[0] <= self.height - 1 and 0 <= pos[1] <= self.width - 1:
    self.reveal_cell(pos)

    def reveal_cont(self, index: Tuple[int, int]):
    """Recursive formula that reveals all adjacent cells only if the selected cell has no adjacent mines. (meaning self.adjacent_mine_count(index) == 0)."""

    val = self.adjacent_mine_count(index)

    if val == 0:
    self.reveal_adjacent(index)

    for pos in get_adjacent(index):
    if (
    0 <= pos[0] <= self.height - 1
    and 0 <= pos[1] <= self.width - 1
    and self.adjacent_mine_count(pos) == 0
    and pos not in self.count
    ):
    self.count.add(pos)
    self.reveal_cont(pos)

    def win(self):
    """Display win."""

    self.view.hide_labels('mine')
    self.view.display_win()
    self.game_state = 'win'

    def lose(self):
    """Display lose. Reveal all cells when a mine is clicked."""

    self.view.hide_labels('mine')

    for x in range(self.height):
    for y in range(self.width):
    self.reveal_cell((x, y))

    self.view.display_lose()

    def flag(self, index: Tuple[int, int]):
    """Allows player to flag cells for possible mines. Does not reveal cell."""

    x, y = index

    button_val = self.view.buttons[x][y]

    if button_val['bg'] == 'grey':
    button_val.configure(bg='yellow', text='FLAG')
    self.cells_flagged.add(index)
    elif button_val['text'] == 'FLAG':
    button_val.configure(bg='grey', text='')
    self.cells_flagged.remove(index)

    self.update_mines()

    def update_mines(self):
    """Update mine counter."""

    mines_left = self.num_mines - len(self.cells_flagged)

    if mines_left >= 0:
    self.view.top_panel.mine_count.set(f'Mines remaining: mines_left')


    def main():
    n = input('Pick a difficulty: Easy, Medium, or Hard: ')

    return Controller(*
    'e': (9, 9, 10),
    'm': (16, 16, 40),
    'h': (30, 16, 99)
    [n.lower()])


    if __name__ == '__main__':
    main()


    Please let me know if I missed anything important and if anything needs clarification.






    share|improve this answer





















    • Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
      – EndreoT
      Mar 28 at 18:16










    • Not really sure. It might fix itself once you restructure his changes.
      – Solomon Ucko
      Mar 28 at 19:14










    • Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
      – Zeta
      Mar 30 at 14:14










    • @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
      – Solomon Ucko
      Mar 30 at 14:18

















    up vote
    0
    down vote













    Here is the revised minesweeper game with a controller that can use different interfaces. It now includes a text interface for the masochists. Also, resetting the game now destroys the root window and creates a new one. Credit for suggestions and edits are given to @Gareth and @Solomon. Anyone with tkinter experience have an answer on how to reuse the same tk window during game reset so that the previously posted problems do not happen?



    """
    Minesweeper

    Implements a basic minesweeper game using the tkinter module.
    Uses a Model-View-Controller structure.
    """

    from functools import reduce
    from itertools import product
    from operator import add
    from random import sample
    from tkinter import Button, Frame, Label, StringVar, Tk
    from typing import Set, Tuple


    def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
    """Returns adjacent coordinates for input index"""

    x, y = index

    return
    (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
    (x - 1, y), (x + 1, y),
    (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



    class Model(object):
    """Creates a board and adds mines to it."""

    def __init__(self, width: int, height: int, num_mines: int):
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.grid = self.create_grid()
    self.add_mines()
    self.grid_coords = self.grid_coords()
    self.adjacent_mine_count()
    self.cells_revealed = set()
    self.cells_flagged = set()
    self.revealed_zeroes = set()
    self.game_state = None

    def create_grid(self) -> list:
    """Returns a (width by height) grid of elements with value of 0."""

    return [[0] * self.width for _ in range(self.height)]

    def add_mines(self):
    """Randomly adds mines to board grid."""

    for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
    self.grid[y][x] = 'm'

    def grid_coords(self) -> list:
    """Returns a list of (x, y) coordinates for every position on grid."""

    return [(x, y) for y in range(self.height) for x in range(self.width)]

    def adjacent_mine_count(self):
    """Sets cell values to the number of their adjacent mines."""

    def is_mine(coords):
    try:
    if coords[0] >= 0 and coords[1] >= 0:
    return self.grid[coords[1]][coords[0]] == 'm'
    else:
    return False
    except IndexError:
    return False

    for position in self.grid_coords:
    x, y = position
    if self.grid[y][x] != "m":
    grid_value = reduce(add, map(is_mine, get_adjacent(position)))
    self.grid[y][x] = grid_value

    def get_cell_value(self, index: Tuple[int, int]) -> int or str:
    """Returns model's cell value at the given index."""

    x, y = index
    return self.grid[y][x]

    class View(Frame):
    """Creates a GUI with a grid of cell buttons."""

    def __init__(self, width: int, height: int,
    num_mines: int, difficulty: str, controller: "Controller"):
    self.master = Tk()
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.difficulty = difficulty
    self.controller = controller
    self.color_dict =
    0: 'white', 1: 'blue', 2: 'green',
    3: 'red', 4: 'orange', 5: 'purple',
    6: 'grey', 7: 'grey', 8: 'grey', "m": "black"

    self.master.title('Minesweeper')

    def create_buttons(self) -> list:
    """Create cell button widgets."""

    def create_button(x, y):
    button = Button(self.master, width=5, bg='grey')
    button.grid(row=y + 5, column=x + 1)
    return button

    return [[create_button(x, y) for x in range(self.width)]
    for y in range(self.height)]

    def initialize_bindings(self):
    """Set up the reveal cell and the flag cell key bindings."""

    for x in range(self.width):
    for y in range(self.height):
    def closure_helper(f, index):
    def g(_):
    f(index)
    return g

    # Bind reveal decision method to left click
    self.buttons[y][x].bind(
    '<Button-1>', closure_helper(
    self.controller.reveal_decision, (x, y)))

    # Bind flag method to right click
    self.buttons[y][x].bind(
    '<Button-3>', closure_helper(
    self.controller.update_flagged_cell, (x, y)))

    # Set up reset button
    self.top_panel.reset_button.bind(
    '<Button>', lambda event: self.controller.reset(event))

    def reset_view(self):
    """Destroys the GUI. Controller will create a new GUI"""

    self.master.destroy()

    def reveal_cell(self, index: Tuple[int, int], value: int or str):
    """Reveals cell's value on GUI."""

    x, y = index
    self.buttons[y][x].configure(text=value, bg=self.color_dict[value])

    def flag_cell(self, index: Tuple[int, int]):
    """Flag cell in GUI"""

    x, y = index
    self.buttons[y][x].configure(text="FLAG", bg="yellow")

    def unflag_cell(self, index: Tuple[int, int]):
    """Unflag cell in GUI"""
    x, y = index
    self.buttons[y][x].configure(text="", bg="grey")

    def update_mines_left(self, mines: int):
    """Updates mine counter widget"""

    self.top_panel.mine_count.set("Mines remaining: " + str(mines))

    def display_loss(self):
    """Display the loss label when lose condition is reached."""

    self.top_panel.loss_label.grid(row=0, columnspan=10)

    def display_win(self):
    """Display the win label when win condition is reached."""

    self.top_panel.win_label.grid(row=0, columnspan=10)

    def mainloop(self):
    self.top_panel = TopPanel(self.master, self.height,
    self.width, self.num_mines)
    self.buttons = self.create_buttons()
    self.top_panel.mines_left.grid(row=0, columnspan=5)
    self.initialize_bindings()
    self.master.mainloop()


    class TopPanel(Frame):
    """Creates a top panel which contains game information."""

    def __init__(self, master: Tk, width: int, height: int, num_mines: int):
    Frame.__init__(self, master)
    self.master = master
    self.num_mines = num_mines
    self.grid()

    self.reset_button = Button(self.master, width=7, text='Reset')
    self.reset_button.grid(row=0)

    self.loss_label = Label(text='You Lose!', bg='red')
    self.win_label = Label(text='You Win!', bg='green')

    self.mine_count = StringVar()
    self.mine_count.set('Mines remaining: ' + str(self.num_mines))
    self.mines_left = Label(textvariable=self.mine_count)


    class TextView(object):
    """Creates a text interface of the minesweeper game."""

    def __init__(self, width: int, height: int,
    num_mines: int, difficulty: str, controller: "Controller"):
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.controller = controller
    self.reveal_dict =
    0: ' 0 ', 1: ' 1 ', 2: ' 2 ',
    3: ' 3 ', 4: ' 4 ', 5: ' 5 ',
    6: ' 6 ', 7: ' 7 ', 8: ' 8 ', "m": "mine"

    self.cell_view = self.cell_view()
    self.show_grid()

    def cell_view(self)-> list:
    """Create text view of cells."""

    return [["cell" for x in range(self.width)]
    for y in range(self.height)]

    def show_grid(self):
    """Prints text grid to console. Includes column numbers."""

    top_row = [str(i) for i in range(self.width)]
    print(" ", *top_row, sep=" "*5)
    for row in range(len(self.cell_view)):
    print(str(row) + ":", *self.cell_view[row], sep=" ")

    def reveal_cell(self, index: Tuple[int, int], value: int or str):
    """Reveals a cell's value in the text view"""

    x, y = index
    self.cell_view[y][x] = self.reveal_dict[value]

    def flag_cell(self, index: Tuple[int, int]):
    """Flags cell in cell_view"""

    x, y = index
    self.cell_view[y][x] = "FLAG"

    def unflag_cell(self, index: Tuple[int, int]):
    """Unflags cell in cell_view"""

    x, y = index
    self.cell_view[y][x] = "cell"

    def update_mines_left(self, mines):
    """Updates mine counter."""

    print("Mines remaining: " + str(mines))

    def display_loss(self):
    """Displays the lose label when loss condition is reached."""

    print("You Lose!")

    def display_win(self):
    """Displays the win label when win condition is reached."""

    print("You Win!")

    def mainloop(self):
    while True:
    try:
    cmd, *coords = input(
    "Choose a cell in the format: "
    + "flag/reveal x y. Type END to quit. ").split()
    if cmd.lower()[0] == "e":
    break
    x, y = coords[0], coords[1]
    if cmd.lower()[0] == "f":
    self.controller.update_flagged_cell((int(x), int(y)))
    elif cmd.lower()[0] == "r":
    self.controller.reveal_decision((int(x), int(y)))
    else:
    print("Unknown command")
    self.show_grid()
    except:
    print("Incorrect selection or format")


    class Controller(object):
    """Sets up button bindings and minesweeper game logic.

    Reveal_decision determines how to reveal cells.
    End conditions are handled by the loss and win methods.
    """

    def __init__(self, width: int, height: int,
    num_mines: int, difficulty: str, view_type: str):
    self.width = width
    self.height = height
    self.num_mines = num_mines
    self.difficulty = difficulty
    self.model = Model(self.width, self.height, self.num_mines)
    if view_type == "GUI":
    self.view = View(self.width, self.height,
    self.num_mines, self.difficulty, self)
    elif view_type == "TEXT":
    self.view = TextView(self.width, self.height,
    self.num_mines, self.difficulty, self)
    self.view.mainloop()

    def reset(self, event):
    """Resets the game"""

    self.view.reset_view()
    self.model = Model(self.width, self.height, self.num_mines)
    self.view = View(self.width, self.height,
    self.num_mines, self.difficulty, self)
    self.view.mainloop()

    def reveal_decision(self, index: Tuple[int, int]):
    """Main decision method determining how to reveal cell."""

    x, y = index

    cell_value = self.model.get_cell_value(index)
    if index in self.model.cells_flagged:
    return None

    if cell_value in range(1, 9):
    self.reveal_cell(index, cell_value)

    elif (
    self.model.grid[y][x] == "m"
    and self.model.game_state != "win"
    ):
    self.loss()

    else:
    self.reveal_zeroes(index)

    # Check for win condition
    cells_unrevealed = self.height * self.width - len(self.model.cells_revealed)
    if cells_unrevealed == self.num_mines and self.model.game_state != "loss":
    self.win()

    def reveal_cell(self, index: Tuple[int, int], value: int or str):
    """Obtains cell value from model and passes the value to view."""

    if index in self.model.cells_flagged:
    return None
    else:
    self.model.cells_revealed.add(index)
    self.view.reveal_cell(index, value)

    def reveal_adjacent(self, index: Tuple[int, int]):
    """Reveals the 8 adjacent cells to the input cell's index."""

    for coords in get_adjacent(index):
    if (
    0 <= coords[0] <= self.width - 1
    and 0 <= coords[1] <= self.height - 1
    ):
    cell_value = self.model.get_cell_value(coords)
    self.reveal_cell(coords, cell_value)

    def reveal_zeroes(self, index: Tuple[int, int]):
    """Reveals all adjacent cells just until a mine is reached."""

    val = self.model.get_cell_value(index)

    if val == 0:
    self.reveal_cell(index, val)
    self.reveal_adjacent(index)

    for coords in get_adjacent(index):
    if (
    0 <= coords[0] <= self.width - 1
    and 0 <= coords[1] <= self.height - 1
    and self.model.get_cell_value(coords) == 0
    and coords not in self.model.revealed_zeroes
    ):
    self.model.revealed_zeroes.add(coords)
    self.reveal_zeroes(coords)

    def update_flagged_cell(self, index: Tuple[int, int]):
    """Flag/unflag cells for possible mines. Does not reveal cell."""

    if (
    index not in self.model.cells_revealed
    and index not in self.model.cells_flagged
    ):
    self.model.cells_flagged.add(index)
    self.view.flag_cell(index)

    elif (
    index not in self.model.cells_revealed
    and index in self.model.cells_flagged
    ):
    self.model.cells_flagged.remove(index)
    self.view.unflag_cell(index)

    self.update_mines()

    def update_mines(self):
    """Update mine counter."""

    mines_left = self.num_mines - len(self.model.cells_flagged)

    if mines_left >= 0:
    self.view.update_mines_left(mines_left)

    def win(self):
    """Sweet sweet victory."""

    self.model.game_state = "win"
    self.view.display_win()

    def loss(self):
    """Show loss, and reveal all cells."""

    self.model.game_state = "loss"
    self.view.display_loss()

    # Reveals all cells
    for row in range(self.height):
    for col in range(self.width):
    cell_value = self.model.get_cell_value((col,row))
    self.view.reveal_cell((col, row), cell_value)


    class InitializeGame(Frame):
    """Sets up minesweepergame. Allows player to choose difficulty"""

    def __init__(self):
    self.root = Tk()
    self.create_view_choice()
    self.create_difficulty_widgets()
    self.root.mainloop()

    def create_view_choice(self):
    "Creates widgets allowing player to choose a view type."""

    self.view_label = Label(self.root, text="Choose a view type")
    self.view_label.grid()
    self.view_types = ["GUI", "TEXT"]
    def create_button(view_type):
    button = Button(self.root, width=7, bg='grey', text=view_type)
    button.grid()
    return button

    self.view_widgets = [
    create_button(view_type) for view_type in self.view_types
    ] + [self.view_label]

    for i in range(2):
    def closure_helper(f, view_choice):
    def g(_):
    f(view_choice)
    return g
    self.view_widgets[i].bind("<Button>", closure_helper(
    self.set_up_difficulty_widgets, self.view_types[i]))

    def create_difficulty_widgets(self):
    """Set up widgets at start of game for difficulty."""

    self.diff_label = Label(self.root, text="Choose a difficulty")
    self.difficulty = ("Easy", "Medium", "Hard")
    def create_button(difficulty):
    button = Button(self.root, width=7, bg='grey', text=difficulty)
    return button

    self.difficulty_widgets = [create_button(diff)
    for diff in self.difficulty]
    self.difficulty_widgets = [self.diff_label] + self.difficulty_widgets

    def set_up_difficulty_widgets(self, view_type: str):
    """Removes view widgets. Sets up difficulty options for view chosen."""

    for widget in self.view_widgets:
    widget.grid_remove()

    if view_type == "TEXT":
    self.difficulty_widgets[0].grid()
    self.difficulty_widgets[1].grid()
    else:
    for widget in self.difficulty_widgets:
    widget.grid()
    self.bind_difficulty_widgets(view_type)

    def bind_difficulty_widgets(self, view_type: str):
    """Binds difficulty buttons."""

    for i in range(1, 4):
    def closure_helper(f, difficulty, view_type):
    def g(_):
    f(difficulty, view_type)
    return g
    self.difficulty_widgets[i].bind(
    "<Button>", closure_helper(
    self.init_game, self.difficulty[i - 1], view_type))

    def init_game(self, difficulty: str, view_type: str):
    """Begins game."""

    self.root.destroy()
    return Controller(*
    'E': (10, 10, 10, difficulty, view_type),
    'M': (16, 16, 40, difficulty, view_type),
    'H': (25, 20, 99, difficulty, view_type)
    [difficulty[0]])


    if __name__ == "__main__":
    game = InitializeGame()





    share|improve this answer





















      Your Answer




      StackExchange.ifUsing("editor", function ()
      return StackExchange.using("mathjaxEditing", function ()
      StackExchange.MarkdownEditor.creationCallbacks.add(function (editor, postfix)
      StackExchange.mathjaxEditing.prepareWmdForMathJax(editor, postfix, [["\$", "\$"]]);
      );
      );
      , "mathjax-editing");

      StackExchange.ifUsing("editor", function ()
      StackExchange.using("externalEditor", function ()
      StackExchange.using("snippets", function ()
      StackExchange.snippets.init();
      );
      );
      , "code-snippets");

      StackExchange.ready(function()
      var channelOptions =
      tags: "".split(" "),
      id: "196"
      ;
      initTagRenderer("".split(" "), "".split(" "), channelOptions);

      StackExchange.using("externalEditor", function()
      // Have to fire editor after snippets, if snippets enabled
      if (StackExchange.settings.snippets.snippetsEnabled)
      StackExchange.using("snippets", function()
      createEditor();
      );

      else
      createEditor();

      );

      function createEditor()
      StackExchange.prepareEditor(
      heartbeatType: 'answer',
      convertImagesToLinks: false,
      noModals: false,
      showLowRepImageUploadWarning: true,
      reputationToPostImages: null,
      bindNavPrevention: true,
      postfix: "",
      onDemand: true,
      discardSelector: ".discard-answer"
      ,immediatelyShowMarkdownHelp:true
      );



      );








       

      draft saved


      draft discarded


















      StackExchange.ready(
      function ()
      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190461%2fpython-3-simple-minesweeper-game-using-tkinter%23new-answer', 'question_page');

      );

      Post as a guest






























      3 Answers
      3






      active

      oldest

      votes








      3 Answers
      3






      active

      oldest

      votes









      active

      oldest

      votes






      active

      oldest

      votes








      up vote
      5
      down vote













      It's a good idea to use the Model–View–Controller pattern. But the implementation needs some work.



      In MVC, the model should contain the complete description of the data being manipulated, together with the operations on that data. In the case of a minesweeper game, the model should consist of the following data:



      1. the size of the playing area;

      2. the locations of the mines;

      3. which squares have been uncovered so far;

      4. the locations of the flags;

      5. the state of the game (win/loss/still playing);

      together with the operations:



      1. set or clear a flag;

      2. uncover a square;

      3. start a new game.

      The idea is that you should be able to port the program to a different kind of interface by swapping out the view and controller, and leaving the model unchanged. But in the implementation in the post, most of the data, and all the operations, have gone into the controller instead. This makes it inconvenient to swap out the controller as all of this would have to be reimplemented in the new controller.






      share|improve this answer

























        up vote
        5
        down vote













        It's a good idea to use the Model–View–Controller pattern. But the implementation needs some work.



        In MVC, the model should contain the complete description of the data being manipulated, together with the operations on that data. In the case of a minesweeper game, the model should consist of the following data:



        1. the size of the playing area;

        2. the locations of the mines;

        3. which squares have been uncovered so far;

        4. the locations of the flags;

        5. the state of the game (win/loss/still playing);

        together with the operations:



        1. set or clear a flag;

        2. uncover a square;

        3. start a new game.

        The idea is that you should be able to port the program to a different kind of interface by swapping out the view and controller, and leaving the model unchanged. But in the implementation in the post, most of the data, and all the operations, have gone into the controller instead. This makes it inconvenient to swap out the controller as all of this would have to be reimplemented in the new controller.






        share|improve this answer























          up vote
          5
          down vote










          up vote
          5
          down vote









          It's a good idea to use the Model–View–Controller pattern. But the implementation needs some work.



          In MVC, the model should contain the complete description of the data being manipulated, together with the operations on that data. In the case of a minesweeper game, the model should consist of the following data:



          1. the size of the playing area;

          2. the locations of the mines;

          3. which squares have been uncovered so far;

          4. the locations of the flags;

          5. the state of the game (win/loss/still playing);

          together with the operations:



          1. set or clear a flag;

          2. uncover a square;

          3. start a new game.

          The idea is that you should be able to port the program to a different kind of interface by swapping out the view and controller, and leaving the model unchanged. But in the implementation in the post, most of the data, and all the operations, have gone into the controller instead. This makes it inconvenient to swap out the controller as all of this would have to be reimplemented in the new controller.






          share|improve this answer













          It's a good idea to use the Model–View–Controller pattern. But the implementation needs some work.



          In MVC, the model should contain the complete description of the data being manipulated, together with the operations on that data. In the case of a minesweeper game, the model should consist of the following data:



          1. the size of the playing area;

          2. the locations of the mines;

          3. which squares have been uncovered so far;

          4. the locations of the flags;

          5. the state of the game (win/loss/still playing);

          together with the operations:



          1. set or clear a flag;

          2. uncover a square;

          3. start a new game.

          The idea is that you should be able to port the program to a different kind of interface by swapping out the view and controller, and leaving the model unchanged. But in the implementation in the post, most of the data, and all the operations, have gone into the controller instead. This makes it inconvenient to swap out the controller as all of this would have to be reimplemented in the new controller.







          share|improve this answer













          share|improve this answer



          share|improve this answer











          answered Mar 26 at 18:42









          Gareth Rees

          41.1k394167




          41.1k394167






















              up vote
              4
              down vote













              The game looks great! The code looks pretty good as well!



              I definitely agree with Gareth Rees about actually separating the parts of the MVC.



              What I changed



              1. I added type hints to all the functions.

              2. I fixed a few typos, as well as adding periods to the ends of the comments.

              3. I changed most of the initializer function calls to be functional-ish (changed from assigning within the function to returning the value from the function and doing the assignment within the initializer).

              4. I changed a lot of the for loops to iterator 'math'.

              5. I extracted out the list of adjacent cells to its own function, as it was repeated a lot.

              6. I changed some of the data types from lists (or strings) to sets or tuples.

              7. I replaced tuple indexing with tuple unpacking.

              8. I attempted to reduce the repetition within the main function.

              9. Maybe a few other smaller things as well.

              What still needs to be done



              1. Separation of the MVC components.

              2. It would be nice if the prompt for the difficulty was GUI-based and it was displayed after each reset.

              The code



              """
              Minesweeper

              Implements a basic minesweeper game using tkinter.
              Uses Model-View-Controller architecture.
              """

              from functools import reduce
              from itertools import product
              from operator import add
              from random import sample
              from tkinter import Button, Frame, Label, StringVar, Tk
              from typing import Set, Tuple


              class Model(object):
              """Creates a board and adds mines to it."""

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.grid = self.create_grid()
              self.add_mines()

              def create_grid(self):
              """Create a self.width by self.height grid of elements with value 0."""

              return [[0] * self.width for _ in range(self.height)]

              def add_mines(self):
              """Randomly adds the amount of self.num_mines to grid."""

              for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
              self.grid[x][y] = 'm'


              class View(Frame):
              """Creates a main window and grid of button cells."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.master.title('Minesweeper')
              self.grid()
              self.top_panel = TopPanel(self.master, self.height, self.width, self.num_mines)
              self.buttons = self.create_buttons()

              def create_buttons(self):
              """Create cell button widgets."""

              def create_button(x, y):
              button = Button(self.master, width=5, bg='grey')
              button.grid(row=x + 1, column=y + 1)
              return button

              return [[create_button(x, y) for y in range(self.height)] for x in range(self.width)]

              def display_lose(self):
              """Display the lose label when lose condition is reached."""

              self.top_panel.loss_label.grid(row=0, columnspan=5)

              def display_win(self):
              """Display the win label when win condition is reached."""

              self.top_panel.win_label.grid(row=0, columnspan=5)

              def hide_labels(self, condition=None):
              """Hides labels based on condition argument."""

              if condition:
              self.top_panel.mines_left.grid_remove()
              else:
              self.top_panel.loss_label.grid_remove()
              self.top_panel.win_label.grid_remove()


              class TopPanel(Frame):
              """Create top panel which houses reset button and win/lose and mines left labels."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.num_mines = num_mines
              self.grid()

              self.reset_button = Button(self.master, width=7, text='Reset')
              self.reset_button.grid(row=0, columnspan=int((width * 7) / 2))

              self.loss_label = Label(text='You Lose!', bg='red')
              self.win_label = Label(text='You Win!', bg='green')

              self.mine_count = StringVar()
              self.mine_count.set('Mines remaining: ' + str(self.num_mines))
              self.mines_left = Label(textvariable=self.mine_count)
              self.mines_left.grid(row=0, columnspan=5)


              def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
              x, y = index

              return
              (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
              (x - 1, y), (x + 1, y),
              (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



              class Controller(object):
              """Sets up button bindings and minesweeper game logic.

              The act of revealing cells is delegated to the methods: adjacent_mine_count(), reveal_cell(), reveal_adjacent(), and reveal_cont(). End conditions are handled by the lose() and win() methods.
              """

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.model = Model(self.width, self.height, self.num_mines)
              self.root = Tk()
              self.view = View(self.root, self.width, self.height, self.num_mines)
              # self.color_dict is used to assign colors to cells
              self.color_dict =
              0: 'white', 1: 'blue', 2: 'green',
              3: 'red', 4: 'orange', 5: 'purple',
              6: 'grey', 7: 'grey', 8: 'grey'

              # self.count keeps track of cells with value of 0 so that they get revealed with self.reveal_cont call only once.
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.initialize_bindings()
              self.root.mainloop()

              def initialize_bindings(self):
              """Set up reveal cell and flag cell key bindings."""

              for x in range(self.height):
              for y in range(self.width):
              def closure_helper(f, index):
              def g(_): f(index)

              return g

              # Right click bind to reveal decision method
              self.view.buttons[x][y].bind('<Button-1>', closure_helper(self.reveal, (x, y)))

              # Left click bind to flag method
              self.view.buttons[x][y].bind('<Button-3>', closure_helper(self.flag, (x, y)))

              # Set up reset button
              self.view.top_panel.reset_button.bind('<Button>', lambda event: self.reset())

              def reset(self):
              """Resets game. Currently, game setup gets slower with each reset call, and window height slightly increases."""

              self.view.hide_labels()
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.model = Model(self.width, self.height, self.num_mines)
              self.view = View(self.root, self.width, self.height, self.num_mines)
              self.initialize_bindings()

              def reveal(self, index: Tuple[int, int]):
              """Main decision method determining how to reveal cell."""

              x, y = index
              val = self.adjacent_mine_count(index)

              if val in range(1, 9):
              self.reveal_cell(index)
              self.count.add(index)

              if self.model.grid[x][y] == 'm' and self.game_state != 'win' and self.view.buttons[x][y]['text'] != 'FLAG':
              self.game_state = 'Loss'
              self.lose()

              # Begin the revealing recursive method when cell value is 0
              if val == 0:
              self.reveal_cont(index)

              def adjacent_mine_count(self, index: Tuple[int, int]) -> int:
              """Returns the number of adjacent mines."""

              def is_mine(pos):
              try:
              return self.model.grid[pos[0]][pos[1]] == 'm'
              except IndexError:
              return False

              return reduce(add, map(is_mine, get_adjacent(index)))

              def reveal_cell(self, index: Tuple[int, int]):
              """Reveals cell value and assigns an associated color for that value."""

              x, y = index

              cells_unrevealed = self.height * self.width - len(self.cells_revealed) - 1

              if self.view.buttons[x][y]['text'] == 'FLAG':
              pass
              elif self.model.grid[x][y] == 'm':
              self.view.buttons[x][y].configure(bg='black')
              else:
              # Checks if cell is in the board limits
              if 0 <= x <= self.height - 1 and 0 <= y <= self.width - 1 and index not in self.cells_revealed:
              value = self.adjacent_mine_count(index)

              self.view.buttons[x][y].configure(text=value, bg=self.color_dict[value])
              self.count.add(index)
              self.cells_revealed.add(index)

              # Removes cell from flagged list when the cell gets revealed
              if index in self.cells_flagged:
              self.cells_flagged.remove(index)
              self.update_mines()

              # Check for win condition
              if cells_unrevealed == self.num_mines and not self.game_state:
              self.win()

              def reveal_adjacent(self, index: Tuple[int, int]):
              """Reveals the 8 adjacent cells to the input cell index."""

              for pos in get_adjacent(index) | index:
              if 0 <= pos[0] <= self.height - 1 and 0 <= pos[1] <= self.width - 1:
              self.reveal_cell(pos)

              def reveal_cont(self, index: Tuple[int, int]):
              """Recursive formula that reveals all adjacent cells only if the selected cell has no adjacent mines. (meaning self.adjacent_mine_count(index) == 0)."""

              val = self.adjacent_mine_count(index)

              if val == 0:
              self.reveal_adjacent(index)

              for pos in get_adjacent(index):
              if (
              0 <= pos[0] <= self.height - 1
              and 0 <= pos[1] <= self.width - 1
              and self.adjacent_mine_count(pos) == 0
              and pos not in self.count
              ):
              self.count.add(pos)
              self.reveal_cont(pos)

              def win(self):
              """Display win."""

              self.view.hide_labels('mine')
              self.view.display_win()
              self.game_state = 'win'

              def lose(self):
              """Display lose. Reveal all cells when a mine is clicked."""

              self.view.hide_labels('mine')

              for x in range(self.height):
              for y in range(self.width):
              self.reveal_cell((x, y))

              self.view.display_lose()

              def flag(self, index: Tuple[int, int]):
              """Allows player to flag cells for possible mines. Does not reveal cell."""

              x, y = index

              button_val = self.view.buttons[x][y]

              if button_val['bg'] == 'grey':
              button_val.configure(bg='yellow', text='FLAG')
              self.cells_flagged.add(index)
              elif button_val['text'] == 'FLAG':
              button_val.configure(bg='grey', text='')
              self.cells_flagged.remove(index)

              self.update_mines()

              def update_mines(self):
              """Update mine counter."""

              mines_left = self.num_mines - len(self.cells_flagged)

              if mines_left >= 0:
              self.view.top_panel.mine_count.set(f'Mines remaining: mines_left')


              def main():
              n = input('Pick a difficulty: Easy, Medium, or Hard: ')

              return Controller(*
              'e': (9, 9, 10),
              'm': (16, 16, 40),
              'h': (30, 16, 99)
              [n.lower()])


              if __name__ == '__main__':
              main()


              Please let me know if I missed anything important and if anything needs clarification.






              share|improve this answer





















              • Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
                – EndreoT
                Mar 28 at 18:16










              • Not really sure. It might fix itself once you restructure his changes.
                – Solomon Ucko
                Mar 28 at 19:14










              • Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
                – Zeta
                Mar 30 at 14:14










              • @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
                – Solomon Ucko
                Mar 30 at 14:18














              up vote
              4
              down vote













              The game looks great! The code looks pretty good as well!



              I definitely agree with Gareth Rees about actually separating the parts of the MVC.



              What I changed



              1. I added type hints to all the functions.

              2. I fixed a few typos, as well as adding periods to the ends of the comments.

              3. I changed most of the initializer function calls to be functional-ish (changed from assigning within the function to returning the value from the function and doing the assignment within the initializer).

              4. I changed a lot of the for loops to iterator 'math'.

              5. I extracted out the list of adjacent cells to its own function, as it was repeated a lot.

              6. I changed some of the data types from lists (or strings) to sets or tuples.

              7. I replaced tuple indexing with tuple unpacking.

              8. I attempted to reduce the repetition within the main function.

              9. Maybe a few other smaller things as well.

              What still needs to be done



              1. Separation of the MVC components.

              2. It would be nice if the prompt for the difficulty was GUI-based and it was displayed after each reset.

              The code



              """
              Minesweeper

              Implements a basic minesweeper game using tkinter.
              Uses Model-View-Controller architecture.
              """

              from functools import reduce
              from itertools import product
              from operator import add
              from random import sample
              from tkinter import Button, Frame, Label, StringVar, Tk
              from typing import Set, Tuple


              class Model(object):
              """Creates a board and adds mines to it."""

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.grid = self.create_grid()
              self.add_mines()

              def create_grid(self):
              """Create a self.width by self.height grid of elements with value 0."""

              return [[0] * self.width for _ in range(self.height)]

              def add_mines(self):
              """Randomly adds the amount of self.num_mines to grid."""

              for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
              self.grid[x][y] = 'm'


              class View(Frame):
              """Creates a main window and grid of button cells."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.master.title('Minesweeper')
              self.grid()
              self.top_panel = TopPanel(self.master, self.height, self.width, self.num_mines)
              self.buttons = self.create_buttons()

              def create_buttons(self):
              """Create cell button widgets."""

              def create_button(x, y):
              button = Button(self.master, width=5, bg='grey')
              button.grid(row=x + 1, column=y + 1)
              return button

              return [[create_button(x, y) for y in range(self.height)] for x in range(self.width)]

              def display_lose(self):
              """Display the lose label when lose condition is reached."""

              self.top_panel.loss_label.grid(row=0, columnspan=5)

              def display_win(self):
              """Display the win label when win condition is reached."""

              self.top_panel.win_label.grid(row=0, columnspan=5)

              def hide_labels(self, condition=None):
              """Hides labels based on condition argument."""

              if condition:
              self.top_panel.mines_left.grid_remove()
              else:
              self.top_panel.loss_label.grid_remove()
              self.top_panel.win_label.grid_remove()


              class TopPanel(Frame):
              """Create top panel which houses reset button and win/lose and mines left labels."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.num_mines = num_mines
              self.grid()

              self.reset_button = Button(self.master, width=7, text='Reset')
              self.reset_button.grid(row=0, columnspan=int((width * 7) / 2))

              self.loss_label = Label(text='You Lose!', bg='red')
              self.win_label = Label(text='You Win!', bg='green')

              self.mine_count = StringVar()
              self.mine_count.set('Mines remaining: ' + str(self.num_mines))
              self.mines_left = Label(textvariable=self.mine_count)
              self.mines_left.grid(row=0, columnspan=5)


              def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
              x, y = index

              return
              (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
              (x - 1, y), (x + 1, y),
              (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



              class Controller(object):
              """Sets up button bindings and minesweeper game logic.

              The act of revealing cells is delegated to the methods: adjacent_mine_count(), reveal_cell(), reveal_adjacent(), and reveal_cont(). End conditions are handled by the lose() and win() methods.
              """

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.model = Model(self.width, self.height, self.num_mines)
              self.root = Tk()
              self.view = View(self.root, self.width, self.height, self.num_mines)
              # self.color_dict is used to assign colors to cells
              self.color_dict =
              0: 'white', 1: 'blue', 2: 'green',
              3: 'red', 4: 'orange', 5: 'purple',
              6: 'grey', 7: 'grey', 8: 'grey'

              # self.count keeps track of cells with value of 0 so that they get revealed with self.reveal_cont call only once.
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.initialize_bindings()
              self.root.mainloop()

              def initialize_bindings(self):
              """Set up reveal cell and flag cell key bindings."""

              for x in range(self.height):
              for y in range(self.width):
              def closure_helper(f, index):
              def g(_): f(index)

              return g

              # Right click bind to reveal decision method
              self.view.buttons[x][y].bind('<Button-1>', closure_helper(self.reveal, (x, y)))

              # Left click bind to flag method
              self.view.buttons[x][y].bind('<Button-3>', closure_helper(self.flag, (x, y)))

              # Set up reset button
              self.view.top_panel.reset_button.bind('<Button>', lambda event: self.reset())

              def reset(self):
              """Resets game. Currently, game setup gets slower with each reset call, and window height slightly increases."""

              self.view.hide_labels()
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.model = Model(self.width, self.height, self.num_mines)
              self.view = View(self.root, self.width, self.height, self.num_mines)
              self.initialize_bindings()

              def reveal(self, index: Tuple[int, int]):
              """Main decision method determining how to reveal cell."""

              x, y = index
              val = self.adjacent_mine_count(index)

              if val in range(1, 9):
              self.reveal_cell(index)
              self.count.add(index)

              if self.model.grid[x][y] == 'm' and self.game_state != 'win' and self.view.buttons[x][y]['text'] != 'FLAG':
              self.game_state = 'Loss'
              self.lose()

              # Begin the revealing recursive method when cell value is 0
              if val == 0:
              self.reveal_cont(index)

              def adjacent_mine_count(self, index: Tuple[int, int]) -> int:
              """Returns the number of adjacent mines."""

              def is_mine(pos):
              try:
              return self.model.grid[pos[0]][pos[1]] == 'm'
              except IndexError:
              return False

              return reduce(add, map(is_mine, get_adjacent(index)))

              def reveal_cell(self, index: Tuple[int, int]):
              """Reveals cell value and assigns an associated color for that value."""

              x, y = index

              cells_unrevealed = self.height * self.width - len(self.cells_revealed) - 1

              if self.view.buttons[x][y]['text'] == 'FLAG':
              pass
              elif self.model.grid[x][y] == 'm':
              self.view.buttons[x][y].configure(bg='black')
              else:
              # Checks if cell is in the board limits
              if 0 <= x <= self.height - 1 and 0 <= y <= self.width - 1 and index not in self.cells_revealed:
              value = self.adjacent_mine_count(index)

              self.view.buttons[x][y].configure(text=value, bg=self.color_dict[value])
              self.count.add(index)
              self.cells_revealed.add(index)

              # Removes cell from flagged list when the cell gets revealed
              if index in self.cells_flagged:
              self.cells_flagged.remove(index)
              self.update_mines()

              # Check for win condition
              if cells_unrevealed == self.num_mines and not self.game_state:
              self.win()

              def reveal_adjacent(self, index: Tuple[int, int]):
              """Reveals the 8 adjacent cells to the input cell index."""

              for pos in get_adjacent(index) | index:
              if 0 <= pos[0] <= self.height - 1 and 0 <= pos[1] <= self.width - 1:
              self.reveal_cell(pos)

              def reveal_cont(self, index: Tuple[int, int]):
              """Recursive formula that reveals all adjacent cells only if the selected cell has no adjacent mines. (meaning self.adjacent_mine_count(index) == 0)."""

              val = self.adjacent_mine_count(index)

              if val == 0:
              self.reveal_adjacent(index)

              for pos in get_adjacent(index):
              if (
              0 <= pos[0] <= self.height - 1
              and 0 <= pos[1] <= self.width - 1
              and self.adjacent_mine_count(pos) == 0
              and pos not in self.count
              ):
              self.count.add(pos)
              self.reveal_cont(pos)

              def win(self):
              """Display win."""

              self.view.hide_labels('mine')
              self.view.display_win()
              self.game_state = 'win'

              def lose(self):
              """Display lose. Reveal all cells when a mine is clicked."""

              self.view.hide_labels('mine')

              for x in range(self.height):
              for y in range(self.width):
              self.reveal_cell((x, y))

              self.view.display_lose()

              def flag(self, index: Tuple[int, int]):
              """Allows player to flag cells for possible mines. Does not reveal cell."""

              x, y = index

              button_val = self.view.buttons[x][y]

              if button_val['bg'] == 'grey':
              button_val.configure(bg='yellow', text='FLAG')
              self.cells_flagged.add(index)
              elif button_val['text'] == 'FLAG':
              button_val.configure(bg='grey', text='')
              self.cells_flagged.remove(index)

              self.update_mines()

              def update_mines(self):
              """Update mine counter."""

              mines_left = self.num_mines - len(self.cells_flagged)

              if mines_left >= 0:
              self.view.top_panel.mine_count.set(f'Mines remaining: mines_left')


              def main():
              n = input('Pick a difficulty: Easy, Medium, or Hard: ')

              return Controller(*
              'e': (9, 9, 10),
              'm': (16, 16, 40),
              'h': (30, 16, 99)
              [n.lower()])


              if __name__ == '__main__':
              main()


              Please let me know if I missed anything important and if anything needs clarification.






              share|improve this answer





















              • Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
                – EndreoT
                Mar 28 at 18:16










              • Not really sure. It might fix itself once you restructure his changes.
                – Solomon Ucko
                Mar 28 at 19:14










              • Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
                – Zeta
                Mar 30 at 14:14










              • @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
                – Solomon Ucko
                Mar 30 at 14:18












              up vote
              4
              down vote










              up vote
              4
              down vote









              The game looks great! The code looks pretty good as well!



              I definitely agree with Gareth Rees about actually separating the parts of the MVC.



              What I changed



              1. I added type hints to all the functions.

              2. I fixed a few typos, as well as adding periods to the ends of the comments.

              3. I changed most of the initializer function calls to be functional-ish (changed from assigning within the function to returning the value from the function and doing the assignment within the initializer).

              4. I changed a lot of the for loops to iterator 'math'.

              5. I extracted out the list of adjacent cells to its own function, as it was repeated a lot.

              6. I changed some of the data types from lists (or strings) to sets or tuples.

              7. I replaced tuple indexing with tuple unpacking.

              8. I attempted to reduce the repetition within the main function.

              9. Maybe a few other smaller things as well.

              What still needs to be done



              1. Separation of the MVC components.

              2. It would be nice if the prompt for the difficulty was GUI-based and it was displayed after each reset.

              The code



              """
              Minesweeper

              Implements a basic minesweeper game using tkinter.
              Uses Model-View-Controller architecture.
              """

              from functools import reduce
              from itertools import product
              from operator import add
              from random import sample
              from tkinter import Button, Frame, Label, StringVar, Tk
              from typing import Set, Tuple


              class Model(object):
              """Creates a board and adds mines to it."""

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.grid = self.create_grid()
              self.add_mines()

              def create_grid(self):
              """Create a self.width by self.height grid of elements with value 0."""

              return [[0] * self.width for _ in range(self.height)]

              def add_mines(self):
              """Randomly adds the amount of self.num_mines to grid."""

              for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
              self.grid[x][y] = 'm'


              class View(Frame):
              """Creates a main window and grid of button cells."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.master.title('Minesweeper')
              self.grid()
              self.top_panel = TopPanel(self.master, self.height, self.width, self.num_mines)
              self.buttons = self.create_buttons()

              def create_buttons(self):
              """Create cell button widgets."""

              def create_button(x, y):
              button = Button(self.master, width=5, bg='grey')
              button.grid(row=x + 1, column=y + 1)
              return button

              return [[create_button(x, y) for y in range(self.height)] for x in range(self.width)]

              def display_lose(self):
              """Display the lose label when lose condition is reached."""

              self.top_panel.loss_label.grid(row=0, columnspan=5)

              def display_win(self):
              """Display the win label when win condition is reached."""

              self.top_panel.win_label.grid(row=0, columnspan=5)

              def hide_labels(self, condition=None):
              """Hides labels based on condition argument."""

              if condition:
              self.top_panel.mines_left.grid_remove()
              else:
              self.top_panel.loss_label.grid_remove()
              self.top_panel.win_label.grid_remove()


              class TopPanel(Frame):
              """Create top panel which houses reset button and win/lose and mines left labels."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.num_mines = num_mines
              self.grid()

              self.reset_button = Button(self.master, width=7, text='Reset')
              self.reset_button.grid(row=0, columnspan=int((width * 7) / 2))

              self.loss_label = Label(text='You Lose!', bg='red')
              self.win_label = Label(text='You Win!', bg='green')

              self.mine_count = StringVar()
              self.mine_count.set('Mines remaining: ' + str(self.num_mines))
              self.mines_left = Label(textvariable=self.mine_count)
              self.mines_left.grid(row=0, columnspan=5)


              def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
              x, y = index

              return
              (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
              (x - 1, y), (x + 1, y),
              (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



              class Controller(object):
              """Sets up button bindings and minesweeper game logic.

              The act of revealing cells is delegated to the methods: adjacent_mine_count(), reveal_cell(), reveal_adjacent(), and reveal_cont(). End conditions are handled by the lose() and win() methods.
              """

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.model = Model(self.width, self.height, self.num_mines)
              self.root = Tk()
              self.view = View(self.root, self.width, self.height, self.num_mines)
              # self.color_dict is used to assign colors to cells
              self.color_dict =
              0: 'white', 1: 'blue', 2: 'green',
              3: 'red', 4: 'orange', 5: 'purple',
              6: 'grey', 7: 'grey', 8: 'grey'

              # self.count keeps track of cells with value of 0 so that they get revealed with self.reveal_cont call only once.
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.initialize_bindings()
              self.root.mainloop()

              def initialize_bindings(self):
              """Set up reveal cell and flag cell key bindings."""

              for x in range(self.height):
              for y in range(self.width):
              def closure_helper(f, index):
              def g(_): f(index)

              return g

              # Right click bind to reveal decision method
              self.view.buttons[x][y].bind('<Button-1>', closure_helper(self.reveal, (x, y)))

              # Left click bind to flag method
              self.view.buttons[x][y].bind('<Button-3>', closure_helper(self.flag, (x, y)))

              # Set up reset button
              self.view.top_panel.reset_button.bind('<Button>', lambda event: self.reset())

              def reset(self):
              """Resets game. Currently, game setup gets slower with each reset call, and window height slightly increases."""

              self.view.hide_labels()
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.model = Model(self.width, self.height, self.num_mines)
              self.view = View(self.root, self.width, self.height, self.num_mines)
              self.initialize_bindings()

              def reveal(self, index: Tuple[int, int]):
              """Main decision method determining how to reveal cell."""

              x, y = index
              val = self.adjacent_mine_count(index)

              if val in range(1, 9):
              self.reveal_cell(index)
              self.count.add(index)

              if self.model.grid[x][y] == 'm' and self.game_state != 'win' and self.view.buttons[x][y]['text'] != 'FLAG':
              self.game_state = 'Loss'
              self.lose()

              # Begin the revealing recursive method when cell value is 0
              if val == 0:
              self.reveal_cont(index)

              def adjacent_mine_count(self, index: Tuple[int, int]) -> int:
              """Returns the number of adjacent mines."""

              def is_mine(pos):
              try:
              return self.model.grid[pos[0]][pos[1]] == 'm'
              except IndexError:
              return False

              return reduce(add, map(is_mine, get_adjacent(index)))

              def reveal_cell(self, index: Tuple[int, int]):
              """Reveals cell value and assigns an associated color for that value."""

              x, y = index

              cells_unrevealed = self.height * self.width - len(self.cells_revealed) - 1

              if self.view.buttons[x][y]['text'] == 'FLAG':
              pass
              elif self.model.grid[x][y] == 'm':
              self.view.buttons[x][y].configure(bg='black')
              else:
              # Checks if cell is in the board limits
              if 0 <= x <= self.height - 1 and 0 <= y <= self.width - 1 and index not in self.cells_revealed:
              value = self.adjacent_mine_count(index)

              self.view.buttons[x][y].configure(text=value, bg=self.color_dict[value])
              self.count.add(index)
              self.cells_revealed.add(index)

              # Removes cell from flagged list when the cell gets revealed
              if index in self.cells_flagged:
              self.cells_flagged.remove(index)
              self.update_mines()

              # Check for win condition
              if cells_unrevealed == self.num_mines and not self.game_state:
              self.win()

              def reveal_adjacent(self, index: Tuple[int, int]):
              """Reveals the 8 adjacent cells to the input cell index."""

              for pos in get_adjacent(index) | index:
              if 0 <= pos[0] <= self.height - 1 and 0 <= pos[1] <= self.width - 1:
              self.reveal_cell(pos)

              def reveal_cont(self, index: Tuple[int, int]):
              """Recursive formula that reveals all adjacent cells only if the selected cell has no adjacent mines. (meaning self.adjacent_mine_count(index) == 0)."""

              val = self.adjacent_mine_count(index)

              if val == 0:
              self.reveal_adjacent(index)

              for pos in get_adjacent(index):
              if (
              0 <= pos[0] <= self.height - 1
              and 0 <= pos[1] <= self.width - 1
              and self.adjacent_mine_count(pos) == 0
              and pos not in self.count
              ):
              self.count.add(pos)
              self.reveal_cont(pos)

              def win(self):
              """Display win."""

              self.view.hide_labels('mine')
              self.view.display_win()
              self.game_state = 'win'

              def lose(self):
              """Display lose. Reveal all cells when a mine is clicked."""

              self.view.hide_labels('mine')

              for x in range(self.height):
              for y in range(self.width):
              self.reveal_cell((x, y))

              self.view.display_lose()

              def flag(self, index: Tuple[int, int]):
              """Allows player to flag cells for possible mines. Does not reveal cell."""

              x, y = index

              button_val = self.view.buttons[x][y]

              if button_val['bg'] == 'grey':
              button_val.configure(bg='yellow', text='FLAG')
              self.cells_flagged.add(index)
              elif button_val['text'] == 'FLAG':
              button_val.configure(bg='grey', text='')
              self.cells_flagged.remove(index)

              self.update_mines()

              def update_mines(self):
              """Update mine counter."""

              mines_left = self.num_mines - len(self.cells_flagged)

              if mines_left >= 0:
              self.view.top_panel.mine_count.set(f'Mines remaining: mines_left')


              def main():
              n = input('Pick a difficulty: Easy, Medium, or Hard: ')

              return Controller(*
              'e': (9, 9, 10),
              'm': (16, 16, 40),
              'h': (30, 16, 99)
              [n.lower()])


              if __name__ == '__main__':
              main()


              Please let me know if I missed anything important and if anything needs clarification.






              share|improve this answer













              The game looks great! The code looks pretty good as well!



              I definitely agree with Gareth Rees about actually separating the parts of the MVC.



              What I changed



              1. I added type hints to all the functions.

              2. I fixed a few typos, as well as adding periods to the ends of the comments.

              3. I changed most of the initializer function calls to be functional-ish (changed from assigning within the function to returning the value from the function and doing the assignment within the initializer).

              4. I changed a lot of the for loops to iterator 'math'.

              5. I extracted out the list of adjacent cells to its own function, as it was repeated a lot.

              6. I changed some of the data types from lists (or strings) to sets or tuples.

              7. I replaced tuple indexing with tuple unpacking.

              8. I attempted to reduce the repetition within the main function.

              9. Maybe a few other smaller things as well.

              What still needs to be done



              1. Separation of the MVC components.

              2. It would be nice if the prompt for the difficulty was GUI-based and it was displayed after each reset.

              The code



              """
              Minesweeper

              Implements a basic minesweeper game using tkinter.
              Uses Model-View-Controller architecture.
              """

              from functools import reduce
              from itertools import product
              from operator import add
              from random import sample
              from tkinter import Button, Frame, Label, StringVar, Tk
              from typing import Set, Tuple


              class Model(object):
              """Creates a board and adds mines to it."""

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.grid = self.create_grid()
              self.add_mines()

              def create_grid(self):
              """Create a self.width by self.height grid of elements with value 0."""

              return [[0] * self.width for _ in range(self.height)]

              def add_mines(self):
              """Randomly adds the amount of self.num_mines to grid."""

              for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
              self.grid[x][y] = 'm'


              class View(Frame):
              """Creates a main window and grid of button cells."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.master.title('Minesweeper')
              self.grid()
              self.top_panel = TopPanel(self.master, self.height, self.width, self.num_mines)
              self.buttons = self.create_buttons()

              def create_buttons(self):
              """Create cell button widgets."""

              def create_button(x, y):
              button = Button(self.master, width=5, bg='grey')
              button.grid(row=x + 1, column=y + 1)
              return button

              return [[create_button(x, y) for y in range(self.height)] for x in range(self.width)]

              def display_lose(self):
              """Display the lose label when lose condition is reached."""

              self.top_panel.loss_label.grid(row=0, columnspan=5)

              def display_win(self):
              """Display the win label when win condition is reached."""

              self.top_panel.win_label.grid(row=0, columnspan=5)

              def hide_labels(self, condition=None):
              """Hides labels based on condition argument."""

              if condition:
              self.top_panel.mines_left.grid_remove()
              else:
              self.top_panel.loss_label.grid_remove()
              self.top_panel.win_label.grid_remove()


              class TopPanel(Frame):
              """Create top panel which houses reset button and win/lose and mines left labels."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.num_mines = num_mines
              self.grid()

              self.reset_button = Button(self.master, width=7, text='Reset')
              self.reset_button.grid(row=0, columnspan=int((width * 7) / 2))

              self.loss_label = Label(text='You Lose!', bg='red')
              self.win_label = Label(text='You Win!', bg='green')

              self.mine_count = StringVar()
              self.mine_count.set('Mines remaining: ' + str(self.num_mines))
              self.mines_left = Label(textvariable=self.mine_count)
              self.mines_left.grid(row=0, columnspan=5)


              def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
              x, y = index

              return
              (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
              (x - 1, y), (x + 1, y),
              (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



              class Controller(object):
              """Sets up button bindings and minesweeper game logic.

              The act of revealing cells is delegated to the methods: adjacent_mine_count(), reveal_cell(), reveal_adjacent(), and reveal_cont(). End conditions are handled by the lose() and win() methods.
              """

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.model = Model(self.width, self.height, self.num_mines)
              self.root = Tk()
              self.view = View(self.root, self.width, self.height, self.num_mines)
              # self.color_dict is used to assign colors to cells
              self.color_dict =
              0: 'white', 1: 'blue', 2: 'green',
              3: 'red', 4: 'orange', 5: 'purple',
              6: 'grey', 7: 'grey', 8: 'grey'

              # self.count keeps track of cells with value of 0 so that they get revealed with self.reveal_cont call only once.
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.initialize_bindings()
              self.root.mainloop()

              def initialize_bindings(self):
              """Set up reveal cell and flag cell key bindings."""

              for x in range(self.height):
              for y in range(self.width):
              def closure_helper(f, index):
              def g(_): f(index)

              return g

              # Right click bind to reveal decision method
              self.view.buttons[x][y].bind('<Button-1>', closure_helper(self.reveal, (x, y)))

              # Left click bind to flag method
              self.view.buttons[x][y].bind('<Button-3>', closure_helper(self.flag, (x, y)))

              # Set up reset button
              self.view.top_panel.reset_button.bind('<Button>', lambda event: self.reset())

              def reset(self):
              """Resets game. Currently, game setup gets slower with each reset call, and window height slightly increases."""

              self.view.hide_labels()
              self.count = set()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.game_state = None
              self.model = Model(self.width, self.height, self.num_mines)
              self.view = View(self.root, self.width, self.height, self.num_mines)
              self.initialize_bindings()

              def reveal(self, index: Tuple[int, int]):
              """Main decision method determining how to reveal cell."""

              x, y = index
              val = self.adjacent_mine_count(index)

              if val in range(1, 9):
              self.reveal_cell(index)
              self.count.add(index)

              if self.model.grid[x][y] == 'm' and self.game_state != 'win' and self.view.buttons[x][y]['text'] != 'FLAG':
              self.game_state = 'Loss'
              self.lose()

              # Begin the revealing recursive method when cell value is 0
              if val == 0:
              self.reveal_cont(index)

              def adjacent_mine_count(self, index: Tuple[int, int]) -> int:
              """Returns the number of adjacent mines."""

              def is_mine(pos):
              try:
              return self.model.grid[pos[0]][pos[1]] == 'm'
              except IndexError:
              return False

              return reduce(add, map(is_mine, get_adjacent(index)))

              def reveal_cell(self, index: Tuple[int, int]):
              """Reveals cell value and assigns an associated color for that value."""

              x, y = index

              cells_unrevealed = self.height * self.width - len(self.cells_revealed) - 1

              if self.view.buttons[x][y]['text'] == 'FLAG':
              pass
              elif self.model.grid[x][y] == 'm':
              self.view.buttons[x][y].configure(bg='black')
              else:
              # Checks if cell is in the board limits
              if 0 <= x <= self.height - 1 and 0 <= y <= self.width - 1 and index not in self.cells_revealed:
              value = self.adjacent_mine_count(index)

              self.view.buttons[x][y].configure(text=value, bg=self.color_dict[value])
              self.count.add(index)
              self.cells_revealed.add(index)

              # Removes cell from flagged list when the cell gets revealed
              if index in self.cells_flagged:
              self.cells_flagged.remove(index)
              self.update_mines()

              # Check for win condition
              if cells_unrevealed == self.num_mines and not self.game_state:
              self.win()

              def reveal_adjacent(self, index: Tuple[int, int]):
              """Reveals the 8 adjacent cells to the input cell index."""

              for pos in get_adjacent(index) | index:
              if 0 <= pos[0] <= self.height - 1 and 0 <= pos[1] <= self.width - 1:
              self.reveal_cell(pos)

              def reveal_cont(self, index: Tuple[int, int]):
              """Recursive formula that reveals all adjacent cells only if the selected cell has no adjacent mines. (meaning self.adjacent_mine_count(index) == 0)."""

              val = self.adjacent_mine_count(index)

              if val == 0:
              self.reveal_adjacent(index)

              for pos in get_adjacent(index):
              if (
              0 <= pos[0] <= self.height - 1
              and 0 <= pos[1] <= self.width - 1
              and self.adjacent_mine_count(pos) == 0
              and pos not in self.count
              ):
              self.count.add(pos)
              self.reveal_cont(pos)

              def win(self):
              """Display win."""

              self.view.hide_labels('mine')
              self.view.display_win()
              self.game_state = 'win'

              def lose(self):
              """Display lose. Reveal all cells when a mine is clicked."""

              self.view.hide_labels('mine')

              for x in range(self.height):
              for y in range(self.width):
              self.reveal_cell((x, y))

              self.view.display_lose()

              def flag(self, index: Tuple[int, int]):
              """Allows player to flag cells for possible mines. Does not reveal cell."""

              x, y = index

              button_val = self.view.buttons[x][y]

              if button_val['bg'] == 'grey':
              button_val.configure(bg='yellow', text='FLAG')
              self.cells_flagged.add(index)
              elif button_val['text'] == 'FLAG':
              button_val.configure(bg='grey', text='')
              self.cells_flagged.remove(index)

              self.update_mines()

              def update_mines(self):
              """Update mine counter."""

              mines_left = self.num_mines - len(self.cells_flagged)

              if mines_left >= 0:
              self.view.top_panel.mine_count.set(f'Mines remaining: mines_left')


              def main():
              n = input('Pick a difficulty: Easy, Medium, or Hard: ')

              return Controller(*
              'e': (9, 9, 10),
              'm': (16, 16, 40),
              'h': (30, 16, 99)
              [n.lower()])


              if __name__ == '__main__':
              main()


              Please let me know if I missed anything important and if anything needs clarification.







              share|improve this answer













              share|improve this answer



              share|improve this answer











              answered Mar 26 at 21:54









              Solomon Ucko

              822313




              822313











              • Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
                – EndreoT
                Mar 28 at 18:16










              • Not really sure. It might fix itself once you restructure his changes.
                – Solomon Ucko
                Mar 28 at 19:14










              • Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
                – Zeta
                Mar 30 at 14:14










              • @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
                – Solomon Ucko
                Mar 30 at 14:18
















              • Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
                – EndreoT
                Mar 28 at 18:16










              • Not really sure. It might fix itself once you restructure his changes.
                – Solomon Ucko
                Mar 28 at 19:14










              • Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
                – Zeta
                Mar 30 at 14:14










              • @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
                – Solomon Ucko
                Mar 30 at 14:18















              Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
              – EndreoT
              Mar 28 at 18:16




              Thank you Solomon for the suggestions and edits! Any thoughts on improving the reset call speed? I will restructure the code using your and Gareth Rees' ideas.
              – EndreoT
              Mar 28 at 18:16












              Not really sure. It might fix itself once you restructure his changes.
              – Solomon Ucko
              Mar 28 at 19:14




              Not really sure. It might fix itself once you restructure his changes.
              – Solomon Ucko
              Mar 28 at 19:14












              Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
              – Zeta
              Mar 30 at 14:14




              Sorry for hijacking this. Either way: I was about to retract my concerns on metamemelords question, as long as it was going to be edited (and its title changed), but it got deleted by them :/.
              – Zeta
              Mar 30 at 14:14












              @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
              – Solomon Ucko
              Mar 30 at 14:18




              @Zeta, that's fair. I wonder why they deleted it in the middle of a discussion, but oh well.
              – Solomon Ucko
              Mar 30 at 14:18










              up vote
              0
              down vote













              Here is the revised minesweeper game with a controller that can use different interfaces. It now includes a text interface for the masochists. Also, resetting the game now destroys the root window and creates a new one. Credit for suggestions and edits are given to @Gareth and @Solomon. Anyone with tkinter experience have an answer on how to reuse the same tk window during game reset so that the previously posted problems do not happen?



              """
              Minesweeper

              Implements a basic minesweeper game using the tkinter module.
              Uses a Model-View-Controller structure.
              """

              from functools import reduce
              from itertools import product
              from operator import add
              from random import sample
              from tkinter import Button, Frame, Label, StringVar, Tk
              from typing import Set, Tuple


              def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
              """Returns adjacent coordinates for input index"""

              x, y = index

              return
              (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
              (x - 1, y), (x + 1, y),
              (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



              class Model(object):
              """Creates a board and adds mines to it."""

              def __init__(self, width: int, height: int, num_mines: int):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.grid = self.create_grid()
              self.add_mines()
              self.grid_coords = self.grid_coords()
              self.adjacent_mine_count()
              self.cells_revealed = set()
              self.cells_flagged = set()
              self.revealed_zeroes = set()
              self.game_state = None

              def create_grid(self) -> list:
              """Returns a (width by height) grid of elements with value of 0."""

              return [[0] * self.width for _ in range(self.height)]

              def add_mines(self):
              """Randomly adds mines to board grid."""

              for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
              self.grid[y][x] = 'm'

              def grid_coords(self) -> list:
              """Returns a list of (x, y) coordinates for every position on grid."""

              return [(x, y) for y in range(self.height) for x in range(self.width)]

              def adjacent_mine_count(self):
              """Sets cell values to the number of their adjacent mines."""

              def is_mine(coords):
              try:
              if coords[0] >= 0 and coords[1] >= 0:
              return self.grid[coords[1]][coords[0]] == 'm'
              else:
              return False
              except IndexError:
              return False

              for position in self.grid_coords:
              x, y = position
              if self.grid[y][x] != "m":
              grid_value = reduce(add, map(is_mine, get_adjacent(position)))
              self.grid[y][x] = grid_value

              def get_cell_value(self, index: Tuple[int, int]) -> int or str:
              """Returns model's cell value at the given index."""

              x, y = index
              return self.grid[y][x]

              class View(Frame):
              """Creates a GUI with a grid of cell buttons."""

              def __init__(self, width: int, height: int,
              num_mines: int, difficulty: str, controller: "Controller"):
              self.master = Tk()
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.difficulty = difficulty
              self.controller = controller
              self.color_dict =
              0: 'white', 1: 'blue', 2: 'green',
              3: 'red', 4: 'orange', 5: 'purple',
              6: 'grey', 7: 'grey', 8: 'grey', "m": "black"

              self.master.title('Minesweeper')

              def create_buttons(self) -> list:
              """Create cell button widgets."""

              def create_button(x, y):
              button = Button(self.master, width=5, bg='grey')
              button.grid(row=y + 5, column=x + 1)
              return button

              return [[create_button(x, y) for x in range(self.width)]
              for y in range(self.height)]

              def initialize_bindings(self):
              """Set up the reveal cell and the flag cell key bindings."""

              for x in range(self.width):
              for y in range(self.height):
              def closure_helper(f, index):
              def g(_):
              f(index)
              return g

              # Bind reveal decision method to left click
              self.buttons[y][x].bind(
              '<Button-1>', closure_helper(
              self.controller.reveal_decision, (x, y)))

              # Bind flag method to right click
              self.buttons[y][x].bind(
              '<Button-3>', closure_helper(
              self.controller.update_flagged_cell, (x, y)))

              # Set up reset button
              self.top_panel.reset_button.bind(
              '<Button>', lambda event: self.controller.reset(event))

              def reset_view(self):
              """Destroys the GUI. Controller will create a new GUI"""

              self.master.destroy()

              def reveal_cell(self, index: Tuple[int, int], value: int or str):
              """Reveals cell's value on GUI."""

              x, y = index
              self.buttons[y][x].configure(text=value, bg=self.color_dict[value])

              def flag_cell(self, index: Tuple[int, int]):
              """Flag cell in GUI"""

              x, y = index
              self.buttons[y][x].configure(text="FLAG", bg="yellow")

              def unflag_cell(self, index: Tuple[int, int]):
              """Unflag cell in GUI"""
              x, y = index
              self.buttons[y][x].configure(text="", bg="grey")

              def update_mines_left(self, mines: int):
              """Updates mine counter widget"""

              self.top_panel.mine_count.set("Mines remaining: " + str(mines))

              def display_loss(self):
              """Display the loss label when lose condition is reached."""

              self.top_panel.loss_label.grid(row=0, columnspan=10)

              def display_win(self):
              """Display the win label when win condition is reached."""

              self.top_panel.win_label.grid(row=0, columnspan=10)

              def mainloop(self):
              self.top_panel = TopPanel(self.master, self.height,
              self.width, self.num_mines)
              self.buttons = self.create_buttons()
              self.top_panel.mines_left.grid(row=0, columnspan=5)
              self.initialize_bindings()
              self.master.mainloop()


              class TopPanel(Frame):
              """Creates a top panel which contains game information."""

              def __init__(self, master: Tk, width: int, height: int, num_mines: int):
              Frame.__init__(self, master)
              self.master = master
              self.num_mines = num_mines
              self.grid()

              self.reset_button = Button(self.master, width=7, text='Reset')
              self.reset_button.grid(row=0)

              self.loss_label = Label(text='You Lose!', bg='red')
              self.win_label = Label(text='You Win!', bg='green')

              self.mine_count = StringVar()
              self.mine_count.set('Mines remaining: ' + str(self.num_mines))
              self.mines_left = Label(textvariable=self.mine_count)


              class TextView(object):
              """Creates a text interface of the minesweeper game."""

              def __init__(self, width: int, height: int,
              num_mines: int, difficulty: str, controller: "Controller"):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.controller = controller
              self.reveal_dict =
              0: ' 0 ', 1: ' 1 ', 2: ' 2 ',
              3: ' 3 ', 4: ' 4 ', 5: ' 5 ',
              6: ' 6 ', 7: ' 7 ', 8: ' 8 ', "m": "mine"

              self.cell_view = self.cell_view()
              self.show_grid()

              def cell_view(self)-> list:
              """Create text view of cells."""

              return [["cell" for x in range(self.width)]
              for y in range(self.height)]

              def show_grid(self):
              """Prints text grid to console. Includes column numbers."""

              top_row = [str(i) for i in range(self.width)]
              print(" ", *top_row, sep=" "*5)
              for row in range(len(self.cell_view)):
              print(str(row) + ":", *self.cell_view[row], sep=" ")

              def reveal_cell(self, index: Tuple[int, int], value: int or str):
              """Reveals a cell's value in the text view"""

              x, y = index
              self.cell_view[y][x] = self.reveal_dict[value]

              def flag_cell(self, index: Tuple[int, int]):
              """Flags cell in cell_view"""

              x, y = index
              self.cell_view[y][x] = "FLAG"

              def unflag_cell(self, index: Tuple[int, int]):
              """Unflags cell in cell_view"""

              x, y = index
              self.cell_view[y][x] = "cell"

              def update_mines_left(self, mines):
              """Updates mine counter."""

              print("Mines remaining: " + str(mines))

              def display_loss(self):
              """Displays the lose label when loss condition is reached."""

              print("You Lose!")

              def display_win(self):
              """Displays the win label when win condition is reached."""

              print("You Win!")

              def mainloop(self):
              while True:
              try:
              cmd, *coords = input(
              "Choose a cell in the format: "
              + "flag/reveal x y. Type END to quit. ").split()
              if cmd.lower()[0] == "e":
              break
              x, y = coords[0], coords[1]
              if cmd.lower()[0] == "f":
              self.controller.update_flagged_cell((int(x), int(y)))
              elif cmd.lower()[0] == "r":
              self.controller.reveal_decision((int(x), int(y)))
              else:
              print("Unknown command")
              self.show_grid()
              except:
              print("Incorrect selection or format")


              class Controller(object):
              """Sets up button bindings and minesweeper game logic.

              Reveal_decision determines how to reveal cells.
              End conditions are handled by the loss and win methods.
              """

              def __init__(self, width: int, height: int,
              num_mines: int, difficulty: str, view_type: str):
              self.width = width
              self.height = height
              self.num_mines = num_mines
              self.difficulty = difficulty
              self.model = Model(self.width, self.height, self.num_mines)
              if view_type == "GUI":
              self.view = View(self.width, self.height,
              self.num_mines, self.difficulty, self)
              elif view_type == "TEXT":
              self.view = TextView(self.width, self.height,
              self.num_mines, self.difficulty, self)
              self.view.mainloop()

              def reset(self, event):
              """Resets the game"""

              self.view.reset_view()
              self.model = Model(self.width, self.height, self.num_mines)
              self.view = View(self.width, self.height,
              self.num_mines, self.difficulty, self)
              self.view.mainloop()

              def reveal_decision(self, index: Tuple[int, int]):
              """Main decision method determining how to reveal cell."""

              x, y = index

              cell_value = self.model.get_cell_value(index)
              if index in self.model.cells_flagged:
              return None

              if cell_value in range(1, 9):
              self.reveal_cell(index, cell_value)

              elif (
              self.model.grid[y][x] == "m"
              and self.model.game_state != "win"
              ):
              self.loss()

              else:
              self.reveal_zeroes(index)

              # Check for win condition
              cells_unrevealed = self.height * self.width - len(self.model.cells_revealed)
              if cells_unrevealed == self.num_mines and self.model.game_state != "loss":
              self.win()

              def reveal_cell(self, index: Tuple[int, int], value: int or str):
              """Obtains cell value from model and passes the value to view."""

              if index in self.model.cells_flagged:
              return None
              else:
              self.model.cells_revealed.add(index)
              self.view.reveal_cell(index, value)

              def reveal_adjacent(self, index: Tuple[int, int]):
              """Reveals the 8 adjacent cells to the input cell's index."""

              for coords in get_adjacent(index):
              if (
              0 <= coords[0] <= self.width - 1
              and 0 <= coords[1] <= self.height - 1
              ):
              cell_value = self.model.get_cell_value(coords)
              self.reveal_cell(coords, cell_value)

              def reveal_zeroes(self, index: Tuple[int, int]):
              """Reveals all adjacent cells just until a mine is reached."""

              val = self.model.get_cell_value(index)

              if val == 0:
              self.reveal_cell(index, val)
              self.reveal_adjacent(index)

              for coords in get_adjacent(index):
              if (
              0 <= coords[0] <= self.width - 1
              and 0 <= coords[1] <= self.height - 1
              and self.model.get_cell_value(coords) == 0
              and coords not in self.model.revealed_zeroes
              ):
              self.model.revealed_zeroes.add(coords)
              self.reveal_zeroes(coords)

              def update_flagged_cell(self, index: Tuple[int, int]):
              """Flag/unflag cells for possible mines. Does not reveal cell."""

              if (
              index not in self.model.cells_revealed
              and index not in self.model.cells_flagged
              ):
              self.model.cells_flagged.add(index)
              self.view.flag_cell(index)

              elif (
              index not in self.model.cells_revealed
              and index in self.model.cells_flagged
              ):
              self.model.cells_flagged.remove(index)
              self.view.unflag_cell(index)

              self.update_mines()

              def update_mines(self):
              """Update mine counter."""

              mines_left = self.num_mines - len(self.model.cells_flagged)

              if mines_left >= 0:
              self.view.update_mines_left(mines_left)

              def win(self):
              """Sweet sweet victory."""

              self.model.game_state = "win"
              self.view.display_win()

              def loss(self):
              """Show loss, and reveal all cells."""

              self.model.game_state = "loss"
              self.view.display_loss()

              # Reveals all cells
              for row in range(self.height):
              for col in range(self.width):
              cell_value = self.model.get_cell_value((col,row))
              self.view.reveal_cell((col, row), cell_value)


              class InitializeGame(Frame):
              """Sets up minesweepergame. Allows player to choose difficulty"""

              def __init__(self):
              self.root = Tk()
              self.create_view_choice()
              self.create_difficulty_widgets()
              self.root.mainloop()

              def create_view_choice(self):
              "Creates widgets allowing player to choose a view type."""

              self.view_label = Label(self.root, text="Choose a view type")
              self.view_label.grid()
              self.view_types = ["GUI", "TEXT"]
              def create_button(view_type):
              button = Button(self.root, width=7, bg='grey', text=view_type)
              button.grid()
              return button

              self.view_widgets = [
              create_button(view_type) for view_type in self.view_types
              ] + [self.view_label]

              for i in range(2):
              def closure_helper(f, view_choice):
              def g(_):
              f(view_choice)
              return g
              self.view_widgets[i].bind("<Button>", closure_helper(
              self.set_up_difficulty_widgets, self.view_types[i]))

              def create_difficulty_widgets(self):
              """Set up widgets at start of game for difficulty."""

              self.diff_label = Label(self.root, text="Choose a difficulty")
              self.difficulty = ("Easy", "Medium", "Hard")
              def create_button(difficulty):
              button = Button(self.root, width=7, bg='grey', text=difficulty)
              return button

              self.difficulty_widgets = [create_button(diff)
              for diff in self.difficulty]
              self.difficulty_widgets = [self.diff_label] + self.difficulty_widgets

              def set_up_difficulty_widgets(self, view_type: str):
              """Removes view widgets. Sets up difficulty options for view chosen."""

              for widget in self.view_widgets:
              widget.grid_remove()

              if view_type == "TEXT":
              self.difficulty_widgets[0].grid()
              self.difficulty_widgets[1].grid()
              else:
              for widget in self.difficulty_widgets:
              widget.grid()
              self.bind_difficulty_widgets(view_type)

              def bind_difficulty_widgets(self, view_type: str):
              """Binds difficulty buttons."""

              for i in range(1, 4):
              def closure_helper(f, difficulty, view_type):
              def g(_):
              f(difficulty, view_type)
              return g
              self.difficulty_widgets[i].bind(
              "<Button>", closure_helper(
              self.init_game, self.difficulty[i - 1], view_type))

              def init_game(self, difficulty: str, view_type: str):
              """Begins game."""

              self.root.destroy()
              return Controller(*
              'E': (10, 10, 10, difficulty, view_type),
              'M': (16, 16, 40, difficulty, view_type),
              'H': (25, 20, 99, difficulty, view_type)
              [difficulty[0]])


              if __name__ == "__main__":
              game = InitializeGame()





              share|improve this answer

























                up vote
                0
                down vote













                Here is the revised minesweeper game with a controller that can use different interfaces. It now includes a text interface for the masochists. Also, resetting the game now destroys the root window and creates a new one. Credit for suggestions and edits are given to @Gareth and @Solomon. Anyone with tkinter experience have an answer on how to reuse the same tk window during game reset so that the previously posted problems do not happen?



                """
                Minesweeper

                Implements a basic minesweeper game using the tkinter module.
                Uses a Model-View-Controller structure.
                """

                from functools import reduce
                from itertools import product
                from operator import add
                from random import sample
                from tkinter import Button, Frame, Label, StringVar, Tk
                from typing import Set, Tuple


                def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
                """Returns adjacent coordinates for input index"""

                x, y = index

                return
                (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
                (x - 1, y), (x + 1, y),
                (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



                class Model(object):
                """Creates a board and adds mines to it."""

                def __init__(self, width: int, height: int, num_mines: int):
                self.width = width
                self.height = height
                self.num_mines = num_mines
                self.grid = self.create_grid()
                self.add_mines()
                self.grid_coords = self.grid_coords()
                self.adjacent_mine_count()
                self.cells_revealed = set()
                self.cells_flagged = set()
                self.revealed_zeroes = set()
                self.game_state = None

                def create_grid(self) -> list:
                """Returns a (width by height) grid of elements with value of 0."""

                return [[0] * self.width for _ in range(self.height)]

                def add_mines(self):
                """Randomly adds mines to board grid."""

                for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
                self.grid[y][x] = 'm'

                def grid_coords(self) -> list:
                """Returns a list of (x, y) coordinates for every position on grid."""

                return [(x, y) for y in range(self.height) for x in range(self.width)]

                def adjacent_mine_count(self):
                """Sets cell values to the number of their adjacent mines."""

                def is_mine(coords):
                try:
                if coords[0] >= 0 and coords[1] >= 0:
                return self.grid[coords[1]][coords[0]] == 'm'
                else:
                return False
                except IndexError:
                return False

                for position in self.grid_coords:
                x, y = position
                if self.grid[y][x] != "m":
                grid_value = reduce(add, map(is_mine, get_adjacent(position)))
                self.grid[y][x] = grid_value

                def get_cell_value(self, index: Tuple[int, int]) -> int or str:
                """Returns model's cell value at the given index."""

                x, y = index
                return self.grid[y][x]

                class View(Frame):
                """Creates a GUI with a grid of cell buttons."""

                def __init__(self, width: int, height: int,
                num_mines: int, difficulty: str, controller: "Controller"):
                self.master = Tk()
                self.width = width
                self.height = height
                self.num_mines = num_mines
                self.difficulty = difficulty
                self.controller = controller
                self.color_dict =
                0: 'white', 1: 'blue', 2: 'green',
                3: 'red', 4: 'orange', 5: 'purple',
                6: 'grey', 7: 'grey', 8: 'grey', "m": "black"

                self.master.title('Minesweeper')

                def create_buttons(self) -> list:
                """Create cell button widgets."""

                def create_button(x, y):
                button = Button(self.master, width=5, bg='grey')
                button.grid(row=y + 5, column=x + 1)
                return button

                return [[create_button(x, y) for x in range(self.width)]
                for y in range(self.height)]

                def initialize_bindings(self):
                """Set up the reveal cell and the flag cell key bindings."""

                for x in range(self.width):
                for y in range(self.height):
                def closure_helper(f, index):
                def g(_):
                f(index)
                return g

                # Bind reveal decision method to left click
                self.buttons[y][x].bind(
                '<Button-1>', closure_helper(
                self.controller.reveal_decision, (x, y)))

                # Bind flag method to right click
                self.buttons[y][x].bind(
                '<Button-3>', closure_helper(
                self.controller.update_flagged_cell, (x, y)))

                # Set up reset button
                self.top_panel.reset_button.bind(
                '<Button>', lambda event: self.controller.reset(event))

                def reset_view(self):
                """Destroys the GUI. Controller will create a new GUI"""

                self.master.destroy()

                def reveal_cell(self, index: Tuple[int, int], value: int or str):
                """Reveals cell's value on GUI."""

                x, y = index
                self.buttons[y][x].configure(text=value, bg=self.color_dict[value])

                def flag_cell(self, index: Tuple[int, int]):
                """Flag cell in GUI"""

                x, y = index
                self.buttons[y][x].configure(text="FLAG", bg="yellow")

                def unflag_cell(self, index: Tuple[int, int]):
                """Unflag cell in GUI"""
                x, y = index
                self.buttons[y][x].configure(text="", bg="grey")

                def update_mines_left(self, mines: int):
                """Updates mine counter widget"""

                self.top_panel.mine_count.set("Mines remaining: " + str(mines))

                def display_loss(self):
                """Display the loss label when lose condition is reached."""

                self.top_panel.loss_label.grid(row=0, columnspan=10)

                def display_win(self):
                """Display the win label when win condition is reached."""

                self.top_panel.win_label.grid(row=0, columnspan=10)

                def mainloop(self):
                self.top_panel = TopPanel(self.master, self.height,
                self.width, self.num_mines)
                self.buttons = self.create_buttons()
                self.top_panel.mines_left.grid(row=0, columnspan=5)
                self.initialize_bindings()
                self.master.mainloop()


                class TopPanel(Frame):
                """Creates a top panel which contains game information."""

                def __init__(self, master: Tk, width: int, height: int, num_mines: int):
                Frame.__init__(self, master)
                self.master = master
                self.num_mines = num_mines
                self.grid()

                self.reset_button = Button(self.master, width=7, text='Reset')
                self.reset_button.grid(row=0)

                self.loss_label = Label(text='You Lose!', bg='red')
                self.win_label = Label(text='You Win!', bg='green')

                self.mine_count = StringVar()
                self.mine_count.set('Mines remaining: ' + str(self.num_mines))
                self.mines_left = Label(textvariable=self.mine_count)


                class TextView(object):
                """Creates a text interface of the minesweeper game."""

                def __init__(self, width: int, height: int,
                num_mines: int, difficulty: str, controller: "Controller"):
                self.width = width
                self.height = height
                self.num_mines = num_mines
                self.controller = controller
                self.reveal_dict =
                0: ' 0 ', 1: ' 1 ', 2: ' 2 ',
                3: ' 3 ', 4: ' 4 ', 5: ' 5 ',
                6: ' 6 ', 7: ' 7 ', 8: ' 8 ', "m": "mine"

                self.cell_view = self.cell_view()
                self.show_grid()

                def cell_view(self)-> list:
                """Create text view of cells."""

                return [["cell" for x in range(self.width)]
                for y in range(self.height)]

                def show_grid(self):
                """Prints text grid to console. Includes column numbers."""

                top_row = [str(i) for i in range(self.width)]
                print(" ", *top_row, sep=" "*5)
                for row in range(len(self.cell_view)):
                print(str(row) + ":", *self.cell_view[row], sep=" ")

                def reveal_cell(self, index: Tuple[int, int], value: int or str):
                """Reveals a cell's value in the text view"""

                x, y = index
                self.cell_view[y][x] = self.reveal_dict[value]

                def flag_cell(self, index: Tuple[int, int]):
                """Flags cell in cell_view"""

                x, y = index
                self.cell_view[y][x] = "FLAG"

                def unflag_cell(self, index: Tuple[int, int]):
                """Unflags cell in cell_view"""

                x, y = index
                self.cell_view[y][x] = "cell"

                def update_mines_left(self, mines):
                """Updates mine counter."""

                print("Mines remaining: " + str(mines))

                def display_loss(self):
                """Displays the lose label when loss condition is reached."""

                print("You Lose!")

                def display_win(self):
                """Displays the win label when win condition is reached."""

                print("You Win!")

                def mainloop(self):
                while True:
                try:
                cmd, *coords = input(
                "Choose a cell in the format: "
                + "flag/reveal x y. Type END to quit. ").split()
                if cmd.lower()[0] == "e":
                break
                x, y = coords[0], coords[1]
                if cmd.lower()[0] == "f":
                self.controller.update_flagged_cell((int(x), int(y)))
                elif cmd.lower()[0] == "r":
                self.controller.reveal_decision((int(x), int(y)))
                else:
                print("Unknown command")
                self.show_grid()
                except:
                print("Incorrect selection or format")


                class Controller(object):
                """Sets up button bindings and minesweeper game logic.

                Reveal_decision determines how to reveal cells.
                End conditions are handled by the loss and win methods.
                """

                def __init__(self, width: int, height: int,
                num_mines: int, difficulty: str, view_type: str):
                self.width = width
                self.height = height
                self.num_mines = num_mines
                self.difficulty = difficulty
                self.model = Model(self.width, self.height, self.num_mines)
                if view_type == "GUI":
                self.view = View(self.width, self.height,
                self.num_mines, self.difficulty, self)
                elif view_type == "TEXT":
                self.view = TextView(self.width, self.height,
                self.num_mines, self.difficulty, self)
                self.view.mainloop()

                def reset(self, event):
                """Resets the game"""

                self.view.reset_view()
                self.model = Model(self.width, self.height, self.num_mines)
                self.view = View(self.width, self.height,
                self.num_mines, self.difficulty, self)
                self.view.mainloop()

                def reveal_decision(self, index: Tuple[int, int]):
                """Main decision method determining how to reveal cell."""

                x, y = index

                cell_value = self.model.get_cell_value(index)
                if index in self.model.cells_flagged:
                return None

                if cell_value in range(1, 9):
                self.reveal_cell(index, cell_value)

                elif (
                self.model.grid[y][x] == "m"
                and self.model.game_state != "win"
                ):
                self.loss()

                else:
                self.reveal_zeroes(index)

                # Check for win condition
                cells_unrevealed = self.height * self.width - len(self.model.cells_revealed)
                if cells_unrevealed == self.num_mines and self.model.game_state != "loss":
                self.win()

                def reveal_cell(self, index: Tuple[int, int], value: int or str):
                """Obtains cell value from model and passes the value to view."""

                if index in self.model.cells_flagged:
                return None
                else:
                self.model.cells_revealed.add(index)
                self.view.reveal_cell(index, value)

                def reveal_adjacent(self, index: Tuple[int, int]):
                """Reveals the 8 adjacent cells to the input cell's index."""

                for coords in get_adjacent(index):
                if (
                0 <= coords[0] <= self.width - 1
                and 0 <= coords[1] <= self.height - 1
                ):
                cell_value = self.model.get_cell_value(coords)
                self.reveal_cell(coords, cell_value)

                def reveal_zeroes(self, index: Tuple[int, int]):
                """Reveals all adjacent cells just until a mine is reached."""

                val = self.model.get_cell_value(index)

                if val == 0:
                self.reveal_cell(index, val)
                self.reveal_adjacent(index)

                for coords in get_adjacent(index):
                if (
                0 <= coords[0] <= self.width - 1
                and 0 <= coords[1] <= self.height - 1
                and self.model.get_cell_value(coords) == 0
                and coords not in self.model.revealed_zeroes
                ):
                self.model.revealed_zeroes.add(coords)
                self.reveal_zeroes(coords)

                def update_flagged_cell(self, index: Tuple[int, int]):
                """Flag/unflag cells for possible mines. Does not reveal cell."""

                if (
                index not in self.model.cells_revealed
                and index not in self.model.cells_flagged
                ):
                self.model.cells_flagged.add(index)
                self.view.flag_cell(index)

                elif (
                index not in self.model.cells_revealed
                and index in self.model.cells_flagged
                ):
                self.model.cells_flagged.remove(index)
                self.view.unflag_cell(index)

                self.update_mines()

                def update_mines(self):
                """Update mine counter."""

                mines_left = self.num_mines - len(self.model.cells_flagged)

                if mines_left >= 0:
                self.view.update_mines_left(mines_left)

                def win(self):
                """Sweet sweet victory."""

                self.model.game_state = "win"
                self.view.display_win()

                def loss(self):
                """Show loss, and reveal all cells."""

                self.model.game_state = "loss"
                self.view.display_loss()

                # Reveals all cells
                for row in range(self.height):
                for col in range(self.width):
                cell_value = self.model.get_cell_value((col,row))
                self.view.reveal_cell((col, row), cell_value)


                class InitializeGame(Frame):
                """Sets up minesweepergame. Allows player to choose difficulty"""

                def __init__(self):
                self.root = Tk()
                self.create_view_choice()
                self.create_difficulty_widgets()
                self.root.mainloop()

                def create_view_choice(self):
                "Creates widgets allowing player to choose a view type."""

                self.view_label = Label(self.root, text="Choose a view type")
                self.view_label.grid()
                self.view_types = ["GUI", "TEXT"]
                def create_button(view_type):
                button = Button(self.root, width=7, bg='grey', text=view_type)
                button.grid()
                return button

                self.view_widgets = [
                create_button(view_type) for view_type in self.view_types
                ] + [self.view_label]

                for i in range(2):
                def closure_helper(f, view_choice):
                def g(_):
                f(view_choice)
                return g
                self.view_widgets[i].bind("<Button>", closure_helper(
                self.set_up_difficulty_widgets, self.view_types[i]))

                def create_difficulty_widgets(self):
                """Set up widgets at start of game for difficulty."""

                self.diff_label = Label(self.root, text="Choose a difficulty")
                self.difficulty = ("Easy", "Medium", "Hard")
                def create_button(difficulty):
                button = Button(self.root, width=7, bg='grey', text=difficulty)
                return button

                self.difficulty_widgets = [create_button(diff)
                for diff in self.difficulty]
                self.difficulty_widgets = [self.diff_label] + self.difficulty_widgets

                def set_up_difficulty_widgets(self, view_type: str):
                """Removes view widgets. Sets up difficulty options for view chosen."""

                for widget in self.view_widgets:
                widget.grid_remove()

                if view_type == "TEXT":
                self.difficulty_widgets[0].grid()
                self.difficulty_widgets[1].grid()
                else:
                for widget in self.difficulty_widgets:
                widget.grid()
                self.bind_difficulty_widgets(view_type)

                def bind_difficulty_widgets(self, view_type: str):
                """Binds difficulty buttons."""

                for i in range(1, 4):
                def closure_helper(f, difficulty, view_type):
                def g(_):
                f(difficulty, view_type)
                return g
                self.difficulty_widgets[i].bind(
                "<Button>", closure_helper(
                self.init_game, self.difficulty[i - 1], view_type))

                def init_game(self, difficulty: str, view_type: str):
                """Begins game."""

                self.root.destroy()
                return Controller(*
                'E': (10, 10, 10, difficulty, view_type),
                'M': (16, 16, 40, difficulty, view_type),
                'H': (25, 20, 99, difficulty, view_type)
                [difficulty[0]])


                if __name__ == "__main__":
                game = InitializeGame()





                share|improve this answer























                  up vote
                  0
                  down vote










                  up vote
                  0
                  down vote









                  Here is the revised minesweeper game with a controller that can use different interfaces. It now includes a text interface for the masochists. Also, resetting the game now destroys the root window and creates a new one. Credit for suggestions and edits are given to @Gareth and @Solomon. Anyone with tkinter experience have an answer on how to reuse the same tk window during game reset so that the previously posted problems do not happen?



                  """
                  Minesweeper

                  Implements a basic minesweeper game using the tkinter module.
                  Uses a Model-View-Controller structure.
                  """

                  from functools import reduce
                  from itertools import product
                  from operator import add
                  from random import sample
                  from tkinter import Button, Frame, Label, StringVar, Tk
                  from typing import Set, Tuple


                  def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
                  """Returns adjacent coordinates for input index"""

                  x, y = index

                  return
                  (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
                  (x - 1, y), (x + 1, y),
                  (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



                  class Model(object):
                  """Creates a board and adds mines to it."""

                  def __init__(self, width: int, height: int, num_mines: int):
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.grid = self.create_grid()
                  self.add_mines()
                  self.grid_coords = self.grid_coords()
                  self.adjacent_mine_count()
                  self.cells_revealed = set()
                  self.cells_flagged = set()
                  self.revealed_zeroes = set()
                  self.game_state = None

                  def create_grid(self) -> list:
                  """Returns a (width by height) grid of elements with value of 0."""

                  return [[0] * self.width for _ in range(self.height)]

                  def add_mines(self):
                  """Randomly adds mines to board grid."""

                  for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
                  self.grid[y][x] = 'm'

                  def grid_coords(self) -> list:
                  """Returns a list of (x, y) coordinates for every position on grid."""

                  return [(x, y) for y in range(self.height) for x in range(self.width)]

                  def adjacent_mine_count(self):
                  """Sets cell values to the number of their adjacent mines."""

                  def is_mine(coords):
                  try:
                  if coords[0] >= 0 and coords[1] >= 0:
                  return self.grid[coords[1]][coords[0]] == 'm'
                  else:
                  return False
                  except IndexError:
                  return False

                  for position in self.grid_coords:
                  x, y = position
                  if self.grid[y][x] != "m":
                  grid_value = reduce(add, map(is_mine, get_adjacent(position)))
                  self.grid[y][x] = grid_value

                  def get_cell_value(self, index: Tuple[int, int]) -> int or str:
                  """Returns model's cell value at the given index."""

                  x, y = index
                  return self.grid[y][x]

                  class View(Frame):
                  """Creates a GUI with a grid of cell buttons."""

                  def __init__(self, width: int, height: int,
                  num_mines: int, difficulty: str, controller: "Controller"):
                  self.master = Tk()
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.difficulty = difficulty
                  self.controller = controller
                  self.color_dict =
                  0: 'white', 1: 'blue', 2: 'green',
                  3: 'red', 4: 'orange', 5: 'purple',
                  6: 'grey', 7: 'grey', 8: 'grey', "m": "black"

                  self.master.title('Minesweeper')

                  def create_buttons(self) -> list:
                  """Create cell button widgets."""

                  def create_button(x, y):
                  button = Button(self.master, width=5, bg='grey')
                  button.grid(row=y + 5, column=x + 1)
                  return button

                  return [[create_button(x, y) for x in range(self.width)]
                  for y in range(self.height)]

                  def initialize_bindings(self):
                  """Set up the reveal cell and the flag cell key bindings."""

                  for x in range(self.width):
                  for y in range(self.height):
                  def closure_helper(f, index):
                  def g(_):
                  f(index)
                  return g

                  # Bind reveal decision method to left click
                  self.buttons[y][x].bind(
                  '<Button-1>', closure_helper(
                  self.controller.reveal_decision, (x, y)))

                  # Bind flag method to right click
                  self.buttons[y][x].bind(
                  '<Button-3>', closure_helper(
                  self.controller.update_flagged_cell, (x, y)))

                  # Set up reset button
                  self.top_panel.reset_button.bind(
                  '<Button>', lambda event: self.controller.reset(event))

                  def reset_view(self):
                  """Destroys the GUI. Controller will create a new GUI"""

                  self.master.destroy()

                  def reveal_cell(self, index: Tuple[int, int], value: int or str):
                  """Reveals cell's value on GUI."""

                  x, y = index
                  self.buttons[y][x].configure(text=value, bg=self.color_dict[value])

                  def flag_cell(self, index: Tuple[int, int]):
                  """Flag cell in GUI"""

                  x, y = index
                  self.buttons[y][x].configure(text="FLAG", bg="yellow")

                  def unflag_cell(self, index: Tuple[int, int]):
                  """Unflag cell in GUI"""
                  x, y = index
                  self.buttons[y][x].configure(text="", bg="grey")

                  def update_mines_left(self, mines: int):
                  """Updates mine counter widget"""

                  self.top_panel.mine_count.set("Mines remaining: " + str(mines))

                  def display_loss(self):
                  """Display the loss label when lose condition is reached."""

                  self.top_panel.loss_label.grid(row=0, columnspan=10)

                  def display_win(self):
                  """Display the win label when win condition is reached."""

                  self.top_panel.win_label.grid(row=0, columnspan=10)

                  def mainloop(self):
                  self.top_panel = TopPanel(self.master, self.height,
                  self.width, self.num_mines)
                  self.buttons = self.create_buttons()
                  self.top_panel.mines_left.grid(row=0, columnspan=5)
                  self.initialize_bindings()
                  self.master.mainloop()


                  class TopPanel(Frame):
                  """Creates a top panel which contains game information."""

                  def __init__(self, master: Tk, width: int, height: int, num_mines: int):
                  Frame.__init__(self, master)
                  self.master = master
                  self.num_mines = num_mines
                  self.grid()

                  self.reset_button = Button(self.master, width=7, text='Reset')
                  self.reset_button.grid(row=0)

                  self.loss_label = Label(text='You Lose!', bg='red')
                  self.win_label = Label(text='You Win!', bg='green')

                  self.mine_count = StringVar()
                  self.mine_count.set('Mines remaining: ' + str(self.num_mines))
                  self.mines_left = Label(textvariable=self.mine_count)


                  class TextView(object):
                  """Creates a text interface of the minesweeper game."""

                  def __init__(self, width: int, height: int,
                  num_mines: int, difficulty: str, controller: "Controller"):
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.controller = controller
                  self.reveal_dict =
                  0: ' 0 ', 1: ' 1 ', 2: ' 2 ',
                  3: ' 3 ', 4: ' 4 ', 5: ' 5 ',
                  6: ' 6 ', 7: ' 7 ', 8: ' 8 ', "m": "mine"

                  self.cell_view = self.cell_view()
                  self.show_grid()

                  def cell_view(self)-> list:
                  """Create text view of cells."""

                  return [["cell" for x in range(self.width)]
                  for y in range(self.height)]

                  def show_grid(self):
                  """Prints text grid to console. Includes column numbers."""

                  top_row = [str(i) for i in range(self.width)]
                  print(" ", *top_row, sep=" "*5)
                  for row in range(len(self.cell_view)):
                  print(str(row) + ":", *self.cell_view[row], sep=" ")

                  def reveal_cell(self, index: Tuple[int, int], value: int or str):
                  """Reveals a cell's value in the text view"""

                  x, y = index
                  self.cell_view[y][x] = self.reveal_dict[value]

                  def flag_cell(self, index: Tuple[int, int]):
                  """Flags cell in cell_view"""

                  x, y = index
                  self.cell_view[y][x] = "FLAG"

                  def unflag_cell(self, index: Tuple[int, int]):
                  """Unflags cell in cell_view"""

                  x, y = index
                  self.cell_view[y][x] = "cell"

                  def update_mines_left(self, mines):
                  """Updates mine counter."""

                  print("Mines remaining: " + str(mines))

                  def display_loss(self):
                  """Displays the lose label when loss condition is reached."""

                  print("You Lose!")

                  def display_win(self):
                  """Displays the win label when win condition is reached."""

                  print("You Win!")

                  def mainloop(self):
                  while True:
                  try:
                  cmd, *coords = input(
                  "Choose a cell in the format: "
                  + "flag/reveal x y. Type END to quit. ").split()
                  if cmd.lower()[0] == "e":
                  break
                  x, y = coords[0], coords[1]
                  if cmd.lower()[0] == "f":
                  self.controller.update_flagged_cell((int(x), int(y)))
                  elif cmd.lower()[0] == "r":
                  self.controller.reveal_decision((int(x), int(y)))
                  else:
                  print("Unknown command")
                  self.show_grid()
                  except:
                  print("Incorrect selection or format")


                  class Controller(object):
                  """Sets up button bindings and minesweeper game logic.

                  Reveal_decision determines how to reveal cells.
                  End conditions are handled by the loss and win methods.
                  """

                  def __init__(self, width: int, height: int,
                  num_mines: int, difficulty: str, view_type: str):
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.difficulty = difficulty
                  self.model = Model(self.width, self.height, self.num_mines)
                  if view_type == "GUI":
                  self.view = View(self.width, self.height,
                  self.num_mines, self.difficulty, self)
                  elif view_type == "TEXT":
                  self.view = TextView(self.width, self.height,
                  self.num_mines, self.difficulty, self)
                  self.view.mainloop()

                  def reset(self, event):
                  """Resets the game"""

                  self.view.reset_view()
                  self.model = Model(self.width, self.height, self.num_mines)
                  self.view = View(self.width, self.height,
                  self.num_mines, self.difficulty, self)
                  self.view.mainloop()

                  def reveal_decision(self, index: Tuple[int, int]):
                  """Main decision method determining how to reveal cell."""

                  x, y = index

                  cell_value = self.model.get_cell_value(index)
                  if index in self.model.cells_flagged:
                  return None

                  if cell_value in range(1, 9):
                  self.reveal_cell(index, cell_value)

                  elif (
                  self.model.grid[y][x] == "m"
                  and self.model.game_state != "win"
                  ):
                  self.loss()

                  else:
                  self.reveal_zeroes(index)

                  # Check for win condition
                  cells_unrevealed = self.height * self.width - len(self.model.cells_revealed)
                  if cells_unrevealed == self.num_mines and self.model.game_state != "loss":
                  self.win()

                  def reveal_cell(self, index: Tuple[int, int], value: int or str):
                  """Obtains cell value from model and passes the value to view."""

                  if index in self.model.cells_flagged:
                  return None
                  else:
                  self.model.cells_revealed.add(index)
                  self.view.reveal_cell(index, value)

                  def reveal_adjacent(self, index: Tuple[int, int]):
                  """Reveals the 8 adjacent cells to the input cell's index."""

                  for coords in get_adjacent(index):
                  if (
                  0 <= coords[0] <= self.width - 1
                  and 0 <= coords[1] <= self.height - 1
                  ):
                  cell_value = self.model.get_cell_value(coords)
                  self.reveal_cell(coords, cell_value)

                  def reveal_zeroes(self, index: Tuple[int, int]):
                  """Reveals all adjacent cells just until a mine is reached."""

                  val = self.model.get_cell_value(index)

                  if val == 0:
                  self.reveal_cell(index, val)
                  self.reveal_adjacent(index)

                  for coords in get_adjacent(index):
                  if (
                  0 <= coords[0] <= self.width - 1
                  and 0 <= coords[1] <= self.height - 1
                  and self.model.get_cell_value(coords) == 0
                  and coords not in self.model.revealed_zeroes
                  ):
                  self.model.revealed_zeroes.add(coords)
                  self.reveal_zeroes(coords)

                  def update_flagged_cell(self, index: Tuple[int, int]):
                  """Flag/unflag cells for possible mines. Does not reveal cell."""

                  if (
                  index not in self.model.cells_revealed
                  and index not in self.model.cells_flagged
                  ):
                  self.model.cells_flagged.add(index)
                  self.view.flag_cell(index)

                  elif (
                  index not in self.model.cells_revealed
                  and index in self.model.cells_flagged
                  ):
                  self.model.cells_flagged.remove(index)
                  self.view.unflag_cell(index)

                  self.update_mines()

                  def update_mines(self):
                  """Update mine counter."""

                  mines_left = self.num_mines - len(self.model.cells_flagged)

                  if mines_left >= 0:
                  self.view.update_mines_left(mines_left)

                  def win(self):
                  """Sweet sweet victory."""

                  self.model.game_state = "win"
                  self.view.display_win()

                  def loss(self):
                  """Show loss, and reveal all cells."""

                  self.model.game_state = "loss"
                  self.view.display_loss()

                  # Reveals all cells
                  for row in range(self.height):
                  for col in range(self.width):
                  cell_value = self.model.get_cell_value((col,row))
                  self.view.reveal_cell((col, row), cell_value)


                  class InitializeGame(Frame):
                  """Sets up minesweepergame. Allows player to choose difficulty"""

                  def __init__(self):
                  self.root = Tk()
                  self.create_view_choice()
                  self.create_difficulty_widgets()
                  self.root.mainloop()

                  def create_view_choice(self):
                  "Creates widgets allowing player to choose a view type."""

                  self.view_label = Label(self.root, text="Choose a view type")
                  self.view_label.grid()
                  self.view_types = ["GUI", "TEXT"]
                  def create_button(view_type):
                  button = Button(self.root, width=7, bg='grey', text=view_type)
                  button.grid()
                  return button

                  self.view_widgets = [
                  create_button(view_type) for view_type in self.view_types
                  ] + [self.view_label]

                  for i in range(2):
                  def closure_helper(f, view_choice):
                  def g(_):
                  f(view_choice)
                  return g
                  self.view_widgets[i].bind("<Button>", closure_helper(
                  self.set_up_difficulty_widgets, self.view_types[i]))

                  def create_difficulty_widgets(self):
                  """Set up widgets at start of game for difficulty."""

                  self.diff_label = Label(self.root, text="Choose a difficulty")
                  self.difficulty = ("Easy", "Medium", "Hard")
                  def create_button(difficulty):
                  button = Button(self.root, width=7, bg='grey', text=difficulty)
                  return button

                  self.difficulty_widgets = [create_button(diff)
                  for diff in self.difficulty]
                  self.difficulty_widgets = [self.diff_label] + self.difficulty_widgets

                  def set_up_difficulty_widgets(self, view_type: str):
                  """Removes view widgets. Sets up difficulty options for view chosen."""

                  for widget in self.view_widgets:
                  widget.grid_remove()

                  if view_type == "TEXT":
                  self.difficulty_widgets[0].grid()
                  self.difficulty_widgets[1].grid()
                  else:
                  for widget in self.difficulty_widgets:
                  widget.grid()
                  self.bind_difficulty_widgets(view_type)

                  def bind_difficulty_widgets(self, view_type: str):
                  """Binds difficulty buttons."""

                  for i in range(1, 4):
                  def closure_helper(f, difficulty, view_type):
                  def g(_):
                  f(difficulty, view_type)
                  return g
                  self.difficulty_widgets[i].bind(
                  "<Button>", closure_helper(
                  self.init_game, self.difficulty[i - 1], view_type))

                  def init_game(self, difficulty: str, view_type: str):
                  """Begins game."""

                  self.root.destroy()
                  return Controller(*
                  'E': (10, 10, 10, difficulty, view_type),
                  'M': (16, 16, 40, difficulty, view_type),
                  'H': (25, 20, 99, difficulty, view_type)
                  [difficulty[0]])


                  if __name__ == "__main__":
                  game = InitializeGame()





                  share|improve this answer













                  Here is the revised minesweeper game with a controller that can use different interfaces. It now includes a text interface for the masochists. Also, resetting the game now destroys the root window and creates a new one. Credit for suggestions and edits are given to @Gareth and @Solomon. Anyone with tkinter experience have an answer on how to reuse the same tk window during game reset so that the previously posted problems do not happen?



                  """
                  Minesweeper

                  Implements a basic minesweeper game using the tkinter module.
                  Uses a Model-View-Controller structure.
                  """

                  from functools import reduce
                  from itertools import product
                  from operator import add
                  from random import sample
                  from tkinter import Button, Frame, Label, StringVar, Tk
                  from typing import Set, Tuple


                  def get_adjacent(index: Tuple[int, int]) -> Set[Tuple[int, int]]:
                  """Returns adjacent coordinates for input index"""

                  x, y = index

                  return
                  (x - 1, y - 1), (x, y - 1), (x + 1, y - 1),
                  (x - 1, y), (x + 1, y),
                  (x - 1, y + 1), (x, y + 1), (x + 1, y + 1),



                  class Model(object):
                  """Creates a board and adds mines to it."""

                  def __init__(self, width: int, height: int, num_mines: int):
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.grid = self.create_grid()
                  self.add_mines()
                  self.grid_coords = self.grid_coords()
                  self.adjacent_mine_count()
                  self.cells_revealed = set()
                  self.cells_flagged = set()
                  self.revealed_zeroes = set()
                  self.game_state = None

                  def create_grid(self) -> list:
                  """Returns a (width by height) grid of elements with value of 0."""

                  return [[0] * self.width for _ in range(self.height)]

                  def add_mines(self):
                  """Randomly adds mines to board grid."""

                  for x, y in sample(list(product(range(self.width), range(self.height))), self.num_mines):
                  self.grid[y][x] = 'm'

                  def grid_coords(self) -> list:
                  """Returns a list of (x, y) coordinates for every position on grid."""

                  return [(x, y) for y in range(self.height) for x in range(self.width)]

                  def adjacent_mine_count(self):
                  """Sets cell values to the number of their adjacent mines."""

                  def is_mine(coords):
                  try:
                  if coords[0] >= 0 and coords[1] >= 0:
                  return self.grid[coords[1]][coords[0]] == 'm'
                  else:
                  return False
                  except IndexError:
                  return False

                  for position in self.grid_coords:
                  x, y = position
                  if self.grid[y][x] != "m":
                  grid_value = reduce(add, map(is_mine, get_adjacent(position)))
                  self.grid[y][x] = grid_value

                  def get_cell_value(self, index: Tuple[int, int]) -> int or str:
                  """Returns model's cell value at the given index."""

                  x, y = index
                  return self.grid[y][x]

                  class View(Frame):
                  """Creates a GUI with a grid of cell buttons."""

                  def __init__(self, width: int, height: int,
                  num_mines: int, difficulty: str, controller: "Controller"):
                  self.master = Tk()
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.difficulty = difficulty
                  self.controller = controller
                  self.color_dict =
                  0: 'white', 1: 'blue', 2: 'green',
                  3: 'red', 4: 'orange', 5: 'purple',
                  6: 'grey', 7: 'grey', 8: 'grey', "m": "black"

                  self.master.title('Minesweeper')

                  def create_buttons(self) -> list:
                  """Create cell button widgets."""

                  def create_button(x, y):
                  button = Button(self.master, width=5, bg='grey')
                  button.grid(row=y + 5, column=x + 1)
                  return button

                  return [[create_button(x, y) for x in range(self.width)]
                  for y in range(self.height)]

                  def initialize_bindings(self):
                  """Set up the reveal cell and the flag cell key bindings."""

                  for x in range(self.width):
                  for y in range(self.height):
                  def closure_helper(f, index):
                  def g(_):
                  f(index)
                  return g

                  # Bind reveal decision method to left click
                  self.buttons[y][x].bind(
                  '<Button-1>', closure_helper(
                  self.controller.reveal_decision, (x, y)))

                  # Bind flag method to right click
                  self.buttons[y][x].bind(
                  '<Button-3>', closure_helper(
                  self.controller.update_flagged_cell, (x, y)))

                  # Set up reset button
                  self.top_panel.reset_button.bind(
                  '<Button>', lambda event: self.controller.reset(event))

                  def reset_view(self):
                  """Destroys the GUI. Controller will create a new GUI"""

                  self.master.destroy()

                  def reveal_cell(self, index: Tuple[int, int], value: int or str):
                  """Reveals cell's value on GUI."""

                  x, y = index
                  self.buttons[y][x].configure(text=value, bg=self.color_dict[value])

                  def flag_cell(self, index: Tuple[int, int]):
                  """Flag cell in GUI"""

                  x, y = index
                  self.buttons[y][x].configure(text="FLAG", bg="yellow")

                  def unflag_cell(self, index: Tuple[int, int]):
                  """Unflag cell in GUI"""
                  x, y = index
                  self.buttons[y][x].configure(text="", bg="grey")

                  def update_mines_left(self, mines: int):
                  """Updates mine counter widget"""

                  self.top_panel.mine_count.set("Mines remaining: " + str(mines))

                  def display_loss(self):
                  """Display the loss label when lose condition is reached."""

                  self.top_panel.loss_label.grid(row=0, columnspan=10)

                  def display_win(self):
                  """Display the win label when win condition is reached."""

                  self.top_panel.win_label.grid(row=0, columnspan=10)

                  def mainloop(self):
                  self.top_panel = TopPanel(self.master, self.height,
                  self.width, self.num_mines)
                  self.buttons = self.create_buttons()
                  self.top_panel.mines_left.grid(row=0, columnspan=5)
                  self.initialize_bindings()
                  self.master.mainloop()


                  class TopPanel(Frame):
                  """Creates a top panel which contains game information."""

                  def __init__(self, master: Tk, width: int, height: int, num_mines: int):
                  Frame.__init__(self, master)
                  self.master = master
                  self.num_mines = num_mines
                  self.grid()

                  self.reset_button = Button(self.master, width=7, text='Reset')
                  self.reset_button.grid(row=0)

                  self.loss_label = Label(text='You Lose!', bg='red')
                  self.win_label = Label(text='You Win!', bg='green')

                  self.mine_count = StringVar()
                  self.mine_count.set('Mines remaining: ' + str(self.num_mines))
                  self.mines_left = Label(textvariable=self.mine_count)


                  class TextView(object):
                  """Creates a text interface of the minesweeper game."""

                  def __init__(self, width: int, height: int,
                  num_mines: int, difficulty: str, controller: "Controller"):
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.controller = controller
                  self.reveal_dict =
                  0: ' 0 ', 1: ' 1 ', 2: ' 2 ',
                  3: ' 3 ', 4: ' 4 ', 5: ' 5 ',
                  6: ' 6 ', 7: ' 7 ', 8: ' 8 ', "m": "mine"

                  self.cell_view = self.cell_view()
                  self.show_grid()

                  def cell_view(self)-> list:
                  """Create text view of cells."""

                  return [["cell" for x in range(self.width)]
                  for y in range(self.height)]

                  def show_grid(self):
                  """Prints text grid to console. Includes column numbers."""

                  top_row = [str(i) for i in range(self.width)]
                  print(" ", *top_row, sep=" "*5)
                  for row in range(len(self.cell_view)):
                  print(str(row) + ":", *self.cell_view[row], sep=" ")

                  def reveal_cell(self, index: Tuple[int, int], value: int or str):
                  """Reveals a cell's value in the text view"""

                  x, y = index
                  self.cell_view[y][x] = self.reveal_dict[value]

                  def flag_cell(self, index: Tuple[int, int]):
                  """Flags cell in cell_view"""

                  x, y = index
                  self.cell_view[y][x] = "FLAG"

                  def unflag_cell(self, index: Tuple[int, int]):
                  """Unflags cell in cell_view"""

                  x, y = index
                  self.cell_view[y][x] = "cell"

                  def update_mines_left(self, mines):
                  """Updates mine counter."""

                  print("Mines remaining: " + str(mines))

                  def display_loss(self):
                  """Displays the lose label when loss condition is reached."""

                  print("You Lose!")

                  def display_win(self):
                  """Displays the win label when win condition is reached."""

                  print("You Win!")

                  def mainloop(self):
                  while True:
                  try:
                  cmd, *coords = input(
                  "Choose a cell in the format: "
                  + "flag/reveal x y. Type END to quit. ").split()
                  if cmd.lower()[0] == "e":
                  break
                  x, y = coords[0], coords[1]
                  if cmd.lower()[0] == "f":
                  self.controller.update_flagged_cell((int(x), int(y)))
                  elif cmd.lower()[0] == "r":
                  self.controller.reveal_decision((int(x), int(y)))
                  else:
                  print("Unknown command")
                  self.show_grid()
                  except:
                  print("Incorrect selection or format")


                  class Controller(object):
                  """Sets up button bindings and minesweeper game logic.

                  Reveal_decision determines how to reveal cells.
                  End conditions are handled by the loss and win methods.
                  """

                  def __init__(self, width: int, height: int,
                  num_mines: int, difficulty: str, view_type: str):
                  self.width = width
                  self.height = height
                  self.num_mines = num_mines
                  self.difficulty = difficulty
                  self.model = Model(self.width, self.height, self.num_mines)
                  if view_type == "GUI":
                  self.view = View(self.width, self.height,
                  self.num_mines, self.difficulty, self)
                  elif view_type == "TEXT":
                  self.view = TextView(self.width, self.height,
                  self.num_mines, self.difficulty, self)
                  self.view.mainloop()

                  def reset(self, event):
                  """Resets the game"""

                  self.view.reset_view()
                  self.model = Model(self.width, self.height, self.num_mines)
                  self.view = View(self.width, self.height,
                  self.num_mines, self.difficulty, self)
                  self.view.mainloop()

                  def reveal_decision(self, index: Tuple[int, int]):
                  """Main decision method determining how to reveal cell."""

                  x, y = index

                  cell_value = self.model.get_cell_value(index)
                  if index in self.model.cells_flagged:
                  return None

                  if cell_value in range(1, 9):
                  self.reveal_cell(index, cell_value)

                  elif (
                  self.model.grid[y][x] == "m"
                  and self.model.game_state != "win"
                  ):
                  self.loss()

                  else:
                  self.reveal_zeroes(index)

                  # Check for win condition
                  cells_unrevealed = self.height * self.width - len(self.model.cells_revealed)
                  if cells_unrevealed == self.num_mines and self.model.game_state != "loss":
                  self.win()

                  def reveal_cell(self, index: Tuple[int, int], value: int or str):
                  """Obtains cell value from model and passes the value to view."""

                  if index in self.model.cells_flagged:
                  return None
                  else:
                  self.model.cells_revealed.add(index)
                  self.view.reveal_cell(index, value)

                  def reveal_adjacent(self, index: Tuple[int, int]):
                  """Reveals the 8 adjacent cells to the input cell's index."""

                  for coords in get_adjacent(index):
                  if (
                  0 <= coords[0] <= self.width - 1
                  and 0 <= coords[1] <= self.height - 1
                  ):
                  cell_value = self.model.get_cell_value(coords)
                  self.reveal_cell(coords, cell_value)

                  def reveal_zeroes(self, index: Tuple[int, int]):
                  """Reveals all adjacent cells just until a mine is reached."""

                  val = self.model.get_cell_value(index)

                  if val == 0:
                  self.reveal_cell(index, val)
                  self.reveal_adjacent(index)

                  for coords in get_adjacent(index):
                  if (
                  0 <= coords[0] <= self.width - 1
                  and 0 <= coords[1] <= self.height - 1
                  and self.model.get_cell_value(coords) == 0
                  and coords not in self.model.revealed_zeroes
                  ):
                  self.model.revealed_zeroes.add(coords)
                  self.reveal_zeroes(coords)

                  def update_flagged_cell(self, index: Tuple[int, int]):
                  """Flag/unflag cells for possible mines. Does not reveal cell."""

                  if (
                  index not in self.model.cells_revealed
                  and index not in self.model.cells_flagged
                  ):
                  self.model.cells_flagged.add(index)
                  self.view.flag_cell(index)

                  elif (
                  index not in self.model.cells_revealed
                  and index in self.model.cells_flagged
                  ):
                  self.model.cells_flagged.remove(index)
                  self.view.unflag_cell(index)

                  self.update_mines()

                  def update_mines(self):
                  """Update mine counter."""

                  mines_left = self.num_mines - len(self.model.cells_flagged)

                  if mines_left >= 0:
                  self.view.update_mines_left(mines_left)

                  def win(self):
                  """Sweet sweet victory."""

                  self.model.game_state = "win"
                  self.view.display_win()

                  def loss(self):
                  """Show loss, and reveal all cells."""

                  self.model.game_state = "loss"
                  self.view.display_loss()

                  # Reveals all cells
                  for row in range(self.height):
                  for col in range(self.width):
                  cell_value = self.model.get_cell_value((col,row))
                  self.view.reveal_cell((col, row), cell_value)


                  class InitializeGame(Frame):
                  """Sets up minesweepergame. Allows player to choose difficulty"""

                  def __init__(self):
                  self.root = Tk()
                  self.create_view_choice()
                  self.create_difficulty_widgets()
                  self.root.mainloop()

                  def create_view_choice(self):
                  "Creates widgets allowing player to choose a view type."""

                  self.view_label = Label(self.root, text="Choose a view type")
                  self.view_label.grid()
                  self.view_types = ["GUI", "TEXT"]
                  def create_button(view_type):
                  button = Button(self.root, width=7, bg='grey', text=view_type)
                  button.grid()
                  return button

                  self.view_widgets = [
                  create_button(view_type) for view_type in self.view_types
                  ] + [self.view_label]

                  for i in range(2):
                  def closure_helper(f, view_choice):
                  def g(_):
                  f(view_choice)
                  return g
                  self.view_widgets[i].bind("<Button>", closure_helper(
                  self.set_up_difficulty_widgets, self.view_types[i]))

                  def create_difficulty_widgets(self):
                  """Set up widgets at start of game for difficulty."""

                  self.diff_label = Label(self.root, text="Choose a difficulty")
                  self.difficulty = ("Easy", "Medium", "Hard")
                  def create_button(difficulty):
                  button = Button(self.root, width=7, bg='grey', text=difficulty)
                  return button

                  self.difficulty_widgets = [create_button(diff)
                  for diff in self.difficulty]
                  self.difficulty_widgets = [self.diff_label] + self.difficulty_widgets

                  def set_up_difficulty_widgets(self, view_type: str):
                  """Removes view widgets. Sets up difficulty options for view chosen."""

                  for widget in self.view_widgets:
                  widget.grid_remove()

                  if view_type == "TEXT":
                  self.difficulty_widgets[0].grid()
                  self.difficulty_widgets[1].grid()
                  else:
                  for widget in self.difficulty_widgets:
                  widget.grid()
                  self.bind_difficulty_widgets(view_type)

                  def bind_difficulty_widgets(self, view_type: str):
                  """Binds difficulty buttons."""

                  for i in range(1, 4):
                  def closure_helper(f, difficulty, view_type):
                  def g(_):
                  f(difficulty, view_type)
                  return g
                  self.difficulty_widgets[i].bind(
                  "<Button>", closure_helper(
                  self.init_game, self.difficulty[i - 1], view_type))

                  def init_game(self, difficulty: str, view_type: str):
                  """Begins game."""

                  self.root.destroy()
                  return Controller(*
                  'E': (10, 10, 10, difficulty, view_type),
                  'M': (16, 16, 40, difficulty, view_type),
                  'H': (25, 20, 99, difficulty, view_type)
                  [difficulty[0]])


                  if __name__ == "__main__":
                  game = InitializeGame()






                  share|improve this answer













                  share|improve this answer



                  share|improve this answer











                  answered Apr 5 at 3:13









                  EndreoT

                  564




                  564






















                       

                      draft saved


                      draft discarded


























                       


                      draft saved


                      draft discarded














                      StackExchange.ready(
                      function ()
                      StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fcodereview.stackexchange.com%2fquestions%2f190461%2fpython-3-simple-minesweeper-game-using-tkinter%23new-answer', 'question_page');

                      );

                      Post as a guest













































































                      Popular posts from this blog

                      Python Lists

                      Aion

                      JavaScript Array Iteration Methods