diff --git a/minesweeperLVLs.py b/minesweeperLVLs.py index 7a3718f..450a4c4 100644 --- a/minesweeperLVLs.py +++ b/minesweeperLVLs.py @@ -3,238 +3,278 @@ import random import string +from enum import Enum, unique + +@unique +class FlagResult(Enum): + SPOT_CLEAR = 1 + NO_FLAGS_LEFT = 2 + OK = 3 + +@unique +class CheckResult(Enum): + SPOT_FLAGGED = 1 + SPOT_ALREADY_SHOWN = 2 + OK = 3 + EXPLODE = 4 + class Color: RED = "\033[31m" GREEN = "\033[32m" YELLOW = "\033[33m" END = "\033[0m" -class Minesweeper: - # build board and define how many mines/level - def __init__(self): +class Board: + red_x = f"{Color.RED}X{Color.END}" + + def __init__(self, rows, cols, mines): self.dict_board = {} - self.dict_space = {} - self.count_mines = 0 + self.mines_and_nums = {} self.count_flags = 0 - self.board_rows = 0 - self.board_cols = 0 - self.num_mines = 0 - - def get_level(self): - choice = input("Please choose a level (Beginner, Intermediate, Advanced, Custom): ").lower() - return choice - - def define_level(self): - while True: - level = ["beginner", "intermediate", "advanced", "custom"] - choice = self.get_level() - if choice in level: - # beginner = 9x9 w/ 10 mines - if choice == level[0]: - self.board_rows = 9 - self.board_cols = 9 - self.num_mines = 10 - break - # intermediate = 16x16 w/ 40 mines - elif choice == level[1]: - self.board_rows = 16 - self.board_cols = 16 - self.num_mines = 40 - break - # advanced = 30x16 w/ 99 mines - elif choice == level[2]: - self.board_rows = 16 - self.board_cols = 30 - self.num_mines = 99 - break - # custom = user inputs their custom level - elif choice == level[3]: - while True: - try: - ### This causes the program to go on forever placing bombs :( - self.board_rows = int(input("Please enter a number 1-80 for rows: ")) - self.board_cols = int(input("Please enter a number 1-80 for columns: ")) - self.num_mines = int(input("Please enter a number 1-99 for bombs: ")) - if 0 < self.board_rows <= 80 and 0 < self.board_cols <= 80 and 0 < self.num_mines <= 99 and self.num_mines < (self.board_rows * self.board_cols) / 2: - break - else: - print("Can't make a board like that! D:") - except: - print("Can't make a board like that! D:") - break - else: - print("What kind of level is that?!?!?! >:O") + self.board_rows = rows + self.board_cols = cols + self.countSpaces = self.board_rows * self.board_cols + self.num_mines = mines + self.make_board() def make_board(self): - # starts with board being printed in the terminal - # self.board = [["O" for _ in range(self.boardRows)] for _ in range(self.boardCols)] - # for x in self.board: # try to use 2 loops to print without list - # self.gameBoard = " ".join(x) - # print(self.gameBoard) for r in range(self.board_rows): for c in range(self.board_cols): self.dict_board[(r, c)] = "O" - self.countSpaces = (self.board_rows * self.board_cols) - self.print_board() - + self.place_mines() + + def place_mines(self): + mines_to_place = ['*'] * self.num_mines + [' '] * ((self.board_rows * self.board_cols) - self.num_mines) + random.shuffle(mines_to_place) + for r in range(self.board_rows): + for c in range(self.board_cols): + m = mines_to_place.pop() + if m == '*': + coord = (r, c) + if coord not in self.mines_and_nums or self.mines_and_nums[coord] != "*": + self.mines_and_nums[coord] = "*" + self.place_nums_around_mine(r, c) + + def place_nums_around_mine(self, row, col): + for neighbor_coord in self.neighbor_coords(row, col): + if neighbor_coord not in self.mines_and_nums: + self.mines_and_nums[neighbor_coord] = 1 + elif neighbor_coord in self.mines_and_nums and self.mines_and_nums[neighbor_coord] != "*": + self.mines_and_nums[neighbor_coord] += 1 + def print_board(self): + max_row_num_width = len(str(self.board_rows - 1)) + horiz_rule = ' ' * (max_row_num_width + 1) + '-' * (self.board_cols * 2 + 3) + corner_spaces = ' ' * (max_row_num_width + 3) + if self.board_cols > 10: + horiz_nums = corner_spaces + ' '.join(str(c // 10) if c >= 10 else ' ' for c in range(self.board_cols)) + '\n' + else: + horiz_nums = '' + horiz_nums += corner_spaces + ' '.join(str(c % 10) for c in range(self.board_cols)) + print(horiz_nums) + print(horiz_rule) + for r in range(self.board_rows): + row_num = f'{" " * max_row_num_width}{r}'[-max_row_num_width:] + print(f'{row_num} | ', end = '') for c in range(self.board_cols): print(self.dict_board[(r,c)], end = " ") - print() + print(f'| {row_num}') - def get_user_move(self): - # when coordinate is inputed (row x column) checks the space - self.user = input("Please enter a 'coordinate' to uncover a space or 'X, coordinate' to place or remove a flag: ").upper() - self.user = self.user.translate(str.maketrans(string.punctuation, (" " * len(string.punctuation)))) - self.user = self.user.split() + print(horiz_rule) + print(horiz_nums) - def make_move(self): - while True: - self.get_user_move() - # determines if the user is inputting a space - if len(self.user) == 2: - try: - self.move = tuple(map(int, self.user)) - if self.move in self.dict_board: - self.is_flagging = False - break - else: - print("That's not even a space on the board >_>") - except: - print("NO! >:O") - # determines if a user is inputting a flag - elif len(self.user) == 3 and self.user[0] == "X": - try: - self.flag = tuple(map(int, self.user[1:])) - if self.count_mines == 0: - print("You really want to place a flag on your first move?") - elif self.flag in self.dict_board: - self.place_flag() - self.is_flagging = True - break - else: - print("Sure. I can place a flag in the middle of nowhere for you! :D") - except: - print("*BOOM* No flag for you! >:D") - else: - print("Invalid input. Please try again.") + def neighbor_coords(self, row, col): + # finds neighboring spots in all 8 directions + for r in range(row - 1, row + 2): + for c in range(col - 1, col + 2): + if r >= 0 and c >= 0 and r < self.board_rows and c < self.board_cols and (r != row or c != col): + yield r, c - def place_flag(self): - if self.count_flags <= self.num_mines: - if self.dict_board[self.flag] == "O": - self.dict_board[self.flag] = f"{Color.RED}X{Color.END}" + def toggle_flag(self, move_coord): + if self.dict_board[move_coord] == "O": + if self.count_flags < self.num_mines: + self.dict_board[move_coord] = Board.red_x self.count_flags += 1 - self.print_board() - elif self.dict_board[self.flag] == f"{Color.RED}X{Color.END}": - self.dict_board[self.flag] = "O" - self.count_flags -= 1 - self.print_board() + return FlagResult.OK else: - print("You want to waste flag on a space you already uncovered??? o_O") + return FlagResult.NO_FLAGS_LEFT + elif self.dict_board[move_coord] == Board.red_x: + self.dict_board[move_coord] = "O" + self.count_flags -= 1 + return FlagResult.OK + else: + return FlagResult.SPOT_CLEAR - def place_mines(self): - # check how many mines we have placed - while self.count_mines < self.num_mines: - # randomly place the mines / find coordinates for row and colum - mine_row = random.choice(range(self.board_rows)) - mine_cols = random.choice(range(self.board_cols)) - coor_mine = mine_row, mine_cols - # check if a mine is already there - # if there is a mine already there, pass - if (coor_mine in self.dict_space and self.dict_space[coor_mine] == "*") or coor_mine == self.move: - # print(self.countMines) - pass - # if the no mine / replace the space with mine - # else: - elif coor_mine not in self.dict_space or self.dict_space[coor_mine] != "*": - self.dict_space[coor_mine] = "*" - # if true will add a mine to the counter - self.count_mines += 1 - self.place_nums(mine_row, mine_cols) - - # def printMines(self): - # print(self.dictSpace) - - # method will use the coordinates of the mines that are placed - def place_nums(self, row, col): - # finds coordinate of top left, top center, top right, left side, right side, bottom left, bottom center, bottom right surrounding the mine - spaceList = [(row - 1, col - 1), (row - 1, col), (row - 1, col + 1), (row, col - 1), (row, col + 1), (row + 1, col - 1), (row + 1, col), (row + 1, col + 1)] - # goes through the coordinates one by one - for space in spaceList: - # checks to see if the coordinate is a valid coordinate on the board - skips it if it is not - if space[0] < 0 or space[1] < 0 or space[0] > (self.board_rows - 1) or space[1] > (self.board_cols - 1): - continue - # if the coordinate is not in the dictionary yet, adds key and value - if space not in self.dict_space: - self.dict_space[space] = 1 - # if the coordinate is already a key, increments the value up by 1 - elif space in self.dict_space and self.dict_space[space] != "*": #and self.dictSpace[space] != "M": - self.dict_space[space] += 1 - - def check_move(self): - if self.is_flagging == False: - # if mine is there at coordinate - game over - if self.dict_board[self.move] == "O": - if self.move in self.dict_space: - if self.dict_space[self.move] != "*": - self.dict_board[self.move] = self.dict_space[self.move] - self.countSpaces -= 1 - self.print_board() - else: - self.dict_board[self.move] = self.dict_space[self.move] - self.print_board() - print("Game Over!") - exit() - else: - self.dict_board[self.move] = " " - self.countSpaces -= 1 - self.uncover_space(self.move[0], self.move[1]) - self.print_board() - elif self.dict_board[self.move] == f"{Color.RED}X{Color.END}": - print("Uhmmm, you want to dig up a flag you put down??") - elif self.dict_board[self.move] != "O": - print("You already uncovered this space -_-") + def check_move(self, move_coord): + if self.dict_board[move_coord] == Board.red_x: + return CheckResult.SPOT_FLAGGED + elif self.dict_board[move_coord] != "O": + return CheckResult.SPOT_ALREADY_SHOWN + + else: + # if first move, don't let user hit a bomb + is_first_move = self.countSpaces == self.board_rows * self.board_cols + if is_first_move and move_coord in self.mines_and_nums and self.mines_and_nums[move_coord] == "*": + ls = [coord for coord, x in self.mines_and_nums.items() if x != '*'] + random.shuffle(ls) + free_coord = ls[0] + self.mines_and_nums[move_coord] = ' ' + self.mines_and_nums[free_coord] = '*' + self.place_nums_around_mine(free_coord[0], free_coord[1]) + + self.dict_board[move_coord] = self.mines_and_nums[move_coord] if move_coord in self.mines_and_nums else ' ' + self.countSpaces -= 1 + if move_coord not in self.mines_and_nums: + self.uncover_space(move_coord[0], move_coord[1]) + return CheckResult.OK + else: + if self.mines_and_nums[move_coord] == "*": + return CheckResult.EXPLODE + else: + return CheckResult.OK def uncover_space(self, row, col): - spaceList = [(row - 1, col - 1), (row - 1, col), (row - 1, col + 1), (row, col - 1), (row, col + 1), (row + 1, col - 1), (row + 1, col), (row + 1, col + 1)] - for space in spaceList: - if space[0] < 0 or space[1] < 0 or space[0] > (self.board_rows - 1) or space[1] > (self.board_cols - 1): - continue + for space in self.neighbor_coords(row, col): if self.dict_board[space] == "O": - if space not in self.dict_space: + if space not in self.mines_and_nums: self.dict_board[space] = " " self.countSpaces -= 1 self.uncover_space(space[0], space[1]) - elif space in self.dict_space and self.dict_space[space] != "*": - self.dict_board[space] = self.dict_space[space] + elif space in self.mines_and_nums and self.mines_and_nums[space] != "*": + self.dict_board[space] = self.mines_and_nums[space] self.countSpaces -= 1 - - def user_win(self): - if self.countSpaces == self.num_mines: - print("Congrats you won! :D") - exit() - - def game_play(self): - self.user_win() - self.make_move() - self.check_move() - self.game_play() + + def move_in_range(self, coord): + return coord in self.dict_board + + def has_moved(self): + return self.countSpaces != self.board_rows * self.board_cols + + def isGameWon(self): + # when all the blank spaces are uncovered - winner is pronounced! + return self.countSpaces == self.num_mines + + +class Minesweeper: + # beginner = 9x9 w/ 10 mines + # intermediate = 16x16 w/ 40 mines + # advanced = 30x16 w/ 99 mines + board_size_settings = [ + { + 'board_rows': 9, + 'board_cols': 9, + 'num_mines': 10 + }, { + 'board_rows': 16, + 'board_cols': 16, + 'num_mines': 40 + }, { + 'board_rows': 16, + 'board_cols': 30, + 'num_mines': 99 + } + ] + levels = ["b", "i", "a", "c"] + + + def __init__(self): + self.board = None + + def get_level(self): + while True: + choice = input("Please choose a level: (B)eginner, (I)ntermediate, (A)dvanced, (C)ustom or (Q)uit: ").lower() + choice = choice[0] if choice else '' + if choice == 'q': + raise SystemExit() + elif choice in Minesweeper.levels: + return choice + else: + print("What kind of level is that?!?!?! >:O") + + def define_level(self): + # Get dimensions and number of mines with which to construct the board + choice = self.get_level() + if choice in Minesweeper.levels[:3]: + i = Minesweeper.levels.index(choice) + settings = Minesweeper.board_size_settings + rows = settings[i]['board_rows'] + cols = settings[i]['board_cols'] + mines = settings[i]['num_mines'] + self.board = Board(rows, cols, mines) + # custom level + else: + while True: + try: + rows = int(input("Please enter a number 1-80 for rows: ")) + cols = int(input("Please enter a number 1-80 for columns: ")) + mines = int(input("Please enter a number 1-99 for bombs: ")) + if 0 < rows <= 80 and 0 < cols <= 80 and 0 < mines < min(100, (board_rows * board_cols) / 2): + self.board = Board(rows, cols, mines) + return + else: + print("Can't make a board like that! D:") + except: + print("Can't make a board like that! D:") + + def get_user_move(self): + s = input("Please enter ' ' to uncover a space or 'X ' to place or remove a flag: ") + ls = s.upper().translate(str.maketrans(string.punctuation, " " * len(string.punctuation))).split() + try: + coords = tuple(map(int, ls[-2:])) + if len(ls) == 2: + return (False, ) + coords + elif len(ls) == 3 and ls[0] == 'X': + return (True, ) + coords + except Exception as e: + print(f'Bad input: {e}. Please try again.') + return self.get_user_move() + + def make_move(self): + self.board.print_board() + + is_flagging, row, col = self.get_user_move() + move_coord = row, col + + if not self.board.move_in_range(move_coord): + print("That's not even a space on the board >_>") + elif not is_flagging: + self.check_move(move_coord) + else: + if self.board.has_moved(): + self.toggle_flag(move_coord) + else: + print("You really want to place a flag on your first move?") + self.make_move() + + def toggle_flag(self, move_coord): + res = self.board.toggle_flag(move_coord) + + if res == FlagResult.NO_FLAGS_LEFT: + print('You already placed as many flags as there are bombs.') + + elif res == FlagResult.SPOT_CLEAR: + print("You want to waste flag on a space you already uncovered??? o_O") + + def check_move(self, move_coord): + res = self.board.check_move(move_coord) + + if res == CheckResult.SPOT_FLAGGED: + print("Uhmmm, you want to dig up a flag you put down??") + + elif res == CheckResult.SPOT_ALREADY_SHOWN: + print("You already uncovered this space -_-") + + elif res == CheckResult.EXPLODE or (res == CheckResult.OK and self.board.isGameWon()): + self.board.print_board() + print("Game Over!" if res == CheckResult.EXPLODE else "Congrats you won! :D") + raise SystemExit() def start_game(self): self.define_level() - self.make_board() - self.make_move() - self.place_mines() - self.check_move() - self.game_play() + while True: + self.make_move() minesweeper = Minesweeper() minesweeper.start_game() -# minesweeper.chooseLevel() -# minesweeper.makeBoard() -# minesweeper.gamePlay() - -# print out the board again so user can see what is available -# when all the blank spaces are uncovered - winner is pronounced! \ No newline at end of file