Antwoorden — Hoofdstuk 12: Eindproject (projectuitwerkingen)
Opmerking:
- Uitleg in het Nederlands; alle code is in het Engels, conform het boek.
- Per projectoptie vind je een referentie‑implementatie die aan de eisen voldoet:
- Minstens 2 modules en 5 functies.
- Persistente opslag waar gevraagd (highscore/todo/questions).
- Basistests: minimaal 5 asserts of 2 unittest‑cases.
- Een korte README‑schets voor studenten.
- Je mag de structuur aanpassen; dit zijn compacte, didactische voorbeelden.
Inhoud
- Project A: Number Guessing Game (met highscore‑bestand)
- Project B: To‑Do CLI (persistente opslag + eenvoudige tests)
- Project C: Tekst‑gebaseerde quiz (vragen uit JSON)
Project A — Number Guessing Game (highscore)
Structuur (voorbeeld):
- guessgame/
- init.py
- game.py (spelloop, input/feedback)
- storage.py (highscore lezen/schrijven)
- cli.py (entry point)
- tests_basic.py (eenvoudige asserts/unittests)
Implementatie (kerncode):
# guessgame/storage.py
from pathlib import Path
from typing import Optional
HIGHSCORE = Path("highscore.txt")
def read_highscore() -> Optional[int]:
if not HIGHSCORE.exists():
return None
try:
txt = HIGHSCORE.read_text(encoding="utf-8").strip()
return int(txt) if txt else None
except ValueError:
return None
def write_highscore(score: int) -> None:
# lower is better (attempts)
prev = read_highscore()
if prev is None or score < prev:
HIGHSCORE.write_text(str(score), encoding="utf-8")
# guessgame/game.py
import random
from typing import Callable, Optional
from .storage import read_highscore, write_highscore
Feedback = str
def pick_secret(low: int = 1, high: int = 100, rng: Optional[random.Random] = None) -> int:
r = rng or random
return r.randint(low, high)
def feedback(secret: int, guess: int) -> Feedback:
if guess < secret:
return "Higher"
if guess > secret:
return "Lower"
return "Correct"
def play_round(secret: int, reader: Callable[[str], str], writer: Callable[[str], None]) -> int:
attempts = 0
while True:
raw = reader("Your guess: ").strip()
if not raw:
writer("Bye!")
break
try:
g = int(raw)
except ValueError:
writer("Enter an integer.")
continue
attempts += 1
fb = feedback(secret, g)
writer(fb)
if fb == "Correct":
break
return attempts
def maybe_update_highscore(attempts: int) -> None:
write_highscore(attempts)
# guessgame/cli.py
from .game import pick_secret, play_round, maybe_update_highscore
from .storage import read_highscore
def main() -> None:
import sys
low, high = 1, 100
if len(sys.argv) == 3:
low = int(sys.argv[1]); high = int(sys.argv[2])
secret = pick_secret(low, high)
print(f"Guess the number between {low} and {high} (empty to quit)")
attempts = play_round(secret, input, print)
if attempts > 0:
print(f"You used {attempts} attempts.")
maybe_update_highscore(attempts)
hs = read_highscore()
if hs is not None:
print(f"Best so far: {hs} attempts")
if __name__ == "__main__":
main()
Basistests (mix van asserts en kleine unittests mogelijk):
# guessgame/tests_basic.py
from guessgame.game import feedback, play_round
# pure function asserts
assert feedback(10, 5) == "Higher"
assert feedback(10, 15) == "Lower"
assert feedback(10, 10) == "Correct"
# simulate I/O for play_round
class IO:
def __init__(self, inputs):
self.inputs = list(inputs)
self.out = []
def reader(self, prompt: str) -> str:
return self.inputs.pop(0) if self.inputs else ""
def writer(self, msg: str) -> None:
self.out.append(msg)
io = IO(["7", "10"]) # suppose secret=10
attempts = play_round(10, io.reader, io.writer)
assert attempts == 2
assert io.out[-1] == "Correct"
README‑schets (voor studenten, in English code allowed, tekst NL):
# Number Guessing Game
- Run: `python -m guessgame.cli [low high]`
- Goal: Guess the secret number; program gives "Higher"/"Lower" feedback.
- Highscore: best (fewest attempts) stored in `highscore.txt`.
- Modules: `game.py` (logic), `storage.py` (file I/O), `cli.py` (entry point).
- Tests: see `tests_basic.py` (asserts on pure functions + simulated I/O).
Project B — To‑Do CLI (persistente opslag)
Structuur (voorbeeld):
- todo/
- init.py
- store.py (bestands‑I/O)
- core.py (bewerkingen op items)
- cli.py (commando’s: add/list/done)
- tests_basic.py
Implementatie (kerncode):
# todo/store.py
from pathlib import Path
from typing import Iterable
TODO = Path("todo.txt")
def read_items() -> list[str]:
if not TODO.exists():
return []
return [line.rstrip("\n") for line in TODO.read_text(encoding="utf-8").splitlines()]
def write_items(items: Iterable[str]) -> None:
text = "\n".join(items) + ("\n" if items else "")
TODO.write_text(text, encoding="utf-8")
# todo/core.py
from typing import Iterable
def add_item(items: list[str], text: str) -> list[str]:
text = text.strip()
return items + [text] if text else items
def list_lines(items: Iterable[str]) -> list[str]:
return [f"{i}. {it}" for i, it in enumerate(items, start=1)]
def done_item(items: list[str], index: int) -> list[str]:
if 1 <= index <= len(items):
new = list(items)
del new[index - 1]
return new
return items
# todo/cli.py
from .store import read_items, write_items
from .core import add_item, list_lines, done_item
def main() -> None:
import sys
args = sys.argv[1:]
if not args:
print("Usage: todo.py [add <text>|list|done <n>]")
raise SystemExit(1)
cmd = args[0]
items = read_items()
if cmd == "add" and len(args) >= 2:
items = add_item(items, " ".join(args[1:]))
write_items(items)
elif cmd == "list":
for line in list_lines(items):
print(line)
elif cmd == "done" and len(args) >= 2:
try:
n = int(args[1])
except ValueError:
print("Please provide an integer index.")
raise SystemExit(1)
items = done_item(items, n)
write_items(items)
else:
print("Unknown command or missing arguments.")
raise SystemExit(1)
if __name__ == "__main__":
main()
Basistests:
# todo/tests_basic.py
from todo.core import add_item, list_lines, done_item
items: list[str] = []
items = add_item(items, "buy milk")
items = add_item(items, "send email")
assert len(items) == 2
assert list_lines(items)[0].startswith("1.")
items2 = done_item(items, 1)
assert len(items2) == 1 and items2[0] == "send email"
# invalid index should keep list unchanged
assert done_item(items, 99) == items
README‑schets:
# To‑Do CLI
Commands:
- `add <text>`: append a new item.
- `list`: show numbered items.
- `done <n>`: remove item n (1‑based).
Modules: `store.py` (file I/O), `core.py` (logic), `cli.py` (entry).
Tests: see `tests_basic.py` for simple asserts over pure functions.
Project C — Tekst‑gebaseerde quiz (JSON)
Structuur (voorbeeld):
- quiz/
- init.py
- io.py (laden vragenbestand)
- engine.py (afspelen quiz, scoring)
- cli.py (entry)
- questions.json (voorbeeldbestand)
- tests_basic.py
Voorbeeld questions.json:
[
{"q": "Capital of France?", "a": "paris"},
{"q": "2+2?", "a": "4"}
]
Implementatie (kerncode):
# quiz/io.py
import json
from pathlib import Path
from typing import Iterable
def load_questions(path: str | Path) -> list[dict]:
p = Path(path)
data = json.loads(p.read_text(encoding="utf-8"))
if not isinstance(data, list):
raise ValueError("questions file must be a JSON list")
return data
# quiz/engine.py
from typing import Callable, Iterable
def ask_one(q: str, answer: str, reader: Callable[[str], str], writer: Callable[[str], None]) -> bool:
user = reader(q + " ").strip().lower()
return user == answer.lower()
def run_quiz(qa: Iterable[dict], reader, writer) -> int:
correct = 0
for item in qa:
if ask_one(item["q"], item["a"], reader, writer):
writer("Correct")
correct += 1
else:
writer("Wrong")
return correct
# quiz/cli.py
from .io import load_questions
from .engine import run_quiz
def main() -> None:
import sys
path = sys.argv[1] if len(sys.argv) >= 2 else "questions.json"
qa = load_questions(path)
print("Answer the questions (empty line to skip)")
def reader(prompt: str) -> str:
try:
return input(prompt)
except EOFError:
return ""
score = run_quiz(qa, reader, print)
print(f"Score: {score}/{len(qa)}")
if __name__ == "__main__":
main()
Basistests:
# quiz/tests_basic.py
from quiz.engine import ask_one, run_quiz
assert ask_one("2+2?", "4", lambda _: "4", lambda _m: None) is True
assert ask_one("2+2?", "4", lambda _: "5", lambda _m: None) is False
class IO:
def __init__(self, answers):
self.answers = list(answers)
self.out = []
def reader(self, _prompt: str) -> str:
return self.answers.pop(0) if self.answers else ""
def writer(self, msg: str) -> None:
self.out.append(msg)
qa = [{"q": "Capital?", "a": "Paris"}, {"q": "2+2?", "a": "4"}]
io = IO(["paris", "4"]) # all correct
score = run_quiz(qa, io.reader, io.writer)
assert score == 2
assert io.out.count("Correct") == 2
README‑schets:
# Quiz (text‑based)
- Provide a JSON file with questions: objects having keys `q` and `a`.
- Run: `python -m quiz.cli questions.json`
- Modules: `io.py` (load), `engine.py` (run/scoring), `cli.py` (entry).
- Tests: see `tests_basic.py` (pure logic tested via simulated I/O).
Tips voor beoordeling (docent):
- Controleer of studenten minimaal 2 modules en 5 functies inzetten (scheiding I/O vs. logica).
- Laat hen een korte README in het project toevoegen met gebruiksvoorbeelden.
- Tests: stimuleer pure functies zodat asserts/unittests eenvoudig zijn.
- Extra punten voor nette foutafhandeling (bestanden/JSON), type hints, en duidelijke naamgeving.