Skip to content

Instantly share code, notes, and snippets.

@danthedaniel
Last active February 11, 2018 23:58
Show Gist options
  • Save danthedaniel/bf11ab4b51da93173e5e3299ee3adccd to your computer and use it in GitHub Desktop.
Save danthedaniel/bf11ab4b51da93173e5e3299ee3adccd to your computer and use it in GitHub Desktop.
Python CLI Minesweeper
#!/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