首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用tkinter的Python 3简单扫雷游戏

使用tkinter的Python 3简单扫雷游戏
EN

Code Review用户
提问于 2018-03-25 21:26:58
回答 2查看 3.7K关注 0票数 10

我对编程比较陌生,我希望在一个组合中使用这个简单的扫雷游戏。几个问题:

  1. 目前,游戏设置会随着每次重置按钮的调用而逐渐变慢,并且窗口的高度会稍微向下增加。这一点在中等和困难的情况下是很明显的。哪些更改可以加快代码的速度?如果可能的话,我想在每次重置的情况下重用相同的窗口。
  2. 代码使用模型-视图-控制器方法.对于使用tkinter的项目来说,这有意义吗?有没有更好的方法?
  3. 有些部分,例如加法变量是重复的,但我无法找到使它们成为实例变量的方法。

如有任何建议/建设性的反馈意见,我们将不胜感激。

代码语言:javascript
复制
"""
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()
EN

回答 2

Code Review用户

发布于 2018-03-26 18:42:14

使用模式是个好主意。但是,实现还需要一些工作。

在MVC中,模型应该包含对正在操作的数据的完整描述,以及对该数据的操作。就扫雷船游戏而言,模型应由以下数据组成:

  1. 运动场的大小;
  2. 地雷的位置;
  3. 到目前为止,哪些广场已经被发现;
  4. 旗帜的位置;
  5. 比赛状态(赢/输/仍在打);

连同这些行动:

  1. 设置或清除旗帜;
  2. 揭开一个正方形;
  3. 开始一个新游戏。

其思想是,您应该能够通过交换视图和控制器将程序移植到另一种接口,并保持模型不变。但是在post的实现中,大部分数据和所有操作都进入了控制器。这使得更换控制器变得不方便,因为所有这些都必须在新控制器中重新实现。

票数 5
EN

Code Review用户

发布于 2018-03-26 21:54:43

这游戏看上去很棒!代码看起来也不错!

我绝对同意与Gareth Rees有关实际分离MVC的各个部分

我改变了什么

  1. 我在所有函数中添加了类型提示。
  2. 我修正了一些排版,并在注释的末尾添加了句号。
  3. 我将大多数初始化器函数调用更改为functional(从在函数中赋值改为从函数返回值,并在初始化程序内执行赋值)。
  4. 我将许多for循环改为迭代器“数学”。
  5. 我把相邻细胞的列表提取出来,以达到它自己的功能,因为它经常被重复。
  6. 我将一些数据类型从列表(或字符串)更改为集或元组。
  7. 我用元组解压代替了元组索引。
  8. 我试图减少主要功能中的重复。
  9. 也许还有其他一些更小的东西。

还需要做什么

  1. MVC组件的分离。
  2. 如果困难的提示是基于GUI的,并在每次重置后显示出来,那就太好了。

代码

代码语言:javascript
复制
"""
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
EN
页面原文内容由Code Review提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://codereview.stackexchange.com/questions/190461

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档