Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.