Fix boggle tree missing multi-letter nodes

The BoggleTree object when inserting nodes did not properly check for nodes
with values of more than 1 letter. Therefore, any words with "qu" after the
root would not be included in the solved board. As of now, only nodes with
a maximum of 2 letters are accounted for, 3+ letter nodes won't be checked.
This commit is contained in:
2023-03-23 12:42:40 -04:00
parent 6d3a58595c
commit 1dc118e953
+29 -43
View File
@@ -2,7 +2,7 @@
from __future__ import annotations from __future__ import annotations
from os import path from os import path
from multiprocessing import Pool from multiprocessing import Pool
from logging import info, debug, warn, error import logging as log
import functools import functools
import operator import operator
@@ -258,6 +258,7 @@ class WordTree:
def insert_node(self, letters: str, parent: WordNode, is_word: bool = False, def insert_node(self, letters: str, parent: WordNode, is_word: bool = False,
children: dict[str, WordNode] = None, board_pos = None): 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`'''
log.debug("inserting node: ", letters)
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)
@@ -272,41 +273,25 @@ class WordTree:
curr_node = self.tree[prefix] curr_node = self.tree[prefix]
word_len = len(word) word_len = len(word)
# Insert remaining nodes as letter groups based on alphabet i = len(prefix)
skip_cnt = 0 i_max = min(self.max_word_len, len(word))
for i, letter in enumerate(word[len(prefix):self.max_word_len]): while i < i_max:
# Skip word letter iterations for length of a previously inserted letter group letters = word[i]
if skip_cnt > 0: log.debug("1 letter seq: ", word, letters, letters in curr_node.children)
skip_cnt -= 1 if letters not in self.alphabet:
continue # Check two letter sequences like ("Qu", "Th", etc.) at current index
letters = word[i:i+2]
try: log.debug("2 letter seq: ", word, letters, letters in curr_node.children)
alpha_index = self.alphabet.index(letter) if letters not in self.alphabet:
except ValueError: # letters not in given alphabet
return False
# Insert letter (single)
if letter == self.alphabet[alpha_index] and letter not in curr_node.children:
self.insert_node(letter, curr_node)
curr_node = curr_node.children[letter]
# Letter already exists
elif letter == self.alphabet[alpha_index]:
curr_node = curr_node.children[letter]
# Check for letter groups (like "Qu") starting with `letter` in alphabet
else:
alpha = self.alphabet[alpha_index]
# Check that letter group is shorter than and matches in the word remainder
if len(alpha) < word_len - i and alpha == word[i+1:i+1+len(alpha)]:
if alpha not in curr_node.children:
self.insert_node(alpha, curr_node )
curr_node = curr_node.children[alpha]
else: # node already exist
curr_node = curr_node.children[alpha]
skip_cnt = len(alpha) - 1
debug(f"LONG GROUP: {alpha}; {word}")
else:
return False return False
# Insert node
if letters not in curr_node.children:
self.insert_node(letters, curr_node)
log.debug("3 letter seq: ", word, letters)
curr_node = curr_node.children[letters]
i += len(letters)
# Mark the last node as a word # Mark the last node as a word
curr_node.is_word = len(word) <= self.max_word_len curr_node.is_word = len(word) <= self.max_word_len
@@ -317,6 +302,7 @@ class WordTree:
if len(word) == 0 or word is None: if len(word) == 0 or word is None:
return curr_node return curr_node
log.debug("Searching...", word, curr_node)
if curr_node is None: if curr_node is None:
curr_node = self.root curr_node = self.root
for letters in curr_node.children: for letters in curr_node.children:
@@ -338,7 +324,7 @@ class WordTree:
''' '''
if word_len > self.max_word_len or word_len >= board.max_word_len: if word_len > self.max_word_len or word_len >= board.max_word_len:
debug(f"MAX WORD LENGTH REACHED! len = {word_len}") log.debug(f"MAX WORD LENGTH REACHED! len = {word_len}")
subtree.active_node = subtree.active_node.parent subtree.active_node = subtree.active_node.parent
self.active_node = self.active_node.parent self.active_node = self.active_node.parent
return return
@@ -347,14 +333,14 @@ class WordTree:
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]
subtree.word_paths.append((subtree.active_node.get_word(board), word_path)) subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
debug("1: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path) log.debug("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
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]
subtree.word_paths.append((subtree.active_node.get_word(board), word_path)) subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
debug("2: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path) log.debug("2: WORD FOUND:", "".join([board.board[x].letters for x in word_path]), word_path)
# Branch for each adjacent board cell # Branch for each adjacent board cell
for cell in board_cell.adjacent_cells: for cell in board_cell.adjacent_cells:
@@ -384,7 +370,7 @@ def build_full_boggle_tree(board: BoggleBoard, wordlist_path: str) -> dict[str,
board_tree = {} board_tree = {}
index = {} index = {}
info("Reading in wordlists...") log.info("Reading in wordlists...")
for letters in alphabet: for letters in alphabet:
if letters == "": if letters == "":
# Skip wordlist read for blocks with empty string # Skip wordlist read for blocks with empty string
@@ -398,16 +384,16 @@ def build_full_boggle_tree(board: BoggleBoard, wordlist_path: str) -> dict[str,
try: try:
wordlist = read_wordlist(path.join(path.abspath(wordlist_path), filename)) wordlist = read_wordlist(path.join(path.abspath(wordlist_path), filename))
index[letters] = wordlist index[letters] = wordlist
info(f">> {letters}: {filename}") log.info(f">> {letters}: {filename}")
except FileNotFoundError: except FileNotFoundError:
info(f">> {letters}: -- Skipping -- no wordlist found for {letters}") log.info(f">> {letters}: -- Skipping -- no wordlist found for {letters}")
index[letters] = {} index[letters] = {}
info("Generating WordTrees...") log.info("Generating WordTrees...")
params = [ [alphabet, board, cell, index[cell.letters] ] for cell in board.board.values()] params = [ [alphabet, board, cell, index[cell.letters] ] for cell in board.board.values()]
with Pool(processes=len(board.board)) as pool: with Pool(processes=len(board.board)) as pool:
for i, res in enumerate(pool.map(build_boggle_tree, params)): for i, res in enumerate(pool.map(build_boggle_tree, params)):
info(f">> {params[i][2]}") log.info(f">> {params[i][2]}")
board_tree[params[i][2].pos] = res board_tree[params[i][2].pos] = res
return board_tree return board_tree