В Python 3 простая игра "Сапер" с помощью tkinter


Я относительно новым для программирования, и я хотел бы использовать эту простую игру Сапер в портфолио. Несколько вопросов:

  1. В настоящее время установки игра становится все медленнее с каждой кнопки сброса вызова, и высота окна немного увеличивает вниз. Это очень заметно на средний и жесткий трудности. Какие изменения могут ускорить этот код? Я хочу, чтобы повторно использовать то же окно с каждой перезагрузки, если это возможно.
  2. Код использует модель-представление-контроллер подхода. Имеет ли это смысл для проекта с использованием tkinter? Есть ли лучший подход?
  3. Некоторые детали, такие как дополнения переменной повторяются, но я не могу найти способ сделать их переменные экземпляра.

Любые предложения/конструктивная обратная связь приветствуется.

"""
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()


Комментарии
3 ответа

Это хорошая идея, чтобы использовать модель–представление–контроллер шаблон. Но реализация требует доработки.

В MVC, модель должна содержать полное описание данных манипулируют, вместе с операции на этих данных. В случае игры Сапер, модель должна состоять из следующих данных:


  1. размер игровой зоны;

  2. расположение мин;

  3. какие квадраты до сих пор нет покрытия;

  4. расположение флагов;

  5. состояние игры (выигрыш/проигрыш/до сих пор играет);

вместе с операциями:


  1. установите или снимите флажок;

  2. раскрыть квадрат;

  3. начать новую игру.

Идея заключается в том, что вы должны быть в состоянии перенести программу на другой вид интерфейса путем замены View и Controller, и оставить модель без изменений. Но при осуществлении в посте, большая часть данных, и все операции, вместо этого пошли в контроллер. Это делает его неудобным для замены контроллера, как все это должно быть переопределена в новый контроллер.

5
ответ дан 26 марта 2018 в 06:03 Источник Поделиться

Игра выглядит великолепно! Код выглядит также очень хорошо!

Я, безусловно, согласен с Гарет Риз на самом деле, разделяющей части в MVC.

Что я изменил


  1. Я добавил тип подсказки ко всем функциям.

  2. Я исправил несколько опечаток, а также при добавлении периодов к концам комментарии.

  3. Я изменил большинство функция инициализатора призывает быть функционального (меняется от назначения внутри функции, чтобы возвратить значение из функции и делать уступки в пределах инициализатора).

  4. Я поменял много for петли для итератора "математика".

  5. Я извлек из списка соседних ячеек в отдельную функцию, так как он был повторен много.

  6. Я изменил некоторые типы данных из списков (или строки) для множеств или кортежей.

  7. Я заменил индексации кортеж кортеж с распаковкой.

  8. Я пытался уменьшить повторения в основной функции.

  9. Возможно, несколько других небольших вещей.

Что еще нужно сделать


  1. Разделение компонентов MVC.

  2. Было бы неплохо, если приглашение на трудность основе графического интерфейса пользователя, и он появляется после каждой перезагрузки.

Код

"""
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()

Пожалуйста, дайте мне знать, если я пропустил что-то важное, и если что-нибудь нуждается в уточнении.

4
ответ дан 26 марта 2018 в 09:03 Источник Поделиться

Вот пересмотрел игру сапер с контроллером, которые могут использовать различные интерфейсы. Теперь она включает в себя текстовый интерфейс для мазохистов. Кроме того, переустановить игру теперь разрушает корень окно и создает новое. Кредит для предложений и правок выдаются @Гарет и @Соломон. Кто-нибудь с опытом tkinter есть ответ на вопрос, как повторно использовать то же окно, тк во время игры сбросить, так что выложенные ранее проблем не бывает?

"""
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()

0
ответ дан 5 апреля 2018 в 03:04 Источник Поделиться