From 3833ba1cdada6002f4b5de6c9cc5d58c671ac0ac Mon Sep 17 00:00:00 2001 From: yv256 <2024211406@bupt.cn> Date: Wed, 27 May 2026 23:12:24 +0800 Subject: [PATCH 1/2] feat:add Snake Water Gun game with unit test --- games/snake_water_gun.py | 103 +++++++++++++++++++++++ tests/games/test_snake_water_gun.py | 124 ++++++++++++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 games/snake_water_gun.py create mode 100644 tests/games/test_snake_water_gun.py diff --git a/games/snake_water_gun.py b/games/snake_water_gun.py new file mode 100644 index 000000000000..aa6ce6b71204 --- /dev/null +++ b/games/snake_water_gun.py @@ -0,0 +1,103 @@ +""" +Snake Water Gun Game +===================== + +A simple command-line game similar to Rock-Paper-Scissors. +Choices: Snake, Water, Gun. + +Rules: + - Snake drinks Water → Snake wins. + - Water damages Gun → Water wins. + - Gun kills Snake → Gun wins. + - Same choice → Tie. + +Functions: + play(player_choice: str) -> str + Play a single round against the computer. + main() -> None + Run an interactive game loop until the user quits. +""" + +import random +import sys +from typing import NoReturn + + +VALID_CHOICES = ("Snake", "Water", "Gun") + +WIN_CONDITIONS = { + "Snake": "Water", # Snake drinks Water + "Water": "Gun", # Water damages Gun + "Gun": "Snake", # Gun kills Snake +} + + +def play(player_choice: str) -> str: + """ + Play one round of Snake Water Gun against the computer. + + The function normalises the player's input (case-insensitive) and + validates it. The computer picks a random choice. It then + determines the winner according to the rules and returns a + descriptive string. + + Args: + player_choice: The player's selection. Any casing is accepted + ("snake", "SNAKE", "Snake", etc.). + + Returns: + A string in one of the following formats: + - "You chose Snake, Computer chose Water. You win!" + - "You chose Water, Computer chose Gun. You lose!" + - "You chose Gun, Computer chose Gun. It's a tie!" + - "Invalid choice: . Please choose Snake, Water, or Gun." + + Examples: + >>> play("Snake") + 'You chose Snake, Computer chose Water. You win!' # if computer picks Water + """ + # Normalise input: strip whitespace, capitalise first letter only + normalised = player_choice.strip().lower().capitalize() + + if normalised not in VALID_CHOICES: + return ( + f"Invalid choice: {player_choice}. " + f"Please choose {', '.join(VALID_CHOICES)}." + ) + + computer_choice = random.choice(VALID_CHOICES) + + if normalised == computer_choice: + result = "It's a tie!" + elif WIN_CONDITIONS[normalised] == computer_choice: + result = "You win!" + else: + result = "You lose!" + + return f"You chose {normalised}, Computer chose {computer_choice}. {result}" + + +def main() -> None: + """ + Run the interactive Snake Water Gun game loop. + + The user is repeatedly prompted for a choice until they type + ``quit`` (case-insensitive). Each round's result is printed + immediately. + """ + print("Welcome to Snake Water Gun!") + print(f"Choices: {', '.join(VALID_CHOICES)}") + print("Enter 'quit' to exit.\n") + + while True: + user_input = input("Your choice: ").strip() + if user_input.lower() == "quit": + print("Thanks for playing. Goodbye!") + break + result = play(user_input) + print(result) + print() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/tests/games/test_snake_water_gun.py b/tests/games/test_snake_water_gun.py new file mode 100644 index 000000000000..13aa0b092480 --- /dev/null +++ b/tests/games/test_snake_water_gun.py @@ -0,0 +1,124 @@ +""" +Unit tests for the Snake Water Gun game. + +Tests cover: + - All win/loss/tie combinations (3×3 matrix). + - Case‑insensitivity and whitespace handling. + - Invalid inputs. + - The interactive ``main()`` loop (with mocked I/O). +""" + +import random +import pytest +from io import StringIO + +from snake_water_gun import play, main, VALID_CHOICES, WIN_CONDITIONS + + +# --------------------------------------------------------------------------- +# Helper: parametrised tests for play() +# --------------------------------------------------------------------------- + +# Map computer choice to expected outcome for a given player choice +OUTCOMES = { + # player chooses Snake + ("Snake", "Snake"): "It's a tie!", + ("Snake", "Water"): "You win!", + ("Snake", "Gun"): "You lose!", + # player chooses Water + ("Water", "Snake"): "You lose!", + ("Water", "Water"): "It's a tie!", + ("Water", "Gun"): "You win!", + # player chooses Gun + ("Gun", "Snake"): "You win!", + ("Gun", "Water"): "You lose!", + ("Gun", "Gun"): "It's a tie!", +} + + +@pytest.mark.parametrize( + "player_choice, computer_choice", + [ + (p, c) + for p in VALID_CHOICES + for c in VALID_CHOICES + ], +) +def test_all_outcomes(player_choice: str, computer_choice: str, monkeypatch) -> None: + """ + Verify that every possible combination returns the correct result string. + """ + monkeypatch.setattr(random, "choice", lambda _: computer_choice) + + result = play(player_choice) + expected_outcome = OUTCOMES[(player_choice, computer_choice)] + expected = ( + f"You chose {player_choice}, Computer chose {computer_choice}. " + f"{expected_outcome}" + ) + assert result == expected + + +@pytest.mark.parametrize( + "raw_input, normalised", + [ + ("snake", "Snake"), + ("SNAKE", "Snake"), + (" water ", "Water"), + ("GuN", "Gun"), + ], +) +def test_case_insensitivity_and_whitespace( + raw_input: str, normalised: str, monkeypatch +) -> None: + """Input is normalised regardless of casing and surrounding spaces.""" + # Fix computer choice to something predictable + monkeypatch.setattr(random, "choice", lambda _: "Gun") + + result = play(raw_input) + # We expect the result string to contain the normalised player choice + assert f"You chose {normalised}" in result + + +@pytest.mark.parametrize( + "invalid_input", + [ + "", + " ", + "rock", + "paper", + "scissors", + "snakes", + "gunwater", + "123", + ], +) +def test_invalid_input_returns_error(invalid_input: str) -> None: + """Invalid choices produce an error message.""" + result = play(invalid_input) + assert result.startswith("Invalid choice:") + + +# --------------------------------------------------------------------------- +# Tests for main() interactive loop +# --------------------------------------------------------------------------- + +def test_main_quit_immediately(monkeypatch, capsys) -> None: + """Typing 'quit' right away exits the loop with a goodbye message.""" + monkeypatch.setattr("sys.stdin", StringIO("quit\n")) + main() + captured = capsys.readouterr() + assert "Thanks for playing. Goodbye!" in captured.out + + +def test_main_one_round_then_quit(monkeypatch, capsys) -> None: + """Play a single round and then quit.""" + inputs = StringIO("Snake\nquit\n") + monkeypatch.setattr("sys.stdin", inputs) + # Force computer choice to make assertion deterministic + monkeypatch.setattr(random, "choice", lambda _: "Water") + + main() + captured = capsys.readouterr() + assert "You chose Snake, Computer chose Water. You win!" in captured.out + assert "Thanks for playing. Goodbye!" in captured.out \ No newline at end of file From 1f8b1220bacae1c7e3ed0eecd26d19069cd3322d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 27 May 2026 15:15:58 +0000 Subject: [PATCH 2/2] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- games/snake_water_gun.py | 6 +++--- tests/games/test_snake_water_gun.py | 9 +++------ 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/games/snake_water_gun.py b/games/snake_water_gun.py index aa6ce6b71204..7c4f57ed9ee6 100644 --- a/games/snake_water_gun.py +++ b/games/snake_water_gun.py @@ -27,8 +27,8 @@ WIN_CONDITIONS = { "Snake": "Water", # Snake drinks Water - "Water": "Gun", # Water damages Gun - "Gun": "Snake", # Gun kills Snake + "Water": "Gun", # Water damages Gun + "Gun": "Snake", # Gun kills Snake } @@ -100,4 +100,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/tests/games/test_snake_water_gun.py b/tests/games/test_snake_water_gun.py index 13aa0b092480..04051ce4fcb9 100644 --- a/tests/games/test_snake_water_gun.py +++ b/tests/games/test_snake_water_gun.py @@ -38,11 +38,7 @@ @pytest.mark.parametrize( "player_choice, computer_choice", - [ - (p, c) - for p in VALID_CHOICES - for c in VALID_CHOICES - ], + [(p, c) for p in VALID_CHOICES for c in VALID_CHOICES], ) def test_all_outcomes(player_choice: str, computer_choice: str, monkeypatch) -> None: """ @@ -103,6 +99,7 @@ def test_invalid_input_returns_error(invalid_input: str) -> None: # Tests for main() interactive loop # --------------------------------------------------------------------------- + def test_main_quit_immediately(monkeypatch, capsys) -> None: """Typing 'quit' right away exits the loop with a goodbye message.""" monkeypatch.setattr("sys.stdin", StringIO("quit\n")) @@ -121,4 +118,4 @@ def test_main_one_round_then_quit(monkeypatch, capsys) -> None: main() captured = capsys.readouterr() assert "You chose Snake, Computer chose Water. You win!" in captured.out - assert "Thanks for playing. Goodbye!" in captured.out \ No newline at end of file + assert "Thanks for playing. Goodbye!" in captured.out