I made a smart Hangman game in Python and now want your opinion about the readability and code quality of this script. I want to improve my overall coding style. So I am happy to hear improvements for naming conventions, proper docstrings and logical/runtime enhancements.
So lets start with a quick explanation how my program works.
- The user chooses a word from a dictionary
- The program gets the possible words from the list based on the length
- The program calculates the character frequency before asking
- If the asked character is not in the secret word the possible word list gets updated again
- If the asked character is in the list the program asks for indices and based on that input the word list gets updated again
This concept works very good. The only problem is if several characters occurs with the same count the program asks in alphabetically order, but this can't be optimized I guess and is acceptable.
Let´s get through the functions.
Here I load the vocabulary file:
def read_vocabuary_file(path):
"""
Function to read the vocabulary file
:param path: Path to file
:return: List with words
"""
words = []
with open(path) as file:
for line in file:
words.append(line.rstrip().lower())
return words
This function checks if the word is in the list
def check_word(word, words):
"""
Helper function to check if choosen word is in list
:param word: Word choosed by user
:param words: Word list
:return: True if word is in list - False if not
"""
if word in words:
return True
else:
return False
Get the character frequency
def get_char_frequency(words):
"""
Function to get the frequency of each character in the word list
:param words: Word list
:return: Frequency dict
"""
char_counts = Counter(letter for word in words for letter in word)
return char_counts
Function to get words with equal length as the hidden word
def get_equal_words(word_length, words):
"""
Function to get possible words with same length
:param word_length: The length of the user word
:param words: List of words
:return: Possible words with equal length
"""
possible_words = [word for word in words if len(word) == word_length]
return possible_words
Update function to remove words with wrong character
def filter_wrong_characters(character, words):
"""
Function to update the word list and remove words where asked character is not in
:param character: The character for that the program asked
:param words: Current word list
:return: Updated word list
"""
possible_words = [word for word in words if character not in word]
return possible_words
Function to get words where correct character is at a given index
def filter_correct_characters(character, indices, words):
"""
Function to update the word list with potential words
:param character: The character for that the program asked
:param indices: The indicies where the characters are
:param words: Current word list
:return: Updated word list
"""
possible_words = []
for idx in indices:
possible_words += [word for word in words if word[int(idx)] == character]
return list(set(possible_words))
Helper function to print the current game state
def print_screen(your_word, wrong_guess):
"""
Helper function to print the current game state
:param your_word: The masked user word
:return: None
"""
print(HANGMAN[wrong_guess])
print(' '.join(str(i) for i in your_word))
for i in range(len(your_word)):
print(i, end=" ")
print()
Helper function to clear the screen for better readability
def clear_screen():
"""
Helper function to clear terminal
:return: None
"""
os.system('cls' if os.name == 'nt' else 'clear')
Finally here is the full source code
from collections import Counter
import os
HANGMAN = ['''
+---+
| |
|
|
|
|
=========''', '''
+---+
| |
O |
|
|
|
=========''', '''
+---+
| |
O |
| |
|
|
=========''', '''
+---+
| |
O |
/| |
|
|
=========''', '''
+---+
| |
O |
/|\ |
|
|
=========''', '''
+---+
| |
O |
/|\ |
/ |
|
=========''', '''
+---+
| |
O |
/|\ |
/ \ |
|
=========''']
def read_vocabuary_file(path):
"""
Function to read the vocabulary file
:param path: Path to file
:return: List with words
"""
words = []
with open(path) as file:
for line in file:
words.append(line.rstrip().lower())
return words
def check_word(word, words):
"""
Helper function to check if choosen word is in list
:param word: Word choosed by user
:param words: Word list
:return: True if word is in list - False if not
"""
if word in words:
return True
else:
return False
def get_char_frequency(words):
"""
Function to get the frequency of each character in the word list
:param words: Word list
:return: Frequency dict
"""
char_counts = Counter(letter for word in words for letter in word)
return char_counts
def get_equal_words(word_length, words):
"""
Function to get possible words with same length
:param word_length: The length of the user word
:param words: List of words
:return: Possible words with equal length
"""
possible_words = [word for word in words if len(word) == word_length]
return possible_words
def filter_wrong_characters(character, words):
"""
Function to update the word list and remove words where asked character is not in
:param character: The character for that the program asked
:param words: Current word list
:return: Updated word list
"""
possible_words = [word for word in words if character not in word]
return possible_words
def filter_correct_characters(character, indices, words):
"""
Function to update the word list with potential words
:param character: The character for that the program asked
:param indices: The indicies where the characters are
:param words: Current word list
:return: Updated word list
"""
possible_words = []
for idx in indices:
possible_words += [word for word in words if word[int(idx)] == character]
return list(set(possible_words))
def print_screen(your_word, wrong_guess):
"""
Helper function to print the current game state
:param your_word: The masked user word
:return: None
"""
print(HANGMAN[wrong_guess])
print(' '.join(str(i) for i in your_word))
for i in range(len(your_word)):
print(i, end=" ")
print()
def clear_screen():
"""
Helper function to clear terminal
:return: None
"""
os.system('cls' if os.name == 'nt' else 'clear')
if __name__ == '__main__':
words = read_vocabuary_file('vocabulary.txt')
word = str(input('Please enter a word which is in the vocabulary: \t'))
word_in_list = False
while word_in_list == False:
if check_word(word, words):
word_in_list = True
else:
word = str(input('Word not in list, please choose another one: \t'))
words = get_equal_words(len(word), words)
playing = True
your_word = ["_"] * len(word)
wrong_guess = 0
clear_screen()
print_screen(your_word, wrong_guess)
while playing:
word_counts = get_char_frequency(words)
for char in ''.join(str(i) for i in your_word):
if char in word_counts:
del word_counts[char]
# try/except block if user messed up the input
# for example if he said no but the word contains the asked character
# breaks list of possible words
try:
most_common = word_counts.most_common()[0]
except IndexError:
print('Ops, something went wrong')
break
character_contained = str(input(f'Is there a [{most_common[0][0]}] ? - [Y]ES or [N]O :\t'))
while character_contained.lower() not in ['yes', 'y', 'no', 'n']:
print("Only yes/no is allowd")
character_contained = str(input(f'Is there a [{most_common[0][0]}] ? - [Y]ES or [N]O :\t'))
if character_contained.lower() in ['yes', 'y']:
valid_indices = False
while not valid_indices:
indices = list(input('At which indices? Please start with 0 and separate them with a space:').split())
for idx in indices:
try:
your_word[int(idx)] = most_common[0][0]
valid_indices = True
except:
print("Something went wrong. Please check your input again")
words = filter_correct_characters(most_common[0][0], indices, words)
else:
words = filter_wrong_characters(most_common[0][0], words)
wrong_guess += 1
clear_screen()
print_screen(your_word, wrong_guess)
if wrong_guess == 6:
clear_screen()
print('GAME OVER')
print_screen(your_word, wrong_guess)
playing = False
if len(words) == 1:
clear_screen()
print('I found your word:')
your_word = words[0]
print_screen(your_word, wrong_guess)
playing = False
I can't upload the vocabulary file, you can download it here
This can't ge a bit complicated to understand but I didn't found a better solution.
word_counts = get_char_frequency(words)
for char in ''.join(str(i) for i in your_word):
if char in word_counts:
del word_counts[char]
Since I always assign the first element of the frequency dict to the variable most_common
I first have to delete the characters which the program already guesses correctly. If not the program asks for a character it already founds over and over again.