Track words found in WordTree(s) for easier display

This commit is contained in:
2022-08-31 14:28:30 -04:00
parent 31a307e83d
commit 46dd069cfd
2 changed files with 60 additions and 43 deletions
+16 -7
View File
@@ -8,16 +8,25 @@ if __name__ == '__main__':
print('Usage: python3 boggler.py <BOARD_FILE> <WORDLISTS_DIR> [MAX_WORD_LENGTH]') print('Usage: python3 boggler.py <BOARD_FILE> <WORDLISTS_DIR> [MAX_WORD_LENGTH]')
sys.exit(1) sys.exit(1)
b1 = read_boggle_file(Path(sys.argv[1])) board = read_boggle_file(Path(sys.argv[1]))
if len(sys.argv) == 3: if len(sys.argv) == 3:
b1_board = BoggleBoard(b1) boggle_board = BoggleBoard(board)
print(b1_board) print(boggle_board)
boggle_tree = build_full_boggle_tree(b1_board, Path(sys.argv[2])) boggle_tree = build_full_boggle_tree(boggle_board, Path(sys.argv[2]))
elif len(sys.argv) == 4: elif len(sys.argv) == 4:
try : try :
b1_board = BoggleBoard(b1, int(sys.argv[3])) boggle_board = BoggleBoard(board, int(sys.argv[3]))
print(b1_board) print(boggle_board)
boggle_tree = build_full_boggle_tree(b1_board, Path(sys.argv[2])) boggle_tree = build_full_boggle_tree(boggle_board, Path(sys.argv[2]))
print("\nBOARD")
print(boggle_board)
for start_pos, tree in boggle_tree.items():
print(f"\nStarting @ {start_pos}...")
for word in tree.word_paths:
print(f"{word[0]: <{boggle_board.max_word_len}}: {word[1]}")
except ValueError as e: except ValueError as e:
print("The [MAX_WORD_LENGTH] argument must be an integer.") print("The [MAX_WORD_LENGTH] argument must be an integer.")
print("Please try again.") print("Please try again.")
+42 -34
View File
@@ -6,7 +6,8 @@ import sys
class BoardCell: class BoardCell:
'''Boggle Board cell''' '''Boggle Board cell'''
def __init__(self, row: int, col: int, letters: str, adjacent_cells: list[BoardCell] = None) -> BoardCell: def __init__(self, row: int, col: int, letters: str,
adjacent_cells: list[BoardCell] = None) -> BoardCell:
self.__row: int = row self.__row: int = row
self.__col: int = col self.__col: int = col
self.__pos: (int, int) = (self.__row, self.__col) self.__pos: (int, int) = (self.__row, self.__col)
@@ -46,7 +47,7 @@ class BoardCell:
return f"({self.__row}, {self.__col}): {self.__letters}" return f"({self.__row}, {self.__col}): {self.__letters}"
def __repr__(self): def __repr__(self):
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}; {self.__adjacent_cells})" return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
class BoggleBoard: class BoggleBoard:
'''Boggle board structure''' '''Boggle board structure'''
@@ -101,7 +102,7 @@ class BoggleBoard:
return self.__max_word_len return self.__max_word_len
@property @property
def board(self) -> dict((int, int), BoardCell): def board(self) -> dict[(int, int), BoardCell]:
'''Getter for board property''' '''Getter for board property'''
return self.__board return self.__board
@@ -132,7 +133,8 @@ class BoggleBoard:
class WordNode: class WordNode:
'''A node describing a single letter in a WordTree.''' '''A node describing a single letter in a WordTree.'''
def __init__(self, letters: str, is_word: bool = False, parent: WordNode = None, children: dict[str, WordNode] = None, board_pos = None) -> WordNode: def __init__(self, letters: str, is_word: bool = False, parent: WordNode = None,
children: dict[str, WordNode] = None, board_pos = None) -> WordNode:
self.__letters = letters self.__letters = letters
self.__is_word = is_word self.__is_word = is_word
self.__children = children if children is not None else {} self.__children = children if children is not None else {}
@@ -184,21 +186,26 @@ class WordNode:
return path return path
def get_word(self, board: BoggleBoard) -> str:
'''Return word or word fragment based on the WordNode's path property'''
return "".join([board.board[x].letters for x in self.path[::-1]])
def __str__(self): def __str__(self):
return f"WordNode: {self.letters}, {self.is_word}" return f"WordNode: {self.letters}, {self.is_word}, {self.board_pos}"
def __repr__(self): def __repr__(self):
return f"WordNode: {self.letters}, {self.is_word}, {self.children}" return f"WordNode: {self.letters}, {self.is_word}, {self.children}"
class WordTree: class WordTree:
'''A tree populated by WordNode(s) to complete words from a given root letter and wordlist''' '''A tree populated by WordNode(s) to complete words from a given root letter and wordlist'''
def __init__(self, alphabet: str, root: WordNode, words: list[str] = None, max_word_len = 16) -> WordTree: def __init__(self, alphabet: str, root: WordNode, words: list[str] = None,
max_word_len = 16) -> WordTree:
self.__alphabet: str = alphabet self.__alphabet: str = alphabet
self.__wordlist: list[str] = words self.__wordlist: list[str] = words
self.__root: WordNode = root self.__root: WordNode = root
self.__max_word_len: int = max_word_len self.__max_word_len: int = max_word_len
self.__tree: dict[str, WordNode] = {} self.__tree: dict[str, WordNode] = {}
self.__leaf_nodes: dict[str, list[WordNode]] = [] self.__word_paths: dict[str, list[WordNode]] = []
# Generate root node # Generate root node
self.__tree[root.letters] = root self.__tree[root.letters] = root
@@ -235,27 +242,29 @@ class WordTree:
return self.__tree return self.__tree
@property @property
def leaf_nodes_by_word(self): def word_paths(self) -> dict[(int, int), WordNode]:
'''Getter for leaf_nodes property''' '''Getter for leaf_nodes property'''
return self.__leaf_nodes return self.__word_paths
@leaf_nodes_by_word.setter @word_paths.setter
def leaf_nodes_by_word(self, value): def word_paths(self, value):
self.__leaf_nodes = value self.__word_paths = value
def __str__(self): def __str__(self):
return ", ".join(self.wordlist) return ", ".join(self.wordlist)
def insert_node(self, letters: str, parent: WordNode, is_word: bool = False, children: dict[str, WordNode] = None, board_pos = None): def insert_node(self, letters: str, parent: WordNode, is_word: bool = False,
children: dict[str, WordNode] = None, board_pos = None):
'''Create WordNode for `letters` and into WordTree under `parent`''' '''Create WordNode for `letters` and into WordTree under `parent`'''
node = WordNode(letters, is_word, parent, children, board_pos) node = WordNode(letters, is_word, parent, children, board_pos)
parent.add_child_node(node) parent.add_child_node(node)
def __insert_word(self, word: str) -> bool: def __insert_word(self, word: str) -> bool:
'''Returns True if the word could be inserted into the tree with the given alphabet, otherwise returns False''' '''Returns True if the word could be inserted into the tree with the given alphabet,
otherwise returns False'''
# Insert root node # Insert root node
prefix = word[0:len(self.root.letters)] # prefix is equivalent to the first letter block in a word prefix = word[0:len(self.root.letters)] # prefix = first letter block
if word is None or prefix != self.root.letters: if word is None or prefix != self.root.letters:
return False return False
curr_node = self.tree[prefix] curr_node = self.tree[prefix]
@@ -275,7 +284,7 @@ class WordTree:
return False return False
# Insert letter (single) # Insert letter (single)
if letter == self.alphabet[alpha_index] and letter not in curr_node.children: # letter == letter_group implies single letter if letter == self.alphabet[alpha_index] and letter not in curr_node.children:
self.insert_node(letter, curr_node) self.insert_node(letter, curr_node)
curr_node = curr_node.children[letter] curr_node = curr_node.children[letter]
# Letter already exists # Letter already exists
@@ -318,10 +327,8 @@ class WordTree:
return curr_node return curr_node
# TODO: implement tree walk # TODO: implement tree walk
# def walk_tree(self):
# pass
def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, depth: int = 0) -> WordTree: def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, depth: int = 1) -> WordTree:
'''Return subtree of board given a particular root (first letter). '''Return subtree of board given a particular root (first letter).
Keyword arguments: Keyword arguments:
@@ -334,20 +341,20 @@ class WordTree:
# TODO rework active_node refs for recursion so don't have to be reset to parent at every point of return # TODO rework active_node refs for recursion so don't have to be reset to parent at every point of return
if self.active_node.is_word and len(self.active_node.children) == 0: if self.active_node.is_word and len(self.active_node.children) == 0:
word_path = subtree.active_node.path[::-1] word_path = subtree.active_node.path[::-1]
#print("WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path) subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
#print("1: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path)
self.active_node = self.active_node.parent self.active_node = self.active_node.parent
subtree.active_node = subtree.active_node.parent subtree.active_node = subtree.active_node.parent
#subtree.leaf_nodes_by_word.append(self.active_node)
return return
elif self.active_node.is_word: elif self.active_node.is_word:
word_path = subtree.active_node.path[::-1] word_path = subtree.active_node.path[::-1]
#print("WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path) subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
#subtree.leaf_nodes_by_word.append(self.active_node) #print("2: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path)
if depth > self.max_word_len: if depth >= self.max_word_len or depth >= board.max_word_len:
print(f"MAX DEPTH REACHED! Depth = {depth}") #print(f"MAX DEPTH REACHED! Depth = {depth}")
self.active_node = self.active_node.parent
subtree.active_node = subtree.active_node.parent subtree.active_node = subtree.active_node.parent
self.active_node = self.active_node.parent
return return
# Branch for each adjacent board cell # Branch for each adjacent board cell
@@ -359,6 +366,7 @@ class WordTree:
subtree.active_node = subtree.active_node.children[cell.letters] # update subtree pointer subtree.active_node = subtree.active_node.children[cell.letters] # update subtree pointer
self.active_node = self.active_node.children[cell.letters] # update dictionary tree pointer self.active_node = self.active_node.children[cell.letters] # update dictionary tree pointer
self.build_boggle_tree(board, board.board[cell.pos], subtree, depth=depth+1) self.build_boggle_tree(board, board.board[cell.pos], subtree, depth=depth+1)
#print(f"depth: {depth}")
# print(f"DONE SEARCHING at {subtree.active_node}") # print(f"DONE SEARCHING at {subtree.active_node}")
@@ -371,7 +379,6 @@ def build_full_boggle_tree(board: BoggleBoard, wordlist_path: str) -> dict[str,
# TODO: implement multiprocessing / multitthreading for each start letter # TODO: implement multiprocessing / multitthreading for each start letter
# TODO: benchmark on laptop and chromebook # TODO: benchmark on laptop and chromebook
alphabet = sorted(set([cell.letters for cell in board.board.values()])) alphabet = sorted(set([cell.letters for cell in board.board.values()]))
print(f"alphabet: {alphabet}")
board_tree = {} board_tree = {}
index = {} index = {}
@@ -402,12 +409,13 @@ def read_boggle_file(file):
return [x.rstrip().split(',') for x in file.readlines()] return [x.rstrip().split(',') for x in file.readlines()]
if __name__ == "__main__": if __name__ == "__main__":
#b2 = read_boggle_file("boards/b1.csv") #b2 = read_boggle_file(sys.argv[1])
b2 = read_boggle_file(sys.argv[1]) b2 = read_boggle_file("boards/b1.csv")
b2_board = BoggleBoard(b2) b2_board = BoggleBoard(b2)
boggle_tree = build_full_boggle_tree(b2_board, "wordlists/dwyl")
print("\nBOARD")
print(b2_board) print(b2_board)
#boggle_tree = build_full_boggle_tree(b2_board, "wordlists/dwyl")
#print("Search 'quay':", boggle_tree[(1,1)].search('quay').path) w1 = boggle_tree[(0,0)].word_paths
#print("Search 'quip':", boggle_tree[(1,1)].search('quip').path) for w in w1:
#print("Search 'squall':", boggle_tree[(0,0)].search('squall').path) print(f"{w[0]: <{b2_board.max_word_len}}: {w[1]}")
#print("Search 'quash':", boggle_tree[(1,1)].search('quash').path)