-
Notifications
You must be signed in to change notification settings - Fork 1
/
utils.py
314 lines (238 loc) · 9.82 KB
/
utils.py
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
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
"""
FILE: utils.py
DESCRIPTION: Utility functions that we were required to implement
"""
from typing import List, Tuple, Iterable, Optional, Dict, Set, Callable, Union
Board = List[List[str]]
Path = List[Tuple[int, int]]
# region CONSTANTS
NEIGHBOURS_DELTA: Dict[str, Tuple[int, int]] = {
# "direction": (row_delta, col_delta)
"N": (-1, 0),
"NE": (-1, 1),
"E": (0, 1),
"SE": (1, 1),
"S": (1, 0),
"SW": (1, -1),
"W": (0, -1),
"NW": (-1, -1)
}
# endregion CONSTANTS
def load_words_dict(filepath):
"""
Loads a dictionary of words from a file
:param filepath: The path of the file to load
:return: A set of words
"""
with open(filepath) as file:
return set(file.read().splitlines())
def __get_value_by_coordinate(board: Board, coordinate: Tuple[int, int]) -> \
str:
"""
Returns the current value of the given coordinate
:param board: the board
:param coordinate: a coordinate on the board
:return: The value in board of the given coordinate
"""
row, col = coordinate
return board[row][col]
def __are_neighbours(coordinate1: Tuple[int, int],
coordinate2: Tuple[int, int]):
"""
Checks if two coordinates are neighbours
:param coordinate1: the first coordinate to compare
:param coordinate2: the second coordinate to compare
:return: True if both coordinates are neighbours, False otherwise
"""
row1, col1 = coordinate1
for row_delta, col_delta in NEIGHBOURS_DELTA.values():
if (row1 + row_delta, col1 + col_delta) == coordinate2:
return True
return False
def is_valid_path(board: Board, path: Path, words: Iterable[str]) -> \
Optional[str]:
"""
Checks if a path is valid, meaning: whether the path is made of
neighbours, and the word exists in the words' collection
:param board: the board
:param path: the current path of cells chosen
:param words: the words collection
:return: The word if it is valid, None otherwise
"""
word: str = ''
for path_index in range(len(path) - 1):
# check whether two following cells are actually neighbours on board
if not __are_neighbours(path[path_index], path[path_index + 1]):
return None
# add the current coordinate's value
word += __get_value_by_coordinate(board, path[path_index])
# add the last value
word += __get_value_by_coordinate(board, path[-1])
if word in words:
return word
return None
def __is_coordinate_in_board(board: Board, coordinate: Tuple[int, int]) -> \
bool:
"""
Checks if the given coordinate is within the board's boundaries
:param board: the board
:param coordinate: the coordinate to check
:return: True if the coordinate is within the board's boundaries,
False otherwise
"""
return 0 <= coordinate[0] < len(board) and \
0 <= coordinate[1] < len(board[0])
def __find_neighbour_coordinate(coordinate: Tuple[int, int],
delta: Tuple[int, int]) -> Tuple[int, int]:
"""
Computes the neighbour's coordinate by a given delta
:param coordinate: the starting coordinate
:param delta: the delta for the required neighbour
:return: the neighbour's coordinate
"""
neighbour_row = coordinate[0] + delta[0]
neighbour_col = coordinate[1] + delta[1]
return neighbour_row, neighbour_col
def __filter_words_set_by_prefix(words_set: Set[str], prefix: str) -> Set[str]:
"""
Filters the words set, finding only the words starting with the
given prefix
:param words_set: the set to filter
:param prefix: the prefix to filter by
:return: a filtered set
"""
return set(filter(lambda s: s.startswith(prefix), words_set))
def find_length_n_paths(n: int, board: Board, words: Iterable[str]) -> \
List[Path]:
"""
Finds all valid paths of length 'n'
:param n: the length of the required paths
:param board: the board
:param words: the words collection
:return: a list containing all valid paths of length 'n'
"""
def stop(n: int, path: Path, word: str) -> bool:
return len(path) == n
def update(lst_paths: List[Path], path: Path, word: str) -> None:
lst_paths.append(path[:])
lst_paths: List[Path] = []
__backtracking_start(stop, update, n, board, words, lst_paths)
return lst_paths
def find_length_n_words(n: int, board: Board, words: Iterable[str]) -> \
List[Path]:
"""
Finds all paths on the board that yield words of length 'n'
:param n: the length of the required paths
:param board: the board
:param words: the words collection
:return: a list of paths to words of length 'n'; may include more than
one path to a word
"""
def stop(n: int, path: Path, word: str) -> bool:
return len(word) == n
def update(lst_paths: List[Path], path: Path, word: str) -> None:
lst_paths.append(path[:])
lst_paths: List[Path] = []
__backtracking_start(stop, update, n, board, words, lst_paths)
return lst_paths
def max_score_paths(board: Board, words: Iterable[str]) -> List[Path]:
"""
By a given board and words collection, returns a list of valid
routes that provide the maximum score
:param board: the board
:param words: the words collection
:return: a list of valid routes that provide the maximum game score
"""
def stop(n: int, path: Path, word: str) -> bool:
return len(word) == n
def update(dict_paths: Dict[str, List[Path]], path: Path,
word: str) -> None:
if word not in dict_paths.keys():
dict_paths[word] = []
dict_paths[word].append(path[:])
dict_paths: Dict[str, List[Path]] = {}
# perform a similar function to find_length_n_words, for each
# word-length that can be found in the given words bank.
# this has the same effect as iterating over all words, but with
# fewer actions to perform.
word_lengths: Set[int] = set(map(lambda s: len(s), words))
for length in word_lengths:
__backtracking_start(stop, update, length, board, words, dict_paths)
# create a list of the longest paths found, by taking the longest
# path for each word found.
longest_paths: List[Path] = []
for lst_paths in dict_paths.values():
longest_paths.append(max(lst_paths, key=lambda p: len(p)))
return longest_paths
def __backtracking_start(stop_condition: Callable, data_update_func: Callable,
n: int, board: Board, words: Iterable[str],
dataset: Union[List[Path], Dict[str, List[Path]]]) -> \
None:
"""
Start the backtracking action for each coordinate
:param stop_condition: a boolean function that defines the recursion's
stop condition. Recieves the integer 'n', the current path and the
current word.
:param data_update_func: a function that updates the dataset, to be
called only when needed. This will update the dataset, so the function
returns nothing.
:param n: the length required
:param board: the board
:param words: the words collection
:param dataset: either a list of paths, or a dictionary mapping words
to lists of paths.
"""
# create a set out of the iterator, so it can be read more than once
words_set = set(words)
# pick the starting coordinate
for row_index in range(len(board)):
for col_index in range(len(board[row_index])):
# initialize values for this starting coordinate
coordinate = (row_index, col_index)
curr_word = __get_value_by_coordinate(board, coordinate)
filtered_words = __filter_words_set_by_prefix(words_set, curr_word)
# only proceed if words with this prefix can be found.
# otherwise, skip to the next coordinate.
if len(filtered_words) == 0:
continue
curr_path: Path = [coordinate]
# recursively find paths starting from this coordinate
__backtracking_action(stop_condition, data_update_func, n, board,
words_set, dataset, curr_path, curr_word)
def __backtracking_action(stop_condition: Callable, data_update_func: Callable,
n: int, board: Board, words_set: Set[str],
dataset: Union[List[Path], Dict[str, List[Path]]],
curr_path: Path, curr_word: str) -> None:
"""
Crawl the board. To be called from _backtracking_start.
"""
if stop_condition(n, curr_path, curr_word):
if curr_word in words_set:
data_update_func(dataset, curr_path, curr_word)
return
# try all neighbours
for delta in NEIGHBOURS_DELTA.values():
neighbour = __find_neighbour_coordinate(curr_path[-1], delta)
# try only if neighbour is within the board's boundaries
if not __is_coordinate_in_board(board, neighbour):
continue
# try only if the neighbour hasn't been stepped through yet
if neighbour in curr_path:
continue
# extend the word and filter the words set
new_word = curr_word + __get_value_by_coordinate(board, neighbour)
filtered_words = __filter_words_set_by_prefix(words_set, new_word)
if len(filtered_words) == 0:
continue
# extend the path
curr_path.append(neighbour)
# recursive call with the path and word extended
__backtracking_action(stop_condition, data_update_func,
n, board, filtered_words, dataset, curr_path,
new_word)
# clean up for backtracking
curr_path.pop()
if __name__ == "__main__":
print("The file is part of the game 'Boggle'.\n"
"You can play it by running:\n"
"> python boggle.py")