Initial blacken reformat

This commit is contained in:
2023-08-22 16:56:59 -04:00
parent ab2869b858
commit a2630ed82e
9 changed files with 438 additions and 146 deletions
+2 -2
View File
@@ -8,7 +8,7 @@ pip install boggler
To use the script to solve a particular Boggle board configuration, you'll need to do a few things To use the script to solve a particular Boggle board configuration, you'll need to do a few things
1. Create `.csv` of the board state like so: 1. Create `.csv` of the board state like so:
Here is an example `board.csv`. Note the orientation of the board does not matter. Here is an example `board.csv`. Note the orientation of the board does not matter.
```console ```console
$ cat board.csv $ cat board.csv
@@ -45,7 +45,7 @@ To use the script to solve a particular Boggle board configuration, you'll need
To split an English wordlist the `split_wordlist_alpha.sh` script can be used like so: To split an English wordlist the `split_wordlist_alpha.sh` script can be used like so:
```console ```console
$ split_wordlist_alpha.sh my_wordlist.txt . $ split_wordlist_alpha.sh my_wordlist.txt .
``` ```
This will generate a series of sub-dictionaries for each letter in the alphabet in the current directory. This will generate a series of sub-dictionaries for each letter in the alphabet in the current directory.
```console ```console
+53 -28
View File
@@ -9,33 +9,58 @@ from rich.table import Table
from rich import box from rich import box
from .boggler_utils import BoggleBoard, build_full_boggle_tree, read_boggle_file from .boggler_utils import BoggleBoard, build_full_boggle_tree, read_boggle_file
parser = argparse.ArgumentParser( parser = argparse.ArgumentParser(prog="boggler", description="Boggle board game solver")
prog="boggler",
description="Boggle board game solver"
)
parser.add_argument("board", type=Path, help="Path to board CSV file") parser.add_argument("board", type=Path, help="Path to board CSV file")
parser.add_argument("wordlists", type=Path, parser.add_argument(
help="Path to directory of wordlist files. The directory must contain \ "wordlists",
text files of the form words_X.txt where \"X\" is a character of \ type=Path,
the alphabet") help='Path to directory of wordlist files. The directory must contain \
parser.add_argument("max_word_length", nargs="?", type=int, default=16, text files of the form words_X.txt where "X" is a character of \
help="Maximum length of words searched for on provided board") the alphabet',
parser.add_argument("-f", "--format", type=str, )
help="Specify alternative output format including [txt, json]") parser.add_argument(
parser.add_argument("-p", "--include-path", action="store_true", default=False, "max_word_length",
help="Include full paths for each word in output") nargs="?",
parser.add_argument("-s", "--sort", action="store_true", default=False, 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 \ 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 \ block position on the board from top-to-bottom, left-to-right as given in the \
board file.") board file.",
parser.add_argument("-d", "--dedup", action="store_true", default=False, )
parser.add_argument(
"-d",
"--dedup",
action="store_true",
default=False,
help="Remove duplicates from word-only output. Note that de-duplication does not preserve \ 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 \ 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() args = parser.parse_args()
def main(): def main():
"""Command line tool for sovling Boggle boards""" """Command line tool for sovling Boggle boards"""
board = read_boggle_file(args.board) board = read_boggle_file(args.board)
@@ -50,7 +75,9 @@ def main():
if args.format: if args.format:
match args.format.lower(): match args.format.lower():
case "txt": 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)) data = list(chain(*word_paths))
if args.include_path: if args.include_path:
data = [f"{line[0]} {line[1]}" for line in data] data = [f"{line[0]} {line[1]}" for line in data]
@@ -66,10 +93,10 @@ def main():
print(line) print(line)
case "json": 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)) print(json.dumps(data, indent=2, sort_keys=True))
case _: case _:
print(f"Invalid format (-f) option provided: \"{args.format}\"") print(f'Invalid format (-f) option provided: "{args.format}"')
sys.exit() sys.exit()
else: else:
@@ -83,18 +110,16 @@ def main():
show_header=True, show_header=True,
header_style="bold purple", header_style="bold purple",
row_styles=["dim", ""], row_styles=["dim", ""],
box=box.ROUNDED box=box.ROUNDED,
) )
table.add_column("Word") table.add_column("Word")
table.add_column("Path") table.add_column("Path")
for word in tree.word_paths: for word in tree.word_paths:
#print(f"{word[0]: <{boggle_board.max_word_len}}: {word[1]}") # print(f"{word[0]: <{boggle_board.max_word_len}}: {word[1]}")
table.add_row( table.add_row(f"{word[0]}", f"{word[1]}")
f"{word[0]}",
f"{word[1]}"
)
console.print(table) console.print(table)
if __name__ == '__main__':
if __name__ == "__main__":
main() main()
+16 -10
View File
@@ -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 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
def read_dice_file(dice_path: Path): def read_dice_file(dice_path: Path):
'''Return list of die strings from file ignoring all comments (#)''' """Return list of die strings from file ignoring all comments (#)"""
with open(dice_path, 'r+', encoding="utf-8") as file: with open(dice_path, "r+", encoding="utf-8") as file:
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: 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)]) return str(die[randint(0, len(die) - 1)])
def roll_dice(dice: list[str]): def roll_dice(dice: list[str]):
'''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: list[str]):
shuffle(dice) shuffle(dice)
@@ -25,17 +29,19 @@ def get_random_board(dice: list[str]):
board_size = int(floor(sqrt(len(rolls)))) board_size = int(floor(sqrt(len(rolls))))
board = [] board = []
for i in range(0, len(dice), board_size): 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 return board
def get_random_board_csv(dice: list[str]): def get_random_board_csv(dice: list[str]):
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
if __name__ == '__main__':
if __name__ == "__main__":
if len(argv) != 2: if len(argv) != 2:
print('Usage: python3 board_randomizer.py <dice_file>') print("Usage: python3 board_randomizer.py <dice_file>")
else: else:
try: try:
dice = read_dice_file(Path(argv[1])) dice = read_dice_file(Path(argv[1]))
+164 -81
View File
@@ -1,4 +1,4 @@
'''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
@@ -6,10 +6,13 @@ import logging as log
import functools import functools
import operator import operator
class BoardCell: class BoardCell:
'''Boggle Board cell''' """Boggle Board cell"""
def __init__(self, row: int, col: int, letters: str,
adjacent_cells: list[BoardCell] = None) -> None: def __init__(
self, row: int, col: int, letters: str, adjacent_cells: list[BoardCell] = None
) -> None:
self.__row: int = row self.__row: int = row
self.__col: int = col self.__col: int = col
self.__pos: tuple[int, int] = (self.__row, self.__col) self.__pos: tuple[int, int] = (self.__row, self.__col)
@@ -18,27 +21,27 @@ class BoardCell:
@property @property
def row(self) -> int: def row(self) -> int:
'''Getter for row property''' """Getter for row property"""
return self.__row return self.__row
@property @property
def col(self) -> int: def col(self) -> int:
'''Getter for col property''' """Getter for col property"""
return self.__col return self.__col
@property @property
def pos(self) -> tuple[int, int]: def pos(self) -> tuple[int, int]:
'''Getter for pos property''' """Getter for pos property"""
return self.__pos return self.__pos
@property @property
def letters(self) -> str: def letters(self) -> str:
'''Getter for letter property''' """Getter for letter property"""
return self.__letters return self.__letters
@property @property
def adjacent_cells(self) -> list[BoardCell]: def adjacent_cells(self) -> list[BoardCell]:
'''Getter for adjacent_cells property''' """Getter for adjacent_cells property"""
return self.__adjacent_cells return self.__adjacent_cells
@adjacent_cells.setter @adjacent_cells.setter
@@ -51,8 +54,10 @@ class BoardCell:
def __repr__(self): def __repr__(self):
return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}" return f"(BoardCell({self.__row}, {self.__col}): {self.__letters}"
class BoggleBoard: class BoggleBoard:
'''Boggle board structure''' """Boggle board structure"""
def __init__(self, board: list[list[str]], max_word_len: int = 14) -> None: def __init__(self, board: list[list[str]], max_word_len: int = 14) -> None:
self.__height: int = len(board) self.__height: int = len(board)
self.__width: int = len(board[0]) if self.__height > 0 else 0 self.__width: int = len(board[0]) if self.__height > 0 else 0
@@ -75,7 +80,9 @@ class BoggleBoard:
def __str__(self): def __str__(self):
flattened_board_list = [y for x in self.__board_list for y in x] 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 = 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 header_len = self.__width * (max_len + 1) - 1
head = "-" * header_len head = "-" * header_len
header = f"+{head}+\n" header = f"+{head}+\n"
@@ -90,53 +97,61 @@ class BoggleBoard:
@property @property
def height(self) -> int: def height(self) -> int:
'''Getter for height property''' """Getter for height property"""
return self.__height return self.__height
@property @property
def width(self) -> int: def width(self) -> int:
'''Getter for width property''' """Getter for width property"""
return self.__width return self.__width
@property @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 @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
def __get_adjacent_indexes(self, row, col): 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 = [] indexes = []
if row > 0: if row > 0:
indexes.append((row-1, col)) # up indexes.append((row - 1, col)) # up
if col > 0: if col > 0:
indexes.append((row-1, col-1)) # up-left indexes.append((row - 1, col - 1)) # up-left
if col < self.width - 1: 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: if row < self.height - 1:
indexes.append((row+1, col)) # down indexes.append((row + 1, col)) # down
if col > 0: if col > 0:
indexes.append((row+1, col-1)) # down-left indexes.append((row + 1, col - 1)) # down-left
if col < self.width - 1: if col < self.width - 1:
indexes.append((row+1, col+1)) # down-right indexes.append((row + 1, col + 1)) # down-right
if col > 0: if col > 0:
indexes.append((row, col-1)) # left indexes.append((row, col - 1)) # left
if col < self.width - 1: if col < self.width - 1:
indexes.append((row, col+1)) # right indexes.append((row, col + 1)) # right
return indexes return indexes
def get_cell(self, row: int, col: int) -> BoardCell: 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)] return self.__board[(row, col)]
class WordNode: class WordNode:
'''A node describing a single letter in a WordTree.''' """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: 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.__letters = letters
self.__is_word = is_word self.__is_word = is_word
self.__children = children if children is not None else {} self.__children = children if children is not None else {}
@@ -145,12 +160,12 @@ class WordNode:
@property @property
def letters(self) -> str: def letters(self) -> str:
'''Getter for letter property''' """Getter for letter property"""
return self.__letters return self.__letters
@property @property
def is_word(self) -> bool: def is_word(self) -> bool:
'''Getter for is_word property''' """Getter for is_word property"""
return self.__is_word return self.__is_word
@is_word.setter @is_word.setter
@@ -159,26 +174,26 @@ class WordNode:
@property @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
@property @property
def parent(self) -> WordNode: def parent(self) -> WordNode:
'''Getter for parent property''' """Getter for parent property"""
return self.__parent return self.__parent
@property @property
def board_pos(self) -> tuple[int, int]: def board_pos(self) -> tuple[int, int]:
'''Getter for board_pos property''' """Getter for board_pos property"""
return self.__board_pos return self.__board_pos
def add_child_node(self, node): 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 self.children[node.letters] = node
@property @property
def path(self) -> list[WordNode]: 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 curr_node = self
path = [] path = []
path.append(curr_node.board_pos) path.append(curr_node.board_pos)
@@ -189,7 +204,7 @@ class WordNode:
return path return path
def get_word(self, board: BoggleBoard) -> str: 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]]) return "".join([board.board[x].letters for x in self.path[::-1]])
def __str__(self): def __str__(self):
@@ -198,10 +213,17 @@ class WordNode:
def __repr__(self): def __repr__(self):
return f"WordNode: {self.letters}, {self.is_word}, {self.children}" return f"WordNode: {self.letters}, {self.is_word}, {self.children}"
class WordTree: class WordTree:
'''A tree populated by WordNode(s) to complete words from a given root letter and wordlist''' """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: def __init__(
self,
alphabet: list[str],
root: WordNode,
words: list[str] = None,
max_word_len=16,
) -> None:
self.__alphabet = alphabet self.__alphabet = alphabet
self.__wordlist = words self.__wordlist = words
self.__root = root self.__root = root
@@ -220,32 +242,32 @@ class WordTree:
@property @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]:
'''Getter for wordlist property''' """Getter for wordlist property"""
return self.__wordlist return self.__wordlist
@property @property
def root(self) -> WordNode: def root(self) -> WordNode:
'''Getter for root property''' """Getter for root property"""
return self.__root return self.__root
@property @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 @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
@property @property
def word_paths(self) -> dict[(int, int), WordNode]: def word_paths(self) -> dict[(int, int), WordNode]:
'''Getter for leaf_nodes property''' """Getter for leaf_nodes property"""
return self.__word_paths return self.__word_paths
@word_paths.setter @word_paths.setter
@@ -258,19 +280,25 @@ class WordTree:
def __repr__(self): def __repr__(self):
return self.__str__() return self.__str__()
def insert_node(self, letters: str, parent: WordNode, is_word: bool = False, def insert_node(
children: dict[str, WordNode] = None, board_pos = None): self,
'''Create WordNode for `letters` and into WordTree under `parent`''' 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) 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)
def __insert_word(self, word: str) -> bool: def __insert_word(self, word: str) -> bool:
'''Returns True if the word could be inserted into the tree with the given alphabet, """Returns True if the word could be inserted into the tree with the given alphabet,
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 : len(self.root.letters)] # 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]
@@ -280,11 +308,18 @@ class WordTree:
i_max = min(self.max_word_len, len(word)) i_max = min(self.max_word_len, len(word))
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) 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) 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
@@ -300,8 +335,8 @@ class WordTree:
curr_node.is_word = len(word) <= self.max_word_len curr_node.is_word = len(word) <= 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:
'''Return leaf (WordNode) if a given word is in the tree otherwise return None''' """Return leaf (WordNode) if a given word is in the tree otherwise return None"""
if len(word) == 0 or word is None: if len(word) == 0 or word is None:
return curr_node return curr_node
@@ -309,22 +344,33 @@ class WordTree:
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:
if letters == word[len(curr_node.letters):len(curr_node.letters) + len(letters)]: if (
return self.search(word[len(curr_node.letters):], curr_node.children[letters]) 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 # Currently only returns single path for word
# TODO: return all possible paths, maybe create minature WordTree? Or just of list of paths. # TODO: return all possible paths, maybe create minature WordTree? Or just of list of paths.
return curr_node return curr_node
def build_boggle_tree(self, board: BoggleBoard, board_cell: BoardCell, subtree: WordTree, word_len: int = 0) -> WordTree: def build_boggle_tree(
'''Return subtree of board given a particular root (first letter). 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: Keyword arguments:
board -- the board the new tree is based on 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 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 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 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: if word_len > self.max_word_len or word_len >= board.max_word_len:
log.debug(f"MAX WORD LENGTH REACHED! len = {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: 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", "".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 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_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) 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:
# Check dictionary and exclude nodes already in path # 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: if (
new_node = WordNode(cell.letters, self.active_node.is_word, subtree.active_node, board_pos = cell.pos) 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.add_child_node(new_node)
subtree.active_node = subtree.active_node.children[cell.letters] # update subtree pointer subtree.active_node = subtree.active_node.children[
self.active_node = self.active_node.children[cell.letters] # update dictionary tree pointer cell.letters
self.build_boggle_tree(board, board.board[cell.pos], subtree, word_len=word_len+len(board_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 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
def build_boggle_tree(args): 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 (alphabet, board, cell, wordlist) = args
root_node = WordNode(cell.letters) root_node = WordNode(cell.letters)
dict_tree = WordTree(alphabet, root_node, wordlist) dict_tree = WordTree(alphabet, root_node, wordlist)
sub_tree = WordTree(alphabet, WordNode(cell.letters, False, board_pos=cell.pos)) sub_tree = WordTree(alphabet, WordNode(cell.letters, False, board_pos=cell.pos))
return dict_tree.build_boggle_tree(board, cell, sub_tree) 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()])) alphabet = sorted(set([cell.letters for cell in board.board.values()]))
board_tree = {} board_tree = {}
index: dict[str] = {} index: dict[str] = {}
@@ -393,7 +468,9 @@ def build_full_boggle_tree(board: BoggleBoard, wordlist_path: Path) -> dict[str,
index[letters] = {} index[letters] = {}
log.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)):
log.info(">> %s", params[i][2]) 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 return board_tree
def read_wordlist(file): def read_wordlist(file):
'''Return dictionary of words with associated word count (1 by default)''' """Return dictionary of words with associated word count (1 by default)"""
with open(file, 'r', encoding='utf-8') as file: with open(file, "r", encoding="utf-8") as file:
return file.read().split() return file.read().split()
class BadBoardFormat(Exception): class BadBoardFormat(Exception):
pass pass
def read_boggle_file(file): 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) The size of the board is determined by the width (number of comma-separated values)
of the first (non-empty) line in the file. 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 = []
board.append([x.strip() for x in file.readline().split(",")]) board.append([x.strip() for x in file.readline().split(",")])
@@ -425,7 +505,7 @@ def read_boggle_file(file):
if line.strip() == "": if line.strip() == "":
raise BadBoardFormat("board files must contain no blank lines") 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: if len(row) != board_size:
raise BadBoardFormat("the length of each row must be the same") raise BadBoardFormat("the length of each row must be the same")
@@ -433,9 +513,12 @@ def read_boggle_file(file):
return board return board
def find_paths_by_word(board_letters, dictionary_path, max_len): 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_board = BoggleBoard(board_letters, max_len)
boggle_tree = build_full_boggle_tree(boggle_board, dictionary_path) 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 return paths_by_word
+1 -1
View File
@@ -108,7 +108,7 @@ void free_board(Board *b) {
} }
} }
//Vec2[] get_adjacent_indexes(uint8_t board_size, Vec2 pos) { //Vec2[] get_adjacent_indexes(uint8_t board_size, Vec2 pos) {
// // Calculate total possible number of adjacent cells // // Calculate total possible number of adjacent cells
// uint16_t = total_adj_cell_cnt = ((board-size-2) * (board-size-2) * 8) + ((board-size-2) * 4 * 5) + (4 * 3); // uint16_t = total_adj_cell_cnt = ((board-size-2) * (board-size-2) * 8) + ((board-size-2) * 4 * 5) + (4 * 3);
// //
// Vec2[] indexes = malloc(8 * sizeof(Vec2)); // Vec2[] indexes = malloc(8 * sizeof(Vec2));
+1 -1
View File
@@ -4,7 +4,7 @@ if [ -z $1 ] || [ -z $2 ]; then
echo "Usage: WORDLIST OUT_DIR" echo "Usage: WORDLIST OUT_DIR"
exit 0 exit 0
else else
rg "(^a[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_a.txt" rg "(^a[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_a.txt"
rg "(^b[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_b.txt" rg "(^b[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_b.txt"
rg "(^c[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_c.txt" rg "(^c[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_c.txt"
rg "(^d[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_d.txt" rg "(^d[a-z]+).*$" -or '$1' $1 | sort | uniq > "$2/words_d.txt"
+1 -1
View File
@@ -370100,4 +370100,4 @@ zwinglianism
zwinglianist zwinglianist
zwitter zwitter
zwitterion zwitterion
zwitterionic zwitterionic
+15 -5
View File
@@ -4,19 +4,22 @@ from boggler.boggler_utils import read_boggle_file, BadBoardFormat
boards_path = Path(Path(__file__).parent.resolve()) boards_path = Path(Path(__file__).parent.resolve())
@pytest.fixture(name="expected_board_4x4") @pytest.fixture(name="expected_board_4x4")
def fixture_expected_board_4x4(): def fixture_expected_board_4x4():
return [ return [
['u', 'n', 'r', 'e'], ["u", "n", "r", "e"],
['qu', 'n', 'i', 'l'], ["qu", "n", "i", "l"],
['i', 's', 'h', 'a'], ["i", "s", "h", "a"],
['s', 'e', 'l', 'b'] ["s", "e", "l", "b"],
] ]
@pytest.fixture(name="clean_4x4_path") @pytest.fixture(name="clean_4x4_path")
def fixture_clean_4x4_path(): def fixture_clean_4x4_path():
return Path(boards_path, "./boards/4x4_clean.board") return Path(boards_path, "./boards/4x4_clean.board")
def test_clean_4x4_board(expected_board_4x4, clean_4x4_path): def test_clean_4x4_board(expected_board_4x4, clean_4x4_path):
"""Test 4x4 board with clean input""" """Test 4x4 board with clean input"""
assert expected_board_4x4 == read_boggle_file(clean_4x4_path) 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(): def fixture_leading_whitespace_4x4_path():
return Path(boards_path, "./boards/4x4_leading_whitespace.board") return Path(boards_path, "./boards/4x4_leading_whitespace.board")
def test_leading_whitespace_4x4_board(expected_board_4x4, leading_whitespace_4x4_path): def test_leading_whitespace_4x4_board(expected_board_4x4, leading_whitespace_4x4_path):
"""Test 4x4 board with leading whitepace in rows""" """Test 4x4 board with leading whitepace in rows"""
assert expected_board_4x4 == read_boggle_file(leading_whitespace_4x4_path) 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(): def fixture_between_whitespace_4x4_path():
return Path(boards_path, "./boards/4x4_between_whitespace.board") return Path(boards_path, "./boards/4x4_between_whitespace.board")
def test_between_whitespace_4x4_board(expected_board_4x4, between_whitespace_4x4_path): def test_between_whitespace_4x4_board(expected_board_4x4, between_whitespace_4x4_path):
"""Test 4x4 board with whitespace surrounding letters in block""" """Test 4x4 board with whitespace surrounding letters in block"""
assert expected_board_4x4 == read_boggle_file(between_whitespace_4x4_path) 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(): def fixture_trailing_whitespace_4x4_path():
return Path(boards_path, "./boards/4x4_trailing_whitespace.board") 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""" """Test 4x4 board with trailing whitespace in rows"""
assert expected_board_4x4 == read_boggle_file(trailing_whitespace_4x4_path) 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(): def fixture_blank_lines_4x4_path():
return Path(boards_path, "./boards/4x4_blank_lines.board") return Path(boards_path, "./boards/4x4_blank_lines.board")
def test_blank_lines_4x4_board(blank_lines_4x4_path): def test_blank_lines_4x4_board(blank_lines_4x4_path):
"""Test 4x4 board with blank lines in board file""" """Test 4x4 board with blank lines in board file"""
with pytest.raises(BadBoardFormat, match="must contain no blank lines"): 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(): def fixture_inconsistent_width_4x4_path():
return Path(boards_path, "./boards/4x4_inconsistent_width.board") return Path(boards_path, "./boards/4x4_inconsistent_width.board")
def test_inconsistent_width_4x4_board(inconsistent_width_4x4_path): def test_inconsistent_width_4x4_board(inconsistent_width_4x4_path):
"""Test board with irregular number of blocks per row""" """Test board with irregular number of blocks per row"""
with pytest.raises(BadBoardFormat, match="length of each row must be the same"): with pytest.raises(BadBoardFormat, match="length of each row must be the same"):
+185 -17
View File
@@ -5,15 +5,17 @@ from boggler.boggler_utils import BoggleBoard, build_full_boggle_tree
WORDLISTS_DIR = Path("./boggler/wordlists/dwyl/").absolute() WORDLISTS_DIR = Path("./boggler/wordlists/dwyl/").absolute()
@pytest.fixture @pytest.fixture
def board_4x4(): def board_4x4():
return [ return [
['u','n','r','e'], ["u", "n", "r", "e"],
['qu','n','i','l'], ["qu", "n", "i", "l"],
['i','s','h','a'], ["i", "s", "h", "a"],
['s','e','l','b'] ["s", "e", "l", "b"],
] ]
@pytest.fixture @pytest.fixture
def expected_words_max_4(): def expected_words_max_4():
return [ return [
@@ -345,15 +347,65 @@ def expected_words_max_4():
("blah", [(3, 3), (3, 2), (2, 3), (2, 2)]), ("blah", [(3, 3), (3, 2), (2, 3), (2, 2)]),
] ]
@pytest.fixture @pytest.fixture
def expected_words_max_20(): def expected_words_max_20():
return [ return [
("u", [(0, 0)]), ("u", [(0, 0)]),
("un", [(0, 0), (1, 1)]), ("un", [(0, 0), (1, 1)]),
("unn", [(0, 0), (1, 1), (0, 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)]), "unrelinquishable",
("unreliable", [(0, 0), (1, 1), (0, 2), (0, 3), (1, 3), (1, 2), (2, 3), (3, 3), (3, 2), (3, 1)]), [
(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)]), ("unrein", [(0, 0), (1, 1), (0, 2), (0, 3), (1, 2), (0, 1)]),
("uns", [(0, 0), (1, 1), (2, 1)]), ("uns", [(0, 0), (1, 1), (2, 1)]),
("unsin", [(0, 0), (1, 1), (2, 1), (1, 2), (0, 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)]), ("unn", [(0, 0), (0, 1), (1, 1)]),
("uni", [(0, 0), (0, 1), (1, 2)]), ("uni", [(0, 0), (0, 1), (1, 2)]),
("unie", [(0, 0), (0, 1), (1, 2), (0, 3)]), ("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)]), "unrelishable",
("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)]), [
(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)]), ("unrein", [(0, 0), (0, 1), (0, 2), (0, 3), (1, 2), (1, 1)]),
("n", [(0, 1)]), ("n", [(0, 1)]),
("ni", [(0, 1), (1, 2)]), ("ni", [(0, 1), (1, 2)]),
@@ -414,14 +515,64 @@ def expected_words_max_20():
("re", [(0, 2), (0, 3)]), ("re", [(0, 2), (0, 3)]),
("rel", [(0, 2), (0, 3), (1, 3)]), ("rel", [(0, 2), (0, 3), (1, 3)]),
("relais", [(0, 2), (0, 3), (1, 3), (2, 3), (1, 2), (2, 1)]), ("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)]), ("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)]), ("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)]), ("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)]), ("rei", [(0, 2), (0, 3), (1, 2)]),
("rein", [(0, 2), (0, 3), (1, 2), (0, 1)]), ("rein", [(0, 2), (0, 3), (1, 2), (0, 1)]),
("reis", [(0, 2), (0, 3), (1, 2), (2, 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)]), ("bless", [(3, 3), (3, 2), (3, 1), (3, 0), (2, 1)]),
] ]
# ----------------------------------------------------- # -----------------------------------------------------
# Solved board max 4-letter words # Solved board max 4-letter words
# ----------------------------------------------------- # -----------------------------------------------------
@@ -848,25 +1000,33 @@ def expected_words_max_20():
def board_4(board_4x4): def board_4(board_4x4):
return BoggleBoard(board_4x4, 4) return BoggleBoard(board_4x4, 4)
@pytest.fixture @pytest.fixture
def tree_4(board_4): def tree_4(board_4):
return build_full_boggle_tree(board_4, Path(WORDLISTS_DIR)) return build_full_boggle_tree(board_4, Path(WORDLISTS_DIR))
@pytest.fixture @pytest.fixture
def found_words_4(tree_4): def found_words_4(tree_4):
return list(chain.from_iterable([x.word_paths for x in tree_4.values()])) return list(chain.from_iterable([x.word_paths for x in tree_4.values()]))
# Tests for solved board with max of 4-letter words # Tests for solved board with max of 4-letter words
def test_word_count_4_board_4x4_max(expected_words_max_4, tree_4): 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): def test_words_full_4_board_4x4_max(expected_words_max_4, found_words_4):
"""Verify words and paths match in exact order""" """Verify words and paths match in exact order"""
assert found_words_4 == expected_words_max_4 assert found_words_4 == expected_words_max_4
def test_format_words_4_to_csv(): def test_format_words_4_to_csv():
pass pass
def test_format_words_4_to_json(): def test_format_words_4_to_json():
pass pass
@@ -880,23 +1040,31 @@ def test_format_words_4_to_json():
def board_20(board_4x4): def board_20(board_4x4):
return BoggleBoard(board_4x4, 20) return BoggleBoard(board_4x4, 20)
@pytest.fixture @pytest.fixture
def tree_20(board_20): def tree_20(board_20):
return build_full_boggle_tree(board_20, WORDLISTS_DIR) return build_full_boggle_tree(board_20, WORDLISTS_DIR)
@pytest.fixture @pytest.fixture
def found_words_20(tree_20): def found_words_20(tree_20):
return list(chain.from_iterable([x.word_paths for x in tree_20.values()])) return list(chain.from_iterable([x.word_paths for x in tree_20.values()]))
# Tests for solved board with max of 20-letter words # Tests for solved board with max of 20-letter words
def test_word_count_20_board_4x4_max(expected_words_max_20, tree_20): 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): def test_words_full_20_board_4x4_max(expected_words_max_20, found_words_20):
assert found_words_20 == expected_words_max_20 assert found_words_20 == expected_words_max_20
def test_format_words_20_to_csv(): def test_format_words_20_to_csv():
pass pass
def test_format_words_20_to_json(): def test_format_words_20_to_json():
pass pass