I wanted to code a small ASCII art minesweeper clone, to see what it looked like and how much code it needed.
The result is not so bad, under 80 new lines of code in total. It looks kinda readable to me, but I wrote it just know, so I'm biased.
Input checking...
I feel like I got pretty much obsessed with input checking, so much that entering garbage will always produce nice error reports to the user and not crash.
Parsing coordinates is a worthy effort
Validating and parsing coordinates of the form LETTER + NUMBER
is a pain, but I feel like that input format is user friendly, so it is going to remain that way.
Separation of concerns
The interaction with the outside world is all inside the main minesweeper
function, because mixing concerns of logic and interaction together makes the code unreadable.
Doctests
The code has some doctests, as I feel like seeing an example usage of a function makes the code easier to read (apart from giving a sanity check).
Re-use
I re-used some of my old code, a module about the game of life and a module about the user interaction for this. You can find them in a gist.
import doctest
import string
import random
import life
from user_interface import general_input
MINE = -1
def minesweeper():
size = general_input("The size of the board? ", type_=int, set_=range(1, 9+1))
mines = general_input("The number of mines? ", type_=int, set_=range(1, size*size+1))
board = random_mineswepper_board(size, mines)
already_seen = set()
while True:
print(''.join(show_board(board, already_seen)))
raw_coordinates = general_input(
"Enter the coordinates: ",
costum_validation = lambda coord: is_coordinate_input_valid(coord, board),
costum_validation_error = """
Coordinate must be in the form LETTER + NUMBER
And must not exceed the size of the board.
Example: B4
"""
)
x, y = parse_coordinates(raw_coordinates)
already_seen.add((x,y))
if is_mine(x, y, board):
print("You hit a mine!\nGame Over|")
print(''.join(show_board(board, already_seen)))
break
def is_coordinate_input_valid(coords, board):
"""
>>> is_coordinate_input_valid('FoooBar', random_mineswepper_board(3, 1))
False
>>> is_coordinate_input_valid('C2', random_mineswepper_board(5, 2))
True
>>> is_coordinate_input_valid('Z4', random_mineswepper_board(3, 2)) # Out of bounds
False
"""
return len(coords) == 2 and \
coords[0].lower() in string.ascii_lowercase and \
coords[1] in string.digits and \
string.ascii_lowercase.index(coords[0].lower()) < len(board) and \
int(coords[1]) <= len(board)
def random_mineswepper_board(size, number_of_mines):
"""
>>> random.seed(0)
>>> random_mineswepper_board(3, 2)
[[1, 1, 0], [-1, 2, 1], [1, 2, -1]]
"""
board = [ [0 for _ in range(size)] for _ in range(size)]
mined = set()
for _ in range(number_of_mines):
x, y = random.randint(0, len(board) - 1), random.randint(0, len(board) - 1)
while (x, y) in mined:
x, y = random.randint(0, len(board) - 1), random.randint(0, len(board) - 1)
board[x][y] = MINE
mined.add((x,y))
return list(life.in_chunks_of(size,
[MINE if cell == MINE else nears.count(MINE)
for cell, nears in life.with_neightbors(board)]))
def show_board(board, already_seen, sep = ' | ', cheat=False):
"""
>>> print(''.join(show_board([[2,2,1],[-1,-1,1],[2,2,1]], {}, cheat=True)))
| A | B | C |
1 | 2 | 2 | 1 |
2 | M | M | 1 |
3 | 2 | 2 | 1 |
<BLANKLINE>
"""
print(' ' + sep + sep.join(string.ascii_uppercase[i] for i in range(0, len(board))) + sep)
for x, _ in enumerate(board):
yield str(x + 1) + sep
for y, _ in enumerate(board[0]):
if True if cheat else (x, y) in already_seen:
yield 'M' if board[x][y] == MINE else str(board[x][y])
else:
yield 'X'
yield sep
yield '\n'
def parse_coordinates(coords):
"""
>>> parse_coordinates('C2')
(1, 2)
"""
return (int(coords[1]) - 1, string.ascii_lowercase.index(coords[0].lower()))
def is_mine(x, y, board):
"""
>>> is_mine(0, 0, [[MINE]])
True
"""
return board[x][y] == MINE
if __name__ == "__main__":
doctest.testmod()
random.seed()
minesweeper()