From bcc86e18f97045b08e743e1db6bda600acb1527e Mon Sep 17 00:00:00 2001 From: Max Regan Date: Mon, 31 Jan 2022 12:35:41 -0500 Subject: [PATCH] Evaluate position based on remaining moves --- src/wordle.py | 110 +++++++++++++++++++++++--------------------------- 1 file changed, 50 insertions(+), 60 deletions(-) diff --git a/src/wordle.py b/src/wordle.py index e70230f..39c5b3c 100644 --- a/src/wordle.py +++ b/src/wordle.py @@ -38,8 +38,16 @@ class WordleState: ) class DictionaryProvider: - def provide(self) -> List[Word]: - return self._get_dictionary(self._word_filter(WORD_LENGTH)) + + def __init__(self, limit: Optional[int]=None): + self.limit = limit + + def provide(self, ) -> List[Word]: + words = self._get_dictionary(self._word_filter(WORD_LENGTH)) + if self.limit: + return words[:self.limit] + else: + return words def _get_dictionary(self, filter_fun: Callable[str, bool] = None) -> List[Word]: l = [] @@ -57,19 +65,42 @@ class DictionaryProvider: len(word) == length and all(map(lambda x: x in string.ascii_lowercase, word)) and not all(map(lambda c: c in "xiv", word)) - #and (word.startswith("s") or word.startswith("z")) + # and (word.startswith("s") or word.startswith("z")) ) # hack to remove some roman numerals return f -class WordleStateEvaluator: +class WordleWordPlayable: + + def is_word_playable(self, state: WordleState, word: Word): + for idx, ch in enumerate(word): + # Position is not already solved + if state.board[idx] is not None and state.board[idx] != ch: + return False + + # Letter is not already excluded + constraint = state.letter_constraints[ch] + if constraint == WordleLetterConstraint.EXCLUDED: + return False + + if ch in state.letters_eliminated[idx]: + return False + + for letter, constraint in state.letter_constraints.items(): + if constraint == WordleLetterConstraint.INCLUDED and letter not in word: + return False + + return True + +class WordleRemainingWordsStateEvaluator: """ A class which takes a WordleState and returns a score for the state """ - def __init__(self, dictionary_provider: DictionaryProvider): - self.dictionary_provider = dictionary_provider + def __init__(self, dictionary_provider: DictionaryProvider, word_playable: WordleWordPlayable): + self.dictionary = dictionary_provider.provide() + self.word_playable = word_playable def evaluate(self, state: WordleState) -> int: """ @@ -77,29 +108,7 @@ class WordleStateEvaluator: """ # Score values are totally arbitrary. This will be improved in the future based on the remaining selections - - # Game is solved. Return a big number. - if all(state.board): - return 999999999 - - score = 0 - for constraint in state.letter_constraints.values(): - if constraint == WordleLetterConstraint.UNKNOWN: - score += 0 - elif constraint == WordleLetterConstraint.EXCLUDED: - score += 10 - elif constraint == WordleLetterConstraint.INCLUDED: - score += 100 - - for letter in state.board: - if letter: - score += 100 - - for letters in state.letters_eliminated: - score += len(letters) - - return score - + return -sum([1 for word in self.dictionary if self.word_playable.is_word_playable(state, word)]) @dataclass class WordleMoveProcessor: @@ -133,19 +142,21 @@ class WordleMoveCalculator: def __init__( self, dictionary_provider: DictionaryProvider, - state_evaluator: WordleStateEvaluator, + state_evaluator, # TODO: parent class WordleStateEvaluator, move_processor: WordleMoveProcessor, + word_playable: WordleWordPlayable, ): self.dictionary = dictionary_provider.provide() self.state_evaluator = state_evaluator self.move_processor = move_processor + self.word_playable = word_playable def calculate( self, state: WordleState, evaluation_depth: int, limit: Optional[int]=None, ) -> List[Tuple[Word, int]]: guesses = [] words = [ - word for word in self.dictionary if self._is_word_playable(state, word) + word for word in self.dictionary if self.word_playable.is_word_playable(state, word) ] move_rankings = [] # type: List[Tuple[Word, int]] @@ -169,36 +180,17 @@ class WordleMoveCalculator: move_rankings = move_rankings[0:limit] return move_rankings - def _is_word_playable(self, state: WordleState, word: Word): - for idx, ch in enumerate(word): - # Position is not already solved - if state.board[idx] is not None and state.board[idx] != ch: - return False - constraint = state.letter_constraints[ch] - - # Letter is not already excluded - if constraint == WordleLetterConstraint.EXCLUDED: - return False - - if ch in state.letters_eliminated[idx]: - return False - - for letter, constraint in state.letter_constraints.items(): - if constraint == WordleLetterConstraint.INCLUDED and letter not in word: - return False - - return True - def main(): logging.basicConfig(format='%(asctime)s %(message)s', level=logging.INFO) - dictionary_provider = DictionaryProvider() - state_evaluator = WordleStateEvaluator(dictionary_provider) + dictionary_provider = DictionaryProvider(limit=100) + word_playable = WordleWordPlayable() + state_evaluator = WordleRemainingWordsStateEvaluator(dictionary_provider, word_playable) move_processor = WordleMoveProcessor() move_calculator = WordleMoveCalculator( - dictionary_provider, state_evaluator, move_processor + dictionary_provider, state_evaluator, move_processor, word_playable ) answer = random.choice(dictionary_provider.provide()) @@ -208,12 +200,10 @@ def main(): while not all(state.board): log.info("Round: %d", iteration) log.debug("State: %s", str(state)) - if iteration == 1: - guess = "sales" - else: - moves = move_calculator.calculate(state, 5, limit=5) - log.info("Best Moves: %s", moves) - guess = moves[0][0] + moves = move_calculator.calculate(state, 1) + log.info("Best Moves: %s", moves[:5]) + log.info("Num words remaining: %d", len(moves)) + guess = moves[0][0] log.info("Guess: %s", guess) state = move_processor.process(state, guess) log.info("Board: %s", str(state.board))