Rock Paper Scissors in C++: Build the Game Step by Step
Rock paper scissors is the perfect second project after a number guessing game. It’s small enough to finish in one sitting, but it makes you handle four real problems: representing choices meaningfully, generating randomness properly, validating input, and structuring a game loop.
We’ll build it in pieces and assemble the full program at the end.
Video Walkthrough
Step 1: Represent the Moves
You could use 1, 2, and 3 for the moves. Don’t — six months later if (a == 2) means nothing to anyone. Use an enum class:
enum class Move { Rock = 0, Paper = 1, Scissors = 2 };
Now the comparison logic reads like the actual game rules. The explicit values 0, 1, 2 are there because we’ll convert to and from the random number generator’s output, and pinning them down makes that conversion safe.
A small helper to print a move:
#include <string>
std::string moveName(Move m) {
switch (m) {
case Move::Rock: return "Rock";
case Move::Paper: return "Paper";
case Move::Scissors: return "Scissors";
}
return "Unknown";
}
Step 2: Decide the Winner
The naive approach is nine if statements. There’s a much shorter way, built on the fact that each move beats exactly the one before it in the cycle rock → paper → scissors → rock:
// Returns: 0 = tie, 1 = player wins, 2 = computer wins
int decideWinner(Move player, Move computer) {
if (player == computer) return 0;
int p = static_cast<int>(player);
int c = static_cast<int>(computer);
// player wins when their move is exactly one step "above" the computer's,
// wrapping around with modulo so Scissors(2) beats Paper(1) and Rock(0)
// loses to Paper(1)
return ((p - c + 3) % 3 == 1) ? 1 : 2;
}
Why + 3? Because p - c can be -2, and in C++ the modulo operator on a negative number gives a negative result. Adding 3 before taking % 3 keeps the value in the range 0–2. This is worth remembering — it comes up any time you wrap an index around.
If the modulo trick feels like showing off, a plain version is perfectly fine:
int decideWinnerSimple(Move player, Move computer) {
if (player == computer) return 0;
if ((player == Move::Rock && computer == Move::Scissors) ||
(player == Move::Paper && computer == Move::Rock) ||
(player == Move::Scissors && computer == Move::Paper)) {
return 1;
}
return 2;
}
Clear beats clever when someone else has to maintain it.
Step 3: The Computer’s Move
Use <random>, not rand():
#include <random>
Move randomMove() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<int> dist(0, 2);
return static_cast<Move>(dist(gen));
}
All three objects are static, so they’re created once on the first call and reused. Recreating and reseeding the generator on every call is a common mistake — it can produce the same value repeatedly, because the seed barely changes between rapid calls.
std::uniform_int_distribution(0, 2) gives each of the three values genuinely equal probability. The old rand() % 3 approach introduces a slight bias, because RAND_MAX usually isn’t evenly divisible by 3. See random numbers in C++ for the details.
Step 4: Input That Survives Bad Typing
If the player types banana when you asked for a number, std::cin enters a failure state and every subsequent read fails immediately — giving you an infinite loop of prompts. Handle it:
#include <iostream>
#include <limits>
int readChoice() {
int choice = 0;
while (true) {
std::cout << "1 = Rock, 2 = Paper, 3 = Scissors, 0 = Quit: ";
if (std::cin >> choice && choice >= 0 && choice <= 3) {
return choice;
}
std::cin.clear(); // reset the error flag
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Please enter 0, 1, 2, or 3.\n";
}
}
clear() resets the stream’s error state; ignore(...) throws away everything up to and including the newline, so the bad input doesn’t get re-read. Both are needed — one without the other doesn’t work. cin.ignore and clearing the buffer covers exactly why.
The Complete Program
#include <iostream>
#include <string>
#include <random>
#include <limits>
enum class Move { Rock = 0, Paper = 1, Scissors = 2 };
std::string moveName(Move m) {
switch (m) {
case Move::Rock: return "Rock";
case Move::Paper: return "Paper";
case Move::Scissors: return "Scissors";
}
return "Unknown";
}
Move randomMove() {
static std::random_device rd;
static std::mt19937 gen(rd());
static std::uniform_int_distribution<int> dist(0, 2);
return static_cast<Move>(dist(gen));
}
int decideWinner(Move player, Move computer) {
if (player == computer) return 0;
int p = static_cast<int>(player);
int c = static_cast<int>(computer);
return ((p - c + 3) % 3 == 1) ? 1 : 2;
}
int readChoice() {
int choice = 0;
while (true) {
std::cout << "1 = Rock, 2 = Paper, 3 = Scissors, 0 = Quit: ";
if (std::cin >> choice && choice >= 0 && choice <= 3) {
return choice;
}
std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cout << "Please enter 0, 1, 2, or 3.\n";
}
}
int main() {
int playerScore = 0;
int computerScore = 0;
int ties = 0;
std::cout << "=== Rock Paper Scissors ===\n\n";
while (true) {
int choice = readChoice();
if (choice == 0) break;
Move player = static_cast<Move>(choice - 1);
Move computer = randomMove();
std::cout << "You chose " << moveName(player)
<< ", computer chose " << moveName(computer) << ". ";
int result = decideWinner(player, computer);
if (result == 0) {
std::cout << "Tie!\n";
++ties;
} else if (result == 1) {
std::cout << "You win!\n";
++playerScore;
} else {
std::cout << "Computer wins.\n";
++computerScore;
}
std::cout << "Score - You: " << playerScore
<< " | Computer: " << computerScore
<< " | Ties: " << ties << "\n\n";
}
std::cout << "\nFinal score - You: " << playerScore
<< ", Computer: " << computerScore
<< ", Ties: " << ties << "\n";
if (playerScore > computerScore) std::cout << "You came out ahead!\n";
else if (computerScore > playerScore) std::cout << "The computer wins overall.\n";
else std::cout << "Dead even.\n";
return 0;
}
Compile and run:
g++ -std=c++17 -Wall rps.cpp -o rps
./rps
Sample session:
=== Rock Paper Scissors ===
1 = Rock, 2 = Paper, 3 = Scissors, 0 = Quit: 1
You chose Rock, computer chose Scissors. You win!
Score - You: 1 | Computer: 0 | Ties: 0
1 = Rock, 2 = Paper, 3 = Scissors, 0 = Quit: 2
You chose Paper, computer chose Scissors. Computer wins.
Score - You: 1 | Computer: 1 | Ties: 0
Notice that choice - 1 converts the menu number to the enum value. That’s why pinning Rock = 0 mattered back in step 1 — the mapping is now obvious rather than accidental.
Extensions Worth Trying
Once it runs, these each teach something new:
- Best of five. Stop when either score reaches 3. Needs a loop condition instead of
while (true). - Letters instead of numbers. Accept
r,p,s— practice with character input and string comparison. - Rock Paper Scissors Lizard Spock. Five moves, and the modulo trick still works: a move beats the two before it in the cycle.
- Move history. Store every round in a vector and print a summary at the end.
- A computer that adapts. Count what the player throws most and counter it. Suddenly it’s an AI project.
Related Articles
- C++ Number Guessing Game
- C++ Tic Tac Toe Game
- Random Numbers in C++
- enum class vs enum in C++
- C++ cin.ignore and Clearing the Input Buffer
- C++ Beginner Projects
Take Your C++ Further
If you’re looking to go deeper with C++, the C++ Better Explained Ebook is perfect for you — whether you’re a complete beginner or looking to solidify your understanding. Just $19.