Compare commits

...
Author SHA1 Message Date
Tirsvad d570489a33 Implement coffee machine core for MIL-002
Add the menu, coin values, starting resources and message texts to
constants.py. Implement report, is_resource_sufficient, process_coins,
is_transaction_successful, make_coffee and the main loop. Money is
counted in whole cents. The smoke test now feeds "off" so the loop
exits.

Closes #6
Closes #7
Closes #8
Closes #9
Closes #10
Closes #11
Closes #12

Task: MIL-002#1
Task: MIL-002#2
Task: MIL-002#3
Task: MIL-002#4
Task: MIL-002#5
Task: MIL-002#6
Task: MIL-002#7
2026-10-06 19:49:49 +08:00
Tirsvad 0f26d8d91e Merge pull request 'MIL-001: README with setup, run and test instructions' (#20) from mil-001-readme into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Failing after 4s
Reviewed-on: Tirsvad-Udemy-100_days_of_code/015-coffee_machine#20
2026-10-06 13:45:59 +02:00
Tirsvad c2a8767fb0 Add README with setup, run, test and Doxygen instructions
Follow the Higher Lower README template: project description, local
.venv setup with pip upgrade, running the machine, tests and code
checks, source documentation, layout and license.

Closes #5
Task: MIL-001#5
2026-10-06 19:45:09 +08:00
Tirsvad 322efae5b4 Merge pull request 'MIL-001: project skeleton and tooling' (#19) from mil-001-project-setup into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 5s
Reviewed-on: Tirsvad-Udemy-100_days_of_code/015-coffee_machine#19
2026-10-04 18:31:21 +02:00
Tirsvad 572074ae73 Set up project skeleton and tooling for MIL-001
Add pyproject.toml (Python >=3.13, no runtime dependencies, pytest, ruff
and mypy as dev extras), a Doxyfile that scans src/, the
src/coffee_machine package with stub constants and main modules, and a
pytest smoke test. Ignore Doxygen output in .gitignore.

Closes #1
Closes #2
Closes #3
Closes #4
Refs #5

Task: MIL-001#1
Task: MIL-001#2
Task: MIL-001#3
Task: MIL-001#4
2026-10-05 00:30:18 +08:00
Tirsvad c3e225b049 Merge pull request 'Add planning baseline: BC, SA, PP and MIL-001 to MIL-003' (#18) from plan-baseline into main
TirSystem/github-action: Sync GitHub mirror metadata / sync-metadata (push) Successful in 6s
Reviewed-on: Tirsvad-Udemy-100_days_of_code/015-coffee_machine#18
2026-10-04 18:26:38 +02:00
9 changed files with 370 additions and 1 deletions
+3
View File
@@ -174,3 +174,6 @@ cython_debug/
# PyPI configuration file # PyPI configuration file
.pypirc .pypirc
# Doxygen output
docs/doxygen/
+14
View File
@@ -0,0 +1,14 @@
# Doxygen configuration for the coffee machine. Run: doxygen Doxyfile
PROJECT_NAME = "Coffee Machine"
PROJECT_BRIEF = "Day 15 of Udemy's 100 Days of Code Python bootcamp"
OUTPUT_DIRECTORY = docs/doxygen
INPUT = src
FILE_PATTERNS = *.py
RECURSIVE = YES
OPTIMIZE_OUTPUT_JAVA = YES
EXTRACT_ALL = YES
JAVADOC_AUTOBRIEF = YES
GENERATE_HTML = YES
GENERATE_LATEX = NO
QUIET = YES
WARN_IF_UNDOCUMENTED = YES
+101 -1
View File
@@ -1,2 +1,102 @@
# 015-coffee_machine # Coffee Machine
A console game from Udemy's *100 Days of Code: The Complete Python Pro Bootcamp*
(day 15). A virtual coffee machine serves espresso, latte and cappuccino. Pay with
US coins (quarters, dimes, nickels and pennies). The machine checks its water, milk
and coffee before it serves, refunds you if you pay too little and gives change if
you pay too much.
```
What would you like? (espresso/latte/cappuccino): latte
Please insert coins.
How many quarters?: 10
How many dimes?: 0
How many nickels?: 0
How many pennies?: 0
Here is your latte. Enjoy!
```
- Python 3.13 or newer, no runtime dependencies.
- Type `report` to see the water, milk, coffee and money in the machine, and `off`
to switch the machine off.
- The function names follow the assignment: `is_resource_sufficient`,
`process_coins`, `is_transaction_successful`, `make_coffee`.
- Money is counted in whole cents inside the program, so change is never off by a
rounding error.
## Requirements
- [Python](https://www.python.org/downloads/) 3.13 or newer
- Optional: [Doxygen](https://www.doxygen.nl/) to build the source documentation
## Set up
Create a local virtual environment in `.venv`, activate it, and upgrade `pip`.
Windows (PowerShell):
```powershell
python -m venv .venv
.venv\Scripts\Activate.ps1
python -m pip install --upgrade pip
```
Windows (Git Bash), Linux and macOS:
```bash
python -m venv .venv
source .venv/Scripts/activate # Linux/macOS: source .venv/bin/activate
python -m pip install --upgrade pip
```
The machine itself needs nothing more. To run the tests and the code checks, install
the development tools:
```bash
python -m pip install -e ".[dev]"
```
## Run the coffee machine
```bash
python -m coffee_machine
```
Type `espresso`, `latte` or `cappuccino` and press Enter, then enter the coins.
Type `report` for the machine's resources, or `off` to quit.
## Run the tests
```bash
python -m pytest
```
Check the code style and types:
```bash
python -m ruff check src tests
python -m ruff format --check src tests
python -m mypy src tests
```
## Build the source documentation
The source uses Doxygen comments. The HTML output goes to `docs/doxygen/html`.
```bash
doxygen Doxyfile
```
## Project layout
| Path | Content |
| --- | --- |
| `src/coffee_machine/` | The machine: `main.py` (the loop), `constants.py` (menu, coins, starting resources) |
| `tests/` | pytest tests |
| `docs/` | Planning and review documents (business case, plan, milestones) |
| `pyproject.toml` | Project configuration |
| `Doxyfile` | Doxygen configuration |
## License
GNU Affero General Public License v3.0. See [LICENSE](LICENSE).
+32
View File
@@ -0,0 +1,32 @@
[build-system]
requires = ["setuptools>=68"]
build-backend = "setuptools.build_meta"
[project]
name = "coffee-machine"
version = "0.1.0"
description = "Console coffee machine: Day 15 of Udemy's 100 Days of Code Python bootcamp."
readme = "README.md"
requires-python = ">=3.13"
license = { file = "LICENSE" }
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=8", "ruff", "mypy"]
[project.scripts]
coffee-machine = "coffee_machine.main:main"
[tool.setuptools.packages.find]
where = ["src"]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
[tool.ruff]
line-length = 88
[tool.mypy]
strict = true
mypy_path = "src"
+4
View File
@@ -0,0 +1,4 @@
"""!
@file __init__.py
@brief Console coffee machine package.
"""
+8
View File
@@ -0,0 +1,8 @@
"""!
@file __main__.py
@brief Allow `python -m coffee_machine`.
"""
from coffee_machine.main import main
main()
+54
View File
@@ -0,0 +1,54 @@
"""!
@file constants.py
@brief Constants for the coffee machine: menu, coins, starting resources, texts.
Money is stored in whole cents so that sums and change are exact.
"""
from typing import TypedDict
class Drink(TypedDict):
"""!
@brief A menu entry: the ingredients it needs and its price.
"""
ingredients: dict[str, int]
cost: int
MENU: dict[str, Drink] = {
"espresso": {"ingredients": {"water": 50, "coffee": 18}, "cost": 150},
"latte": {
"ingredients": {"water": 200, "milk": 150, "coffee": 24},
"cost": 250,
},
"cappuccino": {
"ingredients": {"water": 250, "milk": 100, "coffee": 24},
"cost": 300,
},
}
## Value of each US coin in cents, in the order the machine asks for them.
COIN_VALUES: dict[str, int] = {
"quarters": 25,
"dimes": 10,
"nickels": 5,
"pennies": 1,
}
INITIAL_RESOURCES: dict[str, int] = {"water": 300, "milk": 200, "coffee": 100}
## Unit printed after each resource in the report.
RESOURCE_UNITS: dict[str, str] = {"water": "ml", "milk": "ml", "coffee": "g"}
CENTS_PER_DOLLAR = 100
COMMAND_REPORT = "report"
COMMAND_OFF = "off"
PROMPT_ORDER = "What would you like? (espresso/latte/cappuccino): "
MESSAGE_INSERT_COINS = "Please insert coins."
MESSAGE_NOT_ENOUGH_MONEY = "Sorry that's not enough money. Money refunded."
MESSAGE_UNKNOWN_DRINK = "Sorry, we do not serve that."
MESSAGE_INVALID_NUMBER = "Please enter a whole number of coins, zero or more."
+138
View File
@@ -0,0 +1,138 @@
"""!
@file main.py
@brief Entry point and logic of the coffee machine.
"""
from coffee_machine.constants import (
CENTS_PER_DOLLAR,
COIN_VALUES,
COMMAND_OFF,
COMMAND_REPORT,
INITIAL_RESOURCES,
MENU,
MESSAGE_INSERT_COINS,
MESSAGE_INVALID_NUMBER,
MESSAGE_NOT_ENOUGH_MONEY,
MESSAGE_UNKNOWN_DRINK,
PROMPT_ORDER,
RESOURCE_UNITS,
)
def format_money(cents: int) -> str:
"""!
@brief Format an amount in cents as dollars.
@param cents Amount in whole cents.
@return The amount such as "$2.50".
"""
return f"${cents / CENTS_PER_DOLLAR:.2f}"
def report(resources: dict[str, int], profit: int) -> None:
"""!
@brief Print the resources left in the machine and the money earned.
@param resources Remaining amount of each resource.
@param profit Money earned so far, in cents.
"""
for name, amount in resources.items():
print(f"{name.capitalize()}: {amount}{RESOURCE_UNITS[name]}")
print(f"Money: {format_money(profit)}")
def is_resource_sufficient(
order_ingredients: dict[str, int], resources: dict[str, int]
) -> bool:
"""!
@brief Check that the machine has enough of every ingredient for an order.
@param order_ingredients Amount of each ingredient the drink needs.
@param resources Remaining amount of each resource.
@return True if every ingredient is available, otherwise False after
printing the first missing one.
"""
for name, needed in order_ingredients.items():
if needed > resources[name]:
print(f"Sorry there is not enough {name}.")
return False
return True
def _ask_coin_count(coin_name: str) -> int:
"""!
@brief Ask how many coins of one kind are inserted, until the answer is valid.
@param coin_name Plural coin name, such as "quarters".
@return The number of coins, zero or more.
"""
while True:
answer = input(f"How many {coin_name}?: ")
try:
count = int(answer)
except ValueError:
count = -1
if count >= 0:
return count
print(MESSAGE_INVALID_NUMBER)
def process_coins() -> int:
"""!
@brief Ask for the coins and total their value.
@return The total inserted, in cents.
"""
print(MESSAGE_INSERT_COINS)
return sum(
_ask_coin_count(coin_name) * value for coin_name, value in COIN_VALUES.items()
)
def is_transaction_successful(money_received: int, drink_cost: int) -> bool:
"""!
@brief Check the payment, refund it if too small, otherwise give change.
@param money_received Money inserted, in cents.
@param drink_cost Price of the drink, in cents.
@return True if the payment covers the price, otherwise False.
"""
if money_received < drink_cost:
print(MESSAGE_NOT_ENOUGH_MONEY)
return False
change = money_received - drink_cost
if change > 0:
print(f"Here is {format_money(change)} in change.")
return True
def make_coffee(
drink_name: str, order_ingredients: dict[str, int], resources: dict[str, int]
) -> None:
"""!
@brief Use up the drink's ingredients and serve it.
@param drink_name Name of the drink.
@param order_ingredients Amount of each ingredient the drink needs.
@param resources Remaining amount of each resource; updated in place.
"""
for name, needed in order_ingredients.items():
resources[name] -= needed
print(f"Here is your {drink_name}. Enjoy!")
def main() -> None:
"""!
@brief Run the coffee machine until the user types "off".
"""
resources = dict(INITIAL_RESOURCES)
profit = 0
while True:
choice = input(PROMPT_ORDER).strip().lower()
if choice == COMMAND_OFF:
return
if choice == COMMAND_REPORT:
report(resources, profit)
elif choice in MENU:
drink = MENU[choice]
if not is_resource_sufficient(drink["ingredients"], resources):
continue
payment = process_coins()
if is_transaction_successful(payment, drink["cost"]):
profit += drink["cost"]
make_coffee(choice, drink["ingredients"], resources)
else:
print(MESSAGE_UNKNOWN_DRINK)
+16
View File
@@ -0,0 +1,16 @@
"""!
@file test_smoke.py
@brief Smoke test that the package imports.
"""
import pytest
from coffee_machine import main as main_module
def test_main_is_callable(monkeypatch: pytest.MonkeyPatch) -> None:
"""!
@brief The entry point starts and stops on "off".
"""
monkeypatch.setattr("builtins.input", lambda _prompt: "off")
main_module.main()