4 Commits
Author SHA1 Message Date
cblanken 612482587d Bump package version 2024-06-25 22:15:02 -04:00
cblanken 4587e7bc70 Optimize boggle tree operations
- Cache WordNode and WordTree properties with frequent lookups
- Reduce `len()` calls
- Remove unnecessary debug logs
2024-06-25 22:07:22 -04:00
cblanken 9b7b05f79d Fix min python version 2024-06-14 11:09:06 -04:00
cblanken 72f3d20401 Bump patch version 2024-06-14 10:36:21 -04:00
2 changed files with 34 additions and 56 deletions
+25 -37
View File
@@ -1,4 +1,5 @@
"""Boggler Utils""" """Boggler Utils"""
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
from multiprocessing import Pool from multiprocessing import Pool
@@ -95,22 +96,22 @@ class BoggleBoard:
body += header body += header
return f"{header}{body}" return f"{header}{body}"
@property @functools.cached_property
def height(self) -> int: def height(self) -> int:
"""Getter for height property""" """Getter for height property"""
return self.__height return self.__height
@property @functools.cached_property
def width(self) -> int: def width(self) -> int:
"""Getter for width property""" """Getter for width property"""
return self.__width return self.__width
@property @functools.cached_property
def max_word_len(self) -> int: def max_word_len(self) -> int:
"""Getter for maximum word length property""" """Getter for maximum word length property"""
return self.__max_word_len return self.__max_word_len
@property @functools.cached_property
def board(self) -> dict[tuple[int, int], BoardCell]: def board(self) -> dict[tuple[int, int], BoardCell]:
"""Getter for board property""" """Getter for board property"""
return self.__board return self.__board
@@ -158,11 +159,16 @@ class WordNode:
self.__parent = parent self.__parent = parent
self.__board_pos = board_pos self.__board_pos = board_pos
@property @functools.cached_property
def letters(self) -> str: def letters(self) -> str:
"""Getter for letter property""" """Getter for letter property"""
return self.__letters return self.__letters
@functools.cached_property
def letters_count(self) -> int:
"""Getter for letters count"""
return len(self.__letters)
@property @property
def is_word(self) -> bool: def is_word(self) -> bool:
"""Getter for is_word property""" """Getter for is_word property"""
@@ -172,7 +178,7 @@ class WordNode:
def is_word(self, value): def is_word(self, value):
self.__is_word = value self.__is_word = value
@property @functools.cached_property
def children(self) -> dict[str, WordNode]: def children(self) -> dict[str, WordNode]:
"""Getter for children property""" """Getter for children property"""
return self.__children return self.__children
@@ -221,7 +227,7 @@ class WordTree:
self, self,
alphabet: list[str], alphabet: list[str],
root: WordNode, root: WordNode,
words: list[str] = None, words: list[str] | None = None,
max_word_len=16, max_word_len=16,
) -> None: ) -> None:
self.__alphabet = alphabet self.__alphabet = alphabet
@@ -240,27 +246,27 @@ class WordTree:
for word in words: for word in words:
self.__insert_word(word) self.__insert_word(word)
@property @functools.cached_property
def alphabet(self) -> list: def alphabet(self) -> list:
"""Getter for alphabet property""" """Getter for alphabet property"""
return self.__alphabet return self.__alphabet
@property @property
def wordlist(self) -> list[str]: def wordlist(self) -> list[str] | None:
"""Getter for wordlist property""" """Getter for wordlist property"""
return self.__wordlist return self.__wordlist
@property @functools.cached_property
def root(self) -> WordNode: def root(self) -> WordNode:
"""Getter for root property""" """Getter for root property"""
return self.__root return self.__root
@property @functools.cached_property
def max_word_len(self) -> int: def max_word_len(self) -> int:
"""Getter for max_word_len property""" """Getter for max_word_len property"""
return self.__max_word_len return self.__max_word_len
@property @functools.cached_property
def tree(self) -> dict[str, WordNode]: def tree(self) -> dict[str, WordNode]:
"""Getter for tree property""" """Getter for tree property"""
return self.__tree return self.__tree
@@ -289,7 +295,6 @@ class WordTree:
board_pos=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: %s", 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)
@@ -298,41 +303,31 @@ class WordTree:
otherwise returns False""" otherwise returns False"""
# Insert root node # Insert root node
prefix = word[0 : len(self.root.letters)] # prefix = first letter block prefix = word[0 : self.root.letters_count] # 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]
word_len = len(word) word_len = len(word)
i = len(prefix) i = len(prefix)
i_max = min(self.max_word_len, len(word)) i_max = min(self.max_word_len, word_len)
while i < i_max: while i < i_max:
letters = word[i] letters = word[i]
log.debug(
"1 letter seq: %s %s %s", word, letters, letters in curr_node.children
)
if letters not in self.alphabet: if letters not in self.alphabet:
# Check two letter sequences like ("Qu", "Th", etc.) at current index # Check two letter sequences like ("Qu", "Th", etc.) at current index
letters = word[i : i + 2] letters = word[i : i + 2]
log.debug(
"2 letter seq: %s %s %s",
word,
letters,
letters in curr_node.children,
)
if letters not in self.alphabet: if letters not in self.alphabet:
return False return False
# Insert node # Insert node
if letters not in curr_node.children: if letters not in curr_node.children:
self.insert_node(letters, curr_node) self.insert_node(letters, curr_node)
log.debug("3 letter seq: %s %s", word, letters)
curr_node = curr_node.children[letters] curr_node = curr_node.children[letters]
i += len(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 = word_len <= self.max_word_len
return True return True
def search(self, word: str, curr_node=None) -> WordNode: def search(self, word: str, curr_node=None) -> WordNode:
@@ -373,7 +368,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:
log.debug(f"MAX WORD LENGTH REACHED! len = {word_len}") # MAX WORD LENGTH REACHED!
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 subtree return subtree
@@ -382,22 +377,15 @@ 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))
log.debug(
"1: WORD FOUND: %s %s", # 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 subtree return subtree
elif self.active_node.is_word: elif self.active_node.is_word:
# Word found
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))
log.debug(
"2: WORD FOUND: %s %s",
"".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:
+9 -19
View File
@@ -1,32 +1,22 @@
[project]
name = "boggler"
version = "2.0.1"
authors = [
{ name="Cameron Blankenbuehler", email="cameron.blankenbuehler@gmail.com" },
]
description = "Utilities for solving the Boggle word game."
readme="README.md"
license = { file="LICENSE" }
requires-python = ">=3.7"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
]
[project.urls] [project.urls]
"Homepage" = "https://github.com/cblanken/boggler" "Homepage" = "https://github.com/cblanken/boggler"
"Bug Tracker" = "https://github.com/cblanken/boggler/issues" "Bug Tracker" = "https://github.com/cblanken/boggler/issues"
[tool.poetry] [tool.poetry]
name = "boggler" name = "boggler"
version = "2.0.1" version = "2.0.4"
description = "Utilities for solving the Boggle word game." description = "Utilities for solving the Boggle word game."
authors = ["Cameron Blankenbuehler <cameron.blankenbuehler@protonmail.com>"] authors = ["Cameron Blankenbuehler <cameron.blankenbuehler@gmail.com>"]
license = "LICENSE"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
]
readme = "README.md" readme = "README.md"
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = "^3.11" python = "^3.8"
rich = "^13.4.1" rich = "^13.4.1"
[tool.poetry.scripts] [tool.poetry.scripts]