Project: Snake, Water, Gun
Build a best-of-five Snake, Water, Gun game with pure rule functions, validated input, and tests that never depend on luck.
By the end, you’ll be able to…
- Express the game rules as a pure function with a clear contract.
- Inject randomness so a match can be reproduced in tests.
- Run a bounded match with validated input and a quit command.
- Test every outcome without playing the game manually.
01Write the rules before the interface
Snake drinks water, water douses gun, and gun shoots snake. The rules are a small decision table, not a pile of input statements. Start with a function that receives the player's move and the computer's move and returns 'win', 'lose', or 'draw'. Keep printing and input outside this function so the rules can be tested directly.
Validate the move before comparing it. A rule function should reject an unknown choice with ValueError rather than silently treating it as a loss. The error message should list the accepted moves so the caller can recover.
02Make the computer fair and testable
random.choice gives each legal move the same chance, but a test cannot assert a specific result against an unseeded global generator. Pass a random.Random instance into the chooser, or pass the computer's move directly into the rule function. The game can still feel random while the test remains deterministic.
A seed is for reproducibility, not fairness or security. Use a dedicated Random instance rather than reseeding the module-level generator, which can surprise other code in the same process.
03Run a bounded match
A best-of-five match ends when one player reaches three wins. Draws do not add to either score. Keep the loop condition explicit: while player_score < 3 and computer_score < 3. A round counter alone is not enough unless draws are handled separately.
Read input as text, normalize it with strip().lower(), and handle the quit command before conversion. If you convert with int too early, 'quit' becomes a ValueError and the user cannot leave. Print the current score after each round so the match state is visible.
04Test the rules without playing
There are only nine combinations, so a table-driven test can cover every branch. Assert the expected result for each pair and assert that an invalid move raises ValueError. This is faster and more reliable than playing five rounds and hoping the interesting case appears.
Add one integration test for the scoring loop with a scripted sequence of moves. Keep the real input loop out of the test by extracting play_round(player, computer, scores) or by injecting a move source. The goal is confidence in the rules, not a simulation of keyboard timing. Run the rule table before the scoring test; if either fails, fix the rule function before touching the interface.
import unittestBEATS = {"snake": "water", "water": "gun", "gun": "snake"}def decide(player, computer): if player not in BEATS or computer not in BEATS: raise ValueError("choose snake, water, or gun") if player == computer: return "draw" return "win" if BEATS[player] == computer else "lose"class RuleTests(unittest.TestCase): def test_every_pair(self): expected = { ("snake", "water"): "win", ("snake", "gun"): "lose", ("water", "gun"): "win", ("gun", "snake"): "win", } for pair, result in expected.items(): self.assertEqual(decide(*pair), result) for move in BEATS: self.assertEqual(decide(move, move), "draw") def test_invalid_move(self): with self.assertRaises(ValueError): decide("fire", "water")unittest.main(argv=[""], exit=False)The ideas worth keeping.
- Keep the rules pure and the interface separate.
- Inject randomness so tests can be deterministic.
- Bound the match and validate every move before using it.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Implement the decision table
Write decide(player, computer) using a BEATS dictionary. Return win, lose, or draw and raise ValueError for an unknown move. Demonstrate all three outcomes.
Explore a worked solution
BEATS = {"snake": "water", "water": "gun", "gun": "snake"}def decide(player, computer): if player not in BEATS or computer not in BEATS: raise ValueError("choose snake, water, or gun") if player == computer: return "draw" return "win" if BEATS[player] == computer else "lose"print(decide("snake", "water"))print(decide("snake", "gun"))print(decide("water", "water"))The dictionary stores only the winning relationship. A pair not found in the dictionary is a loss unless the moves are equal, which keeps the table small and easy to audit.
Simulate a seeded match
Use random.Random(3) to play five rounds with both moves chosen by the generator. Track wins, losses, and draws, then print the final tally.
Explore a worked solution
import randomBEATS = {"snake": "water", "water": "gun", "gun": "snake"}MOVES = list(BEATS)rng = random.Random(3)score = {"win": 0, "lose": 0, "draw": 0}for _ in range(5): player = rng.choice(MOVES) computer = rng.choice(MOVES) if player == computer: score["draw"] += 1 elif BEATS[player] == computer: score["win"] += 1 else: score["lose"] += 1print(score)A seeded generator makes the demonstration reproducible. The score dictionary counts each outcome without separate variables and makes the final tally easy to print.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Implement the decision table
Write decide(player, computer) using a BEATS dictionary. Return win, lose, or draw and raise ValueError for an unknown move. Demonstrate all three outcomes.
decide("snake" "water")
decide("snake" "gun")
decide("water" "water")Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
Pause. Put the pieces together.
You’ve reached the end of Functions and reusable programs. Before moving on, use this task to check what you can do independently.
Separate a calculation logic from input/output display, import your function from a second file, and call it without running unwanted code on import.
Try it in your own environment and write down the result. This is a self-check; reading completion does not evaluate your code.
Review this moduleOne lesson closer.
Reading is a start. Trying it yourself makes the connection.