Created
November 18, 2012 23:59
-
-
Save bobmurder/4108242 to your computer and use it in GitHub Desktop.
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 re | |
import sys | |
from collections import defaultdict, namedtuple | |
from itertools import chain, count | |
from operator import add | |
from random import choice | |
from string import uppercase | |
DEBUG=True | |
VERBOSE=False | |
######################################################################## | |
# LOOKUP TABLES | |
######################################################################## | |
#=================================================== | |
# Shift Table | |
#=================================================== | |
def shift_table(): | |
''' List of tuples to add with a cell to find neighbors ''' | |
x_vals = [-1, -1, -1, 0, 0, 1, 1, 1] | |
y_vals = [-1, 0, 1, -1, 1, -1, 0, 1] | |
return zip(x_vals, y_vals) | |
shifts = shift_table() | |
#=================================================== | |
# Valid Cells Table | |
#=================================================== | |
def valid_cell_table(height=15, width=15): | |
''' A set of tuples where each tuple is a valid cell coordinate pair ''' | |
return set([(i, j) for i in range(width) for j in range(height)]) | |
valid_cells = valid_cell_table() | |
#=================================================== | |
# User Cell Choice Table | |
#=================================================== | |
def letter_table(height=15, width=15): | |
''' A dict for looking up the tuple equivalent to A1-Z26. | |
Range: A1: (0, 0) to "%s%s" % (letters[width], height+1) ''' | |
keys = ['%s%s' % (c, n) for c in uppercase[:width] | |
for n in range(1, height + 1)] | |
values = [cell for cell in sorted(valid_cells)] | |
return {k: v for k, v in zip(keys, values)} | |
letters = letter_table() | |
#=================================================== | |
# Neighbors lookup table | |
#=================================================== | |
# Helper functions | |
def shift_cell(cell, shift): | |
''' Add cell + shift, and return the result ''' | |
x1, x2, y1, y2 = cell + shift | |
candidate = (x1 + y1, x2 + y2) | |
return candidate | |
def valid_neighbors(cell): | |
''' Return a list of valid neighbors for `cell` ''' | |
neighbors = [] | |
for shift in shifts: | |
candidate = shift_cell(cell, shift) | |
if candidate in valid_cells: | |
neighbors.append(candidate) | |
return neighbors | |
# Table Creation | |
def neighbors_table(): | |
''' Each key is a tuple (0, 0) and the value is a list of valid neighors. | |
neighbors[(0, 0)] == [(0, 1), (1, 0), (1, 1)] ''' | |
return {cell: valid_neighbors(cell) for cell in valid_cells} | |
neighbors = neighbors_table() | |
nums = [str(n) for n in range(1, 9)] | |
#=================================================== | |
# Cell value/symbol tables | |
#=================================================== | |
cell_values = {'null': ' ', | |
'mine': 'X', | |
'safe': '9', | |
'flag': nums} | |
cell_symbols = {'mark': '!', | |
'poss': '?', | |
'hide': '#'} | |
######################################################################## | |
# CLASSES / DATA STRUCTURES | |
######################################################################## | |
class Cell(object): | |
''' | |
A class representing a minesweeper cell. | |
ARGUMENTS: | |
x, y - positive cartesian coordinates, starting at 0, 0 | |
''' | |
def __init__(self, x, y, value=cell_values['null'], | |
symbol=cell_symbols['hide'], hidden=True): | |
self.x = x | |
self.y = y | |
self.value = value | |
self.symbol = symbol | |
self.hidden = hidden | |
def __str__(self): | |
return "(%s, %s)" % (self.x, self.y) | |
def coords(self): | |
''' Return an (x, y) tuple for lookups ''' | |
return (self.x, self.y) | |
def protect(self): | |
''' Guard cells during mine placement to prevent the | |
player from losing on turn one. This swaps from | |
null to safe, or safe to null ''' | |
if self.value == cell_values['null']: | |
self.value = cell_values['safe'] | |
else: | |
self.value = cell_values['null'] | |
def reveal(self): | |
''' Reveal a cell ''' | |
self.hidden = False | |
self.symbol = self.value | |
def mark(self): | |
''' Change the mark a user has made on a cell. The cycle is | |
hide -> flag -> possible -> hide -> flag ... ''' | |
if self.symbol == cell_symbols['hide']: | |
self.symbol = cell_symbols['flag'] | |
elif self.symbol == cell_symbols['flag']: | |
self.symbol = cell_symbols['poss'] | |
elif self.symbol == cell_symbols['poss']: | |
self.symbol = cell_symbols['hide'] | |
else: | |
raise ValueError('Invalid symbol') | |
def check_type(self, cell_type): | |
''' Determine a cell's type based on its value ''' | |
assert cell_type in cell_values.keys() | |
return self.value in cell_values[cell_type] | |
class Game(object): | |
''' This class holds and updates the cell grid, counter values, | |
and handles output. ''' | |
def __init__(self, cells, mines, flags, height=15, width=15): | |
# these don't get modified by the class instance at all | |
self.height = height | |
self.width = width | |
self.mines = mines | |
self.flags = flags | |
# these are modified by the class instance | |
self.cells = cells | |
self.marks = [] | |
self.revealed = set() | |
# initialize counters | |
self.turn_count = 0 | |
self.mark_count = len(self.marks) | |
self.mine_count = len(self.mines) | |
# win flag | |
self.won = False | |
self.game_over = False | |
def won(self): | |
''' Determine if we've won ''' | |
marked = sorted(self.marks) == sorted(self.mines) | |
uncovered = all([cells[cell].hidden | |
for cell in valid_cells if not cell in self.mines]) | |
return marked and uncovered | |
# Counter methods | |
def increment(self, counter): | |
self.counter += 1 | |
def decrement(self, counter): | |
self.counter -= 1 | |
def reveal(self, origin): | |
# get current cell | |
cell = self.cells[origin] | |
# if it is a mine, declare a game over | |
if cell.value == cell_values['mine']: | |
self.game_over = True | |
return False | |
if origin in self.revealed: | |
return True | |
# if it is an empty cell, reveal it, reveal adjacent numbers, | |
# then recurse onto adjacent empty spaces | |
elif cell.value == cell_values['null']: | |
self.unhide(cell, origin) | |
cell_neighbors = neighbors[origin] | |
for neighbor in cell_neighbors: | |
# get neighbor cell instance | |
neighbor_cell = self.cells[neighbor] | |
# if cell value is a number | |
if neighbor_cell.value in cell_values['flag']: | |
self.unhide(neighbor_cell, neighbor) | |
# if cell value is ' ' | |
elif neighbor_cell.value == cell_values['null']: | |
return self.reveal(neighbor) | |
else: | |
print "you idiot, fix this!!" | |
# if it is a number, reveal it an return | |
elif cell.value in cell_values['flag']: | |
self.unhide(cell, origin) | |
return | |
def unhide(self, cell, origin): | |
cell.reveal() | |
self.revealed.add(origin) | |
def output(self): | |
grid = symbol_grid(self.cells) | |
header = self._header() | |
display = self._display(grid) | |
prompt = self._prompt() | |
status_bar = self._status_bar() | |
return header + display +'\n' + prompt + '\n\n' + status_bar | |
def update(self, cell, choice): | |
if choice == 'R': | |
self.reveal(cell) | |
elif choice == 'M': | |
self.cells[cell].mark() | |
elif choice == 'Q': | |
# we either quit, or print the grid again | |
while True: | |
ans = raw_input("Are you sure? Y/n") | |
if ans == 'Y': | |
sys.exit('Thanks for playing') | |
elif ans == 'n': | |
self.output() | |
else: | |
sys.exit("Prevent this shit") | |
# private functions for creating the output string | |
def _header(self, head=0): | |
''' | |
Returns column headers from A-width | |
''' | |
header = [uppercase[i] for i in range(self.width)] | |
header.insert(head, ' ') | |
header.append(' \n') | |
return '|'.join(header) | |
def _display(self, grid, head=0): | |
for idx, row in enumerate(grid): | |
i = idx + 1 | |
if len(str(i)) == 1: | |
row.insert(head, ' %s' % i) | |
else: | |
row.insert(head, '%s' % i) | |
row.append(' \n') | |
return ''.join(['|'.join(row) for row in grid]) | |
def _prompt(self): | |
''' Prompt that prints each turn ''' | |
return "(R)eveal, (M)ark, (Q)uit" | |
def _status_bar(self): | |
return 'Unmarked mines: %s' % (len(self.mines) - len(self.marks)) | |
def guess(): | |
while True: | |
user_guess = raw_input("Enter your guess > ") | |
if user_guess in valid_inputs: | |
return user_guess | |
else: | |
print "Invalid guess." | |
continue | |
def cells(width=15, height=15): | |
''' | |
cells returns a dict that holds all the Cell instances. | |
The keys are coordinate tuples, and the values are Cell instances. | |
A Grid instance holds a list of lists, and the list items are tuples | |
corresponding to the keys from cells. Cell instances are updated and | |
displayed by accessing values of this dictionary. Creating this | |
dictionary separates map generation from map updating, which should | |
simplify things. | |
''' | |
return {pair: Cell(*pair) for pair in valid_cells} | |
#=================================================== | |
# Grid initialization functions | |
#=================================================== | |
def protect_area(origin): | |
''' | |
This function is called twice. The first time, it changes the value | |
of each cell to '9', an otherwise impossible value. This prevents | |
the user from dying on turn 1. After mine placement is finished, | |
this function is called again, which changes all '9' valued cells | |
back to ' ' cells. | |
''' | |
for cell in neighbors[origin]: | |
cells[cell].protect() | |
cells[origin].protect() | |
def random_cell(height=15, width=15): | |
''' Return a tuple of valid cell coordinates ''' | |
random_cell = (random.choice(range(width)), random.choice(range(height))) | |
assert random_cell in valid_cells | |
return random_cell | |
def place_mines(mines, initial_pick=(7, 7), height=15, width=15): | |
''' Place mines on the grid by setting a cell's value to 'X' | |
Return a list of mine (x, y) tuples. ''' | |
protect_area(initial_pick) | |
mine_list = [] | |
while mines: | |
mine = random_cell(height, width) | |
if cells[mine].check_type('null'): | |
cells[mine].value = cell_values['mine'] | |
mine_list.append(mine) | |
mines -= 1 | |
protect_area(initial_pick) | |
return mine_list | |
def place_flags(mines): | |
''' Place numeric flags adjacent to all mines. | |
Return a list of flag cells. ''' | |
flag_cells = defaultdict(int) | |
for mine in mines: | |
neighbor_list = neighbors[mine] | |
for neighbor in neighbor_list: | |
if neighbor not in mines: | |
flag_cells[neighbor] += 1 | |
for cell, value in flag_cells.iteritems(): | |
cells[cell].value = str(value) | |
return flag_cells.keys() | |
def value_grid(cells, height=15, width=15): | |
return [[cells[(i, j)].value for i in range(width)] for j in range(height)] | |
def symbol_grid(cells, height=15, width=15): | |
return [[cells[(i, j)].symbol for i in range(width)] for j in range(height)] | |
if __name__ == '__main__': | |
import sys | |
if DEBUG or VERBOSE: | |
from pprint import pprint | |
if VERBOSE: | |
# print lookup tables | |
print 'shifts' | |
pprint(shifts) | |
print 'valid cells' | |
pprint(valid_cells) | |
print 'letters' | |
pprint(letters) | |
print 'neighbors' | |
pprint(neighbors) | |
n_mines = 20 | |
initial_pick = (7, 7) | |
cells = cells() | |
mines = place_mines(n_mines, initial_pick) | |
flags = place_flags(mines) | |
if DEBUG: | |
print "#" * 40 | |
print "VALUES" | |
pprint(value_grid(cells)) | |
print "#" * 40 | |
print "SYMBOLS" | |
pprint(symbol_grid(cells)) | |
minesweeper = Game(cells, mines, flags) | |
minesweeper.reveal(initial_pick) | |
print minesweeper.output() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment