Rock Paper Scissors Game: Build a Simple Version
Build a simple rock paper scissors game against a computer. Learn the game loop, random choices, win logic, and practical next steps for a beginner project.

A Small Game, a Useful Programming Exercise: Rock Paper Scissors
Rock paper scissors looks easy because the rules fit in a sentence. Turning those rules into a working computer game is a different story. You still have to collect a choice, generate an opponent's move, decide who won, and decide when the game should stop. The same questions show up in bigger software projects - you just get fewer moving parts to hide behind.
A beginner project described by Ashley Augustin on DEV takes the familiar game and builds a one-player version against the computer. It uses Python's random module for the computer's move and introduces a player class as a place to store player information. The project is intentionally small, with a path to a two-player version later.
That makes it a solid case study - not because rock paper scissors needs "serious" software, but because a small game makes the decisions you're making obvious. In this article, I'll walk through how the core loop fits together, what "random" really implies for round outcomes, and when it's worth adding a player class or extra features.
What a computer version needs to do
A digital version doesn't need a complicated architecture. At its simplest, it's just this sequence:
- Ask the player for a move.
- Choose a move for the computer.
- Compare the two moves.
- Report a win, loss, or tie.
- Decide whether to play another round or finish.
The order matters. If the game checks the winner before it has both moves, or compares values that aren't in the same format, even a tiny project can feel weirdly confusing. A clear sequence makes it easier to spot what went wrong.
Start with the rules, not the interface
Before you touch the prompts or menus, write the rules in a form you can test:
- Rock beats scissors.
- Scissors beats paper.
- Paper beats rock.
- Matching moves produce a tie.
Then decide how the program represents moves. Those could be text values, numbers, or a small object-like structure. Representation matters mainly because you need to use it consistently. If the player enters rock but the computer returns Rock, a case-sensitive comparison can treat them as different moves - even though a human would see them as the same choice.
Input handling is part of the game logic, not an afterthought. People will type unexpected words, add spaces, or choose numbers outside the allowed range. The program should either normalize input (for example, strip spaces and force a consistent case) or reject it with a clear prompt. Otherwise, an invalid entry can accidentally produce a "real" result and make a player think the game is broken.
Keep a round separate from the whole game
A round has one job: take two valid moves and determine the outcome. The whole game has a different job: repeat rounds until a stopping condition triggers. Separating those ideas makes the control flow easier to reason about.
A round result can be one of three values: player win, computer win, or tie. The game loop can then decide what to do - show the result, update a score, and/or stop when the end condition is met.
This separation is useful even in a project that's only a few dozen lines. It gives you a natural place to add features later without turning the whole program into one long tangle. It also helps when debugging: if the winner is wrong, focus on the comparison logic; if the game runs too long, focus on the stopping rule.
Define what "the game ends" means
In the original project summary, play continues until either the player or computer wins. That leaves one important detail: what happens on a tie? One common option is to replay the round, since nobody actually won. Another option is to count ties toward a fixed number of rounds. Whatever you pick, make it explicit - don't rely on whatever falls out of your loop.
Your stopping condition should also be precise. "Stop when someone wins" could mean:
- stop after the first decisive round,
- stop when a score reaches a target,
- or stop after a fixed number of rounds and compare totals.
Those aren't interchangeable. Pick one first, then make the code follow that decision.
Random choices, round odds, and game odds
Using a random module is a simple way to generate the computer's move. If each move is selected with equal probability, each move appears with a one-in-three chance. But that doesn't automatically translate into a 33% chance for the player to win the whole match.
For any fixed player move, a uniformly random computer has three possible outcomes: it loses to the player, wins against the player, or ties. So, for a single round, the player has a one-in-three chance of winning, a one-in-three chance of losing, and a one-in-three chance of tying - assuming the computer's choices are truly equally likely and the win/loss rules are implemented correctly.
A tie changes the meaning of the odds
If ties cause another round and the game continues until someone wins, ties don't count as the final result. Under those assumptions, neither side has an advantage: both are equally likely to win a decisive round.
Here's the easy way to think about it: the chance of winning a given round attempt is still one in three, but the chance of eventually winning a match that ends on the first non-tie is different. In that "play until someone wins" setup, it becomes one-half for each side.
This is where people get mixed up, because "chance of winning" can mean two different things:
- Round probability: the chance of winning one attempt.
- Match probability: the chance of winning before the game's stopping condition kicks in.
If you change the match rules - like ending after a fixed number of rounds, scoring ties, or using an opponent the player can predict - the math changes too. The best approach is to describe the random selection accurately rather than promising a particular overall win rate.
Random does not automatically mean fair
For a beginner game, choosing uniformly from three options is usually fine. But if you accidentally use weights, or map random numbers to moves unevenly, one move can show up more often than you expect.
A quick sanity check: run many computer move selections and count how often each move appears. It won't be exactly equal in a short run, but over a large sample it should be reasonably close.
Also, there's a difference between a random opponent and a "smart" opponent. A random computer doesn't learn from what the player does or react to patterns. That's totally fine for the first version: it's predictable to implement and easy to explain. If you want an adaptive opponent later, you'll need to define what information it tracks and how it uses that info. That's a separate feature - you don't get it "for free" just because you used randomness.
A practical way to structure the game
If you're building the first working version, don't try to predict every future feature. Focus on making the current rules easy to follow and easy to verify. A useful way to break it down is: input, computer choice, outcome resolution, and game flow.
1. Validate the player's move
Only accept supported moves.
- If the player types text, normalize it before comparing - strip leading/trailing spaces and use a consistent case.
- If the player picks a number from a menu, confirm that number maps to one of the allowed options.
When the entry is invalid, don't treat it like a round. Tell the player what's accepted and ask again. This avoids a classic beginner bug where invalid input falls through to some default and gives a point to one side without the player actually making a real move.
2. Generate the computer's move
Pick from the same three valid options the player can pick. Using the same set reduces the chance that the comparison logic sees a move it doesn't know how to handle.
Generate the computer's move once per round. Generating a new computer move while you're also checking outcomes can lead to inconsistent results - what the player sees and what the code decides won't match up.
3. Resolve the result in one place
Use a clear set of comparisons (or a rule table). Since there are only three winning combinations, straightforward logic is often easiest to read in a small project. A table can make more sense if you plan to add more options later or if you want to keep "rules" separate from "control flow."
No matter what approach you use, handle ties explicitly. Then make sure every pair of valid moves maps to exactly one outcome. That's your correctness check: there are nine possible player/computer combinations, and each should resolve to win, loss, or tie - nothing missing, nothing duplicated.
4. Control the loop with an explicit condition
Your loop should reflect the stopping rule you chose. If the first decisive round ends the game, ties should trigger another iteration and then a win/loss should exit. If the game is score-based, update the score and stop when the target is reached.
Avoid vague conditions like "while playing" unless the code makes it clear who updates the state and when.
A simple language-neutral outline looks like this:
while the game is not over:
player_move = get_valid_player_move()
computer_move = choose_random_move()
result = compare_moves(player_move, computer_move)
display_moves_and_result(result)
update_game_state(result)
This isn't a full implementation - it's just a map of responsibilities. The programming language and user interface details can change without changing the underlying flow.
Where a player class fits
The original project also introduces a player class to hold player information. That can be a good place to experiment with classes, but it's not automatically helpful just because a program has a player. If the class only stores one name or one value that the rest of the code never uses, you might be adding structure without solving a real problem.
A player class becomes useful when it owns related state or behavior. For example, a player object might track a name, a score, or a history of moves. The real question is whether grouping those details makes the code easier to understand and change.
A practical rule: add the smallest structure that makes today's code clearer. Don't build a whole "framework" for features that might never show up. Refactor when you actually have a requirement.
What to improve next - and what can wait
A small game can grow in lots of directions: two-player mode, scoring, a graphical interface, saved results, or a more strategic opponent. The existence of a feature idea doesn't mean it belongs in the next commit.
A sensible progression
A reasonable order for a beginner project is:
- Make the core round reliable. Validate input, generate one computer move, resolve all outcomes, and show the result.
- Make the stopping rule clear. Decide what ties do, whether the first decisive win ends the game, and whether it's "to a score."
- Add a score only if you need it. Keep and display it consistently, and reset it at the right time.
- Improve the interface. Clear prompts and helpful error messages matter more than polish in a text-based version.
- Consider two-player mode. Reuse the same outcome rules, but collect the second move from a second human.
This keeps the game testable before you add more moving parts. A graphical interface, for example, can make a project feel "done" while you're still hiding bugs in the rules. Get the result logic right first.
Two-player mode is a product decision as well as a code change
Adding a second player isn't just swapping "random computer move" for "another person's input." It also changes the user experience:
- Do both players enter moves on the same screen?
- Does the second player see the first player's move?
- Should either choice be hidden until both are submitted?
Those details affect fairness and usability. If both players are on one device and the first move stays visible, the second player has information the first player didn't. You can accept that limitation in a simple build - but it should be an intentional choice. A more private flow might require both players to submit before revealing anything.
The good part: the outcome rules don't need to be duplicated. Keep outcome resolution independent from where each move came from. A round can use a human choice, a random computer choice, or two human choices without rewriting the win/loss logic.
Test the cases that are easy to overlook
You don't need a huge test suite for a game this small. Start with a compact set of cases:
- Rock vs rock is a tie.
- Rock vs scissors is a player win.
- Rock vs paper is a player loss.
- Repeat those checks with paper and scissors as the player move.
- Invalid input shouldn't produce a winner.
- A tie should follow your replay/scoring rule.
- A decisive result should trigger the stopping behavior you chose.
Testing all nine move combinations catches most mistakes in comparison logic quickly. Separately, you can run repeated random selections to check that all three computer moves appear. This won't prove the generator is statistically perfect, but it will catch mapping errors (like accidentally never selecting one move).
The source project is framed as a beginner's first spin on a familiar game - not a finished product with every edge case documented. That's a fair scope. The real goal is to practice a full development loop: define the rules, implement them, test representative cases, and adjust when the behavior doesn't match your expectations.
Conclusion
A rock paper scissors game is small enough to build quickly, but it's still a great way to practice useful habits: define the rules, keep a round separate from the overall game, validate input, and make randomness behave the way you think it does. It also forces one important design trade-off: whether you add a player class or go with a simpler structure - and when.
Start with a complete, testable one-player game. Check all nine move combinations, decide exactly how ties work, and describe the computer's random behavior accurately. Then add the next feature only when it improves the experience or teaches you something specific. If you want the original reference, you can read Ashley Augustin's DEV post - and if you build your own version, share what you changed and what you learned.