5
\$\begingroup\$

I have recently written this Minesweeper game in Python:

import random

class Cell(object):
    def __init__(self, is_mine, is_visible=False, is_flagged=False):
        self.is_mine = is_mine
        self.is_visible = is_visible
        self.is_flagged = is_flagged

    def show(self):
        self.is_visible = True

    def flag(self):
        self.is_flagged = not self.is_flagged

    def place_mine(self):
        self.is_mine = True


class Board(tuple):
    def __init__(self, tup):
        super().__init__()
        self.is_playing = True

    def __str__(self):
        board_string = ("Mines: " + str(self.remaining_mines) + "\n  " +
                        "".join([str(i) for i in range(len(self))]))
        for (row_id, row) in enumerate(self):
            board_string += "\n" + str(row_id) + " "
            for (col_id, cell) in enumerate(row):
                if cell.is_visible:
                    if cell.is_mine:
                        board_string += "M"
                    else:
                        board_string += str(self.count_surrounding(row_id,
                                                                   col_id))
                elif cell.is_flagged:
                        board_string += "F"
                else:
                    board_string += "X"
            board_string += " " + str(row_id)
        board_string += "\n  " + "".join([str(i) for i in range(len(self))])
        return board_string

    def show(self, row_id, col_id):
        if not self[row_id][col_id].is_visible:
            self[row_id][col_id].show()

            if (self[row_id][col_id].is_mine and not
                self[row_id][col_id].is_flagged):
                self.is_playing = False

            elif self.count_surrounding(row_id, col_id) == 0:
                [self.show(surr_row, surr_col) for (surr_row, surr_col) in
                 self.get_neighbours(row_id, col_id) if
                 self.is_in_range(surr_row, surr_col)]

    def flag(self, row_id, col_id):
        if not self[row_id][col_id].is_visible:
            self[row_id][col_id].flag()
        else:
            print("Cannot add flag, cell already visible.")

    def place_mine(self, row_id, col_id):
        self[row_id][col_id].place_mine()

    def count_surrounding(self, row_id, col_id):
        count = 0
        for (surr_row, surr_col) in self.get_neighbours(row_id, col_id):
            if (self.is_in_range(surr_row, surr_col) and
                self[surr_row][surr_col].is_mine):
                count += 1
        return count

    def get_neighbours(self, row_id, col_id):
        SURROUNDING = ((-1, -1), (-1,  0), (-1,  1),
                       (0 , -1),           (0 ,  1),
                       (1 , -1), (1 ,  0), (1 ,  1))
        neighbours = []
        for (surr_row, surr_col) in SURROUNDING:
            neighbours.append((row_id + surr_row, col_id + surr_col))
        return neighbours

    def is_in_range(self, row_id, col_id):
        return 0 <= row_id < len(self) and 0 <= col_id < len(self)

    @property
    def remaining_mines(self):
        remaining = 0
        for row in self:
            for cell in row:
                if cell.is_mine:
                    remaining += 1
                if cell.is_flagged:
                    remaining -= 1
        return remaining

    @property
    def is_solved(self):
        for row in self:
            for cell in row:
                if not(cell.is_visible or cell.is_flagged):
                    return False
        return True


def create_board(size, mines):
    board = Board(tuple([tuple([Cell(False) for i in range(size)])
                         for j in range(size)]))
    available_pos = list(range((size-1) * (size-1)))
    for i in range(mines):
        new_pos = random.choice(available_pos)
        available_pos.remove(new_pos)
        (row_id, col_id) = (new_pos % 9, new_pos // 9)
        board.place_mine(row_id, col_id)
    return board

def get_move(board):
    INSTRUCTIONS = ("First, enter the column, followed by the row. To add or "
                    "remove a flag, add \"f\" after the row (for example, 64f "
                    "would place a flag on the 6th column, 4th row). Enter "
                    "your move: ")

    move = input("Enter your move (for help enter \"H\"): ")
    if move == "H":
        move = input(INSTRUCTIONS)

    while not is_valid(move, board):
        move = input("Invalid input. Enter your move (for help enter \"H\"): ")
        if move == "H":
            move = input(INSTRUCTIONS)

    return (int(move[1]), int(move[0]), True if move[-1] == "f" else False)

def is_valid(move_input, board):
    if move_input == "H" or (len(move_input) not in (2, 3) or
                             not move_input[:1].isdigit() or
                             int(move_input[0]) not in range(len(board)) or
                             int(move_input[1]) not in range(len(board))):
        return False

    if len(move_input) == 3 and move_input[2] != "f":
        return False

    return True

def main():
    SIZE = 10
    MINES = 9
    board = create_board(SIZE, MINES)
    print(board)
    while board.is_playing and not board.is_solved:
        (row_id, col_id, is_flag) = get_move(board)
        if not is_flag:
            board.show(row_id, col_id)
        else:
            board.flag(row_id, col_id)
        print(board)

    if board.is_solved:
        print("Well done! You solved the board!")
    else:
        print("Uh oh! You blew up!")

if __name__ == "__main__":
    main()

I am currently aware that when counting remaining_mines, the property function runs each time, whereas it would have been more efficient only adding and subtracting from the mine count when a mine or flag is placed.

When I implemented this, however, it looked quite messy, so I chose readability over performance. Was this the right decision? I also think the for a in b: for c in a: which are repeated throughout the code could be cleaned up. Finally, I wasn't sure whether to use a list comprehension or a normal loop in the final elif of Board.show().

Have you got any answers to my questions, or any general tips on how to improve the performance or readability?

\$\endgroup\$
  • \$\begingroup\$ Nice code, you could make it more detailed for it to be easier for the user to play, this can be done with something called pygame check it out (pygame.org), download a couple of projects off there and look at its code. Then try to use pygame to make your minesweeper even better! \$\endgroup\$ – VirtualHat Mar 31 '15 at 20:28
3
\$\begingroup\$

From a user interface point of view, it would be good to have :

  • letters for columns and numbers for rows (or the opposite)

  • a clear area for empty cells.

  • something to prevent you from winning just be flagging every single cell.

  • have a way to click on all non-flagged cells around a cell (try left-click and right-click in the same time on the Windows minesweeper, it's really convenient to blow up cells that are known to be safe).

Here's what I'd consider a better version of your code using some of the Python good stuff : list comprehension, things like that.

#!/usr/bin/python3

import random

class Cell(object):
    def __init__(self, is_mine, is_visible=False, is_flagged=False):
        self.is_mine = is_mine
        self.is_visible = is_visible
        self.is_flagged = is_flagged

    def show(self):
        self.is_visible = True

    def flag(self):
        self.is_flagged = not self.is_flagged

    def place_mine(self):
        self.is_mine = True


class Board(tuple):
    def __init__(self, tup):
        super().__init__()
        self.is_playing = True

    def mine_repr(self,row_id, col_id):
        cell = self[row_id][col_id]
        if cell.is_visible:
            if cell.is_mine:
                return "M"
            else:
                surr = self.count_surrounding(row_id, col_id)
                return str(surr) if surr else " "
        elif cell.is_flagged:
            return "F"
        else:
            return "X"

    def __str__(self):
        board_string = ("Mines: " + str(self.remaining_mines) + "\n  " +
                        "".join([str(i) for i in range(len(self))]))
        for (row_id, row) in enumerate(self):
            board_string += ("\n" + str(row_id) + " " + 
                             "".join(self.mine_repr(row_id, col_id) for (col_id, _) in enumerate(row)) +
                             " " + str(row_id))
        board_string += "\n  " + "".join([str(i) for i in range(len(self))])
        return board_string

    def show(self, row_id, col_id):
        cell = self[row_id][col_id]
        if not cell.is_visible:
            cell.show()

            if (cell.is_mine and not
                cell.is_flagged):
                self.is_playing = False
            elif self.count_surrounding(row_id, col_id) == 0:
                for (surr_row, surr_col) in self.get_neighbours(row_id, col_id):
                    if self.is_in_range(surr_row, surr_col):
                        self.show(surr_row, surr_col) 

    def flag(self, row_id, col_id):
        cell = self[row_id][col_id]
        if not cell.is_visible:
            cell.flag()
        else:
            print("Cannot add flag, cell already visible.")

    def place_mine(self, row_id, col_id):
        self[row_id][col_id].place_mine()

    def count_surrounding(self, row_id, col_id):
        return sum(1 for (surr_row, surr_col) in self.get_neighbours(row_id, col_id)
                        if (self.is_in_range(surr_row, surr_col) and
                            self[surr_row][surr_col].is_mine))

    def get_neighbours(self, row_id, col_id):
        SURROUNDING = ((-1, -1), (-1,  0), (-1,  1),
                       (0 , -1),           (0 ,  1),
                       (1 , -1), (1 ,  0), (1 ,  1))
        return ((row_id + surr_row, col_id + surr_col) for (surr_row, surr_col) in SURROUNDING)

    def is_in_range(self, row_id, col_id):
        return 0 <= row_id < len(self) and 0 <= col_id < len(self)

    @property
    def remaining_mines(self):
        remaining = 0
        for row in self:
            for cell in row:
                if cell.is_mine:
                    remaining += 1
                if cell.is_flagged:
                    remaining -= 1
        return remaining

    @property
    def is_solved(self):
        return all((cell.is_visible or cell.is_mine) for row in self for cell in row)

def create_board(size, mines):
    board = Board(tuple([tuple([Cell(False) for i in range(size)])
                         for j in range(size)]))
    available_pos = list(range((size-1) * (size-1)))
    for i in range(mines):
        new_pos = random.choice(available_pos)
        available_pos.remove(new_pos)
        (row_id, col_id) = (new_pos % 9, new_pos // 9)
        board.place_mine(row_id, col_id)
    return board

def get_move(board):
    INSTRUCTIONS = ("First, enter the column, followed by the row. To add or "
                    "remove a flag, add \"f\" after the row (for example, 64f "
                    "would place a flag on the 6th column, 4th row). Enter "
                    "your move: ")

    move = input("Enter your move (for help enter \"H\"): ")
    if move == "H":
        move = input(INSTRUCTIONS)

    while not is_valid(move, board):
        move = input("Invalid input. Enter your move (for help enter \"H\"): ")
        if move == "H":
            move = input(INSTRUCTIONS)

    return (int(move[1]), int(move[0]), move[-1] == "f")

def is_valid(move_input, board):
    if move_input == "H" or (len(move_input) not in (2, 3) or
                             not move_input[:1].isdigit() or
                             int(move_input[0]) not in range(len(board)) or
                             int(move_input[1]) not in range(len(board))):
        return False

    if len(move_input) == 3 and move_input[2] != "f":
        return False

    return True

def main():
    SIZE = 10
    MINES = 9
    board = create_board(SIZE, MINES)
    print(board)
    while board.is_playing and not board.is_solved:
        (row_id, col_id, is_flag) = get_move(board)
        if not is_flag:
            board.show(row_id, col_id)
        else:
            board.flag(row_id, col_id)
        print(board)

    if board.is_solved:
        print("Well done! You solved the board!")
    else:
        print("Uh oh! You blew up!")

if __name__ == "__main__":
    main()
\$\endgroup\$

protected by Malachi May 21 '15 at 18:31

Thank you for your interest in this question. Because it has attracted low-quality or spam answers that had to be removed, posting an answer now requires 10 reputation on this site (the association bonus does not count).

Would you like to answer one of these unanswered questions instead?

Not the answer you're looking for? Browse other questions tagged or ask your own question.