# WORD GAME
# In the word game, a number of letters are presented to the player.The player is expected
# to derive a word from these letters that come from given three command line arguments.
# The letters should be represented according to alphabetical order. The program should be
# run with three command line arguments. Otherwise, it should warn the user, then exit the
# game. The player does not have to use all letters to derive a correct word. The player has
# one chance to find a correct word. If the player finds the longest word in the argument list,
# he/she gets 50 points. If the player finds the shortest word in argument list, he/she gets 10
# points, otherwise he/she gets 30 points. If the player finds a word which is not from the list,
# he/she loses the game.


import os
import sys


letters_allowed = "abcçdefgğhıijklmnoöprqsştuüvwxyz"


class WordChecker:
    """An object for word-checking."""

    def __init__(self, words: list) -> None:
        self.__words = words

    def words_are_valid(self) -> bool:
        """Checks whether the words have any character other than letters in it."""
        for word in self.__words:
            for letter in word:
                if letter not in letters_allowed and letter not in letters_allowed.upper():
                    Presenter.complain_there_are_illegal_characters(letter, word)
                    return False
        return True

    def word_lengths_are_different(self) -> bool:
        """Checks whether the lengths of the words are different, to be able to correctly give points to the contestant."""
        word_len_list = []
        for word in self.__words:
            word_len_list.append(len(word))
        return len(word_len_list) == len(set(word_len_list))

    def there_are_three_words(self) -> bool:
        """Checks whether there are 3 words provided excluding filename argument."""
        if len(self.__words) != 3:
            return False
        return True

    def illegal_characters_used(self, guess: str) -> list:
        """Returns the letters used in guess outside the letter list provided."""
        letters = []
        characters_used_outside_the_list = []
        for word in self.__words:
            for character in word:
                letters.append(character)

        for character in guess:
            if character not in letters:
                characters_used_outside_the_list.append(character)

        return characters_used_outside_the_list


class Sorter:
    """A utility class for sorting lists."""

    @staticmethod
    def sort_letters(letter_list: list) -> list:
        """Returns sorted letter list according to English and Turkish alphabet combined."""
        sorted_letter_list = []
        for letter in letters_allowed:
            for our_letter in letter_list:
                if our_letter == letter:
                    sorted_letter_list.append(letter)
        return sorted_letter_list

    @staticmethod
    def get_word_list_sorted_by_length(words: list) -> list:
        """Returns a list which sorted by word lengths with longest word being the first."""
        sorted_word_list = sorted(words, key=len, reverse=True)
        return sorted_word_list


class WordAnalyser:
    """An object for analysing similarity between words."""

    def __init__(self, words: list) -> None:
        self.__words = words

    def get_minimum_edits_based_similarity(self, guess: str) -> list:
        word_and_similarity = ["", 0]

        for word in self.__words:
            edit_distance = WordAnalyser.get_edit_distance(guess, word)
            similarity = (1.0 - edit_distance / (max([len(guess), len(word)]))) if edit_distance < max([len(guess), len(word)]) else 0.0
            if similarity > word_and_similarity[1]:
                word_and_similarity = [word, similarity]

        return word_and_similarity

    @staticmethod
    def get_edit_distance(source: str, target: str) -> int:
        """
        Levenshtein distance is a measure of similarity between two strings.
        It calculates the minimum one-character edits (deletions, insertions, and substitutions) required to transform one string into another.

        I have used the iterative algorithm of the Levenshtein distance (it is much more efficient compared to recursive one)
        to measure minimum number of one-character edits needed to be done to convert a string into another.

        1. A matrix is initialized measuring in the (m, n) cell the Levenshtein distance between the m-character prefix of one with the n-prefix of the other word.
        2. The matrix can be filled from the upper left to the lower right corner.
        3. Each jump horizontally or vertically corresponds to an insert or a delete, respectively.
        4. The cost is normally set to 1 for each of the operations.
        5. The diagonal jump can cost either one, if the two characters in the row and column do not match else 0, if they match. Each cell always minimizes the cost locally.
        6. This way the number in the lower right corner is the Levenshtein distance between both words.

        And, for all i and j, distance_matrix[i][j] would contain the Levenshtein 
        distance between the first i characters of source string and the 
        first j characters of target string.
        """

        rows = len(source) + 1
        columns = len(target) + 1
        distance_matrix = [[0 for x in range(columns)] for x in range(rows)]

        for i in range(1, rows):
            distance_matrix[i][0] = i

        for i in range(1, columns):
            distance_matrix[0][i] = i

        for column in range(1, columns):
            for row in range(1, rows):
                if source[row - 1] == target[column - 1]:
                    cost = 0
                else:
                    cost = 1
                distance_matrix[row][column] = min(distance_matrix[row - 1][column] + 1,  # deletion
                                                   distance_matrix[row][column - 1] + 1,  # insertion
                                                   distance_matrix[row - 1][column - 1] + cost)  # substitution
        
        return distance_matrix[row][column]


class Util:
    """A utility class."""

    @staticmethod
    def clear_screen() -> None:
        """Clears the screen, so that the contestant won't see the words."""
        os.system('cls' if os.name == 'nt' else 'clear')


class Presenter:
    """A utility class for print related jobs."""

    @staticmethod
    def print_banner() -> None:
        try:
            columns = os.get_terminal_size(0)[0]
        except OSError:
            try:
                columns = os.get_terminal_size(1)[0]
            except OSError:
                columns = 77  # For not taking a risk.

        if columns < 78:
            print("\n   ~ WORD GAME ~\n")
        else:
            print("\n")
            print("   ██╗    ██╗ ██████╗ ██████╗ ██████╗      ██████╗  █████╗ ███╗   ███╗███████╗")
            print("   ██║    ██║██╔═══██╗██╔══██╗██╔══██╗    ██╔════╝ ██╔══██╗████╗ ████║██╔════╝")
            print("   ██║ █╗ ██║██║   ██║██████╔╝██║  ██║    ██║  ███╗███████║██╔████╔██║█████╗  ")
            print("   ██║███╗██║██║   ██║██╔══██╗██║  ██║    ██║   ██║██╔══██║██║╚██╔╝██║██╔══╝  ")
            print("   ╚███╔███╔╝╚██████╔╝██║  ██║██████╔╝    ╚██████╔╝██║  ██║██║ ╚═╝ ██║███████╗")
            print("    ╚══╝╚══╝  ╚═════╝ ╚═╝  ╚═╝╚═════╝      ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝╚══════╝", end="\n\n")

    @staticmethod
    def print_letters(letters: list) -> None:
        print("  Guess the longest word (from the words previously provided) using letters below.")
        print("  Letters: {}".format(str(letters).replace("[", "").replace("]", "")))

    @staticmethod
    def complain_there_are_illegal_characters(letter: str, word: str) -> None:
        print("  The character \'{}\' is not a letter (or at least it does not exist in Turkish or English alphabet)."
              "\n  Therefore \"{}\" is not a word.".format(letter, word))

    @staticmethod
    def complain_there_are_not_three_words(number_of_words: int) -> None:
        print("  You must enter no more or less than 3 words as arguments. You entered {}.\n".format(number_of_words))

    @staticmethod
    def complain_word_lengths_are_indistinct() -> None:
        print("  The word lengths must be distinct for the program to be able to correctly give points to the contestant.\n")

    @staticmethod
    def complain_words_are_not_valid() -> None:
        print("  Since words are words, they must only contain letters.\n")

    @staticmethod
    def print_results(results: dict) -> None:
        guess = results.get("guess")
        words = results.get("words")
        points = results.get("points")
        word = results.get("word")
        similarity = results.get("similarity")
        illegal_characters_used = results.get("illegal_characters_used")

        if points == 50:
            print("\n  Congratulations!\n  You have correctly guessed the longest word \"{}\"  \n  from the word list {}.\n  You won {} points!"
                  .format(guess, words, points))
        elif points == 30:
            print("\n  You guessed it!\n  You have correctly guessed the middle-sized word \"{}\"\n  from the word list {}.\n  You won {} points!"
                  .format(guess, words, points))
        elif points == 10:
            print("\n  It's okay!\n  You have correctly guessed the shortest word \"{}\"\n  from the word list {}.\n  You won {} points!"
                  .format(guess,  words, points))
        else:
            print("\n  Your guess \"{}\" is not in the word list {}.".format(guess,  words))
            print("  Though, it was {}% similar to the word \"{}\".".format(round(similarity * 100, 1), word) if similarity > 0 else
                  "  Your guess has no similarity whatsoever with any word in the list.")
            if len(illegal_characters_used) > 0:
                print("  Also, the character \'{}\' you used is not in the list of letters provided.".format(illegal_characters_used[0])
                      if len(illegal_characters_used) == 1 else
                      "  Also, the characters {} you used are not in the list of letters provided."
                      .format(str(sorted(list(set(illegal_characters_used))))[1:-1]))
            print("  Nevertheless, you lost..." if similarity > 0 else "  You lost...")
        

class Game:
    """Game object. Handles game mechanics."""

    def __init__(self, arguments: list) -> None:
        self.__arguments = arguments
        self.__words = self.__get_word_list()
        self.__word_checker = WordChecker(self.__words)
        self.__word_analyser = WordAnalyser(self.__words)

    def __get_word_list(self) -> list:
        """Returns the word list for the arguments provided, by excluding the filename argument."""
        words = []
        for i in range(1, len(self.__arguments)):
            words.append(self.__arguments[i].lower())
        return words

    def start_game(self) -> None:

        Util.clear_screen()

        Presenter.print_banner()

        if not self.__word_checker.there_are_three_words():
            Presenter.complain_there_are_not_three_words(len(self.__words))
        elif not self.__word_checker.word_lengths_are_different():
            Presenter.complain_word_lengths_are_indistinct()
        elif not self.__word_checker.words_are_valid():
            Presenter.complain_words_are_not_valid()
        else:
            letters = []
            for word in self.__words:
                for letter in word:
                    letters.append(letter)
            letters = Sorter.sort_letters(letters)

            Presenter.print_letters(letters)

            guess = str(input("  Your guess: ")).lower()

            word_list_sorted_by_length = Sorter.get_word_list_sorted_by_length(self.__words)
            
            word, similarity = self.__word_analyser.get_minimum_edits_based_similarity(guess)[0], self.__word_analyser.get_minimum_edits_based_similarity(guess)[1]
            illegal_characters_used = self.__word_checker.illegal_characters_used(guess)

            if guess == word_list_sorted_by_length[0]:
                self.__points = 50
            elif guess == word_list_sorted_by_length[1]:
                self.__points = 30
            elif guess == word_list_sorted_by_length[2]:
                self.__points = 10
            else:
                self.__points = 0
            
            results = {
                "guess": guess,
                "words": self.__words,
                "points": self.__points,
                "word": word,
                "similarity": similarity,
                "illegal_characters_used": illegal_characters_used
            }

            Presenter.print_results(results)

            Game.__end_game()

    @staticmethod
    def __end_game() -> None:
        input("\n  Press enter key to terminate the program...")
        exit(0)


def main(arguments: list) -> None:
    """
    Main method will be executed when the program starts.
    This way, the program will not be executed if it's imported as a module.
    """
    game = Game(arguments)
    game.start_game()


if __name__ == "__main__":
    main(sys.argv)
else:
    exit("This program is not suitable for importing as a module.")
    
# WORD GAME
# In the word game, a number of letters are presented to the player.The player is expected
# to derive a word from these letters that come from given three command line arguments.
# The letters should be represented according to alphabetical order. The program should be
# run with three command line arguments. Otherwise, it should warn the user, then exit the
# game. The player does not have to use all letters to derive a correct word. The player has
# one chance to find a correct word. If the player finds the longest word in the argument list,
# he/she gets 50 points. If the player finds the shortest word in argument list, he/she gets 10
# points, otherwise he/she gets 30 points. If the player finds a word which is not from the list,
# he/she loses the game.


import os
import sys


letters_allowed = "abcçdefgğhıijklmnoöprqsştuüvwxyz"


class WordChecker:
    """An object for word-checking."""

    def __init__(self, words: list) -> None:
        self.__words = words

    def words_are_valid(self) -> bool:
        """Checks whether the words have any character other than letters in it."""
        for word in self.__words:
            for letter in word:
                if letter not in letters_allowed and letter not in letters_allowed.upper():
                    Presenter.complain_there_are_illegal_characters(letter, word)
                    return False
        return True

    def word_lengths_are_different(self) -> bool:
        """Checks whether the lengths of the words are different, to be able to correctly give points to the contestant."""
        word_len_list = []
        for word in self.__words:
            word_len_list.append(len(word))
        return len(word_len_list) == len(set(word_len_list))

    def there_are_three_words(self) -> bool:
        """Checks whether there are 3 words provided excluding filename argument."""
        if len(self.__words) != 3:
            return False
        return True

    def illegal_characters_used(self, guess: str) -> list:
        """Returns the letters used in guess outside the letter list provided."""
        letters = []
        characters_used_outside_the_list = []
        for word in self.__words:
            for character in word:
                letters.append(character)

        for character in guess:
            if character not in letters:
                characters_used_outside_the_list.append(character)

        return characters_used_outside_the_list


class Sorter:
    """A utility class for sorting lists."""

    @staticmethod
    def sort_letters(letter_list: list) -> list:
        """Returns sorted letter list according to English and Turkish alphabet combined."""
        sorted_letter_list = []
        for letter in letters_allowed:
            for our_letter in letter_list:
                if our_letter == letter:
                    sorted_letter_list.append(letter)
        return sorted_letter_list

    @staticmethod
    def get_word_list_sorted_by_length(words: list) -> list:
        """Returns a list which sorted by word lengths with longest word being the first."""
        sorted_word_list = sorted(words, key=len, reverse=True)
        return sorted_word_list


class WordAnalyser:
    """An object for analysing similarity between words."""

    def __init__(self, words: list) -> None:
        self.__words = words

    def get_minimum_edits_based_similarity(self, guess: str) -> list:
        word_and_similarity = ["", 0]

        for word in self.__words:
            edit_distance = WordAnalyser.get_edit_distance(guess, word)
            similarity = (1.0 - edit_distance / (max([len(guess), len(word)]))) if edit_distance < max([len(guess), len(word)]) else 0.0
            if similarity > word_and_similarity[1]:
                word_and_similarity = [word, similarity]

        return word_and_similarity

    @staticmethod
    def get_edit_distance(source: str, target: str) -> int:
        """
        Levenshtein distance is a measure of similarity between two strings.
        It calculates the minimum one-character edits (deletions, insertions, and substitutions) required to transform one string into another.

        I have used the iterative algorithm of the Levenshtein distance (it is much more efficient compared to recursive one)
        to measure minimum number of one-character edits needed to be done to convert a string into another.

        1. A matrix is initialized measuring in the (m, n) cell the Levenshtein distance between the m-character prefix of one with the n-prefix of the other word.
        2. The matrix can be filled from the upper left to the lower right corner.
        3. Each jump horizontally or vertically corresponds to an insert or a delete, respectively.
        4. The cost is normally set to 1 for each of the operations.
        5. The diagonal jump can cost either one, if the two characters in the row and column do not match else 0, if they match. Each cell always minimizes the cost locally.
        6. This way the number in the lower right corner is the Levenshtein distance between both words.

        And, for all i and j, distance_matrix[i][j] would contain the Levenshtein 
        distance between the first i characters of source string and the 
        first j characters of target string.
        """

        rows = len(source) + 1
        columns = len(target) + 1
        distance_matrix = [[0 for x in range(columns)] for x in range(rows)]

        for i in range(1, rows):
            distance_matrix[i][0] = i

        for i in range(1, columns):
            distance_matrix[0][i] = i

        for column in range(1, columns):
            for row in range(1, rows):
                if source[row - 1] == target[column - 1]:
                    cost = 0
                else:
                    cost = 1
                distance_matrix[row][column] = min(distance_matrix[row - 1][column] + 1,  # deletion
                                                   distance_matrix[row][column - 1] + 1,  # insertion
                                                   distance_matrix[row - 1][column - 1] + cost)  # substitution
        
        return distance_matrix[row][column]


class Util:
    """A utility class."""

    @staticmethod
    def clear_screen() -> None:
        """Clears the screen, so that the contestant won't see the words."""
        os.system('cls' if os.name == 'nt' else 'clear')


class Presenter:
    """A utility class for print related jobs."""

    @staticmethod
    def print_banner() -> None:
        try:
            columns = os.get_terminal_size(0)[0]
        except OSError:
            try:
                columns = os.get_terminal_size(1)[0]
            except OSError:
                columns = 77  # For not taking a risk.

        if columns < 78:
            print("\n   ~ WORD GAME ~\n")
        else:
            print("\n")
            print("   ██╗    ██╗ ██████╗ ██████╗ ██████╗      ██████╗  █████╗ ███╗   ███╗███████╗")
            print("   ██║    ██║██╔═══██╗██╔══██╗██╔══██╗    ██╔════╝ ██╔══██╗████╗ ████║██╔════╝")
            print("   ██║ █╗ ██║██║   ██║██████╔╝██║  ██║    ██║  ███╗███████║██╔████╔██║█████╗  ")
            print("   ██║███╗██║██║   ██║██╔══██╗██║  ██║    ██║   ██║██╔══██║██║╚██╔╝██║██╔══╝  ")
            print("   ╚███╔███╔╝╚██████╔╝██║  ██║██████╔╝    ╚██████╔╝██║  ██║██║ ╚═╝ ██║███████╗")
            print("    ╚══╝╚══╝  ╚═════╝ ╚═╝  ╚═╝╚═════╝      ╚═════╝ ╚═╝  ╚═╝╚═╝     ╚═╝╚══════╝", end="\n\n")

    @staticmethod
    def print_letters(letters: list) -> None:
        print("  Guess the longest word (from the words previously provided) using letters below.")
        print("  Letters: {}".format(str(letters).replace("[", "").replace("]", "")))

    @staticmethod
    def complain_there_are_illegal_characters(letter: str, word: str) -> None:
        print("  The character \'{}\' is not a letter (or at least it does not exist in Turkish or English alphabet)."
              "\n  Therefore \"{}\" is not a word.".format(letter, word))

    @staticmethod
    def complain_there_are_not_three_words(number_of_words: int) -> None:
        print("  You must enter no more or less than 3 words as arguments. You entered {}.\n".format(number_of_words))

    @staticmethod
    def complain_word_lengths_are_indistinct() -> None:
        print("  The word lengths must be distinct for the program to be able to correctly give points to the contestant.\n")

    @staticmethod
    def complain_words_are_not_valid() -> None:
        print("  Since words are words, they must only contain letters.\n")

    @staticmethod
    def print_results(results: dict) -> None:
        guess = results.get("guess")
        words = results.get("words")
        points = results.get("points")
        word = results.get("word")
        similarity = results.get("similarity")
        illegal_characters_used = results.get("illegal_characters_used")

        if points == 50:
            print("\n  Congratulations!\n  You have correctly guessed the longest word \"{}\"  \n  from the word list {}.\n  You won {} points!"
                  .format(guess, words, points))
        elif points == 30:
            print("\n  You guessed it!\n  You have correctly guessed the middle-sized word \"{}\"\n  from the word list {}.\n  You won {} points!"
                  .format(guess, words, points))
        elif points == 10:
            print("\n  It's okay!\n  You have correctly guessed the shortest word \"{}\"\n  from the word list {}.\n  You won {} points!"
                  .format(guess,  words, points))
        else:
            print("\n  Your guess \"{}\" is not in the word list {}.".format(guess,  words))
            print("  Though, it was {}% similar to the word \"{}\".".format(round(similarity * 100, 1), word) if similarity > 0 else
                  "  Your guess has no similarity whatsoever with any word in the list.")
            if len(illegal_characters_used) > 0:
                print("  Also, the character \'{}\' you used is not in the list of letters provided.".format(illegal_characters_used[0])
                      if len(illegal_characters_used) == 1 else
                      "  Also, the characters {} you used are not in the list of letters provided."
                      .format(str(sorted(list(set(illegal_characters_used))))[1:-1]))
            print("  Nevertheless, you lost..." if similarity > 0 else "  You lost...")
        

class Game:
    """Game object. Handles game mechanics."""

    def __init__(self, arguments: list) -> None:
        self.__arguments = arguments
        self.__words = self.__get_word_list()
        self.__word_checker = WordChecker(self.__words)
        self.__word_analyser = WordAnalyser(self.__words)

    def __get_word_list(self) -> list:
        """Returns the word list for the arguments provided, by excluding the filename argument."""
        words = []
        for i in range(1, len(self.__arguments)):
            words.append(self.__arguments[i].lower())
        return words

    def start_game(self) -> None:

        Util.clear_screen()

        Presenter.print_banner()

        if not self.__word_checker.there_are_three_words():
            Presenter.complain_there_are_not_three_words(len(self.__words))
        elif not self.__word_checker.word_lengths_are_different():
            Presenter.complain_word_lengths_are_indistinct()
        elif not self.__word_checker.words_are_valid():
            Presenter.complain_words_are_not_valid()
        else:
            letters = []
            for word in self.__words:
                for letter in word:
                    letters.append(letter)
            letters = Sorter.sort_letters(letters)

            Presenter.print_letters(letters)

            guess = str(input("  Your guess: ")).lower()

            word_list_sorted_by_length = Sorter.get_word_list_sorted_by_length(self.__words)
            
            word, similarity = self.__word_analyser.get_minimum_edits_based_similarity(guess)[0], self.__word_analyser.get_minimum_edits_based_similarity(guess)[1]
            illegal_characters_used = self.__word_checker.illegal_characters_used(guess)

            if guess == word_list_sorted_by_length[0]:
                self.__points = 50
            elif guess == word_list_sorted_by_length[1]:
                self.__points = 30
            elif guess == word_list_sorted_by_length[2]:
                self.__points = 10
            else:
                self.__points = 0
            
            results = {
                "guess": guess,
                "words": self.__words,
                "points": self.__points,
                "word": word,
                "similarity": similarity,
                "illegal_characters_used": illegal_characters_used
            }

            Presenter.print_results(results)

            Game.__end_game()

    @staticmethod
    def __end_game() -> None:
        input("\n  Press enter key to terminate the program...")
        exit(0)


def main(arguments: list) -> None:
    """
    Main method will be executed when the program starts.
    This way, the program will not be executed if it's imported as a module.
    """
    game = Game(arguments)
    game.start_game()


if __name__ == "__main__":
    main(sys.argv)
else:
    exit("This program is not suitable for importing as a module.")
    
# WORD GAME # In the word game, a number of letters are presented to the player.The player is expected # to derive a word from these letters that come from given three command line arguments. # The letters should be represented according to alphabetical order. The program should be # run with three command line arguments. Otherwise, it should warn the user, then exit the # game. The player does not have to use all letters to derive a correct word. The player has # one chance to find a correct word. If the player finds the longest word in the argument list, # he/she gets 50 points. If the player finds the shortest word in argument list, he/she gets 10 # points, otherwise he/she gets 30 points. If the player finds a word which is not from the list, # he/she loses the game. An object for word-checking. Checks whether the words have any character other than letters in it. Checks whether the lengths of the words are different, to be able to correctly give points to the contestant. Checks whether there are 3 words provided excluding filename argument. Returns the letters used in guess outside the letter list provided. A utility class for sorting lists. Returns sorted letter list according to English and Turkish alphabet combined. Returns a list which sorted by word lengths with longest word being the first. An object for analysing similarity between words. Levenshtein distance is a measure of similarity between two strings.
         It calculates the minimum one-character edits (deletions, insertions, and substitutions) required to transform one string into another.
 
         I have used the iterative algorithm of the Levenshtein distance (it is much more efficient compared to recursive one)
         to measure minimum number of one-character edits needed to be done to convert a string into another.
 
         1. A matrix is initialized measuring in the (m, n) cell the Levenshtein distance between the m-character prefix of one with the n-prefix of the other word.
         2. The matrix can be filled from the upper left to the lower right corner.
         3. Each jump horizontally or vertically corresponds to an insert or a delete, respectively.
         4. The cost is normally set to 1 for each of the operations.
         5. The diagonal jump can cost either one, if the two characters in the row and column do not match else 0, if they match. Each cell always minimizes the cost locally.
         6. This way the number in the lower right corner is the Levenshtein distance between both words.
 
         And, for all i and j, distance_matrix[i][j] would contain the Levenshtein 
         distance between the first i characters of source string and the 
         first j characters of target string. # deletion # insertion # substitution A utility class. Clears the screen, so that the contestant won't see the words. A utility class for print related jobs. # For not taking a risk. Game object. Handles game mechanics. Returns the word list for the arguments provided, by excluding the filename argument. Main method will be executed when the program starts.
     This way, the program will not be executed if it's imported as a module.