Python Wordle on GitHub

I’m struggling to keep the shape of the solutions dictionary in my mind. Therefore I need something simpler.

The solution dictionary is built like this:

class SolutionDictionary:
    def __init__(self, guesses, solutions):
        self.dict = self.create_dict(guesses, solutions)

    @staticmethod
    def create_dict(guesses, solutions):
        solutions_dict = {}  # guess -> dict (score -> [solutions])
        for guess in guesses:
            guess_dict = {}  # score -> [solutions]
            solutions_dict[guess] = guess_dict
            for solution in solutions:
                score = guess.score(solution)
                if not score in guess_dict:
                    guess_dict[score] = ScoredWords(score)
                guess_dict[score].add_word(solution)
        return solutions_dict

This is a dictionary keyed by guess, pointing to a dictionary keyed by score, such that if score = guess.score(solution), solutions_dict[guess][score] is an instance of ScoredWords containing that score, and all the solutions that result in that score.

I find that impossible to understand, impossible to describe in words. This is not a good sign. I need a better idea. Let me try new words.

For every possible guess, we want a GuessDescription, consisting of a ScoreDescription, consisting of a score, and a list of all the solutions having that score. Let’s rename ScoredWords to ScoreDescription.

Now we have this:

    def create_dict(guesses, solutions):
        solutions_dict = {}  # guess -> dict (score -> [solutions])
        for guess in guesses:
            guess_dict = {}  # score -> [solutions]
            solutions_dict[guess] = guess_dict
            for solution in solutions:
                score = guess.score(solution)
                if not score in guess_dict:
                    guess_dict[score] = ScoreDescription(score)
                guess_dict[score].add_word(solution)
        return solutions_dict

Now let’s make a GuessDescription by intention:

    @staticmethod
    def create_dict(guesses, solutions):
        solutions_dict = {}  # guess -> dict (score -> [solutions])
        for guess in guesses:
            guess_desc = GuessDescription()
            solutions_dict[guess] = guess_desc
            for solution in solutions:
                score = guess.score(solution)
                guess_desc.add_word(score, solution)
        return solutions_dict

That demands this:

class GuessDescription:
    def __init__(self):
        self.score_descriptions = {}

    def add_word(self, score, solution):
        try:
            description = self.score_descriptions[score]
        except KeyError:
            description = ScoreDescription(score)
        description.add_word(solution)

That’ll break some tests, telling us that our refactoring is not complete. The first error is:

    def solutions_for(self, guess, score):
        try:
>           return self.dict[guess][score]
E           TypeError: 'GuessDescription' object is not subscriptable

Yes. I think we can do this to get to the next flaw:

class SolutionDictionary:
    def solutions_for(self, guess, score):
        guess_description = self.dict[guess]
        return guess_description.solutions_for(score)

class GuessDescription:
    def solutions_for(self, score):
        try:
            return self.score_descriptions[score]
        except KeyError:
            return ScoreDescription(score)

Continues to break, now saying:

Expected :<score_description.ScoreDescription object at 0x105a8c090>
Actual   :<score_description.ScoreDescription object at 0x105a8c310>

Looks like we need ScoreDescriptions to understand __eq__. But we have that. What’s up?

Ah. This was wrong:


class GuessDescription:
    def add_word(self, score, solution):
        try:
            description = self.score_descriptions[score]
        except KeyError:
            description = ScoreDescription(score)
            self.score_descriptions[score] = description  # added
        description.add_word(solution)

I wasn’t adding the new ScoreDescription to the dictionary.

One more test failing. Create statistics needs revision. We have:

    def create_statistics(self):
        stats = []
        for word in self.dict:
            word_dict = self.dict[word]  # {score -> scoredWords}
            
            number_of_buckets = len(word_dict)
            max_words = max(len(bucket) for bucket in word_dict.values())
            min_words = min(len(bucket) for bucket in word_dict.values())
            avg_words = sum(len(bucket) for bucket in word_dict.values()) / number_of_buckets
            stat = Statistic(word, number_of_buckets, max_words, min_words, avg_words)
            stats.append(stat)

        def my_key(stat: Statistic):
            return -stat.number_of_buckets

        stats.sort(key=my_key)
        return stats

We need:

    def create_statistics(self):
        stats = []
        for word in self.dict:
            guess_description = self.dict[word]  # {score -> scoredWords}
            
            number_of_buckets = guess_description.number_of_buckets
            max_words = max(len(bucket) for bucket in guess_description.buckets)
            min_words = min(len(bucket) for bucket in guess_description.buckets)
            avg_words = sum(len(bucket) for bucket in guess_description.buckets) / number_of_buckets
            stat = Statistic(word, number_of_buckets, max_words, min_words, avg_words)
            stats.append(stat)

        def my_key(stat: Statistic):
            return -stat.number_of_buckets

        stats.sort(key=my_key)
        return stats

class GuessDescription:
    def __init__(self):
        self.score_descriptions = {}

    def add_word(self, score, solution):
        try:
            description = self.score_descriptions[score]
        except KeyError:
            description = ScoreDescription(score)
            self.score_descriptions[score] = description
        description.add_word(solution)

    @property
    def buckets(self):
        return self.score_descriptions.values()

    @property
    def number_of_buckets(self):
        return len(self.score_descriptions)

    def solutions_for(self, score):
        try:
            return self.score_descriptions[score]
        except KeyError:
            return ScoreDescription(score)

We added the buckets and number_of_buckets properties. Tests are green. I am fried. Commit: refactor to provide GuessDescription and ScoreDescription objects.

Summary

I think these two objects make things easier to understand. I’ll try to explain them next time. Right now … well, I’ll try:

The SolutionDictionaryis a keyed collection (dictionary) of guess word to instances of GuessDescription.

A GuessDescription is a keyed collection of score to instances of ScoreDescription.

ScoreDescription is an object holding a score, and a WordCollection holding all the solutions such that the guess gets that score from each of those solutions.

It’s still weird, isn’t it?

Try this:

SolutionDictionary is an object that knows about guesses, solutions, and scores. It is created from two WordCollections, a collection of guesses and a collection of possible solutions.

Given a SolutionDictionary sd, sd.solutions_for(guess_word, score) returns all the solutions that would give the guess_word the provided score. That is, it returns all the solutions that might be correct given guess_word and its score from Wordle.

Enough. See you next time!