This laboratory exercise will help you practice using loops and conditionals to implement a “useful” program: a rock paper scissors game. While this isn’t the most exciting game, it requires a mix of control flow patterns you’ll use frequently. We will build on the work you complete for this lab, so keep your code organized and write descriptive comments You’d be amazed how quickly you can forget what you were thinking when you wrote some code.
This week, we will start using git and the Gitea server that hosts our repositories for this class.
There is a repository that holds the starter code for today’s lab that you have access to view, but not modify.
These steps will walk you through creating a fork of that repository, essentially a copy that you control that still resides on the Gitea server.
Once you have a fork created you’ll give your lab partner(s) access and clone the repository to MathLAN, which is where you’ll do the actual work of the lab.
Each part of the lab includes a reminder to commit your changes and push the commit back up to Gitea, where you’ll see the code in your fork.
Follow these steps to get set up:
You may still be signed in, but if you see a Sign In button in the upper right, click it and log in with the GrinCo-AAD option below the form.
On the page for the week2 repository, click the Fork button in the upper right.
On the next page, leave all the options set to their defaults and click the Fork Repository button.
You’ll end up back on a page that looks like the original repository, but the title in the upper left should be “YOUR_USERNAME/week2” with a subtitle that says “forked from csc161/week2”.
Click the Settings button in the upper right and then choose the Collaborators section.
For each member of your group, type their username and click Add Collaborator. That should immediately add them to the list of collaborators. This should be the default, but make sure each collaborator has Write permission (displayed next to the button to remove a collaborator). If you can’t find a lab group member, they might not have set up their Gitea account. Fix this now before moving on.
Once you’ve finished adding your lab group to the repository, click the Code button to go back to the repository’s main page. Find the HTTPS button and click it if it isn’t already selected. Copy the text that appears in the field next to the HTTPS and SSH buttons.
Open a terminal and run the following commands to clone your repository.
Make sure you replace COPIED_TEXT with the text you just copied:
$ cd ~/csc161/labs
$ git clone COPIED_TEXT
$ cd week2
$ code .
You should now see VSCode running with a single C source file.
Depending on your VSCode settings you might also see a file named .gitignore.
This file just tells git not to commit the executable program you’ll be compiling.
We generally only save code in repositories, since a compiler can produce the executable from the source and the executable might not work on any systems other than the one that produced it.
Assuming you see the file rps.c in VSCode you should be all set to move on to the first part of the lab.
If we are going to write a program to play rock, paper, scissors, we’ll need the program to be able to make a random choice. This is harder than it may seem at first. Computers are good at following instructions, but behaving unpredictably requires special effort. Luckily, we don’t need our program to be truly random and we can use a pseudo-random number generator (or PRNG) to make “random” choices. A PRNG is uses a small amount of internal state called a seed and a series of mathematical operations to produce values that look random even though they are actually deterministic.
The C standard library gives us a rand function that implements a simple PRNG, but it has one big drawback: every time we run our program we’ll get the same sequence of pseudo-random values.
That’s because rand uses the same seed value every time you run your program.
We can fix that by setting the seed using something that changes: the time.
The example program below sets the seed with srand and then uses rand to generate one pseudo-random value:
#include <stdlib.h>
#include <stdio.h>
#include <time.h>
int main() {
// Set the random seed to the current time in seconds
srand(time(NULL));
// Generate and print a random value
int value = rand();
printf("Generated random value %d\n", value);
}
Notice that this example has more #include lines than the programs you’ve written before.
The stdlib.h header is required for rand and srand, and the time.h header is required for the time function.
This is a common way to set the seed for rand, but we don’t have the terminology to discuss what NULL means yet.
For now, just keep in mind that time(NULL) returns the current time in seconds since the UNIX epoch (midnight on January 1st, 1970).
Any time you use a C function you should check its manual page to learn what it does and how to use it.
Standard C functions live in section 3 of the manual, so we can access the manpage for rand like this:
$ man 3 rand
rand(3) Library Functions Manual rand(3)
NAME
rand, rand_r, srand - pseudo-random number generator
LIBRARY
Standard C library (libc, -lc)
SYNOPSIS
#include <stdlib.h>
...
Notice that the first line after “SYNOPSIS” shows us an include line.
The rest of the manual page is helpful for understanding the functions, including any potential errors you may need to deal with when using these function.
The good news is that rand is pretty easy to use: each time you call it, it returns a new pseudo-random value.
Your starter code included the file rps.c (short for “rock, paper, scissors”).
You will complete today’s lab entirely in this source file.
As you work through these exercises, make sure to compile and run your program frequently.
You can compile the program with this command:
$ clang -Wall -o rps rps.c
Open rps.c in VSCode and make the following changes:
Add the #include lines required to generate pseudo-random values with rand to the top of the source file (we always put #include lines at the beginning of our files).
Use code from the example above to seed the PRNG with the current time.
Use rand to generate a random value.
We are going to use this value to choose “rock”, “paper”, or “scissors”, but rand returns a random value that could fall anywhere in the range of values that can be represented by an integer.
You’ll need to use the % operator to constrain the random value to just 0, 1, or 2.
Take some time to figure out how to do this with your lab group.
Include a line to print the pseudo-random value and test your program several times to make sure you are seeing different values that always fall in the correct range.
Once you have generated a random value, use conditionals to print the choice as “rock”, “paper”, or “scissors” instead of just a number. You are free to choose whatever type of conditional you prefer, but make sure you discuss the different options with your partner(s) before you decide. Your completed program for this part of the lab should behave like this example, although you should see different choices in your testing:
$ ./rps
The computer chose paper.
$ ./rps
The computer chose scissors.
$ ./rps
The computer chose rock.
You’ll be building on this code in the next part of the lab, but this is a point you may want to come back to if anything goes wrong.
Make sure you commit and push your changes before moving on.
Use the commands below to do this, but make sure you edit the commit message passed after the -m flag.
$ git commit -a -m "<write a short description of what you've done here>"
$ git push
The -a flag provided to git commit above tells git to save changes to all the files it knows about.
We’ll use this most of the time, but you can use git add <filename> to tell get to include specific files in the commit.
Without the -a flag, git will only save changes to the files that have been added.
For today’s lab, we’ll always pass -a to git commit because all our work will be in the rps.c file.
Next, we’re going to need a way to get input from the user so they can choose rock, paper, or scissors.
For today’s lab, we’re going to do this using the scanf function.
The scanf function works a bit like printf, but instead of displaying a value to the user it reads input from the user and allows you to use the values they provided.
This example uses scanf to read an integer value from the user and prints the value back to them:
#include <stdio.h>
int main() {
// Show a prompt message
printf("Please type an integer:\n");
// Use scanf to read in an integer
int value;
int num_values_read = scanf("%d", &value);
// Clear any remaining input from the user
while (getchar() != '\n') {}
// Print the value, if there was one
if (num_values_read == 1) {
printf("You typed %d\n", value);
} else {
printf("You didn't type an integer.\n");
}
}
There are a few important details in this example you should take note of:
Notice the parameter &value passed to scanf.
We don’t yet have the terminology to discuss what exactly this is doing, but the ampersand (&) is required.
Assuming scanf reads input successfully, the value variable will be filled in with a number that the user types.
When we call scanf, it gives us back a number that we save in the num_values_read variable.
This allows us to detect and deal with invalid input.
If the user doesn’t type a number, scanf will return a value 0 to indicate that value was not filled in because the user provided invalid input.
The while loop right after the call to scanf consumes any characters left in the line after scanf finishes using the getchar function.
This allows you to call scanf again, which you might do if the user gives invalid input and you ask them to try again.
If you left this loop out, scanf would fail immediately because it leaves the invalid input from the previous attempt in place.
This odd behavior is one reason we’ll move away from using scanf in the future, but for now it is our best option for reading numbers from a user.
The error handling in this code still allows for some invalid inputs.
Tf you type “3a”, the program will read the 3 and ignore the a without reporting an error.
That should be okay for this program.
Complete the exercises below using the scanf example and your knowledge of loops and conditionals.
Make sure you don’t delete your code from part A;
you should add your code for this part inside of main but before the code you wrote in part A.
Add code to rps.c so it prints a menu like this:
$ ./rps
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice:
Next, use the scanf example above to read a number from the user.
If the user gives an invalid input—anything other than 1, 2, or 3—tell them the input was invalid and ask for input again.
Use a loop to keep trying until you get a valid selection from the user.
Don’t forget to clear any remaining input after calling scanf or you could see some odd behavior.
Finally, print the user’s choice. You should print the name of the choice, not just the number the user entered. The following example below shows how your implementation should behave. Note that the example includes output from the program and user input mixed together just as you’d see them in the terminal.
$ ./rps
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: Paper
Invalid choice.
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 50
Invalid choice.
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 2
You chose paper.
The computer chose rock.
As with part A, you’ll want to save the program at this point and commit the changes to your git repository:
$ git commit -a -m "<write a short description of what you've done here>"
$ git push
You now have a program that gathers the choices from the user and computer players. It’s time to decide who wins the game. We’ll implement the standard rules for rock, paper, scissors: rock beats scissors, scissors beat paper, and paper beats rock.
Write conditionals to decide who wins the game, and then print the winner. Your implementation should behave like this example:
$ ./rps
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 2
You chose paper.
The computer chose rock.
You win!
Here is another example that shows how you can handle ties:
$ ./rps
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 3
You chose scissors.
The computer chose scissors.
It's a tie!
Make sure your implementation still handles invalid user input correctly.
Don’t forget to commit and push your changes with git.
A game of rock, paper, scissors doesn’t last for very long so we’ll allow the user to play repeatedly until they decide they are done. This change shouldn’t be too complicated, but it could easily break your code so make sure you’ve committed changes from the previous part before you start.
After the user finishes a game, ask them if they want to play again.
If they choose “yes”, repeat the whole game again.
You’ll end up wrapping most of your program in a loop, but the loop still goes inside the main funciton.
You will need to think carefully about the type of loop you use, the loop condition, and how you’re going to exit the loop when the user chooses not to play again.
The final result should behave like this:
$ ./rps
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 2
You chose paper.
The computer chose paper.
It's a tie!
Do you want to play again?
1. Yes
2. No
Enter the number for your choice: 1
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 1
You chose rock.
The computer chose paper.
You win!
Do you want to play again?
1. Yes
2. No
Enter the number for your choice: 2
Make sure you test your implementation enough to be confident it handles invalid inputs correctly in both places where you ask for user input. As always, commit and push your changes once you’re happy with your implementation.
Finally, we’re going to add code to keep track of wins, ties, and losses as long as the user keeps playing. This should be a relatively small change to your code from the previous part, but you may need to revisit some of the decisions you made about your loops.
Add variables to keep track of the user’s wins, losses, and ties. Make sure you initialize these counts to zero and update them after each game.
Add code to print the count of wins, losses, and ties after each game. The final result should look like this:
$ ./rps
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 2
You chose paper.
The computer chose paper.
It's a tie!
Wins: 0
Ties: 1
Losses: 0
Do you want to play again?
1. Yes
2. No
Enter the number for your choice: 1
What move do you want to play?
1. Rock
2. Paper
3. Scissors
Enter the number for your choice: 1
You chose rock.
The computer chose paper.
You win!
Wins: 1
Ties: 1
Losses: 0
Do you want to play again?
1. Yes
2. No
Enter the number for your choice: 2
As an optional additional challenge, try to print the win/tie/loss record in a more conversational style with proper pluralization.
The example outputs below could be printed as “0 wins, 1 tie, 0 losses” and “1 win, 1 tie, 0 losses”.
You can build a line of output like this using multiple calls to printf where only the last one includes the newline (\n) character.
Commit and push your changes once you’re happy with the program.