我是Python的新手(并且通常是编程的),我最近写了这个Minesweeper游戏:

import random
import string
import os
class Square(object):
    """Represent a square in the cell.

    mine -- if the square has a mine, it's True. Otherwise, False.
    location -- a tuple that represents the (x, y) of the square in the grid.
    grid -- the gird that has the square in it
    """
    signs = ('*', ' ') + tuple([str(n) for n in range(1,9)])
    # reserved signs - the player can't mark a square with those signs
    # (signs of visible squares - it doesn't contain '.' because when the
    # player mark the square with '.', he cancels the previous sign)

    def __init__(self, mine, location, grid):
        self.mine = mine
        self.location = tuple(location)
        self.grid = grid
        self.sign = '.' # sign - how is the square represented to the user.
        self.visible = False # not visible yet
        assert self.legal_square()

    def __str__(self):
        return self.sign

    def expose(self):
        '''Make the square visible to the player (when he exposes it, for
        example).'''
        self.visible = True
        if self.has_mine():
            self.sign = '*' # The sign that means that the square has a mine
                            # and the player exposed it (he lost).

        elif self.num() == 0: # There are no mines near the square
            self.sign = ' '   # The sign that means that the square is clean,
                              # and there are no mines near it.

            x, y = self.location[0], self.location[1]
            # Expose all of the squares near the current square, and all of
            # the squares near them (recursively), until we reach squares
            # with numbers.
            for near_x in range(x-1, x+2):
                for near_y in range(y-1, y+2):
                    if not (near_x == x and near_y == y): # not the same square
                        self.grid.expose_square((near_x, near_y))
                        # If there's no square with these coordinates, this
                        # method returns so there's no any error.

        else: # If the square has no any mine, but there are mines near it.
            self.sign = str(self.num())

    def is_visible(self):
        '''Return True if the square is visible, False otherwise.'''
        return self.visible

    def has_mine(self):
        '''Return True if the square has a mine, False otherwise.'''
        return self.mine

    def num(self):
        '''Return the number of mines near the square.'''
        mines = 0
        x, y = self.location[0], self.location[1] # square's coordinates

        # every square near the current square
        for near_x in range(x-1, x+2):
            for near_y in range(y-1, y+2):
                if not (near_x == x and near_y == y): # not the same square
                    if self.grid.has_mine((near_x, near_y)): mines += 1
                    # If that square has a mine     
        return mines

    def mark(self, sign):
        '''Mark the square with the given sign.

        sign -- string
        '''
        if not self.is_visible() and sign not in Square.signs:
            # The player can only mark invisible squares, with unreserved marks
            self.sign = sign

    def legal_square(self):
        '''Return True if the square is legal, False otherwise.'''
        if not isinstance(self.location, tuple) or len(self.location) != 2:
            return False
        if not isinstance(self.location[0], int)\
         or not isinstance(self.location[1], int):
            return False
        if not isinstance(self.has_mine(), bool):
            return False
        if self.location[0] < 0 or self.location[1] < 0:
            return False
        if not isinstance(self.grid, Grid):
            return False
        if self.visible and (self.sign not in Square.signs):
            return False

        return True

class Grid(object):
    """Represent the grid of the Squares.

    width, height -- grid's dimensions
    mines -- number of mines in the grid
    """
    def __init__(self, width=8, height=8, mines=10):
        self.width, self.height = width, height
        self.mines = mines
        self.create_grid(dummy=True)  # Create a dummy grid (before the first
                                      #                       turn)
        assert self.legal_grid()

    def __str__(self):
        return_s = ''

        width_col_header = len(to_base26(self.height))
        width_col = len(str(self.width))

        # Header row of the table
        return_s += (width_col_header+1) * ' '
        for col in range(len(self.grid[0])):
            return_s += ' {0:^{1}}'.format(col+1, width_col)
        return_s += '\n'
        # Border between header row and the rest of the table
        return_s += (width_col_header+1) * ' '
        for col in range(len(self.grid[0])):
            return_s += ' {0:^{1}}'.format('-', width_col)
        return_s += '\n'

        # The columns with the squares
        for row in range(len(self.grid)):
            return_s += '{0:>{1}}|'.format(to_base26(row+1), width_col_header)
            for square in self.grid[row]:
                return_s += ' {0:^{1}}'.format(square, width_col)
            return_s += '\n'
        return return_s

    def create_grid(self, dummy, safe_square=None):
        '''Create a grid (that consists of Squares).

        dummy -- In the first turn, we don't want  that the player will lose.
                 If there are still no mines, we will call the grid "dummy".
                 Set dummy to True if now is before the first turn, and False
                 otherwise.
        safe_square -- If dummy == True, don't assign a value to it. This is
                       a tuple that represents the x and y of the square that
                       the player chose. This square (and its adjacent squares)
                       should be clean of mines, because we don't want the
                       player will lose right after the first turn, but if now
                       is before the first turn, don't assign a value to it.
                       It's None as a default.
        '''
        safe_squares = [] # squares around the safe_square (including
                          #                                safe_square)
        if not dummy:
            # Initialize safe_squares
            for x in range(safe_square[0]-1, safe_square[0]+2):
                for y in range(safe_square[1]-1, safe_square[1]+2):
                    safe_squares.append((x, y))

        mines = [] # this list will represent the random locations of mines.
        # Initialize mines
        if not dummy:
            self.is_dummy = False

            # Set the random locations of mines.
            for mine in range(self.mines): # Until we reach the wanted number
                                           # of mines
                while True:
                    x, y = random.randint(0, self.width-1),\
                           random.randint(0, self.height-1)
                    # Random coordinates on the grid

                    if (x, y) not in mines and (x, y) not in safe_squares:
                        # If this is a new location, and not on the safe_square
                        break

                mines.append((x, y))
        else:
            self.is_dummy = True

        grid = [] # a list of rows of Squares
        for y in range(self.height):
            row = []
            for x in range(self.width):
                square = Square(((x, y) in mines), (x, y), self)
                # (x, y) in mines -- if (x, y) was chosen as a mine, it's True
                row.append(square)
            grid.append(row[:])

        self.grid = grid

    def has_mine(self, location):
        '''Return True if the square in the given location has a mine, False
        otherwise.

        location -- a tuple (x, y)
        '''
        y, x = location[1], location[0] # coordinates of the square
        if x < 0 or x >= self.width or y < 0 or y >= self.height:
            # If the square doesn't exist, just return False
            return False
        return self.grid[y][x].has_mine()

    def parse_input(self):
        '''Get a location from the player and a mark optionally, and return
        them as a tuple. If the player didn't insert a mark, its value in the
        tuple will be None.'''
        while True: # The only exit is from an exit statement - be careful
            s = raw_input('Type the location of the square you want to '
                          'treat. If you want to mark the square, type '
                          'after the location a space, and then the sign '
                          'you want to use. If you want to cancel a mark, '
                          '"mark" the square with a dot.\n')

            location = s.split()[0]
            try:
                sign = s.split()[1]
            except IndexError:
                sign = None

            letters = ''.join(c for c in location if c in string.letters)
            digits = ''.join((str(c) for c in location if c in string.digits))
            if (letters + digits != location and digits + letters != location)\
               or letters == '' or digits == '':
                # If the input is something like "A2B3" or "AA" or "34"
                print 'Please type an invalid location, like "AA12" or "12AA"'
                continue

            location = (int(digits)-1, base26to_num(letters)-1)

            try:
                x, y = location[0], location[1]
                self.grid[y][x]
            except IndexError:
                print 'Ahhh... The square should be IN the grid.\n'
                continue
            break

        return (location, sign)

    def turn(self):
        '''Let the player play a turn. Return 1 if the player won, -1 if he
        lost, and 0 if he can keep playing.'''
        os.system('cls' if os.name == 'nt' else 'clear') # clear screen
        print self
        location, sign = self.parse_input()
        x, y = location
        square = self.grid[y][x] # The square that the player wanted to treat
        if sign == None: # If the player didn't want to mark the square
            if self.is_dummy:
                self.create_grid(False, (x, y))
            self.expose_square((x, y)) # then he wanted to expose it

            if square.has_mine(): # If the square has a mine, the player lost
                self.lose()
                return (-1) # lose

            # Check if the player won (if he exposed all of the clean squares)
            for row in self.grid:
                for square in row:
                    if not square.has_mine() and not square.is_visible():
                        # If there are clean squares that are not exposed
                        return 0 # not win, not lose
            else: # all of the clean squares are exposed
                self.win()
                return 1 # win
        else: # If the player wanted to mark a square
            square.mark(sign[0])
            return 0

    def play(self):
        '''Let the player play, until he finishes. Return True if he won, and
        False if he lost.'''
        result = 0
        while result == 0:
            result = self.turn()
        return result == 1

    def expose_square(self, location):
        '''Choose the square in the given location and expose it.'''
        y, x = location[1], location[0]
        if x < 0 or x >= self.width or y < 0 or y >= self.height:
            # if the square doesn't exist, do nothing
            return
        if self.grid[y][x].is_visible():
            # if the square is already exposed, do nothing
            return
        if str(self.grid[y][x]) != '.': # the player marked the square
            return
        if self.is_dummy: # if it's the first turn, and there still aren't real
                          # mines,
            self.create_grid((x, y), False) # create a real grid
        square = self.grid[y][x] # the square of the given location
        square.expose()

    def lose(self):
        '''Expose all of the squares that have mines, and print 'Game over'.'''
        os.system('cls' if os.name == 'nt' else 'clear') # clear screen
        # Expose all of the squares that have mines
        for x in range(self.width):
            for y in range(self.height):
                self.expose_square((x, y))

        print self # Print the grid with the mines
        print 'Game over.'

    def win(self):
        '''Print the grid and 'You won!'.'''
        os.system('cls' if os.name == 'nt' else 'clear') # clear screen
        print self
        print 'You won!'

    def legal_grid(self):
        if not isinstance(self.width, int) or not isinstance(self.height, int):
            return False
        if self.width <= 0 or self.height <= 0 or self.mines < 0:
            return False
        if not isinstance(self.mines, int):
            return False
        if self.mines > self.width*self.height-9:
            return False
        return True

def to_base26(num):
    '''Convert a number to a string, that represents the number in base-26,
    without a zero.
    to_base26(1) => 'A'
    to_base26(2) => 'B'
    to_base26(28) => 'AB'

    num: number
    Return: str
    '''
    s = ''
    while num > 0:
        num -= 1
        s = chr(ord('A')+num%26) + s
        num //= 26

    return s

def base26to_num(s):
    '''Convert a string that represents a number in base-26 (1 = 'A',
    28 = 'AB', etc.) to a number.
    base26to_num('A') => 1
    base26to_num('b') => 2
    base26to_num('AB') => 28

    s: str
    Return: number
    '''
    s = ''.join(c for c in s if c in string.letters).upper()
    num = 0
    for letter in s:
        num = num*26 + (ord(letter)-ord('A')) + 1

    return num

grid = Grid(9, 9, 10)
grid.play()
raw_input()


据我所知,代码运行良好,但是我不确定它是否遵循良好做法,或者它的可读性和效率如何。

评论

创建x并允许用户在x位置输入位置的部分在哪里?

#1 楼

纯粹从OOP的角度来看,我认为您的类没有太大意义。

Square应该只是一个正方形,它不应该修改任何其他正方形(就像您在expose函数中所做的那样) )。实际上,正方形甚至不需要知道自己的位置(因为它的容器应该处理该位置)或它的容器。我将拥有一个仅包含其内容的基本正方形类,以及一个仅暴露其自身而没有其他任何东西的暴露方法。

Grid做得太多。它在完成网格工作(使用正方形)的同时,还结合了大多数游戏逻辑。我会将大多数游戏逻辑移到另一个类(如下所述),并将暴露方形逻辑移到这里。另外,我认为为网格数据结构添加一个Squares元组也很有意义。

添加一个MineSweeper类,其中包含当前正在使用的大多数实际游戏逻辑(输入,重绘等) Grid

通常,您的代码应尝试遵循单一责任原则。也就是说,使您的每个类/函数仅对一件事负责。注意:这意味着为迭代邻居等创建函数。


在编程方面:

没有理由生成虚拟网格然后生成一个真实的网格。创建真实的网格,然后如果第一次单击是在地雷上,请将其移动到其他位置。

如果地雷的数量特别多,您的代码可能会中断。这是因为您一直在尝试生成在while true:循环中没有用完的随机位置,如果没有斑点,这将使您的代码一直运行到时间结束。我将看看这些年来使用的其他一些地雷生成算法。或者,您可以将网格划分为多个地雷-10个相等的部分,在这些子网格中选择一个随机位置以添加一个地雷,最后将最后10个随机分布在各个子网格中。请确保不要添加过多的地雷。

评论


\ $ \ begingroup \ $
非常感谢,这样会更有意义。 (请注意,由于采用legal_grid方法,地雷的数量不能大于正方形的数量)
\ $ \ endgroup \ $
– MrHezi
14年7月26日在8:45

\ $ \ begingroup \ $
啊,没听懂,编辑了我的答案以改变它。
\ $ \ endgroup \ $
–mleyfman
2014年7月26日在17:10