2 Commits
Author SHA1 Message Date
cblanken a764a41ff6 Add pygtrie module dependency 2023-08-23 15:42:15 -04:00
cblanken 6dba9313d5 Refactor board components into separate module 2023-08-23 15:41:45 -04:00
6 changed files with 214 additions and 181 deletions
+8 -5
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
n,m,e,qu
```
2. Find or create a dictionary wordlist file
2. Find or create a dictionary wordlist file or create your own
The dictionary wordlist should have each word on a single line like so
```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.
To split an English wordlist, use the `split_wordlist_alpha.sh` script like so:
To split an English wordlist the `split_wordlist_alpha.sh` script can be used like so:
```console
$ split_wordlist_alpha.sh my_wordlist.txt .
```
@@ -97,6 +97,7 @@ BOARD
+---------------+
| O | W | H | A |
+---------------+
Starting @ (0, 0)
╭──────────┬──────────────────────────────────────────────────────────────────╮
│ Word │ Path │
@@ -141,7 +142,8 @@ BOARD
│ aia │ [(0, 1), (0, 2), (1, 2)] │
│ aias │ [(0, 1), (0, 2), (1, 2), (2, 3)] │
╰────────┴──────────────────────────────────────────────────╯
Starting @ (0,2)
Starting @ (0,
2)
╭──────┬──────╮
│ Word │ Path │
├──────┼──────┤
@@ -252,7 +254,8 @@ Starting @ (0,2)
│ yap │ [(1, 3), (1, 2), (0, 3)] │
│ yas │ [(1, 3), (1, 2), (2, 3)] │
╰──────┴──────────────────────────╯
Starting @ (2, 0)
Starting @ (2,
0)
╭──────┬──────╮
│ Word │ Path │
├──────┼──────┤
@@ -388,4 +391,4 @@ Navigate to the project root folder and run the following.
Run `poetry build`
# License
The included [wordlists](boggler/wordlists) are covered by their respective licenses. All other files [MIT](LICENSE) © Cameron Blankenbuehler
The included [wordlists](src/boggler/wordlists) are covered by their respective licenses. All other files MIT © Cameron Blankenbuehler
+135
View File
@@ -0,0 +1,135 @@
from __future__ import annotations
class BoardCell:
"""Boggle Board cell"""
def __init__(
self, row: int, col: int, letters: str, adjacent_cells: list[BoardCell] = None
) -> None:
self.__row: int = row
self.__col: int = col
self.__pos: tuple[int, int] = (self.__row, self.__col)
self.__letters: str = letters
self.__adjacent_cells: list[BoardCell] = adjacent_cells
@property
def row(self) -> int:
"""Getter for row property"""
return self.__row
@property
def col(self) -> int:
"""Getter for col property"""
return self.__col
@property
def pos(self) -> tuple[int, int]:
"""Getter for pos property"""
return self.__pos
@property
def letters(self) -> str:
"""Getter for letter property"""
return self.__letters
@property
def adjacent_cells(self) -> list[BoardCell]:
"""Getter for adjacent_cells property"""
return self.__adjacent_cells
@adjacent_cells.setter
def adjacent_cells(self, value):
self.__adjacent_cells = value
def __str__(self):
return f"({self.__row}, {self.__col}): {self.__letters}"
def __repr__(self):
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
class BoggleBoard:
"""Boggle board structure"""
def __init__(self, board: list[list[str]], max_word_len: int = 14) -> None:
self.__height: int = len(board)
self.__width: int = len(board[0]) if self.__height > 0 else 0
self.__board_list = board
self.__board: dict[tuple[int, int], BoardCell] = {}
# Max word length is limited by size of the board
self.__max_word_len = min(max_word_len, self.__width * self.__height)
# Generate BoardCell for each position on the board
for row in range(0, self.__height):
for col in range(0, self.__width):
self.__board[(row, col)] = BoardCell(row, col, board[row][col])
# Update adjacent cell references for each BoardCell
for cell in self.__board.values():
adjacent_indexes = self.__get_adjacent_indexes(cell.row, cell.col)
cell.adjacent_cells = [self.__board[(x[0], x[1])] for x in adjacent_indexes]
def __str__(self):
flattened_board_list = [y for x in self.__board_list for y in x]
max_len = len(max(flattened_board_list, key=len))
max_len = (
max_len + 1 if max_len % 2 == 0 else max_len + 2
) # keep header_len odd
header_len = self.__width * (max_len + 1) - 1
head = "-" * header_len
header = f"+{head}+\n"
body = ""
for row in self.__board_list:
body += "|"
for col in row:
body += f"{col.upper(): ^{max_len}}|"
body += "\n"
body += header
return f"{header}{body}"
@property
def height(self) -> int:
"""Getter for height property"""
return self.__height
@property
def width(self) -> int:
"""Getter for width property"""
return self.__width
@property
def max_word_len(self) -> int:
"""Getter for maximum word length property"""
return self.__max_word_len
@property
def board(self) -> dict[tuple[int, int], BoardCell]:
"""Getter for board property"""
return self.__board
def __get_adjacent_indexes(self, row, col):
"""Return adjecency list for board of size `row x col`"""
indexes = []
if row > 0:
indexes.append((row - 1, col)) # up
if col > 0:
indexes.append((row - 1, col - 1)) # up-left
if col < self.width - 1:
indexes.append((row - 1, col + 1)) # up-right
if row < self.height - 1:
indexes.append((row + 1, col)) # down
if col > 0:
indexes.append((row + 1, col - 1)) # down-left
if col < self.width - 1:
indexes.append((row + 1, col + 1)) # down-right
if col > 0:
indexes.append((row, col - 1)) # left
if col < self.width - 1:
indexes.append((row, col + 1)) # right
return indexes
def get_cell(self, row: int, col: int) -> BoardCell:
"""Return the value at the specified row x column"""
return self.__board[(row, col)]
+5 -11
View File
@@ -1,14 +1,8 @@
"""Module to generate random Boggle boards for testing"""
from sys import argv, stderr
from random import randint, shuffle
from math import sqrt, floor
from pathlib import Path
from typing import List, TypeAlias
Die: TypeAlias = List[str]
Dice: TypeAlias = List[Die]
def read_dice_file(dice_path: Path):
@@ -17,17 +11,17 @@ def read_dice_file(dice_path: Path):
return [line.rstrip().split(",") for line in file.readlines() if line[0] != "#"]
def roll_die(die: Die):
def roll_die(die: str):
"""Return a face of the given die string to simulate rolling a die"""
return str(die[randint(0, len(die) - 1)])
def roll_dice(dice: Dice):
def roll_dice(dice: list[str]):
"""Return a random roll for each die"""
return [roll_die(die) for die in dice]
def get_random_board(dice: Dice):
def get_random_board(dice: list[str]):
shuffle(dice)
rolls = roll_dice(dice)
@@ -39,7 +33,7 @@ def get_random_board(dice: Dice):
return board
def get_random_board_csv(dice: Dice):
def get_random_board_csv(dice: list[str]):
board = get_random_board(dice)
board = [",".join(row) for row in board]
return board
@@ -55,4 +49,4 @@ if __name__ == "__main__":
for r in range(0, int(floor(sqrt(len(dice))))):
print(board[r])
except Exception as e:
print("Argument must be a valid file path.", file=stderr)
print("Argument must be a valid file path!", file=stderr)
+34 -155
View File
@@ -1,145 +1,11 @@
"""Boggler Utils"""
from __future__ import annotations
from pathlib import Path
from multiprocessing import Pool
import logging as log
import functools
import operator
class BoardCell:
"""Boggle Board cell"""
def __init__(
self, row: int, col: int, letters: str, adjacent_cells: list[BoardCell] = None
) -> None:
self.__row: int = row
self.__col: int = col
self.__pos: tuple[int, int] = (self.__row, self.__col)
self.__letters: str = letters
self.__adjacent_cells: list[BoardCell] = adjacent_cells
@property
def row(self) -> int:
"""Getter for row property"""
return self.__row
@property
def col(self) -> int:
"""Getter for col property"""
return self.__col
@property
def pos(self) -> tuple[int, int]:
"""Getter for pos property"""
return self.__pos
@property
def letters(self) -> str:
"""Getter for letter property"""
return self.__letters
@property
def adjacent_cells(self) -> list[BoardCell]:
"""Getter for adjacent_cells property"""
return self.__adjacent_cells
@adjacent_cells.setter
def adjacent_cells(self, value):
self.__adjacent_cells = value
def __str__(self):
return f"({self.__row}, {self.__col}): {self.__letters}"
def __repr__(self):
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
class BoggleBoard:
"""Boggle board structure"""
def __init__(self, board: list[list[str]], max_word_len: int = 14) -> None:
self.__height: int = len(board)
self.__width: int = len(board[0]) if self.__height > 0 else 0
self.__board_list = board
self.__board: dict[tuple[int, int], BoardCell] = {}
# Max word length is limited by size of the board
self.__max_word_len = min(max_word_len, self.__width * self.__height)
# Generate BoardCell for each position on the board
for row in range(0, self.__height):
for col in range(0, self.__width):
self.__board[(row, col)] = BoardCell(row, col, board[row][col])
# Update adjacent cell references for each BoardCell
for cell in self.__board.values():
adjacent_indexes = self.__get_adjacent_indexes(cell.row, cell.col)
cell.adjacent_cells = [self.__board[(x[0], x[1])] for x in adjacent_indexes]
def __str__(self):
flattened_board_list = [y for x in self.__board_list for y in x]
max_len = len(max(flattened_board_list, key=len))
max_len = (
max_len + 1 if max_len % 2 == 0 else max_len + 2
) # keep header_len odd
header_len = self.__width * (max_len + 1) - 1
head = "-" * header_len
header = f"+{head}+\n"
body = ""
for row in self.__board_list:
body += "|"
for col in row:
body += f"{col.upper(): ^{max_len}}|"
body += "\n"
body += header
return f"{header}{body}"
@functools.cached_property
def height(self) -> int:
"""Getter for height property"""
return self.__height
@functools.cached_property
def width(self) -> int:
"""Getter for width property"""
return self.__width
@functools.cached_property
def max_word_len(self) -> int:
"""Getter for maximum word length property"""
return self.__max_word_len
@functools.cached_property
def board(self) -> dict[tuple[int, int], BoardCell]:
"""Getter for board property"""
return self.__board
def __get_adjacent_indexes(self, row, col):
"""Return adjecency list for board of size `row x col`"""
indexes = []
if row > 0:
indexes.append((row - 1, col)) # up
if col > 0:
indexes.append((row - 1, col - 1)) # up-left
if col < self.width - 1:
indexes.append((row - 1, col + 1)) # up-right
if row < self.height - 1:
indexes.append((row + 1, col)) # down
if col > 0:
indexes.append((row + 1, col - 1)) # down-left
if col < self.width - 1:
indexes.append((row + 1, col + 1)) # down-right
if col > 0:
indexes.append((row, col - 1)) # left
if col < self.width - 1:
indexes.append((row, col + 1)) # right
return indexes
def get_cell(self, row: int, col: int) -> BoardCell:
"""Return the value at the specified row x column"""
return self.__board[(row, col)]
from .board import BoardCell, BoggleBoard
class WordNode:
@@ -159,16 +25,11 @@ class WordNode:
self.__parent = parent
self.__board_pos = board_pos
@functools.cached_property
@property
def letters(self) -> str:
"""Getter for letter property"""
return self.__letters
@functools.cached_property
def letters_count(self) -> int:
"""Getter for letters count"""
return len(self.__letters)
@property
def is_word(self) -> bool:
"""Getter for is_word property"""
@@ -178,7 +39,7 @@ class WordNode:
def is_word(self, value):
self.__is_word = value
@functools.cached_property
@property
def children(self) -> dict[str, WordNode]:
"""Getter for children property"""
return self.__children
@@ -227,7 +88,7 @@ class WordTree:
self,
alphabet: list[str],
root: WordNode,
words: list[str] | None = None,
words: list[str] = None,
max_word_len=16,
) -> None:
self.__alphabet = alphabet
@@ -246,27 +107,27 @@ class WordTree:
for word in words:
self.__insert_word(word)
@functools.cached_property
@property
def alphabet(self) -> list:
"""Getter for alphabet property"""
return self.__alphabet
@property
def wordlist(self) -> list[str] | None:
def wordlist(self) -> list[str]:
"""Getter for wordlist property"""
return self.__wordlist
@functools.cached_property
@property
def root(self) -> WordNode:
"""Getter for root property"""
return self.__root
@functools.cached_property
@property
def max_word_len(self) -> int:
"""Getter for max_word_len property"""
return self.__max_word_len
@functools.cached_property
@property
def tree(self) -> dict[str, WordNode]:
"""Getter for tree property"""
return self.__tree
@@ -295,6 +156,7 @@ class WordTree:
board_pos=None,
):
"""Create WordNode for `letters` and into WordTree under `parent`"""
log.debug("inserting node: %s", letters)
node = WordNode(letters, is_word, parent, children, board_pos)
parent.add_child_node(node)
@@ -303,31 +165,41 @@ class WordTree:
otherwise returns False"""
# Insert root node
prefix = word[0 : self.root.letters_count] # prefix = first letter block
prefix = word[0 : len(self.root.letters)] # prefix = first letter block
if word is None or prefix != self.root.letters:
return False
curr_node = self.tree[prefix]
word_len = len(word)
i = len(prefix)
i_max = min(self.max_word_len, word_len)
i_max = min(self.max_word_len, len(word))
while i < i_max:
letters = word[i]
log.debug(
"1 letter seq: %s %s %s", word, letters, letters in curr_node.children
)
if letters not in self.alphabet:
# Check two letter sequences like ("Qu", "Th", etc.) at current index
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:
return False
# Insert node
if letters not in curr_node.children:
self.insert_node(letters, curr_node)
log.debug("3 letter seq: %s %s", word, letters)
curr_node = curr_node.children[letters]
i += len(letters)
# Mark the last node as a word
curr_node.is_word = word_len <= self.max_word_len
curr_node.is_word = len(word) <= self.max_word_len
return True
def search(self, word: str, curr_node=None) -> WordNode:
@@ -368,7 +240,7 @@ class WordTree:
"""
if word_len > self.max_word_len or word_len >= board.max_word_len:
# MAX WORD LENGTH REACHED!
log.debug(f"MAX WORD LENGTH REACHED! len = {word_len}")
subtree.active_node = subtree.active_node.parent
self.active_node = self.active_node.parent
return subtree
@@ -377,15 +249,22 @@ class WordTree:
if self.active_node.is_word and len(self.active_node.children) == 0:
word_path = subtree.active_node.path[::-1]
subtree.word_paths.append((subtree.active_node.get_word(board), word_path))
# Word found
log.debug(
"1: WORD FOUND: %s %s",
"".join([board.board[x].letters for x in word_path]),
word_path,
)
self.active_node = self.active_node.parent
subtree.active_node = subtree.active_node.parent
return subtree
elif self.active_node.is_word:
# Word found
word_path = subtree.active_node.path[::-1]
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
for cell in board_cell.adjacent_cells:
Generated
+12 -1
View File
@@ -353,6 +353,17 @@ files = [
[package.extras]
plugins = ["importlib-metadata"]
[[package]]
name = "pygtrie"
version = "2.5.0"
description = "A pure Python trie data structure implementation."
optional = false
python-versions = "*"
files = [
{file = "pygtrie-2.5.0-py3-none-any.whl", hash = "sha256:8795cda8105493d5ae159a5bef313ff13156c5d4d72feddefacaad59f8c8ce16"},
{file = "pygtrie-2.5.0.tar.gz", hash = "sha256:203514ad826eb403dab1d2e2ddd034e0d1534bbe4dbe0213bb0593f66beba4e2"},
]
[[package]]
name = "pylint"
version = "2.17.4"
@@ -609,4 +620,4 @@ files = [
[metadata]
lock-version = "2.0"
python-versions = "^3.11"
content-hash = "ebdcc3cb72dd9c6dc736b31d92471656c82dc96ec6b9e67bf2886d5ca1c4f499"
content-hash = "167dc1b59d6da393661109af959693c455aa06c40c18564e16684920f259ae95"
+20 -9
View File
@@ -1,23 +1,34 @@
[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]
"Homepage" = "https://github.com/cblanken/boggler"
"Bug Tracker" = "https://github.com/cblanken/boggler/issues"
[tool.poetry]
name = "boggler"
version = "2.0.4"
version = "2.0.1"
description = "Utilities for solving the Boggle word game."
authors = ["Cameron Blankenbuehler <cameron.blankenbuehler@gmail.com>"]
license = "LICENSE"
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: MIT License",
"Operating System :: POSIX :: Linux",
]
authors = ["Cameron Blankenbuehler <cameron.blankenbuehler@protonmail.com>"]
readme = "README.md"
[tool.poetry.dependencies]
python = "^3.8"
python = "^3.11"
rich = "^13.4.1"
pygtrie = "^2.5.0"
[tool.poetry.scripts]
boggler = "boggler.__main__:main"