Python Hangman Game
Published: 2021-07-08 09:24:33

2021.07.08 16:38

# Hangman game

## Introduction
I wrote a command-line word-guessing game using basic features of Python. It is a project under the Python Bundle - Python I of [codingnomads](https://codingnomads.co/).

I wrote this follow-up article as my learning log.

## 1) Generate a random word
*reference: [Geeksforgeeks - pulling a random word or a string from a line in a text file](https://www.geeksforgeeks.org/pulling-a-random-word-or-string-from-a-line-in-a-text-file-in-python/)*

Since I have some minimal experiences with JavaScript before, I believe that I have the ability to walk the extra mile and to do the optional part.

I wrote a list of words separated by space and store them in a .txt file.

```
hangman.txt
programming strong codingnomads rewarding frustrating
```

The list of words in the .txt file is treated like a list. Random integer is used to randomly pick one word out of the list. 

```
import random
with open(r"C:\Users\User\Desktop\codingnomads\Python-101\projects\hangman.txt", "r") as file:
    data = file.read()
    words = data.split()
    words_pos = random.randint(0, len(words) - 1)
    print(f"position: {words_pos}")
    print(f"word at position: {words[words_pos]}")
```

### 1.1) details with reading the path
*reference: [stackoverflow - unicode escape](https://stackoverflow.com/questions/37400974/unicode-error-unicodeescape-codec-cant-decode-bytes-in-position-2-3-trunca)*

An error will occur if the path is not treated by unicode escape `r"path"`

## 2) Split a word into a list of character
*reference: [stackoverflow - is there a function in python to split a word into a list](https://stackoverflow.com/questions/113655/is-there-a-function-in-python-to-split-a-word-into-a-list)*

The syntax of Python is really intuitive. The syntax to split a word into a list of character is simply `list(word)`.

### 2.1) why list? why not empty string?

Because strings in Python are immutable, which means that I cannot change the content of an empty string. List is a simple structure which supports item assignment.

```
# container = "    "
# container[1] = "a"
# TypeError: 'str' object does not support item assignment
```
    
## 3) Check input

I use a while-loop inside the main while-loop to check user input.

The user can only input:
- alphabet `guess.isalpha()`
- a single alphabet `len(guess) == 1`
- alphabet not yet guessed correctly `guess in container`
```
while container != word:
	guess = input("Guess an alphabet: ").lower()
	while not guess.isalpha() or len(guess) > 1:
  		guess = input("We only allow single character input. Please guess again: ").lower()
  while guess.lower() in container:
  		guess = input(f"{guess.upper()} is already guessed. Please guess another alphabet: ").lower()
```
### 3.1) remember indentation level

Indentation level is important in Python. 

If the `guess in container` loop is indented below `not guess.isalpha() or len(guess) > 1` loop, Python will only check guess which is not an alphabet and with a length longer than 1. The consequences are that Python will only check the bad guesses are in container, and the answer will always be no. This makes the snippet not meaningful.

## 4) Create a counter for how many attempts a user has taken

Position is important. Although Python does not have a strict namespace rule meaning that variable declared inside loop can be accessed outside loop, where to declare a variable is still important.

Remeber to declare the counter outside the main while-loop `while container != word`, otherwise the counter will be re-assigned the value of 0 in every loop. This means the counter not meaningful as it only shows `0` then `1` then `0`.

## 5) Return all index with same value 
*reference: [codegrepper - how to return all index with same value in list](https://www.codegrepper.com/code-examples/python/how+to+return+all+index+with+same+value+in+list)*

Codegrepper suggests a one-line solution with `enumerate()`.

`indices = [i for i, x in enumerate(word) if x == guess]`

*reference: [stackoverflow - index of duplicate items in a python list](https://stackoverflow.com/questions/5419204/index-of-duplicates-items-in-a-python-list)*

Stackoverflow suggests a simple answer which uses the counter as the index to locate the element with the desired value.

```
if elem in string_list:
     counter = 0
     elem_pos = []
     for i in string_list:
         if i == elem:
             elem_pos.append(counter)
         counter = counter + 1
     print(elem_pos)
```
## 6) Assign new value to container list
Now I am working with mutable list not immutable string. Therefore, value of an element in a string can be reassigned.

```
for index in indices:
		if word[index] == guess:
				container[index] = guess
```
## 7) Display container
It is handy to know that `print()` function takes the argument of `sep` (seperation) and `end` (ending). I change the default ending from `"\n"` to `""` such that every element of the container will be displaced in the same line.

```
for char in container:
	if char.isalpha():
		print(f" {char} ", sep=" ", end="")
	else:
		print(" _  ", sep=" ", end="")
print("\n")
```

## 8) End of game message
Python can easily turn a string into a list by `list(str)`, and turn a list into a string by `"".join(list)`.

I want a string here because I want to apply the `str.format` and `str.upper()` methods to make my output prettier.

```
if container == word:
    print(f"""
            Congratulation! You made it!
            The word is {"".join(word).upper()!r}!
    """)

else:
    print(f"""
            Your guesses are close.
            The word is {"".join(word).upper()!r}.
    """)
```

## Full code
```
import random
with open(r"C:\Users\User\Desktop\codingnomads\Python-101\projects\hangman.txt", "r") as file:
    data = file.read()
    words = data.split()
    words_pos = random.randint(0, len(words) - 1)
    # print(f"position: {words_pos}")
    # print(f"word at position: {words[words_pos]}")

word = list(words[words_pos])
container = list(len(word) * " ")
limit = len(word) + 5
counter = 0 
# print(word)
# print(container)

print(f"""
        Welcome! Let's play Hangman, the word-guessing game.
        You will have {limit} chances to guess the word
        Let's begin! 
""")

for char in word:
    print(" _ ", sep=" ", end="")
print("\n\n\n")

while container != word:
    guess = input("Guess an alphabet: ").lower()
    while not guess.isalpha() or len(guess) > 1:
        guess = input("We only allow single character input. Please guess again: ").lower()
    while guess.lower() in container:
        guess = input(f"{guess.upper()} is already guessed. Please guess another alphabet: ").lower()
    counter += 1
    print(f"Number of guessess: {counter}")
    
		if counter > limit:
        print(f"You have already guessed {counter} times. You run out of time.")
        break

    for char in word:
        if guess == char:
            indices = [i for i, x in enumerate(word) if x == guess]
            # print(indices)
            for index in indices:
                if word[index] == guess:
                    container[index] = guess

    for char in container:
        if char.isalpha():
            print(f" {char} ", sep=" ", end="")
        else:
            print(" _  ", sep=" ", end="")
    print("\n")

if container == word:
    print(f"""
            Congratulation! You made it!
            The word is {"".join(word).upper()!r}!
    """)
    # how to join list into a string
    # https://www.simplilearn.com/tutorials/python-tutorial/list-to-string-in-python
else:
    print(f"""
            Your guesses are close.
            The word is {"".join(word).upper()!r}.
    """)
```

Thank you for reading my article. Please feel free to leave any comment.