mirror of
https://codeberg.org/cblanken/boggler.git
synced 2026-07-25 19:19:20 -04:00
Initial blacken reformat
This commit is contained in:
+53
-28
@@ -9,33 +9,58 @@ from rich.table import Table
|
||||
from rich import box
|
||||
from .boggler_utils import BoggleBoard, build_full_boggle_tree, read_boggle_file
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
prog="boggler",
|
||||
description="Boggle board game solver"
|
||||
)
|
||||
parser = argparse.ArgumentParser(prog="boggler", description="Boggle board game solver")
|
||||
|
||||
parser.add_argument("board", type=Path, help="Path to board CSV file")
|
||||
parser.add_argument("wordlists", type=Path,
|
||||
help="Path to directory of wordlist files. The directory must contain \
|
||||
text files of the form words_X.txt where \"X\" is a character of \
|
||||
the alphabet")
|
||||
parser.add_argument("max_word_length", nargs="?", type=int, default=16,
|
||||
help="Maximum length of words searched for on provided board")
|
||||
parser.add_argument("-f", "--format", type=str,
|
||||
help="Specify alternative output format including [txt, json]")
|
||||
parser.add_argument("-p", "--include-path", action="store_true", default=False,
|
||||
help="Include full paths for each word in output")
|
||||
parser.add_argument("-s", "--sort", action="store_true", default=False,
|
||||
parser.add_argument(
|
||||
"wordlists",
|
||||
type=Path,
|
||||
help='Path to directory of wordlist files. The directory must contain \
|
||||
text files of the form words_X.txt where "X" is a character of \
|
||||
the alphabet',
|
||||
)
|
||||
parser.add_argument(
|
||||
"max_word_length",
|
||||
nargs="?",
|
||||
type=int,
|
||||
default=16,
|
||||
help="Maximum length of words searched for on provided board",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-f",
|
||||
"--format",
|
||||
type=str,
|
||||
help="Specify alternative output format including [txt, json]",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-p",
|
||||
"--include-path",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Include full paths for each word in output",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-s",
|
||||
"--sort",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Sort output alphabetically. By default the results are sorted by the starting \
|
||||
block position on the board from top-to-bottom, left-to-right as given in the \
|
||||
board file.")
|
||||
parser.add_argument("-d", "--dedup", action="store_true", default=False,
|
||||
board file.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--dedup",
|
||||
action="store_true",
|
||||
default=False,
|
||||
help="Remove duplicates from word-only output. Note that de-duplication does not preserve \
|
||||
the original order of the output, so it is recommended to also use the sort option when \
|
||||
de-duplicating.")
|
||||
de-duplicating.",
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
|
||||
def main():
|
||||
"""Command line tool for sovling Boggle boards"""
|
||||
board = read_boggle_file(args.board)
|
||||
@@ -50,7 +75,9 @@ def main():
|
||||
if args.format:
|
||||
match args.format.lower():
|
||||
case "txt":
|
||||
word_paths = [start_block.word_paths for start_block in boggle_tree.values()]
|
||||
word_paths = [
|
||||
start_block.word_paths for start_block in boggle_tree.values()
|
||||
]
|
||||
data = list(chain(*word_paths))
|
||||
if args.include_path:
|
||||
data = [f"{line[0]} {line[1]}" for line in data]
|
||||
@@ -66,10 +93,10 @@ def main():
|
||||
print(line)
|
||||
|
||||
case "json":
|
||||
data = { str(k):v.word_paths for k,v in boggle_tree.items()}
|
||||
data = {str(k): v.word_paths for k, v in boggle_tree.items()}
|
||||
print(json.dumps(data, indent=2, sort_keys=True))
|
||||
case _:
|
||||
print(f"Invalid format (-f) option provided: \"{args.format}\"")
|
||||
print(f'Invalid format (-f) option provided: "{args.format}"')
|
||||
sys.exit()
|
||||
|
||||
else:
|
||||
@@ -83,18 +110,16 @@ def main():
|
||||
show_header=True,
|
||||
header_style="bold purple",
|
||||
row_styles=["dim", ""],
|
||||
box=box.ROUNDED
|
||||
box=box.ROUNDED,
|
||||
)
|
||||
table.add_column("Word")
|
||||
table.add_column("Path")
|
||||
for word in tree.word_paths:
|
||||
#print(f"{word[0]: <{boggle_board.max_word_len}}: {word[1]}")
|
||||
table.add_row(
|
||||
f"{word[0]}",
|
||||
f"{word[1]}"
|
||||
)
|
||||
# print(f"{word[0]: <{boggle_board.max_word_len}}: {word[1]}")
|
||||
table.add_row(f"{word[0]}", f"{word[1]}")
|
||||
|
||||
console.print(table)
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
+16
-10
@@ -1,21 +1,25 @@
|
||||
'''Module to generate random Boggle boards for testing'''
|
||||
"""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
|
||||
|
||||
|
||||
def read_dice_file(dice_path: Path):
|
||||
'''Return list of die strings from file ignoring all comments (#)'''
|
||||
with open(dice_path, 'r+', encoding="utf-8") as file:
|
||||
return [line.rstrip().split(',') for line in file.readlines() if line[0] != "#"]
|
||||
"""Return list of die strings from file ignoring all comments (#)"""
|
||||
with open(dice_path, "r+", encoding="utf-8") as file:
|
||||
return [line.rstrip().split(",") for line in file.readlines() if line[0] != "#"]
|
||||
|
||||
|
||||
def roll_die(die: str):
|
||||
'''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)])
|
||||
|
||||
|
||||
def roll_dice(dice: list[str]):
|
||||
'''Return a random roll for each die'''
|
||||
return [ roll_die(die) for die in dice ]
|
||||
"""Return a random roll for each die"""
|
||||
return [roll_die(die) for die in dice]
|
||||
|
||||
|
||||
def get_random_board(dice: list[str]):
|
||||
shuffle(dice)
|
||||
@@ -25,17 +29,19 @@ def get_random_board(dice: list[str]):
|
||||
board_size = int(floor(sqrt(len(rolls))))
|
||||
board = []
|
||||
for i in range(0, len(dice), board_size):
|
||||
board.append(rolls[i:i+board_size])
|
||||
board.append(rolls[i : i + board_size])
|
||||
return board
|
||||
|
||||
|
||||
def get_random_board_csv(dice: list[str]):
|
||||
board = get_random_board(dice)
|
||||
board = [",".join(row) for row in board]
|
||||
return board
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(argv) != 2:
|
||||
print('Usage: python3 board_randomizer.py <dice_file>')
|
||||
print("Usage: python3 board_randomizer.py <dice_file>")
|
||||
else:
|
||||
try:
|
||||
dice = read_dice_file(Path(argv[1]))
|
||||
|
||||
+163
-80
@@ -1,4 +1,4 @@
|
||||
'''Boggler Utils'''
|
||||
"""Boggler Utils"""
|
||||
from __future__ import annotations
|
||||
from pathlib import Path
|
||||
from multiprocessing import Pool
|
||||
@@ -6,10 +6,13 @@ 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:
|
||||
"""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)
|
||||
@@ -18,27 +21,27 @@ class BoardCell:
|
||||
|
||||
@property
|
||||
def row(self) -> int:
|
||||
'''Getter for row property'''
|
||||
"""Getter for row property"""
|
||||
return self.__row
|
||||
|
||||
@property
|
||||
def col(self) -> int:
|
||||
'''Getter for col property'''
|
||||
"""Getter for col property"""
|
||||
return self.__col
|
||||
|
||||
@property
|
||||
def pos(self) -> tuple[int, int]:
|
||||
'''Getter for pos property'''
|
||||
"""Getter for pos property"""
|
||||
return self.__pos
|
||||
|
||||
@property
|
||||
def letters(self) -> str:
|
||||
'''Getter for letter property'''
|
||||
"""Getter for letter property"""
|
||||
return self.__letters
|
||||
|
||||
@property
|
||||
def adjacent_cells(self) -> list[BoardCell]:
|
||||
'''Getter for adjacent_cells property'''
|
||||
"""Getter for adjacent_cells property"""
|
||||
return self.__adjacent_cells
|
||||
|
||||
@adjacent_cells.setter
|
||||
@@ -51,8 +54,10 @@ class BoardCell:
|
||||
def __repr__(self):
|
||||
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
|
||||
|
||||
|
||||
class BoggleBoard:
|
||||
'''Boggle board structure'''
|
||||
"""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
|
||||
@@ -75,7 +80,9 @@ class BoggleBoard:
|
||||
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
|
||||
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"
|
||||
@@ -90,53 +97,61 @@ class BoggleBoard:
|
||||
|
||||
@property
|
||||
def height(self) -> int:
|
||||
'''Getter for height property'''
|
||||
"""Getter for height property"""
|
||||
return self.__height
|
||||
|
||||
@property
|
||||
def width(self) -> int:
|
||||
'''Getter for width property'''
|
||||
"""Getter for width property"""
|
||||
return self.__width
|
||||
|
||||
@property
|
||||
def max_word_len(self) -> int:
|
||||
'''Getter for maximum word length property'''
|
||||
"""Getter for maximum word length property"""
|
||||
return self.__max_word_len
|
||||
|
||||
@property
|
||||
def board(self) -> dict[tuple[int, int], BoardCell]:
|
||||
'''Getter for board property'''
|
||||
"""Getter for board property"""
|
||||
return self.__board
|
||||
|
||||
def __get_adjacent_indexes(self, row, col):
|
||||
'''Return adjecency list for board of size `row x col`'''
|
||||
"""Return adjecency list for board of size `row x col`"""
|
||||
indexes = []
|
||||
if row > 0:
|
||||
indexes.append((row-1, col)) # up
|
||||
indexes.append((row - 1, col)) # up
|
||||
if col > 0:
|
||||
indexes.append((row-1, col-1)) # up-left
|
||||
indexes.append((row - 1, col - 1)) # up-left
|
||||
if col < self.width - 1:
|
||||
indexes.append((row-1, col+1)) # up-right
|
||||
indexes.append((row - 1, col + 1)) # up-right
|
||||
if row < self.height - 1:
|
||||
indexes.append((row+1, col)) # down
|
||||
indexes.append((row + 1, col)) # down
|
||||
if col > 0:
|
||||
indexes.append((row+1, col-1)) # down-left
|
||||
indexes.append((row + 1, col - 1)) # down-left
|
||||
if col < self.width - 1:
|
||||
indexes.append((row+1, col+1)) # down-right
|
||||
indexes.append((row + 1, col + 1)) # down-right
|
||||
if col > 0:
|
||||
indexes.append((row, col-1)) # left
|
||||
indexes.append((row, col - 1)) # left
|
||||
if col < self.width - 1:
|
||||
indexes.append((row, col+1)) # right
|
||||
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 the value at the specified row x column"""
|
||||
return self.__board[(row, col)]
|
||||
|
||||
|
||||
class WordNode:
|
||||
'''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, None)) -> None:
|
||||
"""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, None),
|
||||
) -> None:
|
||||
self.__letters = letters
|
||||
self.__is_word = is_word
|
||||
self.__children = children if children is not None else {}
|
||||
@@ -145,12 +160,12 @@ class WordNode:
|
||||
|
||||
@property
|
||||
def letters(self) -> str:
|
||||
'''Getter for letter property'''
|
||||
"""Getter for letter property"""
|
||||
return self.__letters
|
||||
|
||||
@property
|
||||
def is_word(self) -> bool:
|
||||
'''Getter for is_word property'''
|
||||
"""Getter for is_word property"""
|
||||
return self.__is_word
|
||||
|
||||
@is_word.setter
|
||||
@@ -159,26 +174,26 @@ class WordNode:
|
||||
|
||||
@property
|
||||
def children(self) -> dict[str, WordNode]:
|
||||
'''Getter for children property'''
|
||||
"""Getter for children property"""
|
||||
return self.__children
|
||||
|
||||
@property
|
||||
def parent(self) -> WordNode:
|
||||
'''Getter for parent property'''
|
||||
"""Getter for parent property"""
|
||||
return self.__parent
|
||||
|
||||
@property
|
||||
def board_pos(self) -> tuple[int, int]:
|
||||
'''Getter for board_pos property'''
|
||||
"""Getter for board_pos property"""
|
||||
return self.__board_pos
|
||||
|
||||
def add_child_node(self, node):
|
||||
'''Add child node to `children` dictionary, indexed by the nodes' `letter`'''
|
||||
"""Add child node to `children` dictionary, indexed by the nodes' `letter`"""
|
||||
self.children[node.letters] = node
|
||||
|
||||
@property
|
||||
def path(self) -> list[WordNode]:
|
||||
'''Return list of nodes from current node to the root'''
|
||||
"""Return list of nodes from current node to the root"""
|
||||
curr_node = self
|
||||
path = []
|
||||
path.append(curr_node.board_pos)
|
||||
@@ -189,7 +204,7 @@ class WordNode:
|
||||
return path
|
||||
|
||||
def get_word(self, board: BoggleBoard) -> str:
|
||||
'''Return word or word fragment based on the WordNode's path property'''
|
||||
"""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):
|
||||
@@ -198,10 +213,17 @@ class WordNode:
|
||||
def __repr__(self):
|
||||
return f"WordNode: {self.letters}, {self.is_word}, {self.children}"
|
||||
|
||||
|
||||
class WordTree:
|
||||
'''A tree populated by WordNode(s) to complete words from a given root letter and wordlist'''
|
||||
def __init__(self, alphabet: list[str], root: WordNode, words: list[str] = None,
|
||||
max_word_len = 16) -> None:
|
||||
"""A tree populated by WordNode(s) to complete words from a given root letter and wordlist"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
alphabet: list[str],
|
||||
root: WordNode,
|
||||
words: list[str] = None,
|
||||
max_word_len=16,
|
||||
) -> None:
|
||||
self.__alphabet = alphabet
|
||||
self.__wordlist = words
|
||||
self.__root = root
|
||||
@@ -220,32 +242,32 @@ class WordTree:
|
||||
|
||||
@property
|
||||
def alphabet(self) -> list:
|
||||
'''Getter for alphabet property'''
|
||||
"""Getter for alphabet property"""
|
||||
return self.__alphabet
|
||||
|
||||
@property
|
||||
def wordlist(self) -> list[str]:
|
||||
'''Getter for wordlist property'''
|
||||
"""Getter for wordlist property"""
|
||||
return self.__wordlist
|
||||
|
||||
@property
|
||||
def root(self) -> WordNode:
|
||||
'''Getter for root property'''
|
||||
"""Getter for root property"""
|
||||
return self.__root
|
||||
|
||||
@property
|
||||
def max_word_len(self) -> int:
|
||||
'''Getter for max_word_len property'''
|
||||
"""Getter for max_word_len property"""
|
||||
return self.__max_word_len
|
||||
|
||||
@property
|
||||
def tree(self) -> dict[str, WordNode]:
|
||||
'''Getter for tree property'''
|
||||
"""Getter for tree property"""
|
||||
return self.__tree
|
||||
|
||||
@property
|
||||
def word_paths(self) -> dict[(int, int), WordNode]:
|
||||
'''Getter for leaf_nodes property'''
|
||||
"""Getter for leaf_nodes property"""
|
||||
return self.__word_paths
|
||||
|
||||
@word_paths.setter
|
||||
@@ -258,19 +280,25 @@ class WordTree:
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
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`'''
|
||||
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`"""
|
||||
log.debug("inserting node: %s", letters)
|
||||
node = WordNode(letters, is_word, parent, children, board_pos)
|
||||
parent.add_child_node(node)
|
||||
|
||||
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
|
||||
prefix = word[0:len(self.root.letters)] # 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]
|
||||
@@ -280,11 +308,18 @@ class WordTree:
|
||||
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)
|
||||
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)
|
||||
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
|
||||
@@ -300,8 +335,8 @@ class WordTree:
|
||||
curr_node.is_word = len(word) <= self.max_word_len
|
||||
return True
|
||||
|
||||
def search(self, word: str, curr_node = None) -> WordNode:
|
||||
'''Return leaf (WordNode) if a given word is in the tree otherwise return None'''
|
||||
def search(self, word: str, curr_node=None) -> WordNode:
|
||||
"""Return leaf (WordNode) if a given word is in the tree otherwise return None"""
|
||||
if len(word) == 0 or word is None:
|
||||
return curr_node
|
||||
|
||||
@@ -309,22 +344,33 @@ class WordTree:
|
||||
if curr_node is None:
|
||||
curr_node = self.root
|
||||
for letters in curr_node.children:
|
||||
if letters == word[len(curr_node.letters):len(curr_node.letters) + len(letters)]:
|
||||
return self.search(word[len(curr_node.letters):], curr_node.children[letters])
|
||||
if (
|
||||
letters
|
||||
== word[len(curr_node.letters) : len(curr_node.letters) + len(letters)]
|
||||
):
|
||||
return self.search(
|
||||
word[len(curr_node.letters) :], curr_node.children[letters]
|
||||
)
|
||||
|
||||
# Currently only returns single path for word
|
||||
# TODO: return all possible paths, maybe create minature WordTree? Or just of list of paths.
|
||||
return curr_node
|
||||
|
||||
def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, word_len: int = 0) -> WordTree:
|
||||
'''Return subtree of board given a particular root (first letter).
|
||||
def build_boggle_tree(
|
||||
self,
|
||||
board: BoggleBoard,
|
||||
board_cell: BoardCell,
|
||||
subtree: WordTree,
|
||||
word_len: int = 0,
|
||||
) -> WordTree:
|
||||
"""Return subtree of board given a particular root (first letter).
|
||||
|
||||
Keyword arguments:
|
||||
board -- the board the new tree is based on
|
||||
board_node -- a pointer on the board where new branch nodes can be inserted into the tree
|
||||
dict_node -- a pointer on the dictionary where nodes are read from for validation
|
||||
subtree -- the partial tree passed to the next recursive step for generating branches
|
||||
'''
|
||||
"""
|
||||
|
||||
if word_len > self.max_word_len or word_len >= board.max_word_len:
|
||||
log.debug(f"MAX WORD LENGTH REACHED! len = {word_len}")
|
||||
@@ -336,39 +382,68 @@ 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))
|
||||
log.debug("1: WORD FOUND: %s %s", "".join([board.board[x].letters for x in word_path]), word_path)
|
||||
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_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)
|
||||
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:
|
||||
# Check dictionary and exclude nodes already in path
|
||||
if cell.letters in self.active_node.children and cell.pos not in subtree.active_node.path:
|
||||
new_node = WordNode(cell.letters, self.active_node.is_word, subtree.active_node, board_pos = cell.pos)
|
||||
if (
|
||||
cell.letters in self.active_node.children
|
||||
and cell.pos not in subtree.active_node.path
|
||||
):
|
||||
new_node = WordNode(
|
||||
cell.letters,
|
||||
self.active_node.is_word,
|
||||
subtree.active_node,
|
||||
board_pos=cell.pos,
|
||||
)
|
||||
subtree.active_node.add_child_node(new_node)
|
||||
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.build_boggle_tree(board, board.board[cell.pos], subtree, word_len=word_len+len(board_cell.letters))
|
||||
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.build_boggle_tree(
|
||||
board,
|
||||
board.board[cell.pos],
|
||||
subtree,
|
||||
word_len=word_len + len(board_cell.letters),
|
||||
)
|
||||
|
||||
self.active_node = self.active_node.parent
|
||||
subtree.active_node = subtree.active_node.parent
|
||||
return subtree
|
||||
|
||||
|
||||
def build_boggle_tree(args):
|
||||
'''Build boggle tree from arguments for process Pool'''
|
||||
"""Build boggle tree from arguments for process Pool"""
|
||||
(alphabet, board, cell, wordlist) = args
|
||||
root_node = WordNode(cell.letters)
|
||||
dict_tree = WordTree(alphabet, root_node, wordlist)
|
||||
sub_tree = WordTree(alphabet, WordNode(cell.letters, False, board_pos=cell.pos))
|
||||
return dict_tree.build_boggle_tree(board, cell, sub_tree)
|
||||
|
||||
def build_full_boggle_tree(board: BoggleBoard, wordlist_path: Path) -> dict[str, WordTree]:
|
||||
'''Return dictionary of WordTree(s) for every letter on a BoggleBoard'''
|
||||
|
||||
def build_full_boggle_tree(
|
||||
board: BoggleBoard, wordlist_path: Path
|
||||
) -> dict[str, WordTree]:
|
||||
"""Return dictionary of WordTree(s) for every letter on a BoggleBoard"""
|
||||
alphabet = sorted(set([cell.letters for cell in board.board.values()]))
|
||||
board_tree = {}
|
||||
index: dict[str] = {}
|
||||
@@ -393,7 +468,9 @@ def build_full_boggle_tree(board: BoggleBoard, wordlist_path: Path) -> dict[str,
|
||||
index[letters] = {}
|
||||
|
||||
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:
|
||||
for i, res in enumerate(pool.map(build_boggle_tree, params)):
|
||||
log.info(">> %s", params[i][2])
|
||||
@@ -401,22 +478,25 @@ def build_full_boggle_tree(board: BoggleBoard, wordlist_path: Path) -> dict[str,
|
||||
|
||||
return board_tree
|
||||
|
||||
|
||||
def read_wordlist(file):
|
||||
'''Return dictionary of words with associated word count (1 by default)'''
|
||||
with open(file, 'r', encoding='utf-8') as file:
|
||||
"""Return dictionary of words with associated word count (1 by default)"""
|
||||
with open(file, "r", encoding="utf-8") as file:
|
||||
return file.read().split()
|
||||
|
||||
|
||||
class BadBoardFormat(Exception):
|
||||
pass
|
||||
|
||||
|
||||
def read_boggle_file(file):
|
||||
'''Return list of rows from Boggle board csv file
|
||||
"""Return list of rows from Boggle board csv file
|
||||
|
||||
The size of the board is determined by the width (number of comma-separated values)
|
||||
of the first (non-empty) line in the file.
|
||||
|
||||
'''
|
||||
with open(file, 'r', encoding='utf-8') as file:
|
||||
"""
|
||||
with open(file, "r", encoding="utf-8") as file:
|
||||
board = []
|
||||
|
||||
board.append([x.strip() for x in file.readline().split(",")])
|
||||
@@ -425,7 +505,7 @@ def read_boggle_file(file):
|
||||
if line.strip() == "":
|
||||
raise BadBoardFormat("board files must contain no blank lines")
|
||||
|
||||
row = [x.strip() for x in line.strip().split(',')]
|
||||
row = [x.strip() for x in line.strip().split(",")]
|
||||
if len(row) != board_size:
|
||||
raise BadBoardFormat("the length of each row must be the same")
|
||||
|
||||
@@ -433,9 +513,12 @@ def read_boggle_file(file):
|
||||
|
||||
return board
|
||||
|
||||
|
||||
def find_paths_by_word(board_letters, dictionary_path, max_len):
|
||||
'''Return list of paths by word'''
|
||||
"""Return list of paths by word"""
|
||||
boggle_board = BoggleBoard(board_letters, max_len)
|
||||
boggle_tree = build_full_boggle_tree(boggle_board, dictionary_path)
|
||||
paths_by_word = functools.reduce(operator.iconcat, [x.word_paths for x in boggle_tree.values()], [])
|
||||
paths_by_word = functools.reduce(
|
||||
operator.iconcat, [x.word_paths for x in boggle_tree.values()], []
|
||||
)
|
||||
return paths_by_word
|
||||
|
||||
@@ -4,19 +4,22 @@ from boggler.boggler_utils import read_boggle_file, BadBoardFormat
|
||||
|
||||
boards_path = Path(Path(__file__).parent.resolve())
|
||||
|
||||
|
||||
@pytest.fixture(name="expected_board_4x4")
|
||||
def fixture_expected_board_4x4():
|
||||
return [
|
||||
['u', 'n', 'r', 'e'],
|
||||
['qu', 'n', 'i', 'l'],
|
||||
['i', 's', 'h', 'a'],
|
||||
['s', 'e', 'l', 'b']
|
||||
["u", "n", "r", "e"],
|
||||
["qu", "n", "i", "l"],
|
||||
["i", "s", "h", "a"],
|
||||
["s", "e", "l", "b"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture(name="clean_4x4_path")
|
||||
def fixture_clean_4x4_path():
|
||||
return Path(boards_path, "./boards/4x4_clean.board")
|
||||
|
||||
|
||||
def test_clean_4x4_board(expected_board_4x4, clean_4x4_path):
|
||||
"""Test 4x4 board with clean input"""
|
||||
assert expected_board_4x4 == read_boggle_file(clean_4x4_path)
|
||||
@@ -27,6 +30,7 @@ def test_clean_4x4_board(expected_board_4x4, clean_4x4_path):
|
||||
def fixture_leading_whitespace_4x4_path():
|
||||
return Path(boards_path, "./boards/4x4_leading_whitespace.board")
|
||||
|
||||
|
||||
def test_leading_whitespace_4x4_board(expected_board_4x4, leading_whitespace_4x4_path):
|
||||
"""Test 4x4 board with leading whitepace in rows"""
|
||||
assert expected_board_4x4 == read_boggle_file(leading_whitespace_4x4_path)
|
||||
@@ -37,6 +41,7 @@ def test_leading_whitespace_4x4_board(expected_board_4x4, leading_whitespace_4x4
|
||||
def fixture_between_whitespace_4x4_path():
|
||||
return Path(boards_path, "./boards/4x4_between_whitespace.board")
|
||||
|
||||
|
||||
def test_between_whitespace_4x4_board(expected_board_4x4, between_whitespace_4x4_path):
|
||||
"""Test 4x4 board with whitespace surrounding letters in block"""
|
||||
assert expected_board_4x4 == read_boggle_file(between_whitespace_4x4_path)
|
||||
@@ -47,7 +52,10 @@ def test_between_whitespace_4x4_board(expected_board_4x4, between_whitespace_4x4
|
||||
def fixture_trailing_whitespace_4x4_path():
|
||||
return Path(boards_path, "./boards/4x4_trailing_whitespace.board")
|
||||
|
||||
def test_trailing_whitespace_4x4_board(expected_board_4x4, trailing_whitespace_4x4_path):
|
||||
|
||||
def test_trailing_whitespace_4x4_board(
|
||||
expected_board_4x4, trailing_whitespace_4x4_path
|
||||
):
|
||||
"""Test 4x4 board with trailing whitespace in rows"""
|
||||
assert expected_board_4x4 == read_boggle_file(trailing_whitespace_4x4_path)
|
||||
|
||||
@@ -57,6 +65,7 @@ def test_trailing_whitespace_4x4_board(expected_board_4x4, trailing_whitespace_4
|
||||
def fixture_blank_lines_4x4_path():
|
||||
return Path(boards_path, "./boards/4x4_blank_lines.board")
|
||||
|
||||
|
||||
def test_blank_lines_4x4_board(blank_lines_4x4_path):
|
||||
"""Test 4x4 board with blank lines in board file"""
|
||||
with pytest.raises(BadBoardFormat, match="must contain no blank lines"):
|
||||
@@ -68,6 +77,7 @@ def test_blank_lines_4x4_board(blank_lines_4x4_path):
|
||||
def fixture_inconsistent_width_4x4_path():
|
||||
return Path(boards_path, "./boards/4x4_inconsistent_width.board")
|
||||
|
||||
|
||||
def test_inconsistent_width_4x4_board(inconsistent_width_4x4_path):
|
||||
"""Test board with irregular number of blocks per row"""
|
||||
with pytest.raises(BadBoardFormat, match="length of each row must be the same"):
|
||||
|
||||
+185
-17
@@ -5,15 +5,17 @@ from boggler.boggler_utils import BoggleBoard, build_full_boggle_tree
|
||||
|
||||
WORDLISTS_DIR = Path("./boggler/wordlists/dwyl/").absolute()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def board_4x4():
|
||||
return [
|
||||
['u','n','r','e'],
|
||||
['qu','n','i','l'],
|
||||
['i','s','h','a'],
|
||||
['s','e','l','b']
|
||||
["u", "n", "r", "e"],
|
||||
["qu", "n", "i", "l"],
|
||||
["i", "s", "h", "a"],
|
||||
["s", "e", "l", "b"],
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def expected_words_max_4():
|
||||
return [
|
||||
@@ -345,15 +347,65 @@ def expected_words_max_4():
|
||||
("blah", [(3, 3), (3, 2), (2, 3), (2, 2)]),
|
||||
]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def expected_words_max_20():
|
||||
return [
|
||||
("u", [(0, 0)]),
|
||||
("un", [(0, 0), (1, 1)]),
|
||||
("unn", [(0, 0), (1, 1), (0, 1)]),
|
||||
("unrelinquishable", [(0, 0), (1, 1), (0, 2), (0, 3), (1, 3), (1, 2), (0, 1), (1, 0), (2, 0), (2, 1), (2, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
("unrelishable", [(0, 0), (1, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 1), (2, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
("unreliable", [(0, 0), (1, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
(
|
||||
"unrelinquishable",
|
||||
[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(0, 1),
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
(
|
||||
"unrelishable",
|
||||
[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
(
|
||||
"unreliable",
|
||||
[
|
||||
(0, 0),
|
||||
(1, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
("unrein", [(0, 0), (1, 1), (0, 2), (0, 3), (1, 2), (0, 1)]),
|
||||
("uns", [(0, 0), (1, 1), (2, 1)]),
|
||||
("unsin", [(0, 0), (1, 1), (2, 1), (1, 2), (0, 1)]),
|
||||
@@ -371,9 +423,58 @@ def expected_words_max_20():
|
||||
("unn", [(0, 0), (0, 1), (1, 1)]),
|
||||
("uni", [(0, 0), (0, 1), (1, 2)]),
|
||||
("unie", [(0, 0), (0, 1), (1, 2), (0, 3)]),
|
||||
("unrelishable", [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 1), (2, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
("unreliable", [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
("unrelinquishable", [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
(
|
||||
"unrelishable",
|
||||
[
|
||||
(0, 0),
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
(
|
||||
"unreliable",
|
||||
[
|
||||
(0, 0),
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
(
|
||||
"unrelinquishable",
|
||||
[
|
||||
(0, 0),
|
||||
(0, 1),
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(1, 1),
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
("unrein", [(0, 0), (0, 1), (0, 2), (0, 3), (1, 2), (1, 1)]),
|
||||
("n", [(0, 1)]),
|
||||
("ni", [(0, 1), (1, 2)]),
|
||||
@@ -414,14 +515,64 @@ def expected_words_max_20():
|
||||
("re", [(0, 2), (0, 3)]),
|
||||
("rel", [(0, 2), (0, 3), (1, 3)]),
|
||||
("relais", [(0, 2), (0, 3), (1, 3), (2, 3), (1, 2), (2, 1)]),
|
||||
("relinquish", [(0, 2), (0, 3), (1, 3), (1, 2), (0, 1), (1, 0), (2, 0), (2, 1), (2, 2)]),
|
||||
("relinquishes", [(0, 2), (0, 3), (1, 3), (1, 2), (0, 1), (1, 0), (2, 0), (2, 1), (2, 2), (3, 1), (3, 0)]),
|
||||
(
|
||||
"relinquish",
|
||||
[(0, 2), (0, 3), (1, 3), (1, 2), (0, 1), (1, 0), (2, 0), (2, 1), (2, 2)],
|
||||
),
|
||||
(
|
||||
"relinquishes",
|
||||
[
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(0, 1),
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(3, 1),
|
||||
(3, 0),
|
||||
],
|
||||
),
|
||||
("relish", [(0, 2), (0, 3), (1, 3), (1, 2), (2, 1), (2, 2)]),
|
||||
("relishes", [(0, 2), (0, 3), (1, 3), (1, 2), (2, 1), (2, 2), (3, 1), (3, 0)]),
|
||||
("relishable", [(0, 2), (0, 3), (1, 3), (1, 2), (2, 1), (2, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
(
|
||||
"relishable",
|
||||
[
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(2, 3),
|
||||
(3, 3),
|
||||
(3, 2),
|
||||
(3, 1),
|
||||
],
|
||||
),
|
||||
("reliable", [(0, 2), (0, 3), (1, 3), (1, 2), (2, 3), (3, 3), (3, 2), (3, 1)]),
|
||||
("relinquish", [(0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2)]),
|
||||
("relinquishes", [(0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2), (3, 1), (3, 0)]),
|
||||
(
|
||||
"relinquish",
|
||||
[(0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (2, 0), (2, 1), (2, 2)],
|
||||
),
|
||||
(
|
||||
"relinquishes",
|
||||
[
|
||||
(0, 2),
|
||||
(0, 3),
|
||||
(1, 3),
|
||||
(1, 2),
|
||||
(1, 1),
|
||||
(1, 0),
|
||||
(2, 0),
|
||||
(2, 1),
|
||||
(2, 2),
|
||||
(3, 1),
|
||||
(3, 0),
|
||||
],
|
||||
),
|
||||
("rei", [(0, 2), (0, 3), (1, 2)]),
|
||||
("rein", [(0, 2), (0, 3), (1, 2), (0, 1)]),
|
||||
("reis", [(0, 2), (0, 3), (1, 2), (2, 1)]),
|
||||
@@ -839,6 +990,7 @@ def expected_words_max_20():
|
||||
("bless", [(3, 3), (3, 2), (3, 1), (3, 0), (2, 1)]),
|
||||
]
|
||||
|
||||
|
||||
# -----------------------------------------------------
|
||||
# Solved board max 4-letter words
|
||||
# -----------------------------------------------------
|
||||
@@ -848,25 +1000,33 @@ def expected_words_max_20():
|
||||
def board_4(board_4x4):
|
||||
return BoggleBoard(board_4x4, 4)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tree_4(board_4):
|
||||
return build_full_boggle_tree(board_4, Path(WORDLISTS_DIR))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def found_words_4(tree_4):
|
||||
return list(chain.from_iterable([x.word_paths for x in tree_4.values()]))
|
||||
|
||||
|
||||
# Tests for solved board with max of 4-letter words
|
||||
def test_word_count_4_board_4x4_max(expected_words_max_4, tree_4):
|
||||
assert len(expected_words_max_4) == sum(map(lambda x: len(x.word_paths), tree_4.values()))
|
||||
assert len(expected_words_max_4) == sum(
|
||||
map(lambda x: len(x.word_paths), tree_4.values())
|
||||
)
|
||||
|
||||
|
||||
def test_words_full_4_board_4x4_max(expected_words_max_4, found_words_4):
|
||||
"""Verify words and paths match in exact order"""
|
||||
assert found_words_4 == expected_words_max_4
|
||||
|
||||
|
||||
def test_format_words_4_to_csv():
|
||||
pass
|
||||
|
||||
|
||||
def test_format_words_4_to_json():
|
||||
pass
|
||||
|
||||
@@ -880,23 +1040,31 @@ def test_format_words_4_to_json():
|
||||
def board_20(board_4x4):
|
||||
return BoggleBoard(board_4x4, 20)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def tree_20(board_20):
|
||||
return build_full_boggle_tree(board_20, WORDLISTS_DIR)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def found_words_20(tree_20):
|
||||
return list(chain.from_iterable([x.word_paths for x in tree_20.values()]))
|
||||
|
||||
|
||||
# Tests for solved board with max of 20-letter words
|
||||
def test_word_count_20_board_4x4_max(expected_words_max_20, tree_20):
|
||||
assert len(expected_words_max_20) == sum(map(lambda x: len(x.word_paths), tree_20.values()))
|
||||
assert len(expected_words_max_20) == sum(
|
||||
map(lambda x: len(x.word_paths), tree_20.values())
|
||||
)
|
||||
|
||||
|
||||
def test_words_full_20_board_4x4_max(expected_words_max_20, found_words_20):
|
||||
assert found_words_20 == expected_words_max_20
|
||||
|
||||
|
||||
def test_format_words_20_to_csv():
|
||||
pass
|
||||
|
||||
|
||||
def test_format_words_20_to_json():
|
||||
pass
|
||||
|
||||
Reference in New Issue
Block a user