7 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
cblanken a611f4a7e7 Fixup board_randomizer typehints
Invalid `list[str]` type hints were causing errors in the downstream Boggler Flask app
2024-06-14 10:31:17 -04:00
cblankenandGitHub e183ce07c0 Update README.md 2024-04-13 10:19:20 -04:00
cblankenandGitHub 09e6f4a672 Update README.md
- Fix bad link to wordlists
- Add license link
2024-02-14 22:49:13 -05:00
4 changed files with 50 additions and 69 deletions
+5 -8
View File
@@ -18,7 +18,7 @@ To use the script to solve a Boggle board, you'll need to do a few things first.
d,f,e,y d,f,e,y
n,m,e,qu n,m,e,qu
``` ```
2. Find or create a dictionary wordlist file or create your own 2. Find or create a dictionary wordlist file
The dictionary wordlist should have each word on a single line like so The dictionary wordlist should have each word on a single line like so
```console ```console
@@ -44,7 +44,7 @@ To use the script to solve a Boggle board, you'll need to do a few things first.
3. Split the dictionary wordlist into separate files based on the first letter of each word. 3. Split the dictionary wordlist into separate files based on the first letter of each word.
To split an English wordlist the `split_wordlist_alpha.sh` script can be used like so: To split an English wordlist, use the `split_wordlist_alpha.sh` script like so:
```console ```console
$ split_wordlist_alpha.sh my_wordlist.txt . $ split_wordlist_alpha.sh my_wordlist.txt .
``` ```
@@ -97,7 +97,6 @@ BOARD
+---------------+ +---------------+
| O | W | H | A | | O | W | H | A |
+---------------+ +---------------+
Starting @ (0, 0) Starting @ (0, 0)
╭──────────┬──────────────────────────────────────────────────────────────────╮ ╭──────────┬──────────────────────────────────────────────────────────────────╮
│ Word │ Path │ │ Word │ Path │
@@ -142,8 +141,7 @@ BOARD
│ aia │ [(0, 1), (0, 2), (1, 2)] │ │ aia │ [(0, 1), (0, 2), (1, 2)] │
│ aias │ [(0, 1), (0, 2), (1, 2), (2, 3)] │ │ aias │ [(0, 1), (0, 2), (1, 2), (2, 3)] │
╰────────┴──────────────────────────────────────────────────╯ ╰────────┴──────────────────────────────────────────────────╯
Starting @ (0, Starting @ (0,2)
2)
╭──────┬──────╮ ╭──────┬──────╮
│ Word │ Path │ │ Word │ Path │
├──────┼──────┤ ├──────┼──────┤
@@ -254,8 +252,7 @@ Starting @ (0,
│ yap │ [(1, 3), (1, 2), (0, 3)] │ │ yap │ [(1, 3), (1, 2), (0, 3)] │
│ yas │ [(1, 3), (1, 2), (2, 3)] │ │ yas │ [(1, 3), (1, 2), (2, 3)] │
╰──────┴──────────────────────────╯ ╰──────┴──────────────────────────╯
Starting @ (2, Starting @ (2, 0)
0)
╭──────┬──────╮ ╭──────┬──────╮
│ Word │ Path │ │ Word │ Path │
├──────┼──────┤ ├──────┼──────┤
@@ -391,4 +388,4 @@ Navigate to the project root folder and run the following.
Run `poetry build` Run `poetry build`
# License # License
The included [wordlists](src/boggler/wordlists) are covered by their respective licenses. All other files MIT © Cameron Blankenbuehler The included [wordlists](boggler/wordlists) are covered by their respective licenses. All other files [MIT](LICENSE) © Cameron Blankenbuehler
+11 -5
View File
@@ -1,8 +1,14 @@
"""Module to generate random Boggle boards for testing""" """Module to generate random Boggle boards for testing"""
from sys import argv, stderr from sys import argv, stderr
from random import randint, shuffle from random import randint, shuffle
from math import sqrt, floor from math import sqrt, floor
from pathlib import Path from pathlib import Path
from typing import List, TypeAlias
Die: TypeAlias = List[str]
Dice: TypeAlias = List[Die]
def read_dice_file(dice_path: Path): def read_dice_file(dice_path: Path):
@@ -11,17 +17,17 @@ def read_dice_file(dice_path: Path):
return [line.rstrip().split(",") for line in file.readlines() if line[0] != "#"] return [line.rstrip().split(",") for line in file.readlines() if line[0] != "#"]
def roll_die(die: str): def roll_die(die: Die):
"""Return a face of the given die string to simulate rolling a die""" """Return a face of the given die string to simulate rolling a die"""
return str(die[randint(0, len(die) - 1)]) return str(die[randint(0, len(die) - 1)])
def roll_dice(dice: list[str]): def roll_dice(dice: Dice):
"""Return a random roll for each die""" """Return a random roll for each die"""
return [roll_die(die) for die in dice] return [roll_die(die) for die in dice]
def get_random_board(dice: list[str]): def get_random_board(dice: Dice):
shuffle(dice) shuffle(dice)
rolls = roll_dice(dice) rolls = roll_dice(dice)
@@ -33,7 +39,7 @@ def get_random_board(dice: list[str]):
return board return board
def get_random_board_csv(dice: list[str]): def get_random_board_csv(dice: Dice):
board = get_random_board(dice) board = get_random_board(dice)
board = [",".join(row) for row in board] board = [",".join(row) for row in board]
return board return board
@@ -49,4 +55,4 @@ if __name__ == "__main__":
for r in range(0, int(floor(sqrt(len(dice))))): for r in range(0, int(floor(sqrt(len(dice))))):
print(board[r]) print(board[r])
except Exception as e: except Exception as e:
print("Argument must be a valid file path!", file=stderr) print("Argument must be a valid file path.", file=stderr)
+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]