DEV Community

Coding With Patrik
Coding With Patrik

Posted on • Originally published at codingwithpatrik.dev

How to Code Hangman in Go

Hangman is a classic word-guessing game. In this tutorial we will create it in Go.

How to play

In case you don't know how to play hangman, here is how it works:

  1. The computer picks a random word.
  2. The word is hidden as underscores.
  3. Guess one letter at a time.
  4. Correct guesses reveal letters.
  5. Incorrect guesses draw the hangman.
  6. Win by guessing the word before the hangman is fully drawn.

Building the game

Initialize the Project

Start by creating a new Go project:

go mod init hangman
Enter fullscreen mode Exit fullscreen mode

Create a main.go file:

package main
func main() {}
Enter fullscreen mode Exit fullscreen mode

Set Up the Game

Add the following code to the main.go file.

package main

import (
    "fmt"
)

func main() {
    word := "golang"
    attempts := 6
    currentWordState := initializeCurrentWordState(word)

    fmt.Println("Welcome to Hangman!")
    displayCurrentState(currentWordState, attempts)
}
Enter fullscreen mode Exit fullscreen mode

Here we have the word we will be guessing for now it's hardcoded to "golang". Later we will read this from a file. Other variables we have is the number of attempts, the current state of the word. We also display a welcome message and a function to display the current state of the word with remaining attempts.

Next we will create a function to initialize the current word state.

func initializeCurrentWordState(word string) []string {
    currentWordState := make([]string, len(word))
    for i := range currentWordState {
        currentWordState[i] = "_"
    }
    return currentWordState
}
Enter fullscreen mode Exit fullscreen mode

This function creates a slice of the same length as the word and fills it with underscores.

After that we will create a function to display the current state of the word and the number of attempts left.

func displayCurrentState(currentWordState []string, attempts int) {
    fmt.Println("Current word state:", strings.Join(currentWordState, " "))
    fmt.Println("Attempts left:", attempts)
}
Enter fullscreen mode Exit fullscreen mode

Dont forget to import the strings and fmt packages at the top of the file.

Our code should now look like this.

import (
    "fmt"
    "strings"
)
package main

func main() {
    word := "golang"
    attempts := 6
    currentWordState := initializeCurrentWordState(word)


    fmt.Println("Welcome to Hangman!")
    displayCurrentState(currentWordState, attempts)
}

func displayCurrentState(currentWordState []string, attempts int) {
    fmt.Println("Current word state:", strings.Join(currentWordState, " "))
    fmt.Println("Attempts left:", attempts)
}

func initializeCurrentWordState(word string) []string {
    currentWordState := make([]string, len(word))
    for i := range currentWordState {
        currentWordState[i] = "_"
    }
    return currentWordState
}

Enter fullscreen mode Exit fullscreen mode

if we run this.

go run ./
Enter fullscreen mode Exit fullscreen mode

we will get the following output.

Welcome to Hangman!
Current word state: _ _ _ _ _ _
Attempts left: 6
Enter fullscreen mode Exit fullscreen mode

Read User Input & Game Loop

Add bufio and os to the import at the top of the file.

import (
    "bufio"
    "fmt"
    "os"
    "strings"
)
Enter fullscreen mode Exit fullscreen mode

Declare a new scanner variable under the other variables.

word := "golang"
attempts := 6
currentWordState := initializeCurrentWordState(word)
scanner := bufio.NewScanner(os.Stdin)
Enter fullscreen mode Exit fullscreen mode

Next we will create a function to get user input.

func getUserInput(scanner *bufio.Scanner) string {
    scanner.Scan()
    return scanner.Text()
}
Enter fullscreen mode Exit fullscreen mode

This function will read the user input from the scanner and return it.

After that lets create our game loop and function to validate user input. Add it to the main function.

for attempts > 0 {
    displayCurrentState(currentWordState, attempts)
    userInput := getUserInput(scanner)
    if !isValidInput(userInput) {
        fmt.Println("Invalid input. Please enter a single letter.")
        continue
    }
}
Enter fullscreen mode Exit fullscreen mode

It will continue until the user either wins or loses.

Lets create the isValidInput function and import unicode/utf8 at the top of the file.

import (
    "bufio"
    "fmt"
    "os"
    "strings"
    "unicode/utf8"
)
Enter fullscreen mode Exit fullscreen mode
func isValidInput(input string) bool {
    return utf8.RuneCountInString(input) == 1
}
Enter fullscreen mode Exit fullscreen mode

this function will check if the input is a single letter.

Check User Input

Create a new variable called guessedLetters and initialize it.

word := "golang"
attempts := 6
currentWordState := initializeCurrentWordState(word)
scanner := bufio.NewScanner(os.Stdin)
guessedLetters := make(map[string]bool)
Enter fullscreen mode Exit fullscreen mode

This will be a map to store the guessed letters.

Next we will check if the user has already guessed the letter. If it's already guessed we continue the loop. Else we add it to the guessed letters.

if guessedLetters[userInput] {
    fmt.Println("You've already guessed that letter.")
    continue
}

guessedLetters[userInput] = true
Enter fullscreen mode Exit fullscreen mode

After that we will check if the guess is correct and update the current word state.

correctGuess := updateGuessed(word, currentWordState, userInput)
Enter fullscreen mode Exit fullscreen mode

Lets create the updateGuessed function.

func updateGuessed(word string, guessed []string, letter string) bool {
    correctGuess := false
    for i, char := range word {
        if string(char) == letter {
            guessed[i] = letter
            correctGuess = true
        }
    }
    return correctGuess
}
Enter fullscreen mode Exit fullscreen mode

The updateGuessed function checks if a guessed letter is in the word, replaces underscores with the letter at the correct positions if found, and returns true or false based on whether the letter is found.

If the guess is incorrect we will decrement the attempts. Add this to the end of the game loop.

if !correctGuess {
    attempts--
}
Enter fullscreen mode Exit fullscreen mode

Display Hangman State

Next we will display the hangman state. After the if statement that checks if the guess is correct add the following code.

displayHangman(6 - attempts)
Enter fullscreen mode Exit fullscreen mode

Create a new file called hangman_states.go and add the following code.

package main

var hangmanStates = []string{
    `
  +---+
  |   |
      |
      |
      |
      |
=========
`,
    `
  +---+
  |   |
  O   |
      |
      |
      |
=========
`,
    `
  +---+
  |   |
  O   |
  |   |
      |
      |
=========
`,
    `
  +---+
  |   |
  O   |
 /|   |
      |
      |
=========
`,
    `
  +---+
  |   |
  O   |
 /|\  |
      |
      |
=========
`,
    `
  +---+
  |   |
  O   |
 /|\  |
 /    |
      |
=========
`,
    `
  +---+
  |   |
  O   |
 /|\  |
 / \  |
      |
=========
`}
Enter fullscreen mode Exit fullscreen mode

This file containt a slice of strings representing the hangman states.

Next create the displayHangman function. It will be a simple function that prints the hangman state.

func displayHangman(incorrectGuesses int) {
    if incorrectGuesses >= 0 && incorrectGuesses < len(hangmanStates) {
        fmt.Println(hangmanStates[incorrectGuesses])
    }
}
Enter fullscreen mode Exit fullscreen mode

We check if the incorrect guesses is within the range of the hangman states and print the corresponding state.

Or code in main.go should now look like this.

package main

func main() {
    word := "golang"
    currentWordState := initializeCurrentWordState(word)
    attempts := 6
    guessedLetters := make(map[string]bool)
    scanner := bufio.NewScanner(os.Stdin)

    fmt.Println("Welcome to Hangman!")

    for attempts > 0 {
        displayCurrentState(currentWordState, attempts)
        userInput := getUserInput(scanner)

        if !isValidInput(userInput) {
            fmt.Println("Invalid input. Please enter a single letter.")
            continue
        }

        if guessedLetters[userInput] {
            fmt.Println("You've already guessed that letter.")
            continue
        }

        guessedLetters[userInput] = true

        correctGuess := updateGuessed(word, currentWordState, userInput)

        if !correctGuess {
            attempts--
        }

        displayHangman(6 - attempts)
    }
}
Enter fullscreen mode Exit fullscreen mode

Lets run the code and see what happens. We can now see the current state of the word and the hangman state is updated after each guess. We also get a message if we don't enter a valid input and if we have guessed a letter before.

Check if the word is guessed and if the user has lost

Now we need to check if the word is guessed or if the user has lost.

if isWordGuessed(currentWordState, word) {
    fmt.Println("Congratulations! You've guessed the word:", word)
    return
}

if attempts == 0 {
    fmt.Println("Game over! The word was:", word)
    return
}
Enter fullscreen mode Exit fullscreen mode

Add this to the end of the game loop.

Lets create the isWordGuessed function.

func isWordGuessed(guessed []string, word string) bool {
    return strings.Join(guessed, "") == word
}
Enter fullscreen mode Exit fullscreen mode

This function will check if the word is guessed by joining the guessed letters and comparing it to the word.

Generate a random word

The only thing left do is generate a random word from a file.

Download this file words.txt and add it to your project.

Replace word := "golang" with this.

word, err := getRandomWord("words.txt")
if err != nil {
    fmt.Println("Error reading word file:", err)
    return
}
Enter fullscreen mode Exit fullscreen mode

In case we get an error we will print it else we will use the random word.

import math/rand at the top of the file.

import (
    "bufio"
    "fmt"
    "math/rand"
    "os"
    "strings"
    "unicode/utf8"
)
Enter fullscreen mode Exit fullscreen mode

Lets create the getRandomWord function.

func getRandomWord(filename string) (string, error) {
    data, err := os.ReadFile(filename)
    if err != nil {
        return "", err
    }
    words := strings.Split(string(data), "\n")
    return words[rand.Intn(len(words))], nil
}
Enter fullscreen mode Exit fullscreen mode

This function will read the file and split each line into words in a slice. Then it will return a random word from the slice.

We now have a complete hangman game. Feel free to play it and try to guess the word.

Conclusion

You've built a complete Hangman game in Go! This project taught you file handling, randomization, and user interaction. Feel free to expand and enhance the game further. I hope you enjoyed this tutorial and learned something from it.

Originally published on Coding With Patrik

Full source code can be found here Github

Top comments (2)

Collapse
 
nathan_tarbert profile image
Nathan Tarbert

Pretty cool seeing hangman done in Go like this - got me wanting to dust off my old projects now

Collapse
 
coding-with-patrik profile image
Coding With Patrik

Idd! They’re a fun way to revisit the fundamentals and keep the spark alive πŸ™Œ