diff --git a/README.md b/README.md index f48776f..f81e8d5 100644 --- a/README.md +++ b/README.md @@ -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 1. Create `.csv` of the board state like so: - + Here is an example `board.csv`. Note the orientation of the board does not matter. ```console $ 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: ```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. ```console diff --git a/boggler/__main__.py b/boggler/__main__.py index 8160175..432dacf 100755 --- a/boggler/__main__.py +++ b/boggler/__main__.py @@ -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() diff --git a/boggler/board_randomizer.py b/boggler/board_randomizer.py index 859b037..6c81424 100755 --- a/boggler/board_randomizer.py +++ b/boggler/board_randomizer.py @@ -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 ') + print("Usage: python3 board_randomizer.py ") else: try: dice = read_dice_file(Path(argv[1])) diff --git a/boggler/boggler_utils.py b/boggler/boggler_utils.py index b39d8c8..2c7e18a 100755 --- a/boggler/boggler_utils.py +++ b/boggler/boggler_utils.py @@ -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 diff --git a/boggler/c/board.h b/boggler/c/board.h index 9d61f68..2560583 100644 --- a/boggler/c/board.h +++ b/boggler/c/board.h @@ -108,7 +108,7 @@ void free_board(Board *b) { } } //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); // // Vec2[] indexes = malloc(8 * sizeof(Vec2)); diff --git a/boggler/split_wordlist_en.sh b/boggler/split_wordlist_en.sh index 692ae43..e04cb9f 100755 --- a/boggler/split_wordlist_en.sh +++ b/boggler/split_wordlist_en.sh @@ -4,7 +4,7 @@ if [ -z $1 ] || [ -z $2 ]; then echo "Usage: WORDLIST OUT_DIR" exit 0 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 "(^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" diff --git a/boggler/wordlists/dwyl/words_alpha.txt b/boggler/wordlists/dwyl/words_alpha.txt index ba4e63e..1e42fcf 100644 --- a/boggler/wordlists/dwyl/words_alpha.txt +++ b/boggler/wordlists/dwyl/words_alpha.txt @@ -370100,4 +370100,4 @@ zwinglianism zwinglianist zwitter zwitterion -zwitterionic \ No newline at end of file +zwitterionic diff --git a/tests/test_read_boards.py b/tests/test_read_boards.py index a2d65a2..b0ac4cc 100644 --- a/tests/test_read_boards.py +++ b/tests/test_read_boards.py @@ -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"): diff --git a/tests/test_solve_4x4_board.py b/tests/test_solve_4x4_board.py index 5a0af6f..76a93a0 100755 --- a/tests/test_solve_4x4_board.py +++ b/tests/test_solve_4x4_board.py @@ -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