Add number guessing game project scaffold
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
- Add guess_the_number package with set_difficulty, check_answer and play_game, plus constants.py (range, attempts, ASCII logo) - Add pytest tests for difficulty, feedback and full game flow - Add pyproject.toml (Python >= 3.13, no runtime dependencies) - Add docs/Doxyfile and Doxygen comments in the source - Rewrite README with venv setup, run, test and docs instructions - Ignore docs/build/ in .gitignore
This commit is contained in:
@@ -174,3 +174,4 @@ cython_debug/
|
|||||||
# PyPI configuration file
|
# PyPI configuration file
|
||||||
.pypirc
|
.pypirc
|
||||||
|
|
||||||
|
docs/build/
|
||||||
|
|||||||
@@ -1,2 +1,77 @@
|
|||||||
# 012-guess_the_number
|
# Guess The Number
|
||||||
|
|
||||||
|
A terminal Number Guessing Game, built as the day 12 project of Udemy's
|
||||||
|
*100 Days of Code™: The Complete Python Pro Bootcamp*.
|
||||||
|
|
||||||
|
The computer picks a number between 1 and 100. Choose a difficulty and guess it:
|
||||||
|
|
||||||
|
| Level | Attempts |
|
||||||
|
|-------|----------|
|
||||||
|
| easy | 10 |
|
||||||
|
| hard | 5 |
|
||||||
|
|
||||||
|
After each guess you are told whether it was too high or too low.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Python 3.13 or newer
|
||||||
|
- No runtime dependencies (`pytest` is only needed for the tests)
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
Create and activate a local virtual environment, then upgrade pip.
|
||||||
|
|
||||||
|
Windows (PowerShell):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\Activate.ps1
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
```
|
||||||
|
|
||||||
|
Linux / macOS:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
source .venv/bin/activate
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the game
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install -e .
|
||||||
|
python -m guess_the_number
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run the tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pip install -e ".[dev]"
|
||||||
|
python -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
## Documentation
|
||||||
|
|
||||||
|
Source comments use Doxygen. With [Doxygen](https://www.doxygen.nl/) installed:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
doxygen docs/Doxyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
The HTML output is written to `docs/build/html`.
|
||||||
|
|
||||||
|
## Project layout
|
||||||
|
|
||||||
|
```
|
||||||
|
src/guess_the_number/ game code (constants.py, game.py)
|
||||||
|
tests/ pytest tests
|
||||||
|
docs/ Doxyfile
|
||||||
|
```
|
||||||
|
|
||||||
|
The functions `set_difficulty`, `check_answer` and `play_game` follow the
|
||||||
|
course assignment's naming.
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
See [LICENSE](LICENSE).
|
||||||
|
|||||||
+3016
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,25 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=68"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "guess-the-number"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Number Guessing Game - Udemy 100 Days of Code, day 12."
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
license = { file = "LICENSE" }
|
||||||
|
dependencies = []
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
dev = ["pytest>=8"]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
guess-the-number = "guess_the_number.game:play_game"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["src"]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
|
pythonpath = ["src"]
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
## @file __init__.py
|
||||||
|
# @brief Number Guessing Game package.
|
||||||
|
|
||||||
|
from guess_the_number.game import check_answer, play_game, set_difficulty
|
||||||
|
|
||||||
|
__all__ = ["check_answer", "play_game", "set_difficulty"]
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
## @file __main__.py
|
||||||
|
# @brief Entry point: `python -m guess_the_number`.
|
||||||
|
|
||||||
|
from guess_the_number.game import play_game
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
play_game()
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
## @file constants.py
|
||||||
|
# @brief Constants used by the Number Guessing Game.
|
||||||
|
|
||||||
|
## Lowest number that can be picked.
|
||||||
|
MIN_NUMBER = 1
|
||||||
|
## Highest number that can be picked.
|
||||||
|
MAX_NUMBER = 100
|
||||||
|
|
||||||
|
## Attempts available on the easy level.
|
||||||
|
EASY_ATTEMPTS = 10
|
||||||
|
## Attempts available on the hard level.
|
||||||
|
HARD_ATTEMPTS = 5
|
||||||
|
|
||||||
|
## Accepted answers for the easy level.
|
||||||
|
EASY_CHOICE = "easy"
|
||||||
|
## Accepted answers for the hard level.
|
||||||
|
HARD_CHOICE = "hard"
|
||||||
|
|
||||||
|
## ASCII art title shown when the game starts.
|
||||||
|
LOGO = r"""
|
||||||
|
________ ___________.__ _______ ___.
|
||||||
|
/ _____/ __ __ ____ ______ ______ \__ ___/| |__ ____ \ \ __ __ _____\_ |__ ___________
|
||||||
|
/ \ ___| | \_/ __ \ / ___// ___/ | | | | \_/ __ \ / | \| | \/ \| __ \_/ __ \_ __ \
|
||||||
|
\ \_\ \ | /\ ___/ \___ \ \___ \ | | | Y \ ___/ / | \ | / Y Y \ \_\ \ ___/| | \/
|
||||||
|
\______ /____/ \___ >____ >____ > |____| |___| /\___ > \____|__ /____/|__|_| /___ /\___ >__|
|
||||||
|
\/ \/ \/ \/ \/ \/ \/ \/ \/ \/
|
||||||
|
"""
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
## @file game.py
|
||||||
|
# @brief Game logic for the Number Guessing Game.
|
||||||
|
|
||||||
|
import random
|
||||||
|
|
||||||
|
from guess_the_number.constants import (
|
||||||
|
EASY_ATTEMPTS,
|
||||||
|
EASY_CHOICE,
|
||||||
|
HARD_ATTEMPTS,
|
||||||
|
HARD_CHOICE,
|
||||||
|
LOGO,
|
||||||
|
MAX_NUMBER,
|
||||||
|
MIN_NUMBER,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def set_difficulty(level: str) -> int:
|
||||||
|
"""!
|
||||||
|
@brief Convert a difficulty level into a number of attempts.
|
||||||
|
@param level Either "easy" or "hard" (case-insensitive).
|
||||||
|
@return 10 attempts for easy, 5 for hard.
|
||||||
|
@throws ValueError If the level is not recognised.
|
||||||
|
"""
|
||||||
|
level = level.strip().lower()
|
||||||
|
if level == EASY_CHOICE:
|
||||||
|
return EASY_ATTEMPTS
|
||||||
|
if level == HARD_CHOICE:
|
||||||
|
return HARD_ATTEMPTS
|
||||||
|
raise ValueError(f"Unknown difficulty: {level!r}")
|
||||||
|
|
||||||
|
|
||||||
|
def check_answer(guess: int, answer: int, attempts: int) -> int:
|
||||||
|
"""!
|
||||||
|
@brief Compare a guess with the answer and print feedback.
|
||||||
|
@param guess The player's guess.
|
||||||
|
@param answer The number to find.
|
||||||
|
@param attempts Attempts left before this guess.
|
||||||
|
@return Attempts left after this guess (unchanged if the guess is correct).
|
||||||
|
"""
|
||||||
|
if guess > answer:
|
||||||
|
print("Too high.")
|
||||||
|
return attempts - 1
|
||||||
|
if guess < answer:
|
||||||
|
print("Too low.")
|
||||||
|
return attempts - 1
|
||||||
|
print(f"You got it! The answer was {answer}.")
|
||||||
|
return attempts
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_difficulty() -> int:
|
||||||
|
"""!
|
||||||
|
@brief Prompt until the player picks a valid difficulty.
|
||||||
|
@return The number of attempts for the chosen level.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
return set_difficulty(input(f"Choose a difficulty. Type '{EASY_CHOICE}' or '{HARD_CHOICE}': "))
|
||||||
|
except ValueError:
|
||||||
|
print("Please type 'easy' or 'hard'.")
|
||||||
|
|
||||||
|
|
||||||
|
def _ask_guess() -> int:
|
||||||
|
"""!
|
||||||
|
@brief Prompt until the player enters a whole number in range.
|
||||||
|
@return The guess.
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
raw = input("Make a guess: ").strip()
|
||||||
|
if raw.lstrip("-").isdigit() and MIN_NUMBER <= int(raw) <= MAX_NUMBER:
|
||||||
|
return int(raw)
|
||||||
|
print(f"Enter a whole number between {MIN_NUMBER} and {MAX_NUMBER}.")
|
||||||
|
|
||||||
|
|
||||||
|
def play_game() -> None:
|
||||||
|
"""!
|
||||||
|
@brief Run one full game in the terminal.
|
||||||
|
"""
|
||||||
|
print(LOGO)
|
||||||
|
print("Welcome to the Number Guessing Game!")
|
||||||
|
print(f"I'm thinking of a number between {MIN_NUMBER} and {MAX_NUMBER}.")
|
||||||
|
answer = random.randint(MIN_NUMBER, MAX_NUMBER)
|
||||||
|
attempts = _ask_difficulty()
|
||||||
|
|
||||||
|
while attempts > 0:
|
||||||
|
print(f"You have {attempts} attempts remaining to guess the number.")
|
||||||
|
guess = _ask_guess()
|
||||||
|
if guess == answer:
|
||||||
|
check_answer(guess, answer, attempts)
|
||||||
|
return
|
||||||
|
attempts = check_answer(guess, answer, attempts)
|
||||||
|
if attempts > 0:
|
||||||
|
print("Guess again.")
|
||||||
|
|
||||||
|
print(f"You've run out of guesses. The answer was {answer}. You lose.")
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import pytest
|
||||||
|
|
||||||
|
from guess_the_number import game
|
||||||
|
from guess_the_number.constants import EASY_ATTEMPTS, HARD_ATTEMPTS
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_difficulty():
|
||||||
|
assert game.set_difficulty("easy") == EASY_ATTEMPTS
|
||||||
|
assert game.set_difficulty(" HARD ") == HARD_ATTEMPTS
|
||||||
|
|
||||||
|
|
||||||
|
def test_set_difficulty_invalid():
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
game.set_difficulty("medium")
|
||||||
|
|
||||||
|
|
||||||
|
def test_check_answer_high_low_correct(capsys):
|
||||||
|
assert game.check_answer(50, 10, 5) == 4
|
||||||
|
assert "Too high" in capsys.readouterr().out
|
||||||
|
assert game.check_answer(5, 10, 4) == 3
|
||||||
|
assert "Too low" in capsys.readouterr().out
|
||||||
|
assert game.check_answer(10, 10, 3) == 3
|
||||||
|
assert "You got it" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def _run(monkeypatch, inputs, answer=42):
|
||||||
|
monkeypatch.setattr(game.random, "randint", lambda a, b: answer)
|
||||||
|
it = iter(inputs)
|
||||||
|
monkeypatch.setattr("builtins.input", lambda _="": next(it))
|
||||||
|
game.play_game()
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_game_win(monkeypatch, capsys):
|
||||||
|
_run(monkeypatch, ["easy", "abc", "500", "10", "42"])
|
||||||
|
assert "You got it" in capsys.readouterr().out
|
||||||
|
|
||||||
|
|
||||||
|
def test_play_game_lose(monkeypatch, capsys):
|
||||||
|
_run(monkeypatch, ["bogus", "hard"] + ["1"] * HARD_ATTEMPTS)
|
||||||
|
assert "You lose" in capsys.readouterr().out
|
||||||
Reference in New Issue
Block a user