python-data-structures-evil-hangman / py_evil_hangman / word_guesser.py
word_guesser.py
Raw
from itertools import groupby
from random import randrange

class GameState():
    def __init__(self, guesses, word_length, debug):
        self.word_length = word_length
        self.guessed = []
        self.guesses = guesses  # starting number of guesses
        self.done_letters = 0
        self.word = ["-"] * word_length
        self.debug = debug

    def print_state(self, wordsRemaining):
        wrStr = "" if not self.debug else f" ({wordsRemaining} words remain)"
        print(f"\nYou have {self.guesses} incorrect guesses left{wrStr}.")
        print("Used letters: {}".format(" ".join(self.guessed)))
        print("Word: {}".format("".join(self.word)))


# an abstract class defining the word guesser interface
class WordGuesser():
    def __init__(self, guesses, words_file, debug):
        self.debug = debug
        self.guesses = guesses

    def reset(self, word_length):
        self.state = GameState(self.guesses, word_length, self.debug)

    def getGuess(self):
        # return the next best guess
        # use self.gamestate to inquire about the current status of the game
        raise NotImplementedError  # Subclasses should override this method


class WordGuesserHuman(WordGuesser):
    def getGuess(self):
        while True:
            inp = input("Enter guess: ").lower()
            if len(inp) != 1 or inp in self.state.guessed or not inp.isalpha():
                print("Invalid guess.")
            else:
                break
        return inp


# CHECK GAMEMANAGER.PY FOR EXTRA INSTRUCTIONS TO DEPLOY THE AI GUESSER
class WordGuesserAI():
    def __init__(self, guesses, words_file, debug):
        # Read dictionary.txt file as a list (comma-separated not \n separated) then generate
        # a dictionary with each unique word length as a key, and the corresponding word list
        # in the values

        # Step 0: Initialize variables to be used within the class
        self.alphabet = []
        self.filtered_list = None
        self.debug = debug
        self.guesses = guesses

        # Step 1: import file to list
        with open(words_file) as file_obj:
            words = file_obj.read().split()

        # Step 2: generate a dictionary where keys represent a unique word length group
        self.dictionary_by_length = {k: list(g) for k, g in groupby(sorted(words, key=len), len)}

    def reset(self, word_length):
        self.state = GameState(self.guesses, word_length, self.debug)
        # Capture word group of length word_length in self.filtered_list from the dictionary of all possible
        # lengths self.dictionary_by_length defined in the function __init__
        self.filtered_list = self.dictionary_by_length[word_length]

        # Define the alphabet to use to keep track of remaining letter options to guess
        self.alphabet = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's',
                         't', 'u', 'v', 'w', 'x', 'y', 'z']

    def getGuess(self):
        # Steps to generate a letter guess to minimize adversary response each time:
        # 1. Loop over the alphabet variable (which contains remaining letter options) and simulate the adversary response
        #    for each letter (which should be the longest list possible) using guess() and store all simulation lengths in a list.
        #    Documentation for the guess() is available in the word_maker.py file in the current directory
        # 2. Store the minimum and maximum length of adversary responses and, if both are equal (meaning all possible 
        #    guesses return the same adversary response), the program chooses a random letter from the alphabet. Otherwise,
        #    the program returns the letter with minimum adversary response length

        def guess(guess_letter, words, guesser):

            def dashed_word(word, letter):
                # This function takes a word and a letter then returns its dashed version
                # (e.g. dashed_word('act','a') returns 'a--')
                dashed_word = ''
                for single_letter in word:  # loop over each character in word and either append letter or - to dashed_word
                    if single_letter == str(letter):
                        dashed_word += single_letter
                    else:
                        dashed_word += '-'
                return dashed_word

            def letter_combinations(letter, words):
                # This function takes the guessed letter and the list of remaining words, loops over every word in the list
                # and places it under a relevant combination
                dictionary_by_combination = {}
                for word in words:
                    dash = dashed_word(word, letter)  # get the dashed theme of the word given the guessed letter
                    if dash in dictionary_by_combination:  # prevent duplication of keys inside the dictionary
                        dictionary_by_combination[dash].append(word)
                    else:
                        dictionary_by_combination[dash] = [word]
                return dictionary_by_combination

            def max_key(dictionary):
                # This function returns the combination with the most remaining words while choosing the least number of
                # letters possible (including no letters). It returns the combination in the format e.g. '--a-'
                max_count = max(
                    len(v) for v in dictionary.values())  # stores the maximum length of a list under a dict key
                max_key = [k for k, v in dictionary.items() if
                           len(v) == max_count]  # returns a list of keys with max items
                dash_count = []
                for word in max_key:  # loops over every max key choosing the least amount of letters possible
                    dash_count.append(word.count('-'))
                max_dash_count_index = dash_count.index(
                    max(dash_count))  # get index of min letters (always a single value)
                return max_key[max_dash_count_index]

            options_dictionary = letter_combinations(guess_letter, words)
            combination_choice = max_key(options_dictionary)
            resulting_list = options_dictionary[combination_choice]
            if guesser:
                return len(resulting_list)
            else:
                return resulting_list

        def find_optimum_guess(alphabet, words):

            optimum_lengths = []
            for letter in alphabet:
                optimum_lengths.append(guess(letter, words, True))
            min_value = min(optimum_lengths)
            max_value = max(optimum_lengths)
            if min_value == max_value:
                random_index = randrange(0, len(alphabet), 1)
                return alphabet[random_index]
            else:
                index_of_least_error = optimum_lengths.index(min_value)
                return alphabet[index_of_least_error]

        guessed_letter = find_optimum_guess(self.alphabet, self.filtered_list)
        self.alphabet.remove(guessed_letter)
        self.filtered_list = guess(guessed_letter, self.filtered_list, False)
        return guessed_letter