Rules of Hangman game should be well-known to everybody. The player is guessing a word or sentence letter by letter and if he manage to guess the whole secret sentence the hangman’s life is saved. Here is the step by step solution of how I created a very simple Hangman game in Rust to play in the console.

This is my first program in Rust so it very likely that the code is not faultless nor optimal. Feel free to comment if you see some errors! The whole code can be found on Github.

Hangman in Rust

1. Creating a project with Cargo

As the Cargo docs say, Cargo is a build tool, which creates metadata files for your project, fetches and builds the project’s dependencies, builds the project and introduces conventions.

I wanted to keep my program simple, so first I decided that I will not use Cargo and I will invoke rustc manually. I thought it will be easier this way. It was a terrible mistake! Soon it became hard or impossible to build the project without Cargo. I needed to include a library for random and the only one obvious solution was to add it as the dependency in Cargo.toml. So here is my advice: don’t be afraid to use Cargo from the beginning, since it’s really nothing scary and you gain a lot by using it.

Here is my short tutorial of how to use Cargo:

cargo new <project_name> --bin # create new project
cargo build # build
cargo run # run

It’s not very hard to master these three steps.

Benefits of using Cargo:

  • Nice structure of folders,
  • Initialized git repository with .gitignore file,
  • Implemented HelloWorld as a skeleton,
  • Cargo.toml which handles dependencies in an easy way. These dependencies will arrive sooner than you think. It seems to be the most important point.

Check also: Day 1 of 24 days of Rust: Cargo.

2. Input sentences

The Hangman game requires some input sentences/words which the player will be guessing. I copied some English proverbs base to text file and removed commas and other punctuation marks. Proverbs are separated with newline sign. The program reads from such prepared file line by line and chooses randomly one line as a secret line to guess. The implementation of choosing the secret line is presented and discussed below.

UPDATE: The implementation of get_random_line which is presented below is not optimal. Refer to Get random line improved for the improved solution.

2.1 Random number

To use the random feature we need to include rand crate by adding a dependency in Cargo.toml file:

[dependencies]
rand="0.3.0"

The crate must be also linked (line 1) and the use declaration must be added (line 3). Now the program is able to choose the random number. The gen_range method returns the random number from the given range. Here the range corresponds to the number of lines read from the input.txt file.

2.2 Reading from file

Reading the file requires the use declarations from lines 3–6. In the 17 line, two things are happening: the file input.txt is opened and the result of this operation is processed by try! macro. Then the opened file is passed to the BufReader object, which provides the methods for the line by line reading. The try! macro is a part of error handling in Rust. It does the job for the method call it wraps. Let’s take a closer look at it.

2.3 Error handling

One of the Rust main characteristics is safety, so it’s not a surprise that the error handling in Rust is quite a wide topic. In this short code snippet we already get in touch with Result<Type> and two different ways of handling it: try! macro and unwrap/expect.

try! macro

The File::open method returns Result<File> instead of just File. When the given path does not exist it will return Result Error, otherwise it will return the Result Ok wrapping the opened file. To access the file you must do something with this Result. You can process it with match, and provide the path for both possible Results Ok and Err, like presented in Rust by Example:

Unfortunately, this solution makes your code bigger.

The try! macro handles the situation nicely, without increasing your code size. Here you can see the code with and without the usage of try!. With try! it’s much nicer, don’t you think?

expect and unwrap

The File::open is not the only one call here that return the Result type. We handle the Results also in the 12 line and 23 line with methods unwrap and expect. These both methods are basically the same. You can use them for uwrapping the object from the result when you feel brave enough to ignore the Result value. The difference between unwrap and expect is that the latter one provides custom message in case of error. This approach isn’t the safest and can make your program panic! or abort. We will discuss it more in the next section.

Check also: Module std::result and Learning to ‘try!’ things in Rust.

2.4 Iterating over the lines

The BufReader::lines() method returns the iterator over all the lines from this buffer. The resulted line is wrapped by Result – it’s of the type io::Result<String>. In line 23 we just ignore the Result value by using the unwrap method. It returns only the String object and the value of Result is forgotten. You can wonder why we don’t do the proper error handling e.g. with try! macro. It’s because I didn’t found the scenario where the Result Error is returned. Even in examples, the result value is ignored. Usually calling unwrap isn’t the best solution, but it’s not always evil.

Ownership is hard at the beginning

The important observation comes with lines 24 and 25. The loaded line is printed (24) and then appended to the vector (25). It works. But if you swap the order of line 24 and 25 (first append to vector and then print), the code won’t compile. Check it! You will get an error: error: use of moved value: l.

I admit, I have seen such errors many times. It’s very hard to get used to this moved values. The reason for this error is so called ownership. In short words, the value becomes moved (and thus unusable) when it is passed to function directly or assigned (bound) to another object. This is quite complicated and wide topic. In many cases, you can avoid the moved values by passing them by reference.

Check also: Why Rust’s ownership/borrowing is hard.

In line 32, the random secret line is returned, wrapped by the Result Ok. That’s all that was needed for the first part of Hangman. The lines are read from the file and one line is chosen and returned.

2.5 Get random line improved

The proposed implementation of get_random_line requires reading all sentences from the file and storing them in vector, only to access the one random line after that. It’s not very optimal and may cause problems if the sentence base is huge. It turned out that rand crate contains method which is more suitable for our needs: rand::sample. This function randomly chooses any amount of objects accessible through iterator. The improved implementation below.

3. Read user guess

Let’s start the interaction with user. His job is typing a letter repeatedly until he discovers the whole secret word.

The user input is read by function read_guess. The whole line is stored in guess variable (I didn’t find a way to read only one char). Then the guess is trimmed, which removes any whitespaces from the line (in case the user typed the whitespaces before his guess) and then the first char is taken from resulted string. The nth() method returns the Option<char>, because the nth value may not exist. If you know C++ the Option can remind you of boost::optional.

The basic validation is done in validate_user_guess method, which returns true if the first non-white sign was an alphabetical letter and false in all other cases. In line 21 we unwrap the resulted user_guess (we know it’s valid letter since the validate_user_guess returned true) and convert the letter to lowercase (the whole part .to_lowercase().next().unwrap() could be skipped if you want to distinguish the case).

4. Prepare structure for game data

Let’s create a structure that keeps data defining the game state. The fields of GameData are self-explanatory, but if you have doubts refer to comments.

Also an enum is introduced for deeper validation of user input. We already have checked if user typed a letter, but this letter can be either already discovered (and this choice should be skipped), or missed (and we need to decrease the lives number), or guessed. The UserInputStatus will help us to decide on what to do with the letter.

Display the secret word

We need to print the secret word with guessed letters discovered and others hidden. For this purpose we create a function format_masked_string, that returns a string with all undiscovered letters replaced with sign _. It also separates the letters with spaces for nicer look. The input argument is the string to be masked, and the mask is just a string containing all discovered letters.

Putting it all together

Now we have all necessary mechanics. We only need to put it all together and enjoy the hangman game. Match expression located in lines 25-62 takes care of the proper reaction to the letter typed by the player. Match is simple switch equivalent.

The game is already playable, but the user interface is poor. Let’s make it more user friendly.

5. Beautifying

Printing the hangman

Of course we need to print the hangman. I think the code will explain himself ;).

Use some colors

For a little bit nicer output we can add color to our game. I’ve done it with the use of ansi_term crate. Now the messages to user are red, green or yellow depending on their character. This crate is very easy to use. You can save the colorful message to plain String object after calling to_string method.

Clear the screen

This is the feature that I’m afraid will work only on Linux. Every time the game status is changed I redraw the hangman and all. I think it looks better if the old hangman is removed, so I clear the screen with the solution from here: davidbegin/clear-terminal.

Game is ready!

Here is the full, final source code: hangman/src/main.rs.