-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathflood_it.py
More file actions
259 lines (184 loc) · 5.98 KB
/
flood_it.py
File metadata and controls
259 lines (184 loc) · 5.98 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
A raucous ripoff of Flood-It! 2, a game I like to play on my iPhone.
"""
from termcolor import colored
import os
import pprint
import random
BOARD_WIDTH = 12
BOARD_HEIGHT = 12
N_TURNS = 22
class Color(object):
COLORS = (
'r',
'c',
'm',
'y',
'g',
'b'
)
COLORS_TO_OUTPUT = {
'r' : colored(' R ', 'red' , attrs=['bold', 'reverse']),
'c' : colored(' C ', 'cyan' , attrs=['bold', 'reverse']),
'm' : colored(' M ', 'magenta', attrs=['bold', 'reverse']),
'y' : colored(' Y ', 'yellow' , attrs=['bold', 'reverse']),
'g' : colored(' G ', 'green' , attrs=['bold', 'reverse']),
'b' : colored(' B ', 'blue' , attrs=['bold', 'reverse'])
}
def __init__(self, color=None):
if not color:
self.color = self.random_color()
elif self.validate_color(color):
self.color = color
else:
raise ValueError('Invalid color {0}'.format(color))
def __str__(self):
return self.COLORS_TO_OUTPUT[self.color]
def __repr__(self):
return repr(self.color)
def __eq__(self, other):
if type(self) is type(other):
return self.color == other.color
else:
return False
@classmethod
def random_color(cls):
"""Returns a random color name."""
return random.choice(cls.COLORS)
@classmethod
def validate_color(cls, color):
return color in cls.COLORS
class Tile(object):
def __init__(self, x, y, color=None):
"""If no color is provided, a random color is generated."""
self.x = x
self.y = y
if color:
self.color = color
else:
self.color = Color()
def __str__(self):
return str(self.color)
def __repr__(self):
return repr('{0} at ({1}, {2})'.format(self.color, self.x, self.y))
def flood(self, color):
"""Floods this tile with a color (mostly sugar, otherwise just a setter)"""
self.color = color
class Board(object):
def __init__(self, width, height):
"""Initializes a width-by-height board of Tiles."""
self._width = width
self._height = height
self._num_tiles = width * height
self.board = [
[Tile(x, y) for x in range(width)]
for y in range(height)
]
# Start with the top left tile
starting_tile = self.board[0][0]
self.flooded_tiles = {starting_tile}
self.flooded_tiles.update(self.find_floodplane(starting_tile.color))
def __len__(self):
return self._num_tiles
def is_flooded(self):
"""Returns True if the board is flooded, false otherwise."""
return len(self.flooded_tiles) == self._num_tiles
def flood(self, color):
"""
Plays for a round of the game.
!!MUTATES BOARD STATE!!
Args:
color (Color): The color with which to flood the board this round
"""
# Change current flood plane to the new flood color (start flooding)
for tile in self.flooded_tiles:
tile.flood(color)
# This statement does not mutate the board state.
new_flooded_tiles = self.find_floodplane(color)
# Modify board state to include the newly flooded tiles (finish flooding)
self.flooded_tiles.update(new_flooded_tiles)
def find_floodplane(self, color):
"""
Returns the result of flooding the board with a certain color.
!!Does NOT mutate board state!!
Runs a BFS starting with the flooded tiles
Args:
color (Color): The color with which to flood the board this round
Returns (set of Tile):
The new set of flooded tiles.
"""
# Initialize the BFS queue with the top-left tile
queue = [ self.board[0][0] ]
flooded_tiles = set()
while queue:
tile = queue.pop()
# If the color of the tile matches the flooding color, add it
if tile.color == color:
flooded_tiles.add(tile)
# Add the tile's unflooded neighbors
queue.extend(
[neighbor for neighbor in self.safe_get_neighbors(tile)
if neighbor not in flooded_tiles]
)
return flooded_tiles
def safe_get_neighbors(self, tile):
"""Safe neighbor lookup for a tile; handles the edge cases and all."""
neighbors = []
# Look at North, South, East, West neighbors (x, y)
for x_offset, y_offset in [(0, 1), (0, -1), (1, 0), (-1, 0)]:
# Do the index calculation upfront to avoid redoing it in the 'if'
tmp_x = tile.x + x_offset
tmp_y = tile.y + y_offset
if (0 <= tmp_x < self._width and 0 <= tmp_y < self._height):
neighbors.append(self.board[tmp_y][tmp_x]) # [row][column] resolution
return neighbors
def display(self):
"""
Custom display method, let the board display itself.
This should eventually be delegated to a standalone display method.
"""
for row in self.board:
print ''.join('{0:3}'.format(str(tile)) for tile in row)
def blit():
"""Blit the screen"""
# Blit the screen
_ = os.system('clear')
def won(turns, board):
"""Returns True if the game is in a win state."""
return turns < N_TURNS and board.is_flooded()
def get_color(prompt=''):
"""Get the flood color for this round from the user."""
print 'Color options: {0}'.format(Color.COLORS)
user_color = raw_input(prompt).lower()
while not Color.validate_color(user_color):
print "THAT AIN'T A VALID COLOR"
user_color = raw_input(prompt)
return Color(user_color)
def print_gameover_info(turns, board):
if won(turns, board):
print 'You won.'
else:
print 'Nah, you lost.'
def print_game_stats(turn, board):
print 'Turn {0:3d} of {1:3d}'.format(turn, N_TURNS)
def main():
# Clear the screen for the game.
blit()
# Construct board
board = Board(width=BOARD_WIDTH, height=BOARD_HEIGHT)
# Display the board
board.display()
for turn in range(N_TURNS):
print_game_stats(turn, board)
# Play a round
board.flood( get_color('Flood Color: ') )
blit()
board.display()
# Terminate early on a win
if won(turn, board):
break
print_gameover_info(turn, board)
if __name__ == '__main__':
main()