Chapter II: Hangman

After the first chapter you should know how to use keywords fn and mut, types int, f32, string, array as well as converting string input to f32 and some basic array operations with <<, array.len attribute and array.delete().

Game loop

In this chapter we will create a Hangman game that will pick a random word from a predefined array of values. As for any common game, we need to create a loop in which we check the inner state of the game and request a user input or action if necessary.

fn game_loop() {}

fn main() {
    game_loop()
}

To define what should be present in the game loop we need to check the Hangman description. So according to that, the game loop should have a player always guessing either a letter or the whole hidden word until a player makes 6 mistakes. In between the guesses the game should visibly note player’s mistakes and unfold all places of a guessed letter. The game ends either by providing a correct word, guessing all letters correctly or at player’s 6th mistake.

Let’s define the game ending conditions first as having 6 user attempts total, wrap checking for the user attempt in an infinite loop and then stopping the game loop once the conditions are not matching. An infinite loop in V is defined as a for loop without any specified condition. Afterwards the loop can be broken by a break keyword.

fn game_loop() {
    max_attempts := 6
    mut attempts := 0
    for {
        attempts++
        println("User attempt $attempts")
        if attempts >= max_attempts {
            break
        }
    }
}

fn main() {
    game_loop()
    println("Game over")
}

We can see that the amount of attempts is a value that won’t change, therefore we can declare it as a constant with const keyword. Unlike ordinary variables a value to a const is assigned via = instead of := and can’t be changed.

const (
    max_attempts = 6
)

fn game_loop() {
    mut attempts := 0
    for {
        attempts++
        println("User attempt $attempts")
        if attempts >= max_attempts {
            break
        }
    }
}

fn main() {
    game_loop()
    println("Game over")
}

Next we retrieve a user input so we can. If the length of the input is 1 we will treat it as guessing a character from the whole word and display all its occurences if the character is present. If the input is longer than one character, player is guessing the whole word and only if the word matches we display it. For any other case just take it as a failed attempt.

For user input import os, then use its os.get_line() function to retrieve a single line from console - or in other words an input terminated by a single Enter key.

import os

const (
    max_attempts = 6
)

fn game_loop() {
    mut attempts := 0
    guess_word := "hangman"
    mut user_input := ""

    for {
        attempts++
        println("User attempt $attempts")

        user_input = os.get_line()
        if user_input.len > 1 {
            println("Guessing a word: $user_input")
        } else if user_input.len == 1 {
            println("Guessing a character: $user_input")
        }

        if attempts >= max_attempts {
            break
        }
    }
}

fn main() {
    game_loop()
    println("Game over")
}

Once we have the input available, let’s add a sample word hangman to a variable. Then create a mask of that word, a value constructed of - characters in the same length as the guess word. That’s easily achievable with string.repeat(count int) function.

import os

const (
    max_attempts = 6
)

fn game_loop() {
    mut attempts := 0
    guess_word := "hangman"
    display_word := "-".repeat(guess_word.len)
    mut user_input := ""

    for {
        attempts++
        println("Attempt $attempts")
        println("Word: [ $display_word ]")

        user_input = os.get_line()
        if user_input.len > 1 {
            println("Guessing a word: $user_input")
        } else if user_input.len == 1 {
            println("Guessing a character: $user_input")
        }

        if attempts >= max_attempts {
            break
        }
    }
}

fn main() {
    game_loop()
    println("Game over")
}

If a player guesses correctly, go through the variable which stores hangman word, find each occurence of the character and replace the - characters with uncovered ones according to the position in the original word.

import os

const (
    max_attempts = 6
)

fn game_loop() {
    mut attempts := 0
    guess_word := "hangman"
    mut display_word := "-".repeat(guess_word.len)
    mut user_input := ""

    for {
        attempts++
        println("Attempt $attempts")
        println("Word: [ $display_word ]")

        user_input = os.get_line()
        if user_input.len > 1 {
            println("Guessing a word: $user_input")
            if user_input == guess_word {
                display_word = guess_word
            }
        } else if user_input.len == 1 {
            println("Guessing a character: $user_input")
            mut buffer := ""
            for idx, value in guess_word {
                if value == display_word[idx] {
                    buffer += display_word[idx].str()
                    continue
                }

                if value != user_input[0] {
                    buffer += "-"
                    continue
                }

                buffer += guess_word[idx].str()
            }
            display_word = buffer
        }

        if display_word == guess_word {
            println("Correctly guessed!")
            break
        }

        if attempts >= max_attempts {
            println("Game over")
            break
        }
    }
}

fn main() {
    game_loop()
}

Notice the sections where the str() function is called. While a word is stored as a string, that’s in simple terms just an array of byte types. A byte on the other hand is so similar to an int that the str() function is the same for both - int.str(c byte).

Improvements

If we look properly at this large game loop, we can see multiple parts that can be pulled out into separate functions which will increase the readability of the overall code. Let’s move the code working with user input and name it as guess() function. This function will take multiple string parameters and also return one string. We reflect those properties to the function declaration and the result should look like this: fn guess(input string, word string, mask string) string.

fn guess(input string, word string, mask string) string {
    mut new_mask := mask

    if input.len > 1 {
        if input == word {
            new_mask = word
        }
    } else if input.len == 1 {
        mut buffer := ""
        for idx, value in word {
            if value == mask[idx] {
                buffer += mask[idx].str()
                continue
            }

            if value != input[0] {
                buffer += "-"
                continue
            }

            buffer += word[idx].str()
        }
        new_mask = buffer
    }
    return new_mask
}

For the conditions of word matching and attempts we create check_continue() function and make it return a bool type so the for loop can automatically check the value and exit game loop when necessary. Similarly for the match of the guessed word and already uncovered letters we create check_win() function so the logic is kept at one place.

fn check_win(word string, mask string) bool {
    return word == mask
}


fn check_continue(word string, mask string, attempts int) bool {
    return !check_win(word, mask) && attempts < max_attempts
}

Finally, we move out the game summary prints to print_summary() function, clean unnecessary variables and add spacing between code lines.

import os

const (
    max_attempts = 6
)

fn guess(input string, word string, mask string) string {
    mut new_mask := mask

    if input.len > 1 && input == word {
        new_mask = word
    } else if input.len == 1 {
        mut buffer := ""

        for idx, value in word {
            if value == mask[idx] {
                buffer += mask[idx].str()
                continue
            }

            if value != input[0] {
                buffer += "-"
                continue
            }

            buffer += word[idx].str()
        }
        new_mask = buffer
    }
    return new_mask
}


fn check_win(word string, mask string) bool {
    return word == mask
}


fn check_continue(word string, mask string, attempts int) bool {
    return !check_win(word, mask) && attempts < max_attempts
}


fn print_summary(word string, mask string) {
    if check_win(word, mask) {
        println("Correctly guessed!")
    } else {
        println("Game over!")
    }
}


fn game_loop() {
    guess_word := "hangman"
    mut display_word := "-".repeat(guess_word.len)

    mut attempts := 0
    mut do_loop := true

    for do_loop {
        attempts++
        println("Attempt $attempts")
        println("Word: [ $display_word ]")

        display_word = guess(os.get_line(), guess_word, display_word)
        do_loop = check_continue(guess_word, display_word, attempts)
    }

    print_summary(guess_word, display_word)
}

fn main() {
    game_loop()
}

While the current state of the game is workable, we still need to make it dynamic otherwise the game would be always the same and after the first try everyone knows the answer. We can solve this by using a file named words.txt which will contain guess words one per each line.

To read lines from a file on your operating system use os.read_lines(path string) function. Specify just the name of the file to open it from the current “working” directory or in other words, from the folder you run your program from.

After the array of string s is retrieved we need to pull a single word for the game to begin. Import rand and use rand.next(max int) from it. Make sure the maximum value is set to the length of the words array to only access its item by an index within the range of available words.

fn load_word(path string) string {
    lines := os.read_lines(path)
    return lines[rand.next(lines.len)]
}

If you try to include this function into the already existing game, it will most likely have a consistent behavior (always the same word chosen). That’s because a randomness seed needs to be different on each run. For that import time and input time.now() to a call setting the seed - rand.seed(s int).

Since rand.seed(s int) requires an int type we can convert the time.Time into a UNIX timestamp which is a number we can safely use for seeding in this particular case. time.Time stores it in time.Time.uni attribute.

Once the seed is set we can call rand.next(max int). Now change the hard-coded guess word in game_loop() into load_word("words.txt") and create a file named words.txt in the same folder as is the hangman .v file. You can find a sample file in the Appendix section.

import os
import rand
import time


const (
    max_attempts = 6
)


fn load_word(path string) string {
    lines := os.read_lines(path)
    rand.seed(time.now().uni)
    return lines[rand.next(lines.len)]
}


fn guess(input string, word string, mask string) string {
    mut new_mask := mask

    if input.len > 1 && input == word {
        new_mask = word
    } else if input.len == 1 {
        mut buffer := ""

        for idx, value in word {
            if value == mask[idx] {
                buffer += mask[idx].str()
                continue
            }

            if value != input[0] {
                buffer += "-"
                continue
            }

            buffer += word[idx].str()
        }
        new_mask = buffer
    }
    return new_mask
}


fn check_win(word string, mask string) bool {
    return word == mask
}


fn check_continue(word string, mask string, attempts int) bool {
    return !check_win(word, mask) && attempts < max_attempts
}


fn print_summary(word string, mask string) {
    if check_win(word, mask) {
        println("Correctly guessed!")
    } else {
        println("Game over!")
    }
}


fn game_loop() {
    guess_word := load_word("words.txt")
    mut display_word := "-".repeat(guess_word.len)

    mut attempts := 0
    mut do_loop := true

    for do_loop {
        attempts++
        println("Attempt $attempts")
        println("Word: [ $display_word ]")

        display_word = guess(os.get_line(), guess_word, display_word)
        do_loop = check_continue(guess_word, display_word, attempts)
    }

    print_summary(guess_word, display_word)
}

fn main() {
    game_loop()
}