Last active
February 11, 2018 23:58
-
-
Save danthedaniel/bf11ab4b51da93173e5e3299ee3adccd to your computer and use it in GitHub Desktop.
Python CLI Minesweeper
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env python | |
| import random | |
| import itertools | |
| import os | |
| from termcolor import colored | |
| def randbool(per=2): | |
| """Get a random boolean value.""" | |
| return random.randint(0, per - 1) == 0 | |
| def get_type(type, prompt): | |
| """Get a value from user input with the specified type. | |
| Continues prompting until valid input is provided. | |
| Arguments | |
| --------- | |
| type : callable | |
| Function with 1 parameter that returns the desired type. | |
| prompt : str | |
| String to display in the input prompt. | |
| Returns | |
| ------- | |
| A value of the specified type. | |
| """ | |
| while True: | |
| try: | |
| return type(input(prompt)) | |
| except ValueError: | |
| print("Invalid input") | |
| class Grid(object): | |
| """Minesweeper grid.""" | |
| def __init__(self, width, height): | |
| """Instantiate a Grid. | |
| Arguments | |
| --------- | |
| width : int | |
| The width of the game board. | |
| height : int | |
| The height of the game board. | |
| """ | |
| if width < 4 or height < 6: | |
| raise ValueError("Dimensions too small: {}x{}".format(width, height)) | |
| self.width = width | |
| self.height = height | |
| self.grid = self.gen_grid(lambda _x, _y: randbool(per=8)) | |
| self.counts = self.gen_grid(self.count_nearby) | |
| self.mask = self.gen_grid(True) | |
| self.count_colors = [ | |
| 'blue', | |
| 'green', | |
| 'red', | |
| 'red', | |
| 'red', | |
| 'red', | |
| 'red', | |
| 'red' | |
| ] | |
| self.message_text = None | |
| self.game_over = False | |
| @property | |
| def game_won(self): | |
| """True when the game has been won.""" | |
| # Check if there are no masked spots without bombs underneath | |
| for x, y in self.locations: | |
| if self.mask[x][y] and not self.grid[x][y]: | |
| return False | |
| return True | |
| @property | |
| def locations(self): | |
| """Generator that allows for iteration over all location in the grid.""" | |
| for y in range(0, self.height): | |
| for x in range(0, self.width): | |
| yield (x, y) | |
| def show_message(self): | |
| """Display and wipe the message.""" | |
| if self.message_text: | |
| print(self.message_text) | |
| self.message_text = None | |
| def gen_grid(self, val): | |
| """Generate a grid of size self.width x self.height. | |
| Arguments | |
| --------- | |
| val : callable or object | |
| When a callable, invokes the function with arguments x, y. Otherwise | |
| just inserts the value. | |
| Returns | |
| ------- | |
| list of lists with dimension self.width x self.height. | |
| """ | |
| return [ | |
| [ | |
| val(x, y) if hasattr(val, "__call__") else val | |
| for y in range(0, self.height) | |
| ] | |
| for x in range(0, self.width) | |
| ] | |
| def count_nearby(self, x, y): | |
| """Given a location x, y, count all grid locations nearby with a bomb. | |
| Arguments | |
| --------- | |
| x : int | |
| Valid x-location on the grid. | |
| y : int | |
| Valid y-location on the grid. | |
| Returns | |
| ------- | |
| Number of adjacent bombs (diagonal and directly adjacent). | |
| """ | |
| count = 0 | |
| deltas = set(itertools.permutations([1, 1, 0, -1, -1], 2)) | |
| for delta in deltas: | |
| if self.check_coord(x + delta[0], y + delta[1]): | |
| if self.grid[x + delta[0]][y + delta[1]]: | |
| count += 1 | |
| return count | |
| def count_char(self, x, y): | |
| """Given a location x, y, provide a printable character. | |
| Arguments | |
| --------- | |
| x : int | |
| Valid x-location on the grid. | |
| y : int | |
| Valid y-location on the grid. | |
| Returns | |
| ------- | |
| Character to display in the terminal. | |
| """ | |
| if self.counts[x][y] == 0: | |
| return " " | |
| else: | |
| count = self.counts[x][y] | |
| return colored(str(count), self.count_colors[count - 1]) | |
| def show(self): | |
| """Output the game state to stdout.""" | |
| for x, y in self.locations: | |
| if self.mask[x][y]: | |
| char = colored("X", "grey") | |
| else: | |
| count_char = self.count_char(x, y) | |
| char = colored("+", "magenta") if self.grid[x][y] else count_char | |
| print(char + " ", end="") | |
| if x == (self.width - 1): | |
| print() | |
| def check_coord(self, x, y): | |
| """Given a coordinate pair, determine if the location is on the grid.""" | |
| return (x >= 0) and (y >= 0) and (x < self.width) and (y < self.height) | |
| def pick(self, x, y): | |
| """Check for a bomb in the location specified. | |
| Arguments | |
| --------- | |
| x : int | |
| An x-coordinate. | |
| y : int | |
| A y-coordinate. | |
| """ | |
| if not self.check_coord(x, y): | |
| self.message_text = "Invalid location: {}x{}".format(x, y) | |
| return | |
| if self.game_over: | |
| self.message_text = "Game is over. No more selection is allowed." | |
| return | |
| if self.mask[x][y]: | |
| if self.grid[x][y]: | |
| self.game_over = True | |
| self.message_text = "Game over." | |
| self.remove_mask() | |
| else: | |
| self.expose_area(x, y) | |
| if self.game_won: | |
| self.game_over = True | |
| self.message_text = "You've won." | |
| else: | |
| self.message_text = "That area is already uncovered." | |
| def remove_mask(self): | |
| """Expose the entire game board.""" | |
| self.mask = [ | |
| [False for _ in range(0, self.height)] | |
| for _ in range(0, self.width) | |
| ] | |
| def expose_area(self, x, y): | |
| """Expose the game board starting in location x, y. | |
| Will recursively expose the surrounding area, terminating when a spot is | |
| reached that has adjacent bombs or is a bomb itself. | |
| Arguments | |
| --------- | |
| x : int | |
| An x-coordinate. | |
| y : int | |
| A y-coordinate. | |
| """ | |
| # Don't attempt to expose if the coordinates are invalid | |
| if not self.check_coord(x, y): | |
| return | |
| # Stop the search if we have hit an unmasked area | |
| if not self.mask[x][y]: | |
| return | |
| # Don't expose bombs | |
| if self.grid[x][y]: | |
| return | |
| self.mask[x][y] = False | |
| # Stop the search if we have hit a non-zero nearby count | |
| if self.counts[x][y] > 0: | |
| return | |
| self.expose_area(x - 1, y) | |
| self.expose_area(x + 1, y) | |
| self.expose_area(x, y - 1) | |
| self.expose_area(x, y + 1) | |
| def main(): | |
| """Minesweeper driver function.""" | |
| os.system("clear") | |
| try: | |
| # Set grid size relative to the terminal size. | |
| rows, columns = os.popen('stty size', 'r').read().split() | |
| game = Grid(int(columns) // 2, int(rows) - 4) | |
| while not game.game_over: | |
| game.show() | |
| game.show_message() | |
| x = get_type(int, "Enter an x coordinate: ") | |
| y = get_type(int, "Enter a y coordinate: ") | |
| os.system("clear") | |
| game.pick(x, y) | |
| game.show() | |
| print("gg") | |
| except KeyboardInterrupt: | |
| print() | |
| print("Rage quit.") | |
| pass | |
| if __name__ == "__main__": | |
| main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment