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()
}