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
|
||||
.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