Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions games/snake_water_gun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""

Check failure on line 1 in games/snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (INP001)

games/snake_water_gun.py:1:1: INP001 File `games/snake_water_gun.py` is part of an implicit namespace package. Add an `__init__.py`.
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

Check failure on line 22 in games/snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

games/snake_water_gun.py:22:8: F401 `sys` imported but unused help: Remove unused import: `sys`
from typing import NoReturn

Check failure on line 23 in games/snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

games/snake_water_gun.py:23:20: F401 `typing.NoReturn` imported but unused help: Remove unused import: `typing.NoReturn`

Check failure on line 23 in games/snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (I001)

games/snake_water_gun.py:21:1: I001 Import block is un-sorted or un-formatted help: Organize imports


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: <input>. 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()
121 changes: 121 additions & 0 deletions tests/games/test_snake_water_gun.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
"""

Check failure on line 1 in tests/games/test_snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (INP001)

tests/games/test_snake_water_gun.py:1:1: INP001 File `tests/games/test_snake_water_gun.py` is part of an implicit namespace package. Add an `__init__.py`.
Unit tests for the Snake Water Gun game.

Tests cover:
- All win/loss/tie combinations (3×3 matrix).

Check failure on line 5 in tests/games/test_snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (RUF002)

tests/games/test_snake_water_gun.py:5:39: RUF002 Docstring contains ambiguous `×` (MULTIPLICATION SIGN). Did you mean `x` (LATIN SMALL LETTER X)?
- Case‑insensitivity and whitespace handling.

Check failure on line 6 in tests/games/test_snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (RUF002)

tests/games/test_snake_water_gun.py:6:11: RUF002 Docstring contains ambiguous `‑` (NON-BREAKING HYPHEN). Did you mean `-` (HYPHEN-MINUS)?
- 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

Check failure on line 15 in tests/games/test_snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (F401)

tests/games/test_snake_water_gun.py:15:56: F401 `snake_water_gun.WIN_CONDITIONS` imported but unused help: Remove unused import: `snake_water_gun.WIN_CONDITIONS`

Check failure on line 15 in tests/games/test_snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (I001)

tests/games/test_snake_water_gun.py:11:1: I001 Import block is un-sorted or un-formatted help: Organize imports


# ---------------------------------------------------------------------------
# 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",

Check failure on line 40 in tests/games/test_snake_water_gun.py

View workflow job for this annotation

GitHub Actions / ruff

ruff (PT006)

tests/games/test_snake_water_gun.py:40:5: PT006 Wrong type passed to first argument of `pytest.mark.parametrize`; expected `tuple` help: Use a `tuple` for the first argument
[(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
Loading