Step 47. ★ Project: Rock-Paper-Scissors — Your First Game Completed Without a Manual

Step 47. ★ Project: Rock-Paper-Scissors — Your First Game Completed Without a Manual

Level 1 — Programming and the Computer’s Inner Workings | Difficulty ★★★☆☆ | Estimated time: 4 hours

Prerequisites: Steps 41–46 complete. You can use variables, conditionals, loops, functions, files, and exception handling.

  • What you need: a computer with Python installed, a text editor, and a sheet of paper for notes. Nothing new to install.
  • Caution: today’s exercise is 100% safe. There’s almost no new grammar — instead, every piece of grammar so far deploys in your first comprehensive project.

Buy LEGO blocks and you follow the manual to build one or two models. But real skill appears when you build something from imagination without a manual. If Steps 41–46 were the block manuals, today is the day you build the castle of your imagination. Why rock-paper-scissors of all things? Because it’s small but complete — it has input (my hand), processing (the computer’s hand generation and win/loss judgment), and output (result and record), and loops, conditionals, and exception handling all meet inside one program.


1. Learning Objectives

By the end of this chapter, you will be able to:

  • Write a five-line design (task, input, output, rules, accidents) on paper before coding
  • Make the computer play a random hand with random.choice
  • Organize a complex judgment into a table, then implement it simply with an in check
  • Build a program that survives typos with input validation (strip + in + continue)
  • Experience refactoring by splitting working code into functions

2. Background Knowledge — Today’s Tools and Concepts

Today’s Tools at a Glance

Category Details
Language/environment Python 3 (running script files)
Today’s parts random.choice (random pick), tuples (a, b), in checks, continue, counter variables
Review grammar lists, if/elif/else, while, f-strings, function definitions
Concepts needed the five-line design, random numbers (dice), judgment tables, the incremental-growth work method
Today’s artifact rsp.py — a best-of-three rock-paper-scissors game

2-1. Design First — Five Lines Before Coding

The beginner’s biggest misconception is thinking "coding is just starting to type." Experts design before they type. Five lines on paper is enough.

  1. One sentence about what the program does: "play best-of-three rock-paper-scissors against the user"
  2. Input: scissors/rock/paper (text)
  3. Output: the verdict and the record (win/draw/loss counts)
  4. Rules: the win-condition table
  5. Expected accidents: typo input, empty input

With these five lines, coding becomes "translation"; without them, it becomes "wandering." Today we start from these five lines.

2-2. Random Numbers — The Computer’s Dice

For the computer to play a random hand, it needs dice. Python’s dice are the random module.

import random
hand = random.choice(["scissors", "rock", "paper"])

random.choice(list) picks one item from the list at random. The point is that every run produces a different result. You used random.randint in Step 43’s number-guessing game, remember? Same family.

Five rolls, verified (2026-09-09):

rock
scissors
rock
scissors
rock

Even when the same hand comes up in a row, that’s a perfectly normal face of randomness.

2-3. Judgment Logic — Organize into a Table First

Rock-paper-scissors has nine win/loss combinations. Before lining up nine ifs, organize them into a table and the structure appears.

  • Draw: my hand == computer’s hand (3 cases)
  • I win: (scissors, paper), (rock, scissors), (paper, rock) (3 cases)
  • The rest: I lose (3 cases)

Make "the three winning pairs" into a list, and the judgment shrinks to a single in. The habit of organizing complex conditions into tables applies to every logic design beyond games.


3. Follow Along

3-1. The Skeleton — A One-Round Game

Input (rsp.py)

import random

choices = ["scissors", "rock", "paper"]
me = input("Throw scissors, rock, or paper: ")
computer = random.choice(choices)
print(f"The computer threw {computer}.")

Run it. Several times.

How to read it: the computer’s hand changes with every run. There’s no judgment yet — we’ve only built the skeleton of "receive input, computer throws." The iron rule of building big things: build small, and once it runs, grow it.

3-2. Attaching the Judgment — Turning the Table into Code

Input (continuing in rsp.py)

win_pairs = [("scissors", "paper"), ("rock", "scissors"), ("paper", "rock")]

if me == computer:
    print("It's a draw!")
elif (me, computer) in win_pairs:
    print("You win!")
else:
    print("You lose...")

How to read it: (me, computer) is a tuple — the unchangeable bundle you learned in Step 43. in win_pairs is the question "is that pair in the winning-pairs list?" Thanks to the table, nine ifs organized into three branches.

Predict: when the computer throws "paper" and I throw "scissors," do I win? Find ("scissors","paper") in win_pairs, then run until that combination actually appears and check.

3-3. Refining the Input — Filter at the Entrance

Users will enter things like "scissors " (trailing space) or "scissorsscissors."

Input

me = input("Throw scissors, rock, or paper: ").strip()
if me not in choices:
    print("Please pick only scissors, rock, or paper!")
else:
    # ... judgment code ...

How to read it: .strip() peels whitespace off both ends. And if me not in choices: filters "if not one of the three" first. Weird input gets caught here, so the judgment section can work in peace. Filter at the entrance is Step 46’s defensive programming in field form.

3-4. Loops and Records — Completing Best-of-Three

Now let’s assemble the whole game.

Input (all of rsp.py)

import random

choices = ["scissors", "rock", "paper"]
win_pairs = [("scissors", "paper"), ("rock", "scissors"), ("paper", "rock")]
win = draw = lose = 0

print("=== Rock-Paper-Scissors, best of three! ===")
while win < 2 and lose < 2:
    me = input("scissors, rock, paper: ").strip()
    if me not in choices:
        print("Please pick only scissors, rock, or paper!")
        continue
    computer = random.choice(choices)
    print(f"Computer: {computer}!")
    if me == computer:
        draw = draw + 1
        print("Draw!")
    elif (me, computer) in win_pairs:
        win = win + 1
        print("Win!")
    else:
        lose = lose + 1
        print("Loss...")
    print(f"Current record: {win}W {draw}D {lose}L")

print("=== Game over ===")
if win == 2:
    print("Congratulations! Overall victory!")
else:
    print("Too bad. Aim for the next match!")

An actual run (verified on 2026-09-09 — the first input deliberately had the typo "scissorsscissors"):

=== Rock-Paper-Scissors, best of three! ===
scissors, rock, paper: Please pick only scissors, rock, or paper!
scissors, rock, paper: Computer: paper!
Loss...
Current record: 0W 0D 1L
scissors, rock, paper: Computer: paper!
Win!
Current record: 1W 0D 1L
scissors, rock, paper: Computer: paper!
Draw!
Current record: 1W 1D 1L
scissors, rock, paper: Computer: scissors!
Win!
Current record: 2W 1D 1L
=== Game over ===
Congratulations! Overall victory!

(The computer’s hand differs every run — it’s random, after all.)

How to read it: while win < 2 and lose < 2 — "keep going until either side reaches 2" is the formula of best-of-three. Notice in the verified run that the first input with the typo ("scissorsscissors") was invalidated by continue and no counter went up. A draw brings neither side closer to 2, so rounds continue.

Why: these twenty lines contain variables, lists, tuples, in, if/elif/else, while, and, continue, counters, and f-strings — everything. A gift set of it all.

3-5. Splitting into Functions — Refactoring Practice

Practice splitting working code into parts. Let’s extract the judgment into a function.

Input

def judge(me, computer):
    if me == computer:
        return "draw"
    if (me, computer) in win_pairs:
        return "win"
    return "lose"

Now the game body delegates judgment in one line — result = judge(me, computer) — and raises counters based on whether result is "win"/"draw"/"lose". If the rules change, only this one function needs fixing — Step 44’s "function that does one thing" in the field.

Testing the function standalone, verified (2026-09-09):

judge("rock", "rock")     -> draw
judge("paper", "rock")    -> win
judge("scissors", "paper") -> win
judge("paper", "scissors") -> lose

Predict: why is ("paper","scissors") a lose? The pairs in win_pairs are always in (my hand, computer’s hand) order. ("paper","scissors") isn’t in the winning list, so it falls to else.

3-6. Planting Bugs on Purpose — Debugging Drills

Let’s deliberately plant bugs in the finished game and practice finding them. Copy the file as rsp_bug.py, then deliberately change these three spots.

  1. Change ("scissors", "paper") in win_pairs to ("paper", "scissors")
  2. Change the and in while win < 2 and lose < 2 to or
  3. Delete the continue in the input validation

How to experiment: after each change, run it and write down "what symptom appears" in your notebook. Reference symptoms: 1 is "I beat paper with scissors but it says I lost," 2 is "one side reaches 2 wins but it doesn’t end," 3 is "a typo gets recorded as a loss." Write the symptom first, then explain the cause.

Why: making bugs is the fastest training for finding them. It engraves the "symptom → cause" link into your body, and combined with Step 46’s traceback reading, it becomes real debugging power.

3-7. Drawing the Structure — The Blueprint of My Game

Let’s draw the finished game’s flow in your notebook.

Start → initialize record → [loop: input → validate → computer's hand → judge → update record] → reach 2W/2L → print result → end

Then next to each box, write the line numbers of the code doing that work. When picture and code connect one-to-one, you "own" this program. This blueprint habit shines brighter as programs grow — even a hundred lines isn’t scary with a blueprint.


4. Missions & Exercises

Mission — Game Upgrades

Add the following four features to the finished game yourself:

  1. Save the record: when the game ends, append the final record and date to rsp_log.txt (Step 45’s file writing; for today’s date with the datetime module, use import datetime then datetime.date.today())
  2. Restart menu: after the game ends, ask "Play again? (y/n)" and start a new game on y
  3. Streak display: if it ends 2-0 with no draws, print a special message: "A flawless victory!"
  4. Clue document: at the top of the file, write the game’s rules and controls in five comment lines

When done, demo it to your family. Watch whether the game dies when they inevitably stuff weird things into it. If it dies, that’s the next thing to fix.

Exercises

Question 1. Name all the items of the five-line design you write before coding.

Question 2. If you call random.choice only once, outside the while loop, what symptom appears and why?

Question 3. In best-of-three, if every round is a draw, the game never ends. Is that a bug, or a consequence of the rules? If a consequence of the rules, suggest one way to patch it.

Question 4. If you mistakenly write ("paper", "scissors") instead of ("scissors", "paper") in win_pairs, what symptom appears? What habit prevents this accident?


5. Model Answers & Completion Criteria

Mission Model Answer

Core code for the four additions:

import random
import datetime

def save_log(win, draw, lose):
    today = datetime.date.today()
    with open("rsp_log.txt", "a", encoding="utf-8") as f:
        f.write(f"{today} — {win}W {draw}D {lose}Ln")

# After the game ends:
save_log(win, draw, lose)
if win == 2 and draw == 0:
    print("A flawless victory!")

again = input("Play again? (y/n): ").strip().lower()
if again == "y":
    # reset win, draw, lose to 0 and re-enter the game loop
    ...

For restart, the clean structure is wrapping the whole game loop in a def play_one_game(): function and calling it again from an outer while whenever again == "y".

How to verify: ① Finish two games and open rsp_log.txt to confirm two lines accumulated with dates. ② Check that answers other than "y" at restart ("yes", "sure") are treated as exit, and if you want to allow them, fix it to again in ["y", "yes"]. ③ Confirm the special message appears only when ending 2-0 (it must not appear if a draw is mixed in). ④ Confirm someone else can play the game reading only the five comment lines at the top of the file.

Exercise Answers

Answer 1. ① One sentence about the task, ② input, ③ output, ④ rules, ⑤ expected accidents. With these five lines, coding becomes translation; without them, wandering.

Answer 2. Within one game, the computer throws the same hand every round. Because the dice were rolled only once, before the game started. The hand-generation line must be inside the while so a fresh roll happens every round.

Answer 3. It’s not a bug — it’s a consequence of the rules. Best-of-three doesn’t count draws, so an endless stream of draws is, in principle, infinite. An example patch: keep a round_count variable and end with "game drawn" after 10 rounds. Discovering and patching gaps in the rules yourself is also design.

Answer 4. "I beat paper with scissors, but it says I lost." The first slot of a pair is always "my hand," and ("paper","scissors") isn’t a combination in the list, so it falls to else (loss). Prevention habits: organize the judgment logic into a table before coding, print win_pairs to check with your eyes, and write a comment noting the tuple’s position meaning (front = me, back = computer).

Completion Criteria Checklist

  • [ ] I wrote a five-line design on paper before coding
  • [ ] I can make a fresh computer hand each round with random.choice
  • [ ] I organized the judgment logic into a table and implemented it with in
  • [ ] I completed a best-of-three game that survives typo input
  • [ ] I tried refactoring by extracting the judgment into a function (judge)
  • [ ] I deliberately planted three bugs and connected symptoms to causes
  • [ ] Mission: I added saving, restart, streak display, and the clue document

6. Common Pitfalls & Fixes

Wall 1. The computer throws the same hand every time

Symptom: it differs between runs, but within one game the hand never changes.
Cause: you called random.choice only once, outside the loop. The dice were rolled once before the game started.
Fix: check that the hand-generation line is inside the while. A fresh roll every round.

Wall 2. A typo still raises the round count

Symptom: you typed "scissorsscissors" and it got recorded as a loss.
Cause: the input validation (not in choices) sits after the judgment, or you didn’t use continue after the check. If you only filter without re-asking, the flow leaks into the judgment.
Fix: check the order: validate → if invalid, guide + continue. continue is the sign saying "this round is void." In the finished code, it was verified that typo input doesn’t raise any counter (2026-09-09).

Wall 3. Endless draws loop forever

Symptom: if every round is a draw, the game never ends.
Cause: not a bug — a consequence of the rules. Best-of-three doesn’t count draws, so in principle it’s infinite.
Fix: set a maximum round count like a real game. Example: keep a round_count variable and end with "game drawn" after 10 rounds. Discovering and patching gaps in the rules yourself is also design.

Wall 4. The judgment is wrong (I won but it says I lost)

Symptom: scissors vs. paper says I lost.
Cause: most often, the pair order in win_pairs is flipped. ("paper","scissors") doesn’t mean "paper beats scissors" — it becomes a combination not in the list. The first slot of a pair is always "my hand."
Fix: print win_pairs and check with your eyes, and write a comment noting the tuple’s position meaning (front = me, back = computer). The habit of organizing into a table first prevents this accident.

Wall 5. Trying to make it perfect, I never start

Symptom: trying to write beautiful code from the start, the file stays empty.
Cause: perfectionism. The most common failure in projects.
Fix: remember today’s order. Skeleton (one round) → judgment → validation → loop → function split. At every stage we confirmed "it runs" before moving on. A finished ugly duckling beats an unfinished masterpiece.


7. Summary

Today’s Concepts

Concept One-line description
Five-line design Task / input / output / rules / accidents — on paper before coding
Random numbers Dice for games — roll fresh inside the loop every round
Judgment table Organize complex conditions into a table and one in finishes it
Entrance validation Block weird input up front with strip + in + continue
Incremental growth Build small, and once it runs, grow it

Today’s Grammar & Parts

Part What it does
random.choice(list) Pick one item from a list at random
(me, computer) in win_pairs Check whether the pair is in the list
.strip() Remove whitespace at both ends of input
continue Void this round, back to the top of the loop
win = draw = lose = 0 One-line counter initialization

Instincts More Important Than Commands

"First make it run, then make it good." Today’s order — skeleton → judgment → validation → loop → function split — is the industry’s standard work method and the only shortcut by which a beginner catches up to an expert.

Remember two more things. First, today’s random is a dice for games. It imitates randomness by fixed rules, so knowing the seed makes it predictable — fatal when making passwords or keys. For security purposes there’s a separate module called secrets — "randomness comes in grades" is today’s one line of security. Second, keep the finished rsp.py in your archive. It’s proof that you brought a game into the world from a blank screen — evidence your future self will come looking for on a tiring day.

Congratulations. Today you released into the world "your first program completed without a manual." Starting from an empty file — designing, building, fixing, finishing — the whole cycle. This is a developer’s everyday, and starting today, it’s yours too.


Once every box is checked, Step 47 is complete.