How to make a Rock, Paper, Scissors Game in Rust
Photo by timlewisnm |
We're going to start making games with a hands-on project. In this project we'll build a simple, but real game from scratch. To keep things simple, we'll be implementing the game Rock, Paper, Scissors. This project is heavily based on the example game found in the book The Rust Programming Language found on rust-lang.org. In fact, to make it even easier to follow, we'll build the project by following the same steps as those in the book.
If you'd like to follow along with this project, you can download the code in a .zip file. Commits in the repository will follow the structure of the Rust book.
Rock, Paper, Scissors Rules
Before we begin, lets review the rules for the game of Rock, Paper, Scissors. Rock, Paper, Scissors is a hand game usually played between two people as a fair choosing game to decide between two options represented by each of the players. The game can be played quickly and requires no equipment other than each player being able to make certain hand gestures.
The game is played in rounds until a selected number of wins, are achieved by either player. In each round the players simultaneously select and reveal one of three choices typically represented by hand shapes. The shapes are Rock ("balled fist"), Paper ("flat hand"), and Scissors ("a fist with the first and second fingers extended in a 'V' position.) Each round results in a Win, Loss, or Draw depending on selected choices. Players selecting Rock will win against Scissors ("Rock crushes scissors"), lose against Paper ("Paper covers rock"), and draw against Rock. Players selecting Paper will win against Rock ("Paper covers rock"), lose against Scissors ("Scissors cut paper"), and draw against Paper. Finally, players selecting Scissors will win against Paper ("Scissors cut paper"), lose against Rock ("Rock crushes scissors") and draw against Scissors.
Image by Enzoklop CC BY-SA 3.0 |
As you can see, this is a simple game and only slightly more complicated than the guessing game presented in the The Rust Programming Language book. But those differences will let us explore a few things more deeply than the book's guessing game project does and thus will provide a great first project of my own to take on.
Since this project is so similar to the guessing game from the Rust book, we will move quickly over the areas that are similar. If you find yourself thinking we've not explained enough, read through (or even better, type in the example code) the same section in the guessing game and you'll find additional descriptions of the ares we're skipping over.
Setting Up the Project
To setup the project, go to the directory where you keep your projects. For me, that is the ~/dev directory. From there, make a new project using Cargo:
$ cargo new rock-paper-scissors
$ cd rock-paper-scissors
The first command creates a new project with the given name. The second switches into the newly created directory.
In this project, since we're following the structure provided by the Rust book, we won't cover things the guessing game does. We will cover the things we do that are different and provide a description for what we're doing and why.
To make sure you have everything you need and that the project created successfully, go ahead and run the program:
$ cargo run
Compiling rock-paper-scissors v0.1.0 (/home/rskerr/dev/rock-paper-scissors)
Finished dev [unoptimized + debuginfo] target(s) in 1.27s
Running `target/debug/rock-paper-scissors`
Hello, world!
New projects created with cargo new
include a simple Hello World! program so that projects will compile right out of the gate.
Before our initial commit, we're going to add two additional files - a LICENSE file and a README file. These two files are customary in open source projects and provide needed information for others who see and use the project. The LICENSE file is important because it lets others know what your intentions for sharing are. Without this file, re-use is ambiguous at best, and at worst (at least in the United States) published material should be assumed to be under copyright, which prevents re-use. By adding a LICENSE file, you make your intentions clear and let everyone know your intentions.
In our case, this is a public project and we want to encourage re-use, so we've selected the MIT license. The MIT license grants essentially unlimited rights for others and requires only that the same license is included and granted to others. Since it is a standard license, we've not included its text here.
The second file we've added is a README file. Think of the README file as a kind of starting point for new viewers of the project. README files almost always contain at least a simple description of a project. These files might also include installation and usage instructions, additional system requirements and/or caveats for use. Later, in this project we may add to the README file to provide additional instructions for anyone using the project.
Our README.md file:
# rock-paper-scissors
A simple rock paper scissors game to play.
This game is a game built to learn the Rust programming language and a bit about game programming.
It is based heavily on the example game found in the rust book on rust-lang.org
(https://doc.rust-lang.org/stable/book/ch02-00-guessing-game-tutorial.html). The commit history even follows the major steps presented in the tutorial.
The '.md' at the end of the README filename ('README.md') indicates that the file is a Markdown file. Markdown is a simple formatting language that is both easy to read in text form, but can be taken as hints by display tools to provide a richer view of the file with BOLD, Headings, italics, and other formatting. Since we're pushing this project to a public repository we want viewers to know what they've got and the README file will tell them.
With the preliminaries out of the way, we've completed the first step and can commit our files.
$ git add Cargo.toml LICENSE README.md src
$ git commit -m "Setting Up the Project"
[master (root-commit) bb462b6] Setting Up the Project
4 files changed, 39 insertions(+)
create mode 100644 Cargo.toml
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 src/main.rs
Selecting a Choice
For Rock, Paper, Scissors, we need to accept user inputs that indicate a choice of either Rock, Paper, or Scissors. Just as in the guessing game, we'll start by asking the player to select a choice and just print their response. Unlike the guessing game, however, choices in our game are not numbers, but rather the values Rock, Paper, or Scissors. This simple difference will evolve into a much more interesting program to explore.
We begin with a simple program nearly identical to the guessing game:
//
// Rock, Paper, Scissors
//
// A game by Riskpeep
use std::io;
fn main() {
println!("Hello, Lets play Rock, Paper, Scissors!");
println!("Please select (r)ock, (p)aper, or (s)cissors:");
let mut player_move = String::new();
io::stdin()
.read_line(&mut player_move)
.expect("Failed to read move");
println!("You guessed: {player_move}");
}
Just as in the guessing game we collect a user input, store it for later use, and print out the player's selection. Lets test the results:
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `/home/rskerr/dev/rock-paper-scissors/target/debug/rock-paper-scissors`
Hello, Lets play Rock, Paper, Scissors!
Please select (r)ock, (p)aper, or (s)cissors:
r
You guessed: r
Looking at the output from the last section, it is easy to see that it doesn't look very good. We wanted 'Rock' and we got 'r.' What is going on? The problem is that we're reading in a String and then printing it back to the screen. The player enters 'r' and the program happily sends it back as the guess. Looking closely at the guessing game project, we can see that it too has the same problem but it isn't obvious because when printed, a string containing a number looks just like a number. We'll need to clean this up a bit later when we look at comparing the player's choice with the computer player's choice. Until then we'll just live with the ugly output and work around it.
Adding Rand and Generating a Choice for the Computer Player
Add the rand
crate
In the guessing game, the example uses the rand
crate to generate random guesses for the player to guess. In our game, we have a similar problem in that we need the computer player to make a choice of Rock, Paper, or Scissors to compare with the player. To begin, we add the rand
crate to the Cargo.toml file:
[dependencies]
rand = "0.8.5"
And run cargo build
to pull down rand
's dependencies.
$ cargo build
Updating crates.io index
Downloaded getrandom v0.2.7
Downloaded 1 crate (28.9 KB) in 0.37s
Compiling libc v0.2.126
Compiling cfg-if v1.0.0
Compiling ppv-lite86 v0.2.16
Compiling getrandom v0.2.7
Compiling rand_core v0.6.3
Compiling rand_chacha v0.3.1
Compiling rand v0.8.5
Compiling rock-paper-scissors v0.1.0 (/home/rskerr/dev/rock-paper-scissors)
Finished dev [unoptimized + debuginfo] target(s) in 1m 00s
Generating a Random Choice
The guessing game uses the gen_range
function from the rand
crate to generate random numbers in a range. If we used a similar line, we'd end up with a line like this one:
let comp_move = rand::thread_rng().gen_range(1..=3);
The line does work, however, the generated value is a number in the range 1-3, which isn't what we wanted. Just like taking input section above, we're getting values that do not match our game domain. For our Rock, Paper, Scissors game, we want to generate a choice with a value of Rock, Paper, or Scissors directly. The rand
crate can do this for us, but we need to prepare a few things. First, we need a way to represent the values of Rock, Paper, and Scissors. Since there are only 3 choices and we know what those choices are, Rust enums
provide the perfect way to represent these choices. We can build that like so:
enum RockPaperScissorsGuess {
Rock,
Paper,
Scissors,
}
This enum
creates a new type in Rust that can only have one of these three values. With RockPaperScissorsGuess
, we have a way to represent the choices in our game for both the computer and the player. Now we can generate the random value and indicate the type of the result we're seeking. We can can re-write the random number generation line with a type hint that indicates we desire a result of RockPaperScissorsGuess like this.
let comp_move: RockPaperScissorsGuess = rand::thread_rng().gen();
This is the line we need. The first difference is that we've named the type RockPaperScissorsGuess
to let the compiler know what type we want. The second difference is that we're using the gen()
method rather than gen_range()
method. gen_range()
generates random numbers in the given range. gen_range()
can work on defined types, but only if the types can implement the traits PartialOrd + SampleUniform
, which, as we'll see a bit later we cannot do. std::Rng
implements the more general purpose method gen()
. gen()
works on the Standard
type, which we can specialize by implementing the Distribution
trait for our type. If we compile now, we'll get some errors. Let review each one and then implement changes to fix the errors.
$ cargo build
Compiling rock-paper-scissors v0.1.0 (/home/rskerr/dev/rock-paper-scissors)
error[E0277]: the trait bound `Standard: Distribution<RockPaperScissorsGuess>` is not satisfied
--> src/main.rs:18:64
|
18 | let comp_move: RockPaperScissorsGuess = rand::thread_rng().gen();
| ^^^ the trait `Distribution<RockPaperScissorsGuess>` is not implemented for `Standard`
|
= help: the following implementations were found:
<Standard as Distribution<()>>
<Standard as Distribution<(A, B)>>
<Standard as Distribution<(A, B, C)>>
<Standard as Distribution<(A, B, C, D)>>
and 66 others
note: required by a bound in `gen`
--> /home/rskerr/.cargo/registry/src/github.com-1ecc6299db9ec823/rand-0.8.5/src/rng.rs:94:21
|
94 | where Standard: Distribution<T> {
| ^^^^^^^^^^^^^^^ required by this bound in `gen`
error[E0277]: `RockPaperScissorsGuess` doesn't implement `std::fmt::Display`
--> src/main.rs:29:24
|
29 | println!("I chose {comp_move}");
| ^^^^^^^^^ `RockPaperScissorsGuess` cannot be formatted with the default formatter
|
= help: the trait `std::fmt::Display` is not implemented for `RockPaperScissorsGuess`
= note: in format strings you may be able to use `{:?}` (or {:#?} for pretty-print) instead
= note: this error originates in the macro `$crate::format_args_nl` (in Nightly builds, run with -Z macro-backtrace for more info)
For more information about this error, try `rustc --explain E0277`.
error: could not compile `rock-paper-scissors` due to 2 previous errors
Looking at the errors, we see that the first error is that the trait bound 'Standard: Distribution<RockPaperScissorsGuess>' is not satisfied
, and points to the gen()
method we switched to. The issue is that the gen()
method generates numbers using the Standard
type. Standard
generates values via the use of the Distribution
trait. For primitive types this trait is implemented with a uniform distribution. We can implement that trait to provide any distribution we desire, including for our own defined types. In the code sample, we've indicated that the resulting value should be of type RockPaperScissorsGuess
, and Rust doesn't have an implementation of the Distribution
trait for RockPaperScissorsGuess
. The solution is to provide a trait implementation for the Distribution
trait on the RockPaperScissorsGuess
type. We can do this as shown here:
use rand::distributions::{Distribution, Standard}
impl Distribution<RockPaperScissorsGuess> for Standard {
fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> RockPaperScissorsGuess {
let index: u8 = rng.gen_range(0..3);
match index {
0 => RockPaperScissorsGuess::Rock,
1 => RockPaperScissorsGuess::Paper,
2 => RockPaperScissorsGuess::Scissors,
_ => unreachable!(),
}
}
}
This is a trait implementation. This won't be the last one we'll see for this game. Like the guessing game example, however, we're going to skip lightly over traits. For now it is safe enough to understand that in Rust traits are a generic programming tool similar to the interface concept found in other languages that let programmers define expected methods that exist on a type. Methods that use trait defined required functionality create a 'bound' on the types on which the method operates. Passing a type that doesn't implement the required traits causes a bound failure and the error we see here.
Other than the syntax for defining the trait implementation in the first two lines, the rest of the method is quite straight forward. The trait implements a function called sample that takes as input &self and a mutable reference to rng, and returns a result of type RockPaperScissorsGuess
. The method first generates a random value using syntax similar to what we've seen in the guessing game, and then uses a match to select the values into appropriate values in the RockPaperScissorsGuess enum
, which respectively forms the returned value.
If we add these snippets and compile again, we'll see that we've eliminated the first error. We'll clear the second one in the next section.
Displaying the Choice
The second error we saw was that `RockPaperScissorsGuess` doesn't implement `std::fmt::Display`
. The error points to println!()
line and highlights the comp_move
variable. The issue is that the println!()
macro does not know how to format user-defined types. It uses the Display
trait when printing user-defined types. We can implement this trait as shown here:
use std::fmt
impl fmt::Display for RockPaperScissorsGuess {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RockPaperScissorsGuess::Rock => write!(f, "Rock"),
RockPaperScissorsGuess::Paper => write!(f, "Paper"),
RockPaperScissorsGuess::Scissors => write!(f, "Scissors"),
}
}
}
You can see that the pattern here is similar to the previous correction only even simpler. This is also a trait implementation. The implementation performs a match on the passed value self to map each value into an appropriate value for display. Because not all output is sent to the screen (io::stdout
) we use write!()
instead of println!()
so we can specify the destination as 'f
'.
If we run the program now, we'll see that the output shows the correct value of Rock, Paper, or Scissors for the computer's selection.
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `/home/rskerr/dev/rock-paper-scissors/target/debug/rock-paper-scissors`
Hello, Lets play Rock, Paper, Scissors!
Please select (r)ock, (p)aper, or (s)cissors:
r
You guessed: r
I chose Scissors
Comparing the Choices
At this point, we have choices from both the player and the computer. The next step is to compare the choices to see who won the round.
Selecting a Choice Made Better
Looking at the output from the last section, we have a problem to resolve before we can do the comparison. In the last section, we converted the computer's choice into a value of type RockPaperScissorsGuess
, but the player's input is still a string. If we try to compare two different types we'll get an error. So lets first convert the player's input into a value of the correct type.
Converting player_move
is a slightly more complicated challenge than converting comp_move
because the computer's choice is a string value and could contain anything. The guessing game example uses parse()
to convert the player's input guess from a string to a number. We'll use parse()
as well, but just like with the computer's choice, we'll have some work to do since we're converting to a user-defined type. Once again due to Rust's amazing error messages, we can start by writing out the conversion line and then examining the errors we get back.
Our conversion line will look like this:
let player_move: RockPaperScissorsGuess =
player_move.trim().parse().expect("This is not a valid guess.");
And compiling we see this:
$ cargo build
Compiling rock-paper-scissors v0.1.0 (/home/rskerr/dev/rock-paper-scissors)
error[E0277]: the trait bound `RockPaperScissorsGuess: FromStr` is not satisfied
--> src/main.rs:53:28
|
53 | player_move.trim().parse().expect("This is not a valid guess.");
| ^^^^^ the trait `FromStr` is not implemented for `RockPaperScissorsGuess`
|
note: required by a bound in `core::str::<impl str="">::parse`
For more information about this error, try `rustc --explain E0277`.
error: could not compile `rock-paper-scissors` due to previous error
Once again, we see that we're missing a trait implementation. Are you starting to see a pattern here? In this case, the error message is telling us that Rust needs to convert a str into a RockPaperScissorsGuess
, but doesn't know how do the conversion from a str
value to a RockPaperScissorsGuess
value. The rust parse() method is a generic method that is realized with an implementation of the FromStr trait. Here, the compiler needs an implementation of the trait for the RockPaperScissorsGuess
type. FromStr requires only one method to be implemented, from_str. We can implement the trait with the following code:
#[derive(Debug)]
enum ParseRockPaperScissorsGuessError {
Unknown(String),
}
impl str::FromStr for RockPaperScissorsGuess {
type Err = ParseRockPaperScissorsGuessError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"r" | "rock" => Ok(RockPaperScissorsGuess::Rock),
"p" | "paper" => Ok(RockPaperScissorsGuess::Paper),
"s" | "scissors" => Ok(RockPaperScissorsGuess::Scissors),
_ => Err(ParseRockPaperScissorsGuessError::Unknown(s.to_string())),
}
}
}
There are a couple of interesting parts to this implementation, so lets unpack it a bit. Notice the match
statement branches. In the first branch we show a new kind of pattern ("r" | "rock")
that will match on multiple strings. The '|'
is an OR that lets the statements match on the strings 'r' OR 'rock'. This only scratches the surface of the match statement. match
is one of the workhorses of Rust and is extremely flexible. When we find a rock match, we return a Result::Ok()
with the value set to RockPaperScissorsGuess::Rock
. The next two branches are similar and should be obvious. The last branch in the statement is more interesting, we'll look at that next.
As a systems language, Rust is designed to make safety the default option where possible. One area where this becomes clear is in match
statements. When matching, all possible values must be matched and must have a response. With strings there are unlimited possible inputs. We use the '_'
catch all to match on any string. When we do hit this branch, we've already checked for all of the values we know how to map, so any string that matches here is an error. The method then returns a Result::Err()
value set to contain a custom error type that includes the input string. In the current implementation, we discard the error string, but later on we'll do more with the returned string.
Now we can run again and we'll see that the input string is mapped both to our RockPaperScissorsGuess
type on input, and back to string for display on output.
$ cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.01s
Running `/home/rskerr/dev/rock-paper-scissors/target/debug/rock-paper-scissors`
Hello, Lets play Rock, Paper, Scissors!
Please select (r)ock, (p)aper, or (s)cissors:
rock
You chose Rock
I chose Paper
Comparing the Choices
Now we can do the comparison. Doing this comparison is non-trivial for two reasons. First, the things we're comparing aren't simple types, and Second because there isn't a simple ordering between the values of our types. Together these two issues make for an interesting challenge and at the same time show how Rust is a great tool for solving interesting problems.
First, let us look at comparisons in Rust (and most other languages). When we have two values, we can compare them using a comparison operator such as =, < or >. For these comparisons to work, the operator has to know how to operate on the values being used. For simple types (e.g. u32), this is trivial because the language has built in support for them. With a user defined type, however, this comparison might not be obvious. We all would agree that 5 is greater than 4. But what about 'apples' and 'oranges'? The answer isn't obvious and might depend on who you're asking or the domain you're asking about. In many languages you resolve this problem with operator overloading or a custom function.
Lets have a look at what happens in Rust if we try to compare two values. We can write a line like so:
let result = player_move > comp_move;
When we compile the result we get an error:
$ cargo build
Compiling rock-paper-scissors v0.1.0 (/home/rskerr/dev/rock-paper-scissors)
error[E0369]: binary operation `>` cannot be applied to type `RockPaperScissorsGuess`
--> src/main.rs:76:30
|
76 | let result = player_move > comp_move;
| ----------- ^ --------- RockPaperScissorsGuess
| |
| RockPaperScissorsGuess
|
note: an implementation of `PartialOrd<_>` might be missing for `RockPaperScissorsGuess`
--> src/main.rs:12:1
|
12 | enum RockPaperScissorsGuess {
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^ must implement `PartialOrd<_>`
help: consider annotating `RockPaperScissorsGuess` with `#[derive(PartialEq, PartialOrd)]`
|
12 | #[derive(PartialEq, PartialOrd)]
|
Just as we suspected, Rust doesn't know how to compare these two values of type RockPaperScissorsGuess
. Looking further though, we see how Rust would solve this problem of comparing user defined types. Readers following this project closely might have already guessed, the answer is with traits. Rust places a trait bound on any types used with a comparison operator. To perform comparisons, types must implement a PartialOrd<_> trait.
Partial Ordering is a concept from set and order theory in mathematics. At the risk of creating a circular definition via simplification, a partial ordering is a relationship between members of a set such that the members can be ordered from lowest value to highest value. With a partial order we can examine any two members of the set and definitively say that one member is greater than the other. Wikipedia has an entry that describes Partial ordered sets and the Partial order relation if you want a formal definition.
In contrast to partial orders, cyclic orders arrange the members of a set in a circle. One does not say that Monday is greater than Tuesday, instead, one says that Monday comes after Sunday and before Tuesday. Rather than greater than or less than, with cyclic sets one says before or after. Examples of cyclic sets include the days of the week and and months of the year.
It is in the notion of partial ordering and circular ordering that we see the second problem. '<' and '>' operations are binary operations on a partial order. The set of choices in Rock, Paper, Scissors follow a cyclic order. Look back at the figure in the rules section. From the picture, it is immediately evident that our relationship is circular. So there is not an implementation of the PartialOrd<_> trait that will be suitable for our situation. What is needed is a way to compare two instances of RockPaperScissorsGuess
that is aware of the circular nature of the relationships between their values. We can accomplish this with a custom comparison function implemented as a trait on the type RockPaperScissorsGuess
.
Our compare function will compare the value the trait is implemented (called self) to another RockPaperScissorsGuess
value and return a value of RockPaperScissorsResult
that will indicate the result of the game. We'll also create an enum
to hold the reason for why a given result is obtained. Because there are so many parts to bring together, we'll proceed step-wise and explain what we're doing with each step.
First we'll create the enums
that represent the result and reason for the result:
enum RockPaperScissorsCompare {
RockCrushesScissors,
PaperCoversRock,
ScissorsCutPaper,
}
enum RockPaperScissorsResult {
Win(RockPaperScissorsCompare),
Loss(RockPaperScissorsCompare),
Tie(String),
}
For any given comparison, the result is either a win, a loss, or a tie. RockPaperScissorsResult
represents these values. For a win or a loss, we also want to know why the result is obtained. So we associate a RockPaperScissorsCompare
value with each enum
variant. For ties, we associate a String to hold the choice so we can display it later as the reason. The RockPaperScissorsCompare enum
represents the various reasons why each comparison result occurs. Note that the comparison enum
variants can be used in both wins and losses. One player's win is the other player's loss.
Next, we'll create the compare trait:
pub trait Compare<T, U> {
fn compare(&self, b: &T) -> U;
}
impl Compare<RockPaperScissorsGuess, RockPaperScissorsResult> for RockPaperScissorsGuess{
fn compare(&self, b: &RockPaperScissorsGuess) -> RockPaperScissorsResult {
match self {
RockPaperScissorsGuess::Rock => {
match b {
RockPaperScissorsGuess::Rock =>
RockPaperScissorsResult::Tie,
RockPaperScissorsGuess::Paper =>
RockPaperScissorsResult::Loss(RockPaperScissorsCompare::PaperCoversRock),
RockPaperScissorsGuess::Scissors =>
RockPaperScissorsResult::Win(RockPaperScissorsCompare::RockCrushesScissors)
}
}
// Other branches elided for clarity.
}
}
}
Here we see not only the implementation of a trait, but also a trait definition. The definition is defined using generic notation (the '<' and '>' brackets and type names 'T' and 'U'), and includes just one function compare.
In the implementation, we must define the types for the implementation (i.e. the 'T' and 'U' types), and the type on which it is implemented (which in our case is RockPaperScissorsGuess
.) While it looks complex, close examination will show that the implementation is just two embedded match statements. In the outer match, we're matching on the self value, which is the instance the comparison is called on. The inner match then selects based on the value we're comparing to. This results in a rather verbose function, and one can't help but wonder if there is a simpler approach.
Finally to complete our comparison, we'll implement one more function to display the result of the match:
impl fmt::Display for RockPaperScissorsResult {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
RockPaperScissorsResult::Win(result) => {
match result {
RockPaperScissorsCompare::RockCrushesScissors => write!(f, "Rock crushes scisso
RockPaperScissorsCompare::PaperCoversRock => write!(f, "Paper covers rock"),
RockPaperScissorsCompare::ScissorsCutPaper => write!(f, "Scissors cut paper"),
}
},
RockPaperScissorsResult::Loss(result) => {
match result {
RockPaperScissorsCompare::RockCrushesScissors => write!(f, "Rock crushes scisso
RockPaperScissorsCompare::PaperCoversRock => write!(f, "Paper covers rock"),
RockPaperScissorsCompare::ScissorsCutPaper => write!(f, "Scissors cut paper"),
}
},
RockPaperScissorsResult::Tie => write!(f, ""),
}
}
}
This last function implements the Display trait for RockPaperScissorsResult
so we can use println!() to display the comparison result. We can now add the comparison and print lines to the main function:
let result: RockPaperScissorsResult = player_move.compare(&comp_move);
println!("{}", result);
Running the game shows a full round of Rock, Paper, Scissors!
$ cargo run
Compiling rock-paper-scissors v0.1.0 (/home/rskerr/dev/rock-paper-scissors)
Finished dev [unoptimized + debuginfo] target(s) in 1.16s
Running `/home/rskerr/dev/rock-paper-scissors/target/debug/rock-paper-scissors`
Hello, Lets play Rock, Paper, Scissors!
Please select (r)ock, (p)aper, or (s)cissors:
r
You chose Rock
I chose Paper
You Lost!...Paper covers rock
That wraps up this post. We've covered a lot here. We went from an empty directory to a single round of Rock, Paper, Scissors. Along the way we used enums
to represent our types and wrote a bunch of trait implementations to extend those traits to enable printing on screen and comparisons. We even had a minor diversion into set theory. In the next post, we'll extend the game by adding multiple rounds and some better error handling.
Comments
Post a Comment