Skip to content

Commit

Permalink
Move matching into separate file
Browse files Browse the repository at this point in the history
  • Loading branch information
Joeltronics committed Mar 15, 2022
1 parent d58cc11 commit cc454f5
Show file tree
Hide file tree
Showing 4 changed files with 126 additions and 111 deletions.
78 changes: 2 additions & 76 deletions game_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from colorama import Fore, Back, Style

from enum import Enum, unique
from typing import List, Iterable
from typing import Iterable


FORMAT_UNKOWN = Back.BLACK + Fore.WHITE
Expand All @@ -12,6 +12,7 @@
FORMAT_NOT_IN_SOLUTION = Back.WHITE + Fore.BLACK



@unique
class CharStatus(Enum):
unknown = 0
Expand All @@ -21,7 +22,6 @@ class CharStatus(Enum):




def get_format(char_status: CharStatus) -> str:
return {
CharStatus.unknown: FORMAT_UNKOWN,
Expand All @@ -35,77 +35,3 @@ def format_guess(guess: str, statuses: Iterable[CharStatus]) -> str:
return ''.join([
get_format(status) + character.upper() for character, status in zip(guess, statuses)
]) + Style.RESET_ALL


def get_character_statuses(guess: str, solution: str) -> tuple[CharStatus, CharStatus, CharStatus, CharStatus, CharStatus]:

assert len(guess) == 5
assert len(solution) == 5

guess = guess.lower()
solution = solution.lower()

statuses = [None for _ in range(5)]

unsolved_chars = list(solution)

# 1st pass: green or definite grey (yellow is more complicated, since there could be multiple of the same letter)
for n, character in enumerate(guess):

if character == solution[n]:
statuses[n] = CharStatus.correct
unsolved_chars[n] = ' '

elif character not in solution:
statuses[n] = CharStatus.not_in_solution

# 2nd pass: letters that are in word but in wrong place (not necessarily yellow when multiple of same letter in word)
for n, character in enumerate(guess):
if statuses[n] is None:
assert character in solution
if character in unsolved_chars:
statuses[n] = CharStatus.wrong_position
unsolved_char_idx = unsolved_chars.index(character)
unsolved_chars[unsolved_char_idx] = ' '
else:
statuses[n] = CharStatus.not_in_solution

assert not any([status is None for status in statuses])
return tuple(statuses)


# Inline unit tests

# Basic
assert get_character_statuses(solution='abcde', guess='fghij') == (
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution)
assert get_character_statuses(solution='abcde', guess='acxyz') == (
CharStatus.correct,
CharStatus.wrong_position,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution)

# "multiple of same letter" logic
assert get_character_statuses(solution='mount', guess='books') == (
CharStatus.not_in_solution,
CharStatus.correct,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution)
assert get_character_statuses(solution='mount', guess='brook') == (
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.wrong_position,
CharStatus.not_in_solution,
CharStatus.not_in_solution)
assert get_character_statuses(solution='books', guess='brook') == (
CharStatus.correct,
CharStatus.not_in_solution,
CharStatus.correct,
CharStatus.wrong_position,
CharStatus.wrong_position)
5 changes: 3 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import random

from game_types import *
import matching
from solver import Solver, SolverVerbosity, SolverParams
import word_list
import user_input
Expand Down Expand Up @@ -262,14 +263,14 @@ def _check_guess(guess: str) -> str:
guess = user_input.ask_word(guess_num)

guesses.append(guess)
statuses = get_character_statuses(guess=guess, solution=solution)
statuses = matching.get_character_statuses(guess=guess, solution=solution)

letter_status.add_guess(guess, statuses)
solver.add_guess(guess, statuses)

game_print()
for n, this_guess in enumerate(guesses):
statuses = get_character_statuses(guess=this_guess, solution=solution)
statuses = matching.get_character_statuses(guess=this_guess, solution=solution)
game_print('%i: %s' % (n + 1, format_guess(this_guess, statuses)))
game_print()

Expand Down
113 changes: 113 additions & 0 deletions matching.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
#!/usr/bin/env python3


from game_types import *

from typing import Iterable, List


def _calculate_character_statuses(guess: str, solution: str) -> tuple[CharStatus, CharStatus, CharStatus, CharStatus, CharStatus]:

assert len(guess) == 5
assert len(solution) == 5

guess = guess.lower()
solution = solution.lower()

statuses = [None for _ in range(5)]

unsolved_chars = list(solution)

# 1st pass: green or definite grey (yellow is more complicated, since there could be multiple of the same letter)
for n, character in enumerate(guess):

if character == solution[n]:
statuses[n] = CharStatus.correct
unsolved_chars[n] = ' '

elif character not in solution:
statuses[n] = CharStatus.not_in_solution

# 2nd pass: letters that are in word but in wrong place (not necessarily yellow when multiple of same letter in word)
for n, character in enumerate(guess):
if statuses[n] is None:
assert character in solution
if character in unsolved_chars:
statuses[n] = CharStatus.wrong_position
unsolved_char_idx = unsolved_chars.index(character)
unsolved_chars[unsolved_char_idx] = ' '
else:
statuses[n] = CharStatus.not_in_solution

assert not any([status is None for status in statuses])
return tuple(statuses)


def get_character_statuses(guess: str, solution: str) -> tuple[CharStatus, CharStatus, CharStatus, CharStatus, CharStatus]:
return _calculate_character_statuses(guess=guess, solution=solution)


def is_valid_for_guess(word: str, guess: tuple[str, Iterable[CharStatus]]) -> bool:
guess_word, guess_char_statuses = guess
status_if_this_is_solution = get_character_statuses(guess=guess_word, solution=word)
return status_if_this_is_solution == guess_char_statuses


def solutions_remaining(guess: str, possible_solution: str, solutions: Iterable[str], return_character_status=False) -> List[str]:
"""
If we guess this word, and see this result, figure out which words remain
"""
# TODO: this is a bottleneck, see if it can be optimized
character_status = get_character_statuses(guess, possible_solution)
# TODO: we only need the list length; it may be faster just to instead use:
#new_possible_solutions = sum([is_valid_for_guess(word, (guess, character_status)) for word in solutions])
new_possible_solutions = [word for word in solutions if is_valid_for_guess(word, (guess, character_status))]

if return_character_status:
return new_possible_solutions, character_status
else:
return new_possible_solutions


def num_solutions_remaining(guess: str, possible_solution: str, solutions: Iterable[str]) -> int:
"""
If we guess this word, and see this result, figure out how many possible words could be remaining
"""
return len(solutions_remaining(guess=guess, possible_solution=possible_solution, solutions=solutions))


# Inline unit tests

# Basic
assert _calculate_character_statuses(solution='abcde', guess='fghij') == (
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution)
assert _calculate_character_statuses(solution='abcde', guess='acxyz') == (
CharStatus.correct,
CharStatus.wrong_position,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution)

# "multiple of same letter" logic
assert _calculate_character_statuses(solution='mount', guess='books') == (
CharStatus.not_in_solution,
CharStatus.correct,
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.not_in_solution)
assert _calculate_character_statuses(solution='mount', guess='brook') == (
CharStatus.not_in_solution,
CharStatus.not_in_solution,
CharStatus.wrong_position,
CharStatus.not_in_solution,
CharStatus.not_in_solution)
assert _calculate_character_statuses(solution='books', guess='brook') == (
CharStatus.correct,
CharStatus.not_in_solution,
CharStatus.correct,
CharStatus.wrong_position,
CharStatus.wrong_position)
41 changes: 8 additions & 33 deletions solver.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,10 @@
from math import sqrt
import os
import sys
from typing import Tuple, Iterable, Optional, Union
from typing import Tuple, Iterable, Optional, Union, List

from game_types import *
import matching


RECURSION_HARD_LIMIT = 5
Expand Down Expand Up @@ -116,22 +117,17 @@ def get_num_possible_solutions(self) -> int:
def get_possible_solitions(self) -> List[str]:
return self.possible_solutions

def _is_valid_for_guess(self, word: str, guess: Tuple[str, Iterable[CharStatus]]) -> bool:
guess_word, guess_char_statuses = guess
status_if_this_is_solution = get_character_statuses(guess=guess_word, solution=word)
return status_if_this_is_solution == guess_char_statuses

def _is_valid(self, word: str) -> bool:
return all([
self._is_valid_for_guess(word, guess) for guess in self.guesses
matching.is_valid_for_guess(word, guess) for guess in self.guesses
])

def add_guess(self, guess_word: str, character_statuses: Iterable[CharStatus]):

this_guess = (guess_word, character_statuses)
self.guesses.append(this_guess)

self.possible_solutions = {word for word in self.possible_solutions if self._is_valid_for_guess(word, this_guess)}
self.possible_solutions = {word for word in self.possible_solutions if matching.is_valid_for_guess(word, this_guess)}
assert len(self.possible_solutions) > 0

# TODO: in theory, could use process of elimination to sometimes guarantee position from yellow letters
Expand Down Expand Up @@ -174,27 +170,6 @@ def _remove_solved_letters(word):
def get_most_common_unsolved_letters(self):
return self.get_unsolved_letters_counter().most_common()

def _solutions_remaining(self, guess: str, possible_solution: str, solutions: Iterable[str], return_character_status=False) -> List[str]:
"""
If we guess this word, and see this result, figure out which words remain
"""
# TODO: this is a bottleneck, see if it can be optimized
character_status = get_character_statuses(guess, possible_solution)
# TODO: we only need the list length; it may be faster just to instead use:
#new_possible_solutions = sum([self._is_valid_for_guess(word, (guess, character_status)) for word in solutions])
new_possible_solutions = [word for word in solutions if self._is_valid_for_guess(word, (guess, character_status))]

if return_character_status:
return new_possible_solutions, character_status
else:
return new_possible_solutions

def _num_solutions_remaining(self, guess: str, possible_solution: str, solutions: Iterable[str]) -> int:
"""
If we guess this word, and see this result, figure out how many possible words could be remaining
"""
return len(self._solutions_remaining(guess=guess, possible_solution=possible_solution, solutions=solutions))

def _preliminary_score_guesses(
self,
guesses: Iterable[str],
Expand Down Expand Up @@ -394,7 +369,7 @@ def _solve_fewest_remaining_words(self, max_num_matches: Optional[int] = None) -
The overall algorithm is O(n^3):
1. in _solve_fewest_remaining_words_from_lists(), loop over guesses
2. in _solve_fewest_remaining_words_from_lists(), loop over solutions_to_check_possible
3. in _num_solutions_remaining(), another loop over solutions_to_check_num_remaining
3. in matching.num_solutions_remaining(), another loop over solutions_to_check_num_remaining
"""

# Figure out how much to prune
Expand Down Expand Up @@ -465,8 +440,8 @@ def _score_guess_fewest_remaining_words(
sum_words_remaining = 0
sum_squared = 0
for possible_solution in solutions_to_check_possible:
words_remaining = self._num_solutions_remaining(guess, possible_solution,
solutions=solutions_to_check_num_remaining)
words_remaining = matching.num_solutions_remaining(
guess, possible_solution, solutions=solutions_to_check_num_remaining)
sum_words_remaining += words_remaining
sum_squared += (words_remaining ** 2)
max_words_remaining = max(words_remaining, max_words_remaining) if (
Expand Down Expand Up @@ -714,7 +689,7 @@ def _solve_recursive_inner(

len_at_start_of_loop = len(remaining_possible_solutions)

possible_solutions_this_guess, character_status = self._solutions_remaining(
possible_solutions_this_guess, character_status = matching.solutions_remaining(
guess=guess,
possible_solution=remaining_possible_solutions[0],
solutions=possible_solutions,
Expand Down

0 comments on commit cc454f5

Please sign in to comment.