mirror of
https://codeberg.org/cblanken/boggler.git
synced 2026-07-25 19:19:20 -04:00
Initial blacken reformat
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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));
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -370100,4 +370100,4 @@ zwinglianism
|
|||||||
zwinglianist
|
zwinglianist
|
||||||
zwitter
|
zwitter
|
||||||
zwitterion
|
zwitterion
|
||||||
zwitterionic
|
zwitterionic
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user