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
+53 -28
View File
@@ -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
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 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
View File
@@ -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
+15 -5
View File
@@ -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
View File
@@ -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