Python 3 простая игра Minesweeper с использованием tkinter

Я относительно новичок в программировании, и я хочу использовать эту игру простого тральщика в портфолио. Несколько вопросов:

  1. В настоящее время настройка игры постепенно увеличивается с каждым вызовом кнопки сброса, а высота окна немного увеличивается вниз. Это очень заметно при средних и тяжелых трудностях. Какие изменения могут ускорить этот код? Я хочу повторно использовать одно и то же окно с каждым сбросом, если это возможно.
  2. В коде используется подход Model-View-Controller. Это имеет смысл для проекта с использованием 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()
11 голосов | спросил EndreoT 26 MaramMon, 26 Mar 2018 00:26:58 +03002018-03-26T00:26:58+03:0012 2018, 00:26:58

2 ответа


5

Это хорошая идея использовать шаблон Model-View-Controller. Но реализация требует некоторой работы.

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

  1. размер игровой зоны;
  2. местоположения мин;
  3. какие квадраты были обнаружены до сих пор;
  4. расположение флагов;
  5. состояние игры (выигрыш /проигрывание /воспроизведение);

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

  1. установить или очистить флаг;
  2. открыть квадрат;
  3. начать новую игру.

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

ответил Gareth Rees 26 MarpmMon, 26 Mar 2018 21:42:14 +03002018-03-26T21:42:14+03:0009 2018, 21:42:14
4

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

Я определенно согласен с Гаретом Рисом о фактическом разделении частей MVC .

Что я изменил

  1. Я добавил типы подсказок ко всем функциям.
  2. Я исправил несколько опечаток, а также добавил периоды к концам комментариев.
  3. Я изменил большинство вызовов функций инициализатора на функционал-ish (изменен из назначения внутри функции, чтобы вернуть значение из функции и выполнить назначение в инициализаторе).
  4. Я изменил много циклов for на итератор 'math'.
  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()

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

ответил Solomon Ucko 27 MaramTue, 27 Mar 2018 00:54:43 +03002018-03-27T00:54:43+03:0012 2018, 00:54:43

Похожие вопросы

Популярные теги

security × 330linux × 316macos × 2827 × 268performance × 244command-line × 241sql-server × 235joomla-3.x × 222java × 189c++ × 186windows × 180cisco × 168bash × 158c# × 142gmail × 139arduino-uno × 139javascript × 134ssh × 133seo × 132mysql × 132