This tutorial was part of a whorkshop which took place at the Rust Community Stuttgart on April, 17th 2019.
We will develop a Hangman text based console game within this tutorial.
You'll be creating your individual flavour of the game!
The steps with the corresponding tags are just an orientation, when you are stuck.
Please follow these steps to get the most benefit out of this learning experience.
- Read the instructions at each tag
- Try to implement what this step means
- If you're stuck: First read the hints
- If you don't understand a Rust concept, search the internet and learn about it before you try to conquer the next step. (I know this could be sometimes tadious, but the more effort you put in, the more you will benefit)
- If your code gets to messy, start out fresh at this step from the master branch
Just implement as many additional println!()
statements to see helpful messages during runtime to get a feeling which line of code the program currently executes. (They are our log statements.) We'll get rid of them until the end of the tutorial.
I assume, you're familiar with git
.
I recommend you to have IntelliJ Community Edition installed. When you use this IDE, make sure you have the Rust plugin installed.
This IDE in combination with the Rust plugin is very powerful and makes coding really easy.
I also assume, you already have run an executable like the hello world
program. So you're familiar with cargo new
and cargo run
.
I strongly recommend to install clippy
.
> rustup component add clippy
It is a linter and gives you hints to optimize your source code.
Usage: > cargo clippy
When this passes you can perform a program run: > cargo run
Start with the initial commit.
Checkout the inital commit: git checkout v0.0.0 -b develop
Now you're on the develop branch. This will help you develop your own solution and don't mess with the master branch.
Just checkout out this commit and examine the code. It's your starting point.
HINT: You don't need to bother about the lazy-static macro and crate. (I introduced it to save lots of confusing startup code not relevant to the problem.) You can look it up in the internet if you want to, but it's not necessary to understand the underlying code.
How can we gather input from the user?
The user needs to be prompted to enter a line of text, which you're going to diplay in the next line.
HINT:
Look into the Rust API at module std::io
.
What can be useful?
Wow, this is quite challenging.
We need to split the string into the first character and the rest.
We are going to put all the string function in a separate module: src/strings.rs
.
Rust uses UTF-8 encoded strings internally, but stores them into an 8-bit vector Vec<u8>
, which means that each character can possibly consume up to 5 bytes.
pub fn get_first_unicode_char(s: &str) -> (&str, &str) {
for i in 1..5 {
if let Some(head) = s.get(0..i) { return (head, &s[i..]) }
}
(&s[0..0], s)
}
HINT:
- Use this function in your
main.rs
to display the first character and the rest. - Use the
destructuring
pattern. (Look it up)
Now loop all this.
if the user types in quit
, the loop should break and exit the program.
Your Hangman game will only process upper-case letters, so let's implement this.
At this stage, you should also create a function within the strings.rs
module to get a distinct string to have a counterpart for comparsion of your entries.
The loop should exit with the message You win
when the entry and the search string match.
Now, we will display the hint.
The hint will show in a later step the already matching letters and obfuscate the not matching.
An example:
- search_string is [ HANGMAN ]
- the hint will be [ H_N___N ] if you already matched H and N
HINT:
- create a function
prepare
in thestrings
module - within this function, use the external crate regex to replace any character in the search string with an underscore
- display search string and hint for debug purpose
At this step, we're doing test driven development.
Copy and Paste this snippet (developed by your test department) into the strings
module:
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_find_char_in_string() {
assert_eq!(false, find_char_in_string("", "hallo"));
assert_eq!(false, find_char_in_string("c", "hallo"));
assert_eq!(true, find_char_in_string("a", "hallo"));
assert_eq!(true, find_char_in_string("h", "hallo"));
assert_eq!(true, find_char_in_string("o", "hallo"));
}
}
Your job is to implement the find_char_in_string() function.
HINT:
Use a recursive approach by utilizing get_first_unicode_char()
. (You can doit!)
Use this new function in the main.rs
to print out, if the user performed a hit or not.
Introduce a variable which holds all the hits in a non-redundant string.
Display at least following log messages:
- Character Hit!
- Character Miss!
- And when the variable doesn't already contain the character, the matched characters string. (You need to add the last hit to this string before)
New unit test in strings
module:
#[test]
fn test_create_hint() {
assert_eq!("_____", create_hint("hallo", "").as_str());
assert_eq!("_all_", create_hint("hallo", "la").as_str());
assert_eq!("hallo", create_hint("hallo", "hola").as_str());
}
Create the create_hint function.
HINT: Use a regular expression to do this.
Go to http://creates.io and add the regex
create to your Cargo.toml dependencies.
( HINT 2: Create the regular expression by concatination of strings. )
Introduce an integer variable which holds the amount f misses.
Acceptance Criteria:
- Exit with message
You Loose
when the tries reach 7.
(The hangman has 7 body parts)
Feel free to copy the solution into your program !
Reason about the enum definition:
- Why did we do it like this?
Add the convenience method display
to the Graphic type and use it in the main.
As you have seen in the step 0.0.0 that we use a hashmap to store the graphics. We won't write the whole function calls every time we want to display the graphic.
HINT: Just move the display call to this (static) method.
Change the state to the appropriate graphic and voilà we'll get the right graphic displayed.
Add the good bye
graphics.
Feel free to look in the solution, but implement it by your own !
As the commit message says, show what hidden word was queried to.
This one is a bit more complex to grasp.
We have in each error
graphics a sequence ...........................
.
It needs to be substituted with the hint so we can get rid of plain text message.
HINT:
Implement len_of_string()
by using, once again, recursion.
#[test]
fn test_len_of_string() {
assert_eq!(0, len_of_string(""));
assert_eq!(5, len_of_string("hallo"));
assert_eq!(3, len_of_string("olá"));
}
HINT 2:
- Change the
display
function to have the state argument and the additional hint argument. - Feed the hint into the
draw
function. It doesn't have this parameter, yet, but the main programming effort has to happen within it. - there you use the
len_of_string()
function
Get rid of all unnecessary log messages.
AND YOU'RE DONE !!!
I have to admit, that I was not careful enough in step 0.0.17 and have a bug.
I fixed it and published version v0.1.0
.
Our program currently one disguises one single word (HANGMAN).
It's open to you to create a file with search strings from which we can randomly choose one word to feed it into our game at startup.
This would certainly improve the fun factor when playing it several times.
Also we could prettify
our hint by introducing spaces between the letters.