diff --git a/README.md b/README.md index 47af33c..2d99328 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,8 @@ Day 7 of Udemy's *100 Days of Code™: The Complete Python Pro Bootcamp*. The ga 1. Picking a random word and checking answers 2. Replacing blanks with guesses -3. Checking if the player has won *(this step)* -4. Keeping track of the player's lives +3. Checking if the player has won +4. Keeping track of the player's lives *(this step)* 5. Improving the user experience **Step 1:** the program picks a random word, asks for one letter and tells you whether the letter is in the word. @@ -28,6 +28,8 @@ Day 7 of Udemy's *100 Days of Code™: The Complete Python Pro Bootcamp*. The ga **Step 3:** the game keeps asking for letters until every blank is filled, then prints "You win!". (There are no lives yet, so wrong guesses cost nothing.) +**Step 4:** you start with 6 lives. Every wrong guess costs one and shows the lives left. At 0 lives the game ends with "You lose" and reveals the word. + ## 📋 Requirements - Python 3.13 or newer diff --git a/src/hangman/constants.py b/src/hangman/constants.py index ba6fb20..91924d4 100644 --- a/src/hangman/constants.py +++ b/src/hangman/constants.py @@ -1,28 +1,32 @@ -"""! -@file constants.py -@brief Constants shared across the Hangman game. -""" - -## Character shown for a letter that has not been guessed yet. -BLANK = "_" -## Words the computer picks from. -WORD_LIST = [ - "aardvark", - "baboon", - "camel", - "dolphin", - "elephant", - "flamingo", - "giraffe", - "hedgehog", - "iguana", - "kangaroo", - "leopard", - "octopus", - "penguin", - "squirrel", - "tortoise", -] +"""! +@file constants.py +@brief Constants shared across the Hangman game. +""" + +## Number of wrong guesses the player may make before losing. +MAX_LIVES = 6 +## Character shown for a letter that has not been guessed yet. +BLANK = "_" +## Words the computer picks from. +WORD_LIST = [ + "aardvark", + "baboon", + "camel", + "dolphin", + "elephant", + "flamingo", + "giraffe", + "hedgehog", + "iguana", + "kangaroo", + "leopard", + "octopus", + "penguin", + "squirrel", + "tortoise", +] ## Message shown when the player wins. MSG_WIN = "You win!" +## Message shown when the player loses (the word is appended). +MSG_LOSE = "You lose. The word was: " diff --git a/src/hangman/game.py b/src/hangman/game.py index f8b43f9..e1bfcb2 100644 --- a/src/hangman/game.py +++ b/src/hangman/game.py @@ -5,7 +5,7 @@ import random -from hangman.constants import BLANK, WORD_LIST +from hangman.constants import BLANK, MAX_LIVES, WORD_LIST def choose_word(words: list[str] | None = None) -> str: @@ -32,25 +32,31 @@ class Game: @brief State of one Hangman game. """ - def __init__(self, word: str) -> None: + def __init__(self, word: str, lives: int = MAX_LIVES) -> None: """! @brief Start a new game. @param word The secret word. + @param lives Wrong guesses allowed before losing. """ ## The secret word. self.word = word.lower() + ## Lives left. + self.lives = lives ## Letters guessed so far. self.guesses: set[str] = set() def guess(self, letter: str) -> bool: """! - @brief Process a guess and reveal the letter if it is in the word. + @brief Process a guess; a wrong guess costs a life. @param letter A single letter. @return True if the letter is in the word. """ letter = letter.lower() self.guesses.add(letter) - return is_in_word(self.word, letter) + correct = is_in_word(self.word, letter) + if not correct: + self.lives -= 1 + return correct def display(self) -> str: """! @@ -65,3 +71,17 @@ class Game: @brief True when every letter of the word has been guessed. """ return all(c in self.guesses for c in self.word) + + @property + def lost(self) -> bool: + """! + @brief True when no lives are left. + """ + return self.lives <= 0 + + @property + def over(self) -> bool: + """! + @brief True when the game has been won or lost. + """ + return self.won or self.lost diff --git a/src/hangman/main.py b/src/hangman/main.py index 7d3ca49..a7afa14 100644 --- a/src/hangman/main.py +++ b/src/hangman/main.py @@ -3,7 +3,7 @@ @brief Command-line interface for Hangman. """ -from hangman.constants import MSG_WIN +from hangman.constants import MSG_LOSE, MSG_WIN from hangman.game import Game, choose_word @@ -17,18 +17,18 @@ def say(message: str = "") -> None: def play(game: Game) -> None: """! - @brief Ask for letters until every blank is filled. + @brief Ask for letters until the word is guessed or the lives run out. @param game The game to play. """ say(game.display()) - while not game.won: + while not game.over: letter = input("Guess a letter: ").strip() if game.guess(letter): say(f"Right, '{letter.lower()}' is in the word.") else: - say(f"Wrong, '{letter.lower()}' is not in the word.") + say(f"Wrong, '{letter.lower()}' is not in the word. Lives left: {game.lives}") say(game.display()) - say(MSG_WIN) + say(MSG_WIN if game.won else MSG_LOSE + game.word) def main() -> None: diff --git a/tests/test_game.py b/tests/test_game.py index 9837fa7..898a9b9 100644 --- a/tests/test_game.py +++ b/tests/test_game.py @@ -1,4 +1,4 @@ -from hangman.constants import WORD_LIST +from hangman.constants import MAX_LIVES, WORD_LIST from hangman.game import Game, choose_word, is_in_word @@ -45,3 +45,24 @@ def test_wrong_guesses_do_not_win(): g = Game("hi") g.guess("x") assert not g.won + + +def test_wrong_guess_loses_life(): + g = Game("cat") + assert g.lives == MAX_LIVES + g.guess("z") + assert g.lives == MAX_LIVES - 1 + + +def test_correct_guess_keeps_lives(): + g = Game("cat") + g.guess("c") + assert g.lives == MAX_LIVES + + +def test_lost_when_out_of_lives(): + g = Game("hi", lives=2) + g.guess("x") + assert not g.lost + g.guess("y") + assert g.lost and g.over and not g.won diff --git a/tests/test_main.py b/tests/test_main.py index be33707..bfbce2a 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -13,7 +13,7 @@ def test_play_until_won(monkeypatch, capsys): out = run(monkeypatch, capsys, "hi", ["z", "h", "i"]) assert out.splitlines() == [ "_ _", - "Wrong, 'z' is not in the word.", + "Wrong, 'z' is not in the word. Lives left: 5", "_ _", "Right, 'h' is in the word.", "h _", @@ -26,3 +26,9 @@ def test_play_until_won(monkeypatch, capsys): def test_repeated_letter_fills_all_blanks(monkeypatch, capsys): out = run(monkeypatch, capsys, "aa", ["a"]) assert out.splitlines()[-2:] == ["a a", "You win!"] + + +def test_play_until_lost(monkeypatch, capsys): + out = run(monkeypatch, capsys, "a", list("bcdefg")) + assert "Lives left: 0" in out + assert out.splitlines()[-1] == "You lose. The word was: a"