diff --git a/README.md b/README.md index 92672df..f5baa4d 100644 --- a/README.md +++ b/README.md @@ -3,15 +3,15 @@ Spellsolver is a software that helps to search for the best possible word in Spellcast discord activity. Spellsolver uses a trie to store the valid words, and then iteratively tries all the possible combinations of letters on the board, discarding the ones that don't make valid words and keeping the ones that do. -- Initialization of the trie structure to store valid words in single swap mode can take anywhere from 20 to 30 seconds and uses approximately 1 GB of ram memory, but allows almost all spellsolver queries to be executed in less than a second. -- Double swap mode can be enabled in config.py, but it is not recommended as it significantly increases load times (100 seconds), ram usage (3.6 GB) and query time (up to 20 seconds) +- Initialization of the trie structure to store valid words in single swap mode take 5 seconds, uses approximately 150 MB of ram memory and allows almost all spellsolver queries to be executed in less than two second. +- Double swap mode can be enabled in config.py, but it is not recommended as it significantly increases load times (25 seconds), ram usage (650 MB) and query time (up to 30 seconds) - In case the wordlist.txt file does not exist, a new file will be automatically generated from the sources folder when starting spellsolver using any interface A message like this will be printed on the screen while Spellsolver starts ```bash Spellsolver v1.10 - fabaindaiz WordValidate is being initialized, this will take several seconds -WordValidate successfully initialized (elapsed time: 25.05 seconds) +WordValidate successfully initialized (elapsed time: 4.8 seconds) ``` - #### Inside the docs folder, you will find some documents that detail the operation of spellsolver, as well as notes on how the algorithm is implemented. @@ -19,13 +19,13 @@ WordValidate successfully initialized (elapsed time: 25.05 seconds) ### Requirements - python3 (3.6 or later) +- marisa-trie (for store words) - tk (tkinter for graphicui.py) - fastapi (for webapi.py) - uvicorn (for webapi.py) ### TODO - Add some spellsolver tests to avoid accidentally introducing new bugs -- Add some heuristics to reduce the load and query time of double swap mode ### Notices for contributors - Thank you for your interest in contributing to spellsolver, any improvement will be welcome diff --git a/graphicalui.py b/graphicalui.py index 8595750..4c579ae 100644 --- a/graphicalui.py +++ b/graphicalui.py @@ -1,7 +1,7 @@ import tkinter as tk from typing import Tuple -from src.interfaces.tkinterboard import TkinterBoard +from src.interfaces.tkinter.tkinterboard import TkinterBoard from src.interfaces.baseui import BaseUI diff --git a/requirements.txt b/requirements.txt index cd555ff..3ce42dd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ tk +marisa-trie fastapi uvicorn \ No newline at end of file diff --git a/src/config.py b/src/config.py index 2b11301..422333d 100644 --- a/src/config.py +++ b/src/config.py @@ -1,4 +1,4 @@ -VERSION = "v1.10" +VERSION = "v1.11" DEBUG = False # Wordlist settings @@ -9,23 +9,13 @@ HOST = "127.0.0.1" PORT = 8080 -# Heuristic settings -HEURISTIC = False - -# Multiprocess settings -# Use multiprocessing is slower than single process -MULTIPROCESS = False - # Trie settings -# Use PATRICIA trie is slightly slower than PREFIX trie -# TRIE = "PREFIX" -# TRIE = "PATRICIA" -TRIE = "PREFIX" +# TRIE = "MARISA" +TRIE = "MARISA" # Swap mode settings # Make sure you have enough ram memory (and patience) for the selected swap modes -# SWAP = 0 (no swap) - Memory: 150 MB - Load: 3 sec - Query: 20 ms (mean) -# SWAP = 1 (one swap) - Memory: 990 MB - Load: 30 sec - Query: 1000 ms (mean) -# SWAP = 2 (two swap) - Memory: 3520 MB - Load: 100 sec - Query: 12000 ms (mean) +# SWAP = 0 (no swap) - Memory: 36 MB - Load: 1 sec - Query: 50 ms (mean) +# SWAP = 1 (one swap) - Memory: 150 MB - Load: 5 sec - Query: 2000 ms (mean) +# SWAP = 2 (two swap) - Memory: 650 MB - Load: 25 sec - Query: 30000 ms (mean) SWAP = 1 -# notice: it is recommended not to activate double swap (SWAP = 2) diff --git a/src/interfaces/baseui.py b/src/interfaces/baseui.py index 4c40b24..787a697 100644 --- a/src/interfaces/baseui.py +++ b/src/interfaces/baseui.py @@ -1,7 +1,7 @@ +from src.modules.wordlist.validate import WordValidate +from src.modules.gameboard.resultlist import ResultList +from src.modules.gameboard.gameboard import GameBoard from src.spellsolver import SpellSolver -from src.modules.resultlist import ResultList -from src.modules.validate import WordValidate -from src.modules.gameboard import GameBoard from src.utils.timer import Timer from src.config import VERSION @@ -35,7 +35,7 @@ def __init__(self) -> None: print(f"Spellsolver {VERSION} - fabaindaiz") self.timer.reset_timer() - self.validate.load_wordlist() + self.validate.init_trie() print( f"WordValidate successfully initialized (elapsed time: {self.timer.elapsed_seconds()} seconds)" ) diff --git a/src/interfaces/apirouter.py b/src/interfaces/fastapi/apirouter.py similarity index 97% rename from src/interfaces/apirouter.py rename to src/interfaces/fastapi/apirouter.py index 045ac0b..8474872 100644 --- a/src/interfaces/apirouter.py +++ b/src/interfaces/fastapi/apirouter.py @@ -1,6 +1,6 @@ from typing import Any, Dict, Optional from pydantic import BaseModel -from src.interfaces.baseapi import BaseRouter +from src.interfaces.fastapi.baseapi import BaseRouter from src.interfaces.baseui import BaseUI diff --git a/src/interfaces/baseapi.py b/src/interfaces/fastapi/baseapi.py similarity index 100% rename from src/interfaces/baseapi.py rename to src/interfaces/fastapi/baseapi.py diff --git a/src/interfaces/board.py b/src/interfaces/tkinter/board.py similarity index 94% rename from src/interfaces/board.py rename to src/interfaces/tkinter/board.py index bd355b8..f1ee346 100644 --- a/src/interfaces/board.py +++ b/src/interfaces/tkinter/board.py @@ -2,10 +2,10 @@ from src.config import SWAP from src.interfaces.baseui import BaseUI -from src.interfaces.boardbutton import BoardButton -from src.interfaces.boardlabel import BoardLabel -from src.interfaces.boardtile import BoardTile -from src.modules.resultlist import ResultWord +from src.interfaces.tkinter.boardbutton import BoardButton +from src.interfaces.tkinter.boardlabel import BoardLabel +from src.interfaces.tkinter.boardtile import BoardTile +from src.modules.gameboard.resultlist import ResultWord from src.utils.utils import aux_to_indices diff --git a/src/interfaces/boardbutton.py b/src/interfaces/tkinter/boardbutton.py similarity index 100% rename from src/interfaces/boardbutton.py rename to src/interfaces/tkinter/boardbutton.py diff --git a/src/interfaces/boardentry.py b/src/interfaces/tkinter/boardentry.py similarity index 100% rename from src/interfaces/boardentry.py rename to src/interfaces/tkinter/boardentry.py diff --git a/src/interfaces/boardlabel.py b/src/interfaces/tkinter/boardlabel.py similarity index 97% rename from src/interfaces/boardlabel.py rename to src/interfaces/tkinter/boardlabel.py index be6e7ce..c14adf9 100644 --- a/src/interfaces/boardlabel.py +++ b/src/interfaces/tkinter/boardlabel.py @@ -2,7 +2,7 @@ from tkinter.font import Font from typing import List -from src.modules.gameboard import GameTile +from src.modules.gameboard.gameboard import GameTile class BoardLabel: diff --git a/src/interfaces/boardmenu.py b/src/interfaces/tkinter/boardmenu.py similarity index 100% rename from src/interfaces/boardmenu.py rename to src/interfaces/tkinter/boardmenu.py diff --git a/src/interfaces/boardtile.py b/src/interfaces/tkinter/boardtile.py similarity index 94% rename from src/interfaces/boardtile.py rename to src/interfaces/tkinter/boardtile.py index 14e68ff..65eeaa0 100644 --- a/src/interfaces/boardtile.py +++ b/src/interfaces/tkinter/boardtile.py @@ -1,7 +1,7 @@ import tkinter as tk -from src.interfaces.boardentry import BoardEntry -from src.interfaces.boardmenu import BoardMenu +from src.interfaces.tkinter.boardentry import BoardEntry +from src.interfaces.tkinter.boardmenu import BoardMenu class BoardTile: diff --git a/src/interfaces/multhandler.py b/src/interfaces/tkinter/multhandler.py similarity index 96% rename from src/interfaces/multhandler.py rename to src/interfaces/tkinter/multhandler.py index 571026e..c7db7f7 100644 --- a/src/interfaces/multhandler.py +++ b/src/interfaces/tkinter/multhandler.py @@ -1,4 +1,4 @@ -from src.interfaces.board import Board +from src.interfaces.tkinter.board import Board class MultHandler: diff --git a/src/interfaces/tkinterboard.py b/src/interfaces/tkinter/tkinterboard.py similarity index 87% rename from src/interfaces/tkinterboard.py rename to src/interfaces/tkinter/tkinterboard.py index a70665b..8423052 100644 --- a/src/interfaces/tkinterboard.py +++ b/src/interfaces/tkinter/tkinterboard.py @@ -1,6 +1,6 @@ from src.interfaces.baseui import BaseUI -from src.interfaces.board import Board -from src.interfaces.multhandler import MultHandler +from src.interfaces.tkinter.multhandler import MultHandler +from src.interfaces.tkinter.board import Board class TkinterBoard(Board): diff --git a/src/modules/gameboard.py b/src/modules/gameboard/gameboard.py similarity index 100% rename from src/modules/gameboard.py rename to src/modules/gameboard/gameboard.py diff --git a/src/modules/path.py b/src/modules/gameboard/path.py similarity index 95% rename from src/modules/path.py rename to src/modules/gameboard/path.py index dd3e148..66d91a1 100644 --- a/src/modules/path.py +++ b/src/modules/gameboard/path.py @@ -1,5 +1,5 @@ from typing import List, Tuple -from src.modules.gameboard import GameTile +from src.modules.gameboard.gameboard import GameTile class Path: diff --git a/src/modules/resultlist.py b/src/modules/gameboard/resultlist.py similarity index 97% rename from src/modules/resultlist.py rename to src/modules/gameboard/resultlist.py index 7436025..060dfc3 100644 --- a/src/modules/resultlist.py +++ b/src/modules/gameboard/resultlist.py @@ -1,5 +1,5 @@ from typing import Any, Dict, Generator, List, Tuple -from src.modules.gameboard import GameTile +from src.modules.gameboard.gameboard import GameTile from src.utils.timer import Timer diff --git a/src/modules/trie/base.py b/src/modules/trie/base.py new file mode 100644 index 0000000..c9e18d5 --- /dev/null +++ b/src/modules/trie/base.py @@ -0,0 +1,20 @@ +from typing import Generator +from src.modules.wordlist.wordlist import WordList + + +class Trie: + + def insert_trie(self, loader: WordList) -> None: + raise NotImplementedError() + + def query_trie(self) -> "TrieQuery": + raise NotImplementedError() + + +class TrieQuery: + + def get_key(self, word: str) -> str: + raise NotImplementedError() + + def get_leaf(self, word: str) -> Generator[str, None, None]: + raise NotImplementedError() diff --git a/src/modules/trie/loader.py b/src/modules/trie/loader.py new file mode 100644 index 0000000..2ec0a30 --- /dev/null +++ b/src/modules/trie/loader.py @@ -0,0 +1,18 @@ +from typing import List, Generator +from itertools import combinations +from src.config import SWAP + + +def _word_iter(word, num): + for t in combinations(range(len(word)), num): + yield "".join("0" if i in t else word[i] for i in range(len(word))) + +def word_iter(word: str) -> Generator[str, None, None]: + for num in range(SWAP + 1): + for iword in _word_iter(word, num): + yield iword + +def chunk_iter(words: Generator[str, None, None]) -> Generator[str, None, None]: + """Insert a chunk of words into the trie and return it""" + for word in words: + yield from word_iter(word) diff --git a/src/modules/trie/marisa.py b/src/modules/trie/marisa.py new file mode 100644 index 0000000..3cc5b1a --- /dev/null +++ b/src/modules/trie/marisa.py @@ -0,0 +1,42 @@ +from marisa_trie import RecordTrie +from typing import Generator, List, Tuple +from src.modules.trie.base import Trie, TrieQuery +from src.modules.wordlist.wordlist import WordList +from src.modules.trie.loader import word_iter + + +class MarisaTrie(Trie): + + def __init__(self) -> None: + self.trie: RecordTrie = None + self.words: List[str] = [] + + def insert_trie(self, loader: WordList) -> None: + ind: int = 0 + trie_keys: List[str] = [] + trie_data: List[Tuple[int]] = [] + + for word in loader.get_words(): + for iword in word_iter(word): + trie_keys.append(iword) + trie_data.append((ind,)) + + self.words.append(word) + ind += 1 + self.trie = RecordTrie(" TrieQuery: + return MarisaTrieQuery(self) + + +class MarisaTrieQuery(TrieQuery): + + def __init__(self, trie: Trie) -> None: + self.trie: MarisaTrie = trie + + def get_key(self, word: str) -> str: + return self.trie.trie.has_keys_with_prefix(word) + + def get_leaf(self, word: str) -> Generator[str, None, None]: + for i in self.trie.trie.get(word, []): + yield self.trie.words[i[0]] diff --git a/src/modules/ptrie.py b/src/modules/trie/patricia.py similarity index 100% rename from src/modules/ptrie.py rename to src/modules/trie/patricia.py diff --git a/src/modules/trie.py b/src/modules/trie/prefix.py similarity index 100% rename from src/modules/trie.py rename to src/modules/trie/prefix.py diff --git a/src/modules/validate.py b/src/modules/validate.py deleted file mode 100644 index b670247..0000000 --- a/src/modules/validate.py +++ /dev/null @@ -1,110 +0,0 @@ -from typing import Any, List -from itertools import combinations -from collections import defaultdict -from multiprocessing import Pool, cpu_count -from src.modules.wordlist import WordList -from src.config import MULTIPROCESS, SWAP, TRIE - -if TRIE == "PREFIX": - from src.modules.trie import TrieLeaf, TrieNode -elif TRIE == "PATRICIA": - from src.modules.ptrie import TrieLeaf, TrieNode -else: - raise NotImplementedError() - - -class ValidateLeaf(TrieLeaf): - """Implements TrieLeaf interface to store words""" - - def __init__(self) -> None: - self.words: List[str] = [] - - def insert(self, word: str) -> None: - """Insert a word in the TrieLeaf""" - self.words.append(word) - - def get(self) -> List[str]: - """Get a list of words in the TrieLeaf""" - return self.words - - def heuristic(self) -> Any: - """Get heuristic values from TrieLeaf""" - return { - "words_count": len(self.words), - } - - def merge_leafs(self, leaf: "ValidateLeaf") -> None: - """Merge other_leaf into main_leaf""" - self.words += leaf.words - - -class WordValidate: - """Validate a word using a trie""" - - def __init__(self) -> None: - self.wordlist = WordList() - self.trie: TrieNode = TrieNode(ValidateLeaf) - - def _word_iter(self, word, num): - for t in combinations(range(len(word)), num): - yield "".join("0" if i in t else word[i] for i in range(len(word))) - - def insert(self, word: str, num: int) -> None: - for iword in self._word_iter(word, num): - self.trie.insert(iword, word=word) - - def _insert_chunk(self, words: List[str]) -> TrieNode: - """Insert a chunk of words into the trie and return it""" - for word in words: - for num in range(SWAP + 1): - self.insert(word, num) - return self.trie - - def chunk_process(self, words: List[str]) -> None: - """Insert a chunk of words based on the number of cores into the trie and return it""" - chunk_size = len(words) // cpu_count() - chunks = [words[i:i + chunk_size] for i in range(0, len(words), chunk_size)] - - with Pool(cpu_count()) as pool: - local_tries = pool.map(self._insert_chunk, chunks) - - for local_trie in local_tries: - self.trie.merge_tries(local_trie) - - def bucket_process(self, words: List[str]) -> None: - """Insert a bucket of words based on the first letter into the trie and return it""" - word_buckets = defaultdict(list) - for word in words: - word_buckets[word[0]].append(word) - - with Pool(cpu_count()) as pool: - local_tries = pool.map(self._insert_chunk, word_buckets.values()) - - for local_trie in local_tries: - self.trie.merge_tries(local_trie) - - def load_wordlist(self) -> None: - """Initialize the trie with all words from a file""" - wordlist_file = self.wordlist.open_file() - print("WordValidate is being initialized, this will take several seconds") - - words = [word[:-1] for word in wordlist_file.readlines()] - - process_fun = self.chunk_process if MULTIPROCESS else self._insert_chunk - process_fun(words) - - -if __name__ == "__main__": - validate = WordValidate() - validate.load_wordlist() - - def node_str(node: TrieNode) -> str: - """Return a string representation of a TrieNode""" - return "\n".join( - [f"{swap}: {node.get_leaf(recursive=True, key=swap)}" for swap in SWAP] - ) - - while True: - word = input("Insert a word: ") - node = validate.trie.get_node(word) - print(node_str(node) if node else f"There are no word started in {word}") diff --git a/src/modules/wordlist.py b/src/modules/wordlist.py deleted file mode 100644 index c6be149..0000000 --- a/src/modules/wordlist.py +++ /dev/null @@ -1,82 +0,0 @@ -import os -from typing import Generator, Set, TextIO -from src.config import SOURCES, WORDLIST -from src.utils.utils import is_valid_word - - -class WordList: - """ - Represents a class that can generate and load a wordlist file for Spellsolver. - - Attributes: - source_path (str): The path to the folder containing source files. - dest_path (str): The path to the destination wordlist file. - """ - - def __init__(self): - """ - Initialize a WordList object with source and destination paths. - """ - self.source_path = SOURCES - self.dest_path = WORDLIST - - def open_file(self) -> TextIO: - """ - Load a wordlist file, generate it if it doesn't exist. - - Returns: - TextIO: A file object representing the opened wordlist file. - """ - if not os.path.isfile(self.dest_path): - self.generate_wordlist() - print("Wordlist file successfully generated from sources") - return open(self.dest_path) - - def generate_wordlist(self) -> None: - """ - Generate a wordlist file from multiple files in a source folder. - """ - words = self.fetch_words_from_files() - write_words_to_file(words, path=self.dest_path) - - def fetch_words_from_files(self) -> Set[str]: - """ - Fetch valid words from a list of source files. - - Returns: - Set[str]: A set containing valid words from source files. - """ - words = set() - for file in os.listdir(self.source_path): - full_path = os.path.join(self.source_path, file) - words.update(read_source_file(path=full_path)) - return words - - -def write_words_to_file(words: set, path: str) -> None: - """ - Sort and write a set of valid words to a destination file. - Args: - words (set): A set of valid words to be written to the file. - path (str): The path to the destination file. - """ - sorted_words = sorted(words) - with open(path, "w") as file: - file.writelines(f"{word}\n" for word in sorted_words) - - -def read_source_file(path: str) -> Generator[str, None, None]: - """ - Read valid words from a source file. - - Args: - path (str): The path to the source file. - - Yields: - Generator[str, None, None]: A generator yielding valid words from the source file. - """ - with open(path) as file: - for line in file: - word = line.strip().lower() - if is_valid_word(word): - yield word diff --git a/src/modules/wordlist/generate.py b/src/modules/wordlist/generate.py new file mode 100644 index 0000000..147ebe8 --- /dev/null +++ b/src/modules/wordlist/generate.py @@ -0,0 +1,59 @@ +import os +from typing import Callable, Generator +from src.utils.utils import is_valid_word + + +def fetch_single_file(path: str, validate: Callable[[str], bool]) -> Generator[str, None, None]: + """ + Fetch valid words from a source file. + + Args: + source (str): The path to the source file. + + Yields: + Generator[str, None, None]: A generator yielding valid words from the source file. + """ + with open(path) as file: + for line in file: + word = line.strip().lower() + if validate(word): + yield word + +def fetch_multiple_files(path: str) -> Generator[str, None, None]: + """ + Fetch valid words from a list of source files. + + Args: + source (str): The path to the source folder. + + Yields: + Generator[str, None, None]: A generator yielding valid words from source files. + """ + words = set() + for file in os.listdir(path): + full_path = os.path.join(path, file) + words.update(fetch_single_file(path=full_path, validate=is_valid_word)) + yield from words + +def write_words_to_file(path: str, words: Generator[str, None, None], sort=False) -> None: + """ + Sort and write a set of valid words to a destination file. + Args: + path (str): The path to the destination file. + words (set): A set of valid words to be written to the file. + """ + words = sorted(words) if sort else words + with open(path, "w") as file: + file.writelines(f"{word}\n" for word in words) + +def generate_wordlist(source: str, destination: str) -> None: + """ + Generate a wordlist file from multiple files in a source folder. + + Args: + source (str): The path to the source folder. + destination (str): The path to the destination file. + """ + words = fetch_multiple_files(path=source) + write_words_to_file(path=destination, words=words, sort=True) + print("Wordlist file successfully generated from sources") diff --git a/src/modules/wordlist/validate.py b/src/modules/wordlist/validate.py new file mode 100644 index 0000000..20da898 --- /dev/null +++ b/src/modules/wordlist/validate.py @@ -0,0 +1,23 @@ +from src.modules.trie.base import Trie, TrieQuery +from src.modules.wordlist.wordlist import WordList +from src.config import SWAP, TRIE + +if TRIE == "MARISA": + from src.modules.trie.marisa import MarisaTrie + trie = MarisaTrie() +else: + raise NotImplementedError() + + +class WordValidate: + """Validate a word using a trie""" + + def __init__(self) -> None: + self.wordlist: WordList = WordList() + self.trie: Trie = trie + + def init_trie(self) -> None: + self.trie.insert_trie(self.wordlist) + + def get_trie(self) -> TrieQuery: + return self.trie.query_trie() diff --git a/src/modules/wordlist/wordlist.py b/src/modules/wordlist/wordlist.py new file mode 100644 index 0000000..aa6416a --- /dev/null +++ b/src/modules/wordlist/wordlist.py @@ -0,0 +1,44 @@ +import os +from typing import Generator, TextIO +from src.modules.wordlist.generate import generate_wordlist +from src.config import SOURCES, WORDLIST + + +class WordList: + """ + Represents a class that can generate and load a wordlist file for Spellsolver. + + Attributes: + source_path (str): The path to the folder containing source files. + dest_path (str): The path to the destination wordlist file. + """ + + def __init__(self): + """ + Initialize a WordList object with source and destination paths. + """ + self.source = SOURCES + self.destination = WORDLIST + + def open_file(self) -> TextIO: + """ + Load a wordlist file, generate it if it doesn't exist. + + Returns: + TextIO: A file object representing the opened wordlist file. + """ + if not os.path.exists(self.destination): + generate_wordlist(source=self.source, destination=self.destination) + return open(self.destination, "r") + + def get_words(self) -> Generator[str, None, None]: + """ + Get the next word from the wordlist file. + + Returns: + Generator[str, None, None]: A generator that yields the next word. + """ + file = self.open_file() + for line in file: + yield line.strip().lower() + file.close() diff --git a/src/spellsolver.py b/src/spellsolver.py index 77a3ce9..75a9e47 100644 --- a/src/spellsolver.py +++ b/src/spellsolver.py @@ -1,10 +1,9 @@ from typing import Generator, List -from concurrent.futures import ThreadPoolExecutor -from src.modules.resultlist import ResultList, ResultWord -from src.modules.gameboard import GameBoard, GameTile -from src.modules.validate import WordValidate -from src.modules.trie import TrieNode -from src.modules.path import Path +from src.modules.wordlist.validate import WordValidate +from src.modules.gameboard.gameboard import GameBoard, GameTile +from src.modules.gameboard.resultlist import ResultList, ResultWord +from src.modules.gameboard.path import Path +from src.modules.trie.base import TrieQuery from src.utils.timer import Timer from src.config import SWAP @@ -17,12 +16,12 @@ def __init__(self, validate: WordValidate, gameboard: GameBoard) -> None: self.validate: WordValidate = validate def process_node( - self, node: TrieNode, actual_word: str, actual_path: List[GameTile] + self, trie: TrieQuery, actual_word: str, actual_path: List[GameTile] ) -> Generator[ResultWord, None, None]: """Recursively process a node to find possible valid words""" swaps = [i for i, letter in enumerate(actual_word) if letter == "0"] - for word in node.get_leaf(): + for word in trie.get_leaf(actual_word): path = Path(actual_path).swap_index(word, swaps=swaps) yield ResultWord( points=path.word_points(), @@ -34,62 +33,40 @@ def process_node( def process_path_aux( self, tile: GameTile, - node: TrieNode, + trie: TrieQuery, word: str, path: List[GameTile], swap: int, letter: str, ) -> Generator[ResultWord, None, None]: - child_key = node.get_key(letter) - if child_key: - actual_word = word + child_key - actual_node = node.childs[child_key] - yield from self.process_node(actual_node, actual_word, path) - yield from self.process_path(tile, actual_node, actual_word, path, swap) + actual_word = word + letter + if trie.get_key(actual_word): + yield from self.process_node(trie, actual_word, path) + yield from self.process_path(tile, trie, actual_word, path, swap) def process_path( - self, tile: GameTile, node: TrieNode, word: str, path: List[GameTile], swap: int + self, tile: GameTile, trie: TrieQuery, word: str, path: List[GameTile], swap: int ) -> Generator[ResultWord, None, None]: """Get all posible paths that complete a path using swap""" for actual_tile in tile.suggest_tile(path): actual_path = path + [actual_tile] yield from self.process_path_aux( - actual_tile, node, word, actual_path, swap, actual_tile.letter + actual_tile, trie, word, actual_path, swap, actual_tile.letter ) if swap: yield from self.process_path_aux( - actual_tile, node, word, actual_path, swap - 1, "0" + actual_tile, trie, word, actual_path, swap - 1, "0" ) - - def process_tile(self, tile: GameTile, swap: int) -> Generator[ResultWord, None, None]: - """Process a single tile for valid words""" - return self.process_path( - tile=tile, node=self.validate.trie, word="", path=[tile], swap=swap - ) def process_gameboard(self, swap: int) -> Generator[ResultWord, None, None]: """Iterate over all the squares on the board to start processing the paths""" - with ThreadPoolExecutor() as executor: - results = list(executor.map(self.process_tile, self.gameboard.tiles.values(), [swap] * len(self.gameboard.tiles))) - for tile_results in results: - yield from tile_results + for tile in self.gameboard.tiles.values(): + yield from self.process_path( + tile=tile, trie=self.validate.get_trie(), word="", path=[tile], swap=swap + ) def word_list(self, swap: int, timer: Timer = None) -> ResultList: """Get a valid words list from a solver Spellcast game""" results = ResultList(timer=timer) results.update(self.process_gameboard(swap=min(swap, SWAP))) return results - - -if __name__ == "__main__": - gameboard = GameBoard() - validate = WordValidate() - validate.load_wordlist() - - while True: - gameboard_string = input("Insert a gameboard: ") - gameboard.load(gameboard_string) - spellsolver = SpellSolver(validate, gameboard) - - swap = input("Use swap?: ") - spellsolver.word_list(swap=int(swap)) diff --git a/tests/test_validate.py b/tests/test_validate.py index 5a22d8a..fdef391 100644 --- a/tests/test_validate.py +++ b/tests/test_validate.py @@ -1,6 +1,6 @@ import unittest -from src.modules.validate import WordValidate -from src.modules.trie import TrieNode +from src.modules.wordlist.validate import WordValidate +from src.modules.trie.marisa import MarisaTrie class Validate(unittest.TestCase): @@ -8,9 +8,10 @@ class Validate(unittest.TestCase): def setUp(self) -> None: self.validate: WordValidate = WordValidate() - self.validate.load_wordlist() + self.validate.init_trie() def test_(self) -> None: """""" - node = self.validate.trie.get_node("hello") - self.assertEqual(type(node), TrieNode) + trie = self.validate.trie.query_trie() + self.assertTrue(trie.get_key("epidemic")) + self.assertFalse(trie.get_key("abcdefg"))