Today's outcomes
- Implement and test a pure outcome function.
- Track rounds and scores without counting invalid input.
- Keep seeded test randomness separate from secret generation.
Before you start: Complete Day 28 and its practical challenge.
Understand the concept
Separate game rules from the command line
Rock, paper, scissors has a small enough rule space to test completely. Three player choices and three computer choices create nine ordered combinations. Ties occur when choices match. Rock beats scissors, scissors beats paper, and paper beats rock. Store those relationships explicitly rather than scattering conditions through an input loop.
An outcome function should accept normalized choices and return win, loss, or tie from the player's perspective. It should not ask for input, print a menu, or generate the computer's choice. This pure decision function is easy to test because the same pair always has the same result.
Normalize the user's text at the interaction boundary with strip and lower. The core contract accepts only the exact lowercase names. That gives each layer one responsibility: the interface handles convenient spelling, while the rules reject invalid values rather than manufacturing an outcome.
Use randomness suitable for a game
The random module is appropriate here because the computer's choice is part of an ordinary practice game, not a secret credential. random.choice chooses an element from a sequence. A random.Random(seed) instance can create a repeatable sequence for a demonstration or test. That repeatability was a reason to avoid this generator for yesterday's password material.
Keep random choice outside the outcome function. A test can then supply scissors directly and verify that rock wins without depending on a seed or a particular sequence of random outputs. Reproducibility helps debugging, but an exhaustive rules test is stronger evidence for game logic than observing a few random rounds.
Update state only after a valid round
Track wins, losses, and ties separately. The number of played rounds should equal their sum. Invalid user input is not a round. A quit command is not a round either. Convert and validate before changing counters so a rejected input cannot silently affect the score.
The game loop should clearly display the player's choice, the computer's choice, and the result. After quit, print a final summary. An empty session is valid and has zero in every counter; do not divide by zero to calculate a win rate. A tie is a played round, so include it in the denominator if you later define a rate over all rounds.
Worked example
beats = {"rock": "scissors", "paper": "rock", "scissors": "paper"}
player, computer = "rock", "scissors"
if player == computer:
result = "tie"
elif beats[player] == computer:
result = "win"
else:
result = "loss"
assert result == "win"
score = {"win": 0, "loss": 0, "tie": 0}
score[result] += 1
assert sum(score.values()) == 1
print(player, "vs", computer, "->", result)
| Player / Computer | Rock | Paper | Scissors |
|---|---|---|---|
| Rock | Tie | Loss | Win |
| Paper | Win | Tie | Loss |
| Scissors | Loss | Win | Tie |
Read a row from the player's perspective. Reading the matrix in the opposite direction swaps wins and losses while leaving ties unchanged. This is a useful diagnosis when a game's results are consistently reversed.
Guided practice
- Predict: calculate all nine outcomes by hand, then compare them with the matrix. Include the three ties rather than testing only winning combinations.
- Modify: accept R, P, and S as user-interface aliases. Convert aliases before calling the core function, and keep the core's accepted values unchanged.
- Reconstruct: write a score update function and verify that an invalid result leaves the score unchanged. State whether the function mutates its input or returns a new dictionary.
Keep game state separate from the history you display. If you add a round history later, derive a summary from that history and reconcile it with the counters. This catches bugs that a final “you won” message cannot expose.
Daily challenge
Create a playable command-line game and a test file. The user enters rock, paper, scissors, or quit. Invalid input displays a helpful message and continues without counting a round. Each valid round chooses one computer option, reports its outcome, and updates one counter. Quit reports total rounds and all three counts.
| Requirement | Acceptance evidence |
|---|---|
| Rules | All nine pairs match the matrix |
| Validation | Unknown choice raises ValueError in core |
| State | Each valid round increases exactly one counter |
| Interaction | Invalid input and quit do not count |
| Empty session | Summary shows zero rounds |
Test core logic before running the menu. Then manually enter a bad choice, one valid choice, and quit. Compare the displayed number of rounds with the number of accepted inputs. That tests the boundary the core tests do not cover.
Reference solution
Compare after attempting the challenge
# file: rps_app.py
import random
CHOICES = ("rock", "paper", "scissors")
BEATS = {"rock": "scissors", "paper": "rock", "scissors": "paper"}
def outcome(player, computer):
if player not in CHOICES or computer not in CHOICES:
raise ValueError("choose rock, paper, or scissors")
if player == computer:
return "tie"
return "win" if BEATS[player] == computer else "loss"
def play_round(score, player, computer):
result = outcome(player, computer)
score[result] += 1
return result
def run_game(input_fn=input, choose_fn=random.choice):
score = {"win": 0, "loss": 0, "tie": 0}
while True:
player = input_fn("rock / paper / scissors / quit: ").strip().lower()
if player == "quit":
break
if player not in CHOICES:
print("Please choose rock, paper, scissors, or quit")
continue
computer = choose_fn(CHOICES)
result = play_round(score, player, computer)
print(f"You: {player}; computer: {computer}; result: {result}")
print(f'Rounds: {sum(score.values())}; wins: {score["win"]}; '
f'losses: {score["loss"]}; ties: {score["tie"]}')
return score
if __name__ == "__main__":
run_game()
# file: test_rps.py
from rps_app import outcome, play_round, run_game
expected = {
("rock", "rock"): "tie", ("rock", "paper"): "loss", ("rock", "scissors"): "win",
("paper", "rock"): "win", ("paper", "paper"): "tie", ("paper", "scissors"): "loss",
("scissors", "rock"): "loss", ("scissors", "paper"): "win", ("scissors", "scissors"): "tie",
}
for pair, result in expected.items():
assert outcome(*pair) == result
score = {"win": 0, "loss": 0, "tie": 0}
try:
play_round(score, "lizard", "rock")
except ValueError:
pass
else:
raise AssertionError("Unknown choice accepted")
assert score == {"win": 0, "loss": 0, "tie": 0}
answers = iter(["bad", " ROCK ", "quit"])
assert run_game(lambda prompt: next(answers), lambda choices: "scissors") == {
"win": 1, "loss": 0, "tie": 0}
assert run_game(lambda prompt: "quit") == {"win": 0, "loss": 0, "tie": 0}
print("Nine rules, invalid input, and session accounting passed")
Run python test_rps.py to check the rules and a scripted interaction. Run python rps_app.py to play. The injected input and choice functions are ordinary parameters with defaults; they let the test control boundaries without replacing the real game logic.
Debug and explain
If outcomes reverse, check whose perspective the result names describe. If invalid inputs inflate rounds, move validation ahead of the update. If the test hangs, check that scripted input eventually includes quit. Explain why choosing the computer option outside outcome makes the core easier to test and why the total includes ties.
Review and next step
Save the nine-case matrix, the passing tests, and a short interaction transcript. You now have three projects with different contracts: persistent state, password generation, and deterministic rules with random gameplay. Day 30 asks you to combine the course's tools in a capstone with a defensible result.
Sources and verification
Original teaching examples checked with Python 3.12.14. Library behavior reviewed against these primary references on October 3, 2026. Day 26's examples also use Beautiful Soup 4.14.3.
Check your understanding
Five questions, 20 points each. Pass at 85% or above, which requires all five correct. Feedback explains each answer; revise the lesson and retry when needed.
Review your practical work
Check these statements after running your own challenge. This is your self-review; the website does not execute or assess your Python code.
Pass the knowledge check and finish the practical self-review to save completion.