From ce35e7d479b21d1ba64e987c1110acbec3219143 Mon Sep 17 00:00:00 2001 From: Alex Mueller Date: Sat, 29 Aug 2026 12:46:08 +0200 Subject: [PATCH] =?UTF-8?q?Initialisiere=20didaktische=20Python=20OOP-?= =?UTF-8?q?=C3=9Cbungsreihe=20f=C3=BCr=20Nachhilfe=20(Kapitel=2001=20bis?= =?UTF-8?q?=2006)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 10 ++ 01_einstieg_klassen/README.md | 86 ++++++++++++ 01_einstieg_klassen/aufgabe.py | 94 +++++++++++++ 01_einstieg_klassen/musterloesung.py | 49 +++++++ 01_einstieg_klassen/test_aufgabe.py | 84 ++++++++++++ 02_init_und_self/README.md | 90 ++++++++++++ 02_init_und_self/aufgabe.py | 77 +++++++++++ 02_init_und_self/musterloesung.py | 37 +++++ 02_init_und_self/test_aufgabe.py | 85 ++++++++++++ 03_methoden_und_verhalten/README.md | 69 ++++++++++ 03_methoden_und_verhalten/aufgabe.py | 101 ++++++++++++++ 03_methoden_und_verhalten/musterloesung.py | 51 +++++++ 03_methoden_und_verhalten/test_aufgabe.py | 101 ++++++++++++++ 04_str_und_darstellung/README.md | 77 +++++++++++ 04_str_und_darstellung/aufgabe.py | 105 ++++++++++++++ 04_str_und_darstellung/musterloesung.py | 43 ++++++ 04_str_und_darstellung/test_aufgabe.py | 68 +++++++++ 05_objekte_kombinieren/README.md | 71 ++++++++++ 05_objekte_kombinieren/aufgabe.py | 123 +++++++++++++++++ 05_objekte_kombinieren/musterloesung.py | 52 +++++++ 05_objekte_kombinieren/test_aufgabe.py | 87 ++++++++++++ 06_abschlussprojekt_tamagotchi/README.md | 48 +++++++ 06_abschlussprojekt_tamagotchi/aufgabe.py | 129 ++++++++++++++++++ .../musterloesung.py | 81 +++++++++++ .../tamagotchi_spiel.py | 111 +++++++++++++++ .../test_aufgabe.py | 111 +++++++++++++++ README.md | 53 +++++++ test_all.py | 73 ++++++++++ 28 files changed, 2166 insertions(+) create mode 100644 .gitignore create mode 100644 01_einstieg_klassen/README.md create mode 100644 01_einstieg_klassen/aufgabe.py create mode 100644 01_einstieg_klassen/musterloesung.py create mode 100644 01_einstieg_klassen/test_aufgabe.py create mode 100644 02_init_und_self/README.md create mode 100644 02_init_und_self/aufgabe.py create mode 100644 02_init_und_self/musterloesung.py create mode 100644 02_init_und_self/test_aufgabe.py create mode 100644 03_methoden_und_verhalten/README.md create mode 100644 03_methoden_und_verhalten/aufgabe.py create mode 100644 03_methoden_und_verhalten/musterloesung.py create mode 100644 03_methoden_und_verhalten/test_aufgabe.py create mode 100644 04_str_und_darstellung/README.md create mode 100644 04_str_und_darstellung/aufgabe.py create mode 100644 04_str_und_darstellung/musterloesung.py create mode 100644 04_str_und_darstellung/test_aufgabe.py create mode 100644 05_objekte_kombinieren/README.md create mode 100644 05_objekte_kombinieren/aufgabe.py create mode 100644 05_objekte_kombinieren/musterloesung.py create mode 100644 05_objekte_kombinieren/test_aufgabe.py create mode 100644 06_abschlussprojekt_tamagotchi/README.md create mode 100644 06_abschlussprojekt_tamagotchi/aufgabe.py create mode 100644 06_abschlussprojekt_tamagotchi/musterloesung.py create mode 100644 06_abschlussprojekt_tamagotchi/tamagotchi_spiel.py create mode 100644 06_abschlussprojekt_tamagotchi/test_aufgabe.py create mode 100644 README.md create mode 100644 test_all.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..df05013 --- /dev/null +++ b/.gitignore @@ -0,0 +1,10 @@ +__pycache__/ +*.py[cod] +*$py.class +.pytest_cache/ +.coverage +htmlcov/ +.env +venv/ +.venv/ +.DS_Store diff --git a/01_einstieg_klassen/README.md b/01_einstieg_klassen/README.md new file mode 100644 index 0000000..4a6cfea --- /dev/null +++ b/01_einstieg_klassen/README.md @@ -0,0 +1,86 @@ +# Kapitel 01: Die erste Klasse – Was sind Klassen & Objekte? + +Herzlich willkommen in der Welt der objektorientierten Programmierung (kurz: **OOP**)! 🐶🐱 + +Bisher hast du in Python mit Variablen, Listen, Dictionaries und Funktionen gearbeitet. Das hat super funktioniert! Doch wenn Programme größer werden, wird es schnell unübersichtlich. + +Hier kommen **Klassen** ins Spiel. + +--- + +## 🏗️ Die wichtigste Analogie: Bauplan vs. Objekt + +Stell dir vor, du bist Architektin oder Bäckermeisterin: +- Eine **Klasse** ist wie ein **Bauplan** für ein Haus (oder eine **Plätzchen-Ausstechform**). Der Bauplan selbst ist noch kein Haus, in dem man wohnen kann. Er beschreibt nur: *Wie sieht ein Haus aus? Welche Eigenschaften hat es?* +- Ein **Objekt** (auch **Instanz** genannt) ist das **echte Haus**, das nach diesem Bauplan gebaut wurde (oder das gebackene Plätzchen). Aus *einem* Bauplan kannst du beliebig viele verschiedene Häuser bauen! + +``` + [ KLASSE / BAUPLAN ] + class Haustier: + │ + ─────────┴───────── + │ │ +[ OBJEKT 1 ] [ OBJEKT 2 ] +Bello, Hund, 3 Mimi, Katze, 5 +``` + +--- + +## 🔍 Warum nicht einfach Dictionaries? + +Bisher hättest du Tiere vielleicht so gespeichert: +```python +tier1 = {"name": "Bello", "tierart": "Hund", "alter": 3} +tier2 = {"name": "Mimi", "art": "Katze", "alter": 5} # Ups! Hier heißt der Key "art" statt "tierart"! +``` +Beim Dictionary kann man sich schnell bei Schlüsseln vertippen. Eine Klasse gibt uns eine feste Struktur vor. + +--- + +## 💻 Wie sieht eine Klasse in Python aus? + +Eine einfache Klasse definierst du mit dem Schlüsselwort `class`: + +```python +class Auto: + pass # 'pass' bedeutet: "Hier passiert erst mal nichts" +``` + +Aus dieser Klasse kannst du nun ein Objekt erzeugen (**Instanziierung**): + +```python +# Objekt erstellen: +mein_auto = Auto() + +# Eigenschaften (Attribute) zuweisen mit dem Punkt (.): +mein_auto.farbe = "Rot" +mein_auto.marke = "VW" +mein_auto.ps = 110 + +print(mein_auto.farbe) # Gibt "Rot" aus +``` + +Du kannst ganz einfach ein zweites Auto bauen: +```python +dein_auto = Auto() +dein_auto.farbe = "Blau" +dein_auto.marke = "BMW" +dein_auto.ps = 180 +``` +`mein_auto` und `dein_auto` sind zwei völlig eigenständige Objekte, die aus demselben Bauplan `Auto` entstanden sind! + +--- + +## 🎯 Deine Aufgabe + +Öffne jetzt die Datei `aufgabe.py` und bearbeite die Aufgaben: +1. Erstelle die Klasse `Haustier`. +2. Erzeuge zwei Haustier-Objekte `bello` und `mimi` mit Eigenschaften. +3. Schreibe zwei kleine Hilfsfunktionen, die mit deinen Haustier-Objekten arbeiten. + +### Testen: +Führe deine Tests aus mit: +```bash +python3 test_aufgabe.py +``` +Sobald alle Tests bestanden sind, kannst du mit Kapitel 02 weitermachen! diff --git a/01_einstieg_klassen/aufgabe.py b/01_einstieg_klassen/aufgabe.py new file mode 100644 index 0000000..5b8d88f --- /dev/null +++ b/01_einstieg_klassen/aufgabe.py @@ -0,0 +1,94 @@ +""" +Kapitel 01: Die erste Klasse – Aufgabenblatt +============================================ + +Bearbeite die Aufgaben Schritt für Schritt von TODO 1 bis TODO 5. +Wenn du fertig bist, überprüfe deine Lösung mit: + python3 test_aufgabe.py +""" + +# ============================================================================== +# TODO 1: Definiere eine leere Klasse namens "Haustier". +# Tipp: Verwende das Schlüsselwort 'class' und vorerst 'pass' im Klassenrumpf. +# ============================================================================== + +# Schreibe hier deinen Code für TODO 1: + + + +# ============================================================================== +# TODO 2: Erstelle eine Funktion namens "erstelle_bello()". +# Diese Funktion soll: +# 1. Ein neues Haustier-Objekt erzeugen. +# 2. Dem Objekt folgende Attribute zuweisen: +# - name: "Bello" +# - tierart: "Hund" +# - alter: 3 +# 3. Das fertige Objekt zurückgeben (return). +# ============================================================================== + +def erstelle_bello(): + # Schreibe hier deinen Code für TODO 2: + pass + + +# ============================================================================== +# TODO 3: Erstelle eine Funktion namens "erstelle_mimi()". +# Diese Funktion soll: +# 1. Ein neues Haustier-Objekt erzeugen. +# 2. Dem Objekt folgende Attribute zuweisen: +# - name: "Mimi" +# - tierart: "Katze" +# - alter: 5 +# 3. Das fertige Objekt zurückgeben (return). +# ============================================================================== + +def erstelle_mimi(): + # Schreibe hier deinen Code für TODO 3: + pass + + +# ============================================================================== +# TODO 4: Schreibe eine Funktion "steckbrief_text(haustier)". +# Die Funktion bekommt ein Haustier-Objekt als Parameter übergeben. +# Sie soll einen formatierten Text im folgenden Format zurückgeben: +# "{name} ist ein(e) {tierart} und ist {alter} Jahre alt." +# +# Beispiel: +# Wenn bello übergeben wird, soll zurückgegeben werden: +# "Bello ist ein(e) Hund und ist 3 Jahre alt." +# ============================================================================== + +def steckbrief_text(haustier): + # Schreibe hier deinen Code für TODO 4: + pass + + +# ============================================================================== +# TODO 5: Schreibe eine Funktion "aelteres_tier(tier1, tier2)". +# Die Funktion bekommt zwei Haustier-Objekte übergeben. +# Sie soll dasjenige Haustier-Objekt zurückgeben, dessen 'alter' größer ist. +# Wenn beide Tiere gleich alt sind, soll tier1 zurückgegeben werden. +# ============================================================================== + +def aelteres_tier(tier1, tier2): + # Schreibe hier deinen Code für TODO 5: + pass + + +# ============================================================================== +# Hauptprogramm zum Ausprobieren: +# (Du kannst diese Datei direkt mit 'python3 aufgabe.py' ausführen) +# ============================================================================== +if __name__ == "__main__": + print("--- Teste deine Haustiere ---") + bello = erstelle_bello() + mimi = erstelle_mimi() + + if bello and mimi: + print(steckbrief_text(bello)) + print(steckbrief_text(mimi)) + aelter = aelteres_tier(bello, mimi) + print(f"Das ältere Tier ist: {aelter.name}") + else: + print("Hinweis: Implementiere erst die TODOs, um die Ausgabe zu sehen!") diff --git a/01_einstieg_klassen/musterloesung.py b/01_einstieg_klassen/musterloesung.py new file mode 100644 index 0000000..0aeee4a --- /dev/null +++ b/01_einstieg_klassen/musterloesung.py @@ -0,0 +1,49 @@ +""" +Kapitel 01: Die erste Klasse – Musterlösung +=========================================== +Hier findest du die vollständige Lösung mit Erklärungen. +""" + +# TODO 1: Die Klasse Haustier definieren +class Haustier: + pass + + +# TODO 2: Ein Haustier-Objekt namens Bello erstellen und Attribute zuweisen +def erstelle_bello(): + tier = Haustier() + tier.name = "Bello" + tier.tierart = "Hund" + tier.alter = 3 + return tier + + +# TODO 3: Ein Haustier-Objekt namens Mimi erstellen +def erstelle_mimi(): + tier = Haustier() + tier.name = "Mimi" + tier.tierart = "Katze" + tier.alter = 5 + return tier + + +# TODO 4: Steckbrief generieren +def steckbrief_text(haustier): + return f"{haustier.name} ist ein(e) {haustier.tierart} und ist {haustier.alter} Jahre alt." + + +# TODO 5: Das ältere Tier ermitteln +def aelteres_tier(tier1, tier2): + if tier2.alter > tier1.alter: + return tier2 + else: + return tier1 + + +if __name__ == "__main__": + bello = erstelle_bello() + mimi = erstelle_mimi() + print(steckbrief_text(bello)) + print(steckbrief_text(mimi)) + aelter = aelteres_tier(bello, mimi) + print(f"Das ältere Tier ist: {aelter.name}") diff --git a/01_einstieg_klassen/test_aufgabe.py b/01_einstieg_klassen/test_aufgabe.py new file mode 100644 index 0000000..366cd07 --- /dev/null +++ b/01_einstieg_klassen/test_aufgabe.py @@ -0,0 +1,84 @@ +import unittest +import sys +from pathlib import Path + +# Sicherstellen, dass das aktuelle Verzeichnis im Modulpfad ist +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel01(unittest.TestCase): + + def test_01_klasse_existiert(self): + """Prüft, ob die Klasse Haustier definiert wurde.""" + self.assertTrue( + hasattr(aufgabe, "Haustier"), + "Fehler: Die Klasse 'Haustier' existiert noch nicht. Nutze: class Haustier:" + ) + self.assertTrue( + isinstance(aufgabe.Haustier, type), + "Fehler: 'Haustier' sollte eine Klasse sein!" + ) + + def test_02_erstelle_bello(self): + """Prüft, ob erstelle_bello() ein korrektes Haustier-Objekt liefert.""" + self.assertTrue(hasattr(aufgabe, "erstelle_bello"), "Fehler: Funktion 'erstelle_bello' fehlt!") + bello = aufgabe.erstelle_bello() + self.assertIsNotNone(bello, "Fehler: 'erstelle_bello()' gibt None zurück. Vergiss nicht das 'return'!") + self.assertIsInstance(bello, aufgabe.Haustier, "Fehler: 'bello' muss eine Instanz der Klasse 'Haustier' sein!") + self.assertEqual(getattr(bello, "name", None), "Bello", "Fehler: 'bello.name' muss 'Bello' sein!") + self.assertEqual(getattr(bello, "tierart", None), "Hund", "Fehler: 'bello.tierart' muss 'Hund' sein!") + self.assertEqual(getattr(bello, "alter", None), 3, "Fehler: 'bello.alter' muss 3 sein!") + + def test_03_erstelle_mimi(self): + """Prüft, ob erstelle_mimi() ein korrektes Haustier-Objekt liefert.""" + self.assertTrue(hasattr(aufgabe, "erstelle_mimi"), "Fehler: Funktion 'erstelle_mimi' fehlt!") + mimi = aufgabe.erstelle_mimi() + self.assertIsNotNone(mimi, "Fehler: 'erstelle_mimi()' gibt None zurück. Vergiss nicht das 'return'!") + self.assertIsInstance(mimi, aufgabe.Haustier, "Fehler: 'mimi' muss eine Instanz der Klasse 'Haustier' sein!") + self.assertEqual(getattr(mimi, "name", None), "Mimi", "Fehler: 'mimi.name' muss 'Mimi' sein!") + self.assertEqual(getattr(mimi, "tierart", None), "Katze", "Fehler: 'mimi.tierart' muss 'Katze' sein!") + self.assertEqual(getattr(mimi, "alter", None), 5, "Fehler: 'mimi.alter' muss 5 sein!") + + def test_04_steckbrief_text(self): + """Prüft, ob steckbrief_text() den richtigen String formatiert.""" + self.assertTrue(hasattr(aufgabe, "steckbrief_text"), "Fehler: Funktion 'steckbrief_text' fehlt!") + tier = aufgabe.Haustier() + tier.name = "Rocky" + tier.tierart = "Papagei" + tier.alter = 2 + + ergebnis = aufgabe.steckbrief_text(tier) + erwartet = "Rocky ist ein(e) Papagei und ist 2 Jahre alt." + self.assertEqual( + ergebnis, + erwartet, + f"Fehler: Textausgabe weicht ab!\nErwartet: '{erwartet}'\nErhalten: '{ergebnis}'" + ) + + def test_05_aelteres_tier(self): + """Prüft die Altersvergleich-Funktion.""" + self.assertTrue(hasattr(aufgabe, "aelteres_tier"), "Fehler: Funktion 'aelteres_tier' fehlt!") + t1 = aufgabe.Haustier() + t1.name = "Tier A" + t1.alter = 4 + + t2 = aufgabe.Haustier() + t2.name = "Tier B" + t2.alter = 7 + + t3 = aufgabe.Haustier() + t3.name = "Tier C" + t3.alter = 4 + + self.assertIs(aufgabe.aelteres_tier(t1, t2), t2, "Fehler: Tier B (7) ist älter als Tier A (4)!") + self.assertIs(aufgabe.aelteres_tier(t2, t1), t2, "Fehler: Tier B (7) ist älter als Tier A (4)!") + self.assertIs(aufgabe.aelteres_tier(t1, t3), t1, "Fehler: Bei gleichem Alter soll tier1 zurückgegeben werden!") + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/02_init_und_self/README.md b/02_init_und_self/README.md new file mode 100644 index 0000000..6c29963 --- /dev/null +++ b/02_init_und_self/README.md @@ -0,0 +1,90 @@ +# Kapitel 02: Der Konstruktor `__init__` und das geheimnisvolle `self` 💳 + +Im ersten Kapitel haben wir Attribute noch manuell nach dem Erstellen zugewiesen: +```python +bello = Haustier() +bello.name = "Bello" +bello.alter = 3 +``` +Das ist mühsam und fehleranfällig: Was passiert, wenn man vergisst, `.alter` zu setzen? Richtig: Das Programm stürzt ab, sobald jemand darauf zugreift! + +Deshalb gibt es in Python den **Konstruktor**: `__init__`. + +--- + +## 🛠️ Was ist `__init__`? + +`__init__` steht für *initialisieren* (vorbereiten/startklar machen). + +Es ist eine **spezielle Methode**, die Python **automatisch aufruft**, sobald du ein neues Objekt erstellst: + +```python +class Haustier: + def __init__(self, name, tierart, alter): + self.name = name + self.tierart = tierart + self.alter = alter + +# Jetzt können wir die Werte direkt in die Klammern schreiben! +bello = Haustier("Bello", "Hund", 3) +mimi = Haustier("Mimi", "Katze", 5) + +print(bello.name) # Gibt sofort "Bello" aus +``` +Die doppelten Unterstriche `__` nennt man in Python auch **Dunder** (Double Underscore). Sie signalisieren besondere Python-Funktionen. + +--- + +## 🪞 Was bedeutet eigentlich `self`? + +`self` verwirrt am Anfang fast jeden – ist aber ganz einfach: +- `self` bedeutet auf Deutsch **"ich selbst"** oder **"dieses Objekt hier"**. +- Wenn du `bello = Haustier("Bello", "Hund", 3)` aufrufst, übergibt Python das neu entstandene `bello`-Objekt automatisch als erstes Argument `self`. +- `self.name = name` bedeutet also: *"Speichere den übergebenen Namen in MEINEM EIGENEN Attribut `.name` ab!"* + +``` + Haustier("Bello", "Hund", 3) + │ + ▼ +def __init__(self, name, tierart, alter): + ▲ │ │ │ + │ ▼ ▼ ▼ + bello "Bello" "Hund" 3 + │ + ├── bello.name = "Bello" + ├── bello.tierart = "Hund" + └── bello.alter = 3 +``` + +--- + +## ⚙️ Standardwerte (Default Parameters) + +Manchmal hat ein Attribut einen typischen Startwert (z.B. ein neues Bankkonto startet meist bei `0.0` Euro): + +```python +class Bankkonto: + def __init__(self, inhaber, kontostand=0.0): + self.inhaber = inhaber + self.kontostand = kontostand + +# Konto mit 0.0 Euro Startguthaben: +k1 = Bankkonto("Anna") + +# Konto mit speziellem Startguthaben: +k2 = Bankkonto("Ben", 50.0) +``` + +--- + +## 🎯 Deine Aufgabe: Das Bankkonto + +Öffne `aufgabe.py`. Du programmierst eine Klasse `Bankkonto`: +1. Schreibe den Konstruktor `__init__`. +2. Ergänze Methoden zum Einzahlen (`einzahlen`) und Auszahlen (`auszahlen`), die darauf achten, dass man nicht ins Minus rutscht. +3. Ergänze eine Methode `info_text()`. + +### Testen: +```bash +python3 test_aufgabe.py +``` diff --git a/02_init_und_self/aufgabe.py b/02_init_und_self/aufgabe.py new file mode 100644 index 0000000..3ad4a4b --- /dev/null +++ b/02_init_und_self/aufgabe.py @@ -0,0 +1,77 @@ +""" +Kapitel 02: Der Konstruktor __init__ und self +============================================== + +In dieser Aufgabe erstellst du eine Klasse "Bankkonto" für eine Taschengeld-App. +""" + +class Bankkonto: + # ========================================================================== + # TODO 1: Schreibe die __init__-Methode. + # Parameter: self, inhaber (String), kontostand (Float, Standardwert 0.0) + # + # Speichere die Parameter als Attribute an 'self': + # - self.inhaber + # - self.kontostand + # ========================================================================== + def __init__(self, inhaber, kontostand=0.0): + # Schreibe hier deinen Code für TODO 1: + pass + + # ========================================================================== + # TODO 2: Schreibe die Methode "einzahlen(self, betrag)". + # Regeln: + # 1. Wenn 'betrag' größer als 0 ist: + # - Erhöhe 'self.kontostand' um 'betrag'. + # - Gib True zurück (Einzahlung erfolgreich). + # 2. Wenn 'betrag' kleiner oder gleich 0 ist: + # - Verändere den Kontostand nicht. + # - Gib False zurück (ungültiger Betrag). + # ========================================================================== + def einzahlen(self, betrag): + # Schreibe hier deinen Code für TODO 2: + pass + + # ========================================================================== + # TODO 3: Schreibe die Methode "auszahlen(self, betrag)". + # Regeln: + # 1. Der Betrag muss größer als 0 sein UND es muss genug Geld auf dem Konto + # sein (self.kontostand >= betrag). + # - Ziehe 'betrag' von 'self.kontostand' ab. + # - Gib True zurück (Auszahlung erfolgreich). + # 2. Andernfalls (nicht genug Geld oder ungültiger Betrag): + # - Verändere den Kontostand nicht. + # - Gib False zurück. + # ========================================================================== + def auszahlen(self, betrag): + # Schreibe hier deinen Code für TODO 3: + pass + + # ========================================================================== + # TODO 4: Schreibe die Methode "info_text(self)". + # Sie soll einen String im folgenden Format zurückgeben: + # "Konto von {inhaber}: {kontostand:.2f} Euro" + # + # Beispiel: Bei inhaber="Mia" und kontostand=25.5 -> "Konto von Mia: 25.50 Euro" + # ========================================================================== + def info_text(self): + # Schreibe hier deinen Code für TODO 4: + pass + + +# ============================================================================== +# Hauptprogramm zum Ausprobieren: +# ============================================================================== +if __name__ == "__main__": + print("--- Teste dein Bankkonto ---") + konto = Bankkonto("Mia", 20.0) + print(konto.info_text()) + + erfolg = konto.einzahlen(15.5) + print(f"15.50€ eingezahlt? {erfolg} -> Neuer Stand: {konto.kontostand}€") + + erfolg = konto.auszahlen(50.0) + print(f"50.00€ ausgezahlt? {erfolg} (sollte False sein) -> Stand: {konto.kontostand}€") + + erfolg = konto.auszahlen(10.0) + print(f"10.00€ ausgezahlt? {erfolg} -> Stand: {konto.kontostand}€") diff --git a/02_init_und_self/musterloesung.py b/02_init_und_self/musterloesung.py new file mode 100644 index 0000000..c2becc6 --- /dev/null +++ b/02_init_und_self/musterloesung.py @@ -0,0 +1,37 @@ +""" +Kapitel 02: Der Konstruktor __init__ und self – Musterlösung +============================================================= +""" + +class Bankkonto: + # TODO 1: Konstruktor + def __init__(self, inhaber, kontostand=0.0): + self.inhaber = inhaber + self.kontostand = float(kontostand) + + # TODO 2: Einzahlen mit Betragsüberprüfung + def einzahlen(self, betrag): + if betrag > 0: + self.kontostand += betrag + return True + return False + + # TODO 3: Auszahlen mit Guthabenüberprüfung + def auszahlen(self, betrag): + if betrag > 0 and self.kontostand >= betrag: + self.kontostand -= betrag + return True + return False + + # TODO 4: Formatierter Info-Text + def info_text(self): + return f"Konto von {self.inhaber}: {self.kontostand:.2f} Euro" + + +if __name__ == "__main__": + konto = Bankkonto("Mia", 20.0) + print(konto.info_text()) + konto.einzahlen(15.5) + print(konto.info_text()) + konto.auszahlen(10.0) + print(konto.info_text()) diff --git a/02_init_und_self/test_aufgabe.py b/02_init_und_self/test_aufgabe.py new file mode 100644 index 0000000..b9f84e7 --- /dev/null +++ b/02_init_und_self/test_aufgabe.py @@ -0,0 +1,85 @@ +import unittest +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel02(unittest.TestCase): + + def test_01_init_standardwert(self): + """Prüft __init__ mit Standardwert für kontostand.""" + konto = aufgabe.Bankkonto("Lukas") + self.assertEqual(konto.inhaber, "Lukas", "Fehler: self.inhaber wurde nicht korrekt gesetzt!") + self.assertEqual(konto.kontostand, 0.0, "Fehler: Standardwert für kontostand sollte 0.0 sein!") + + def test_02_init_mit_startguthaben(self): + """Prüft __init__ mit übergebenem Startguthaben.""" + konto = aufgabe.Bankkonto("Sarah", 100.50) + self.assertEqual(konto.inhaber, "Sarah") + self.assertEqual(konto.kontostand, 100.50) + + def test_03_einzahlen_gueltig(self): + """Prüft erfolgreiches Einzahlen.""" + konto = aufgabe.Bankkonto("Tim", 50.0) + erfolg = konto.einzahlen(25.0) + self.assertTrue(erfolg, "Fehler: einzahlen(25.0) sollte True zurückgeben!") + self.assertEqual(konto.kontostand, 75.0, "Fehler: Kontostand sollte nach 25€ Einzahlung auf 50€ bei 75.0€ liegen!") + + def test_04_einzahlen_ungueltig(self): + """Prüft Einzahlen von negativen Beträgen oder 0.""" + konto = aufgabe.Bankkonto("Tim", 50.0) + + erfolg_null = konto.einzahlen(0) + self.assertFalse(erfolg_null, "Fehler: Einzahlen von 0 sollte False zurückgeben!") + self.assertEqual(konto.kontostand, 50.0, "Fehler: Kontostand darf sich bei 0€ nicht ändern!") + + erfolg_neg = konto.einzahlen(-10.0) + self.assertFalse(erfolg_neg, "Fehler: Einzahlen von negativen Beträgen sollte False zurückgeben!") + self.assertEqual(konto.kontostand, 50.0, "Fehler: Kontostand darf sich bei negativem Betrag nicht ändern!") + + def test_05_auszahlen_erfolgreich(self): + """Prüft erfolgreiches Auszahlen.""" + konto = aufgabe.Bankkonto("Emma", 80.0) + erfolg = konto.auszahlen(30.0) + self.assertTrue(erfolg, "Fehler: auszahlen(30.0) bei 80€ Guthaben sollte True zurückgeben!") + self.assertEqual(konto.kontostand, 50.0, "Fehler: Kontostand sollte nach Auszahlung 50.0€ sein!") + + # Gesamtes Guthaben abheben + erfolg_alles = konto.auszahlen(50.0) + self.assertTrue(erfolg_alles, "Fehler: Gesamtes Guthaben abheben sollte möglich sein!") + self.assertEqual(konto.kontostand, 0.0) + + def test_06_auszahlen_zu_viel_oder_ungueltig(self): + """Prüft Auszahlen bei unzureichendem Guthaben oder negativem Betrag.""" + konto = aufgabe.Bankkonto("Emma", 20.0) + + # Zu viel abheben + erfolg = konto.auszahlen(50.0) + self.assertFalse(erfolg, "Fehler: Auszahlen von mehr Geld als vorhanden ist muss False liefern!") + self.assertEqual(konto.kontostand, 20.0, "Fehler: Kontostand darf sich nicht ändern, wenn Auszahlung fehlschlägt!") + + # Negativer Betrag + erfolg_neg = konto.auszahlen(-5.0) + self.assertFalse(erfolg_neg, "Fehler: Auszahlen von negativen Beträgen muss False liefern!") + self.assertEqual(konto.kontostand, 20.0) + + def test_07_info_text(self): + """Prüft die Formatierung der info_text-Methode.""" + konto = aufgabe.Bankkonto("Mia", 25.5) + text = konto.info_text() + erwartet = "Konto von Mia: 25.50 Euro" + self.assertEqual( + text, + erwartet, + f"Fehler: Text weicht ab!\nErwartet: '{erwartet}'\nErhalten: '{text}'" + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/03_methoden_und_verhalten/README.md b/03_methoden_und_verhalten/README.md new file mode 100644 index 0000000..2791da9 --- /dev/null +++ b/03_methoden_und_verhalten/README.md @@ -0,0 +1,69 @@ +# Kapitel 03: Methoden & Verhalten – Wenn Objekte lebendig werden ⚔️ + +Bisher haben unsere Objekte hauptsächlich Daten gespeichert (wie Name oder Kontostand). Aber Objekte können noch viel mehr: **Sie können Aktionen ausführen!** + +Funktionen, die innerhalb einer Klasse definiert sind, nennt man **Methoden**. + +--- + +## 🏃‍♀️ Was unterscheidet eine Methode von einer normalen Funktion? + +1. Eine Methode steht **eingerückt in der Klasse**. +2. Das **erste Argument ist immer `self`**. +3. Über `self` hat die Methode direkten Zugriff auf alle Eigenschaften des Objekts und kann diese verändern. + +```python +class Zauberer: + def __init__(self, name, mana=50): + self.name = name + self.mana = mana + + # Das hier ist eine Methode: + def zauberspruch_wirken(self, mana_kosten): + if self.mana >= mana_kosten: + self.mana -= mana_kosten + print(f"{self.name} wirkt einen Zauber! Verbleibendes Mana: {self.mana}") + return True + else: + print(f"{self.name} hat nicht genug Mana!") + return False + +merlin = Zauberer("Merlin", 30) +merlin.zauberspruch_wirken(10) # self wird automatisch merlin! +``` + +--- + +## 🤝 Wenn Objekte miteinander interagieren! + +Das Spannendste an der objektorientierten Programmierung ist, wenn **ein Objekt mit einem anderen Objekt interagiert**. + +Du kannst ein ganzes Objekt als Parameter an die Methode eines anderen Objekts übergeben: + +```python +class Spieler: + def __init__(self, name, geld): + self.name = name + self.geld = geld + + def bezahle_an(self, empfaenger, betrag): + if self.geld >= betrag: + self.geld -= betrag + empfaenger.geld += betrag # Wir greifen direkt auf das andere Objekt zu! + return True + return False +``` + +--- + +## 🎯 Deine Aufgabe: Das RPG-Duell + +In `aufgabe.py` erstellst du eine Klasse `Held` für ein Rollenspiel: +- Ein Held hat `name`, `leben`, `max_leben` und `angriffskraft`. +- Ein Held kann Schaden erleiden (`schaden_erleiden`), geheilt werden (`heilen`) und einen anderen Helden angreifen (`angreifen(gegner)`). +- Du sorgst mit Bedingungen dafür, dass die Lebenspunkte nicht unter `0` fallen und nicht über `max_leben` steigen! + +### Testen: +```bash +python3 test_aufgabe.py +``` diff --git a/03_methoden_und_verhalten/aufgabe.py b/03_methoden_und_verhalten/aufgabe.py new file mode 100644 index 0000000..bed2842 --- /dev/null +++ b/03_methoden_und_verhalten/aufgabe.py @@ -0,0 +1,101 @@ +""" +Kapitel 03: Methoden & Verhalten – Helden-Duell +============================================== + +In dieser Aufgabe programmierst du eine Klasse 'Held' für ein Rollenspiel. +""" + +class Held: + # ========================================================================== + # TODO 1: Konstruktor __init__(self, name, leben=100, angriffskraft=15) + # Speichere: + # - self.name = name + # - self.leben = leben + # - self.max_leben = leben (das maximale Leben entspricht dem Startleben) + # - self.angriffskraft = angriffskraft + # - self.ist_am_leben = True + # ========================================================================== + def __init__(self, name, leben=100, angriffskraft=15): + # Schreibe hier deinen Code für TODO 1: + pass + + # ========================================================================== + # TODO 2: Methode "schaden_erleiden(self, schaden)" + # Regeln: + # 1. Wenn der Held bereits besiegt ist (not self.ist_am_leben) oder schaden <= 0: + # - Es passiert nichts, gib 0 zurück. + # 2. Ziehe 'schaden' von 'self.leben' ab. + # 3. Wenn 'self.leben' <= 0 wird: + # - Setze 'self.leben' genau auf 0. + # - Setze 'self.ist_am_leben' auf False. + # 4. Gib den erlittenen Schaden (int/float) zurück. + # ========================================================================== + def schaden_erleiden(self, schaden): + # Schreibe hier deinen Code für TODO 2: + pass + + # ========================================================================== + # TODO 3: Methode "heilen(self, heilung)" + # Regeln: + # 1. Wenn der Held besiegt ist (not self.ist_am_leben) oder heilung <= 0: + # - Tote Helden können nicht geheilt werden! Gib 0 zurück. + # 2. Erhöhe das Leben um 'heilung', aber ACHTUNG: + # - 'self.leben' darf niemals größer als 'self.max_leben' werden! + # - Beispiel: Wenn leben=90, max_leben=100 und heilung=20: + # Das Leben wird 100 (nicht 110) und die tatsächliche Heilung war 10. + # 3. Gib die tatsächlich geheilte Menge an Lebenspunkten zurück. + # ========================================================================== + def heilen(self, heilung): + # Schreibe hier deinen Code für TODO 3: + pass + + # ========================================================================== + # TODO 4: Methode "angreifen(self, gegner)" + # Parameter 'gegner' ist ein anderes Held-Objekt! + # Regeln: + # 1. Prüfe, ob BEIDE Helden noch am Leben sind (self.ist_am_leben und gegner.ist_am_leben). + # 2. Wenn ja: + # - Rufe auf dem 'gegner' die Methode 'schaden_erleiden' auf mit self.angriffskraft als Wert. + # - Gib True zurück. + # 3. Wenn einer von beiden (oder beide) nicht mehr am Leben ist: + # - Greife nicht an und gib False zurück. + # ========================================================================== + def angreifen(self, gegner): + # Schreibe hier deinen Code für TODO 4: + pass + + # ========================================================================== + # TODO 5: Methode "status_text(self)" + # Gibt einen formatierten String zurück: + # Wenn am Leben: + # "[{self.name}] HP: {self.leben}/{self.max_leben} | Kraft: {self.angriffskraft} | Status: Lebendig" + # Wenn besiegt: + # "[{self.name}] HP: 0/{self.max_leben} | Kraft: {self.angriffskraft} | Status: Besiegt" + # ========================================================================== + def status_text(self): + # Schreibe hier deinen Code für TODO 5: + pass + + +# ============================================================================== +# Kampfsimulator zum Ausprobieren: +# ============================================================================== +if __name__ == "__main__": + print("⚔️ --- ARENA DER HELDEN --- ⚔️") + ritter = Held("Arthur", leben=80, angriffskraft=20) + drache = Held("Feuerspei", leben=120, angriffskraft=25) + + print(ritter.status_text()) + print(drache.status_text()) + + print("\n--- Runde 1 ---") + ritter.angreifen(drache) + print(drache.status_text()) + + drache.angreifen(ritter) + print(ritter.status_text()) + + print("\n--- Arthur trinkt einen Heiltrank ---") + geheilt = ritter.heilen(15) + print(f"Arthur heilt sich um {geheilt} HP!") + print(ritter.status_text()) diff --git a/03_methoden_und_verhalten/musterloesung.py b/03_methoden_und_verhalten/musterloesung.py new file mode 100644 index 0000000..5a9da71 --- /dev/null +++ b/03_methoden_und_verhalten/musterloesung.py @@ -0,0 +1,51 @@ +""" +Kapitel 03: Methoden & Verhalten – Helden-Duell – Musterlösung +============================================================= +""" + +class Held: + def __init__(self, name, leben=100, angriffskraft=15): + self.name = name + self.leben = leben + self.max_leben = leben + self.angriffskraft = angriffskraft + self.ist_am_leben = True + + def schaden_erleiden(self, schaden): + if not self.ist_am_leben or schaden <= 0: + return 0 + + tatsaechlicher_schaden = min(schaden, self.leben) + self.leben -= schaden + if self.leben <= 0: + self.leben = 0 + self.ist_am_leben = False + return tatsaechlicher_schaden + + def heilen(self, heilung): + if not self.ist_am_leben or heilung <= 0: + return 0 + + alter_wert = self.leben + self.leben = min(self.max_leben, self.leben + heilung) + tatsaechlich_geheilt = self.leben - alter_wert + return tatsaechlich_geheilt + + def angreifen(self, gegner): + if self.ist_am_leben and gegner.ist_am_leben: + gegner.schaden_erleiden(self.angriffskraft) + return True + return False + + def status_text(self): + status_str = "Lebendig" if self.ist_am_leben else "Besiegt" + return f"[{self.name}] HP: {self.leben}/{self.max_leben} | Kraft: {self.angriffskraft} | Status: {status_str}" + + +if __name__ == "__main__": + ritter = Held("Arthur", 80, 20) + drache = Held("Feuerspei", 120, 25) + print(ritter.status_text()) + print(drache.status_text()) + ritter.angreifen(drache) + print(drache.status_text()) diff --git a/03_methoden_und_verhalten/test_aufgabe.py b/03_methoden_und_verhalten/test_aufgabe.py new file mode 100644 index 0000000..974c576 --- /dev/null +++ b/03_methoden_und_verhalten/test_aufgabe.py @@ -0,0 +1,101 @@ +import unittest +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel03(unittest.TestCase): + + def test_01_init(self): + """Prüft die Initialisierung des Helden.""" + h = aufgabe.Held("Galahad", 90, 18) + self.assertEqual(h.name, "Galahad") + self.assertEqual(h.leben, 90) + self.assertEqual(h.max_leben, 90) + self.assertEqual(h.angriffskraft, 18) + self.assertTrue(h.ist_am_leben) + + def test_02_schaden_erleiden(self): + """Prüft Schadensberechnung und K.O.-Logik.""" + h = aufgabe.Held("Krieger", 50, 10) + + # Normaler Schaden + schaden = h.schaden_erleiden(20) + self.assertEqual(schaden, 20) + self.assertEqual(h.leben, 30) + self.assertTrue(h.ist_am_leben) + + # Schaden, der das Leben auf 0 oder darunter bringt + h.schaden_erleiden(40) + self.assertEqual(h.leben, 0, "Leben darf nach Über-Schaden nicht negativ werden, sondern muss 0 sein!") + self.assertFalse(h.ist_am_leben, "Held muss nach 0 HP als besiegt (ist_am_leben=False) markiert werden!") + + # Schaden an bereits besiegtem Helden + schaden_tot = h.schaden_erleiden(10) + self.assertEqual(schaden_tot, 0) + self.assertEqual(h.leben, 0) + + def test_03_heilen(self): + """Prüft die Heilungsfunktion und max_leben Begrenzung.""" + h = aufgabe.Held("Priester", 100, 5) + h.leben = 70 + + # Teilweise Heilung + geheilt = h.heilen(15) + self.assertEqual(geheilt, 15) + self.assertEqual(h.leben, 85) + + # Heilung über max_leben hinaus + geheilt_cap = h.heilen(30) + self.assertEqual(geheilt_cap, 15, "Es sollten nur 15 HP geheilt werden, da max_leben=100!") + self.assertEqual(h.leben, 100) + + # Heilung eines toten Helden + h.leben = 0 + h.ist_am_leben = False + geheilt_tot = h.heilen(50) + self.assertEqual(geheilt_tot, 0, "Ein besiegter Held kann nicht geheilt werden!") + self.assertEqual(h.leben, 0) + + def test_04_angreifen(self): + """Prüft die Interaktion beim Angriff.""" + held1 = aufgabe.Held("Held", 50, 20) + held2 = aufgabe.Held("Monster", 30, 10) + + erfolg = held1.angreifen(held2) + self.assertTrue(erfolg) + self.assertEqual(held2.leben, 10) + self.assertTrue(held2.ist_am_leben) + + # Zweiter Angriff besiegt Monster + erfolg2 = held1.angreifen(held2) + self.assertTrue(erfolg2) + self.assertEqual(held2.leben, 0) + self.assertFalse(held2.ist_am_leben) + + # Weiterer Angriff auf besiegtes Monster schlägt fehl + erfolg3 = held1.angreifen(held2) + self.assertFalse(erfolg3, "Ein Angriff auf ein besiegtes Ziel sollte False zurückgeben!") + + def test_05_status_text(self): + """Prüft die Statusanzeige.""" + h = aufgabe.Held("Arthur", 100, 20) + self.assertEqual( + h.status_text(), + "[Arthur] HP: 100/100 | Kraft: 20 | Status: Lebendig" + ) + h.schaden_erleiden(100) + self.assertEqual( + h.status_text(), + "[Arthur] HP: 0/100 | Kraft: 20 | Status: Besiegt" + ) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/04_str_und_darstellung/README.md b/04_str_und_darstellung/README.md new file mode 100644 index 0000000..d121e0d --- /dev/null +++ b/04_str_und_darstellung/README.md @@ -0,0 +1,77 @@ +# Kapitel 04: Schöne Ausgaben mit `__str__` 🧾 + +Hast du schon mal versucht, ein selbst erstelltes Objekt direkt mit `print()` auszugeben? + +```python +class Buch: + def __init__(self, titel, autor): + self.titel = titel + self.autor = autor + +mein_buch = Buch("Harry Potter", "J.K. Rowling") +print(mein_buch) +``` +Die Ausgabe sieht dann so aus: +``` +<__main__.Buch object at 0x7fa2b0485d90> +``` +Das ist kryptisch und hilft im Alltag nicht weiter. Aber Python hat dafür eine geniale Dunder-Methode: **`__str__`**! + +--- + +## ✨ Die `__str__`-Methode + +Wann immer Python ein Objekt in einen Text umwandeln soll (z.B. bei `print(mein_objekt)` oder `str(mein_objekt)` oder in einem f-String `f"Das Buch: {mein_objekt}"`), schaut Python nach, ob deine Klasse eine `__str__`-Methode hat. + +Wenn ja, ruft Python sie automatisch auf: + +```python +class Buch: + def __init__(self, titel, autor): + self.titel = titel + self.autor = autor + + def __str__(self): + # Muss immer einen STRING zurückgeben (return)! + return f"'{self.titel}' von {self.autor}" + +mein_buch = Buch("Harry Potter", "J.K. Rowling") +print(mein_buch) # Ausgabe: 'Harry Potter' von J.K. Rowling 🎉 +``` + +--- + +## 🧺 Objekte in Listen verwalten + +Oft hat man Objekte, die eine Liste von *anderen* Objekten verwalten (z.B. ein Warenkorb, der Artikel enthält, oder ein Bus, der Fahrgäste enthält): + +```python +class Schultasche: + def __init__(self): + self.buecher = [] # Startet mit einer leeren Liste + + def buch_einpacken(self, buch): + self.buecher.append(buch) + + def inhalt_anzeigen(self): + for b in self.buecher: + print(f"- {b}") # Hier wird automatisch b.__str__() genutzt! +``` + +--- + +## 🎯 Deine Aufgabe: Der Supermarkt-Kassenzettel + +In `aufgabe.py` programmierst du zwei Klassen: +1. `Artikel`: Ein Produkt mit Name, Einzelpreis und Anzahl. + - `__str__`: Erzeugt einen Kassenbon-Text wie `"3x Apfel (je 0.50 €) = 1.50 €"`. + - `gesamtpreis()`: Berechnet den Preis für diesen Artikel (`preis * anzahl`). +2. `Warenkorb`: Verwaltet eine Liste von `Artikel`-Objekten. + - `artikel_hinzufuegen()`: Packt einen Artikel in den Warenkorb. + - `gesamtsumme()`: Berechnet die Gesamtsumme aller Artikel. + - `bon_text()`: Erzeugt einen formatierten mehrzeiligen Kassenbon. + +### Testen: +```bash +python3 test_aufgabe.py +``` diff --git a/04_str_und_darstellung/aufgabe.py b/04_str_und_darstellung/aufgabe.py new file mode 100644 index 0000000..445cb0d --- /dev/null +++ b/04_str_und_darstellung/aufgabe.py @@ -0,0 +1,105 @@ +""" +Kapitel 04: __str__ und Objektlisten – Supermarkt & Warenkorb +============================================================ + +In dieser Aufgabe erstellst du zwei Klassen: 'Artikel' und 'Warenkorb'. +""" + +class Artikel: + # ========================================================================== + # TODO 1: Konstruktor für Artikel + # Parameter: self, name (str), preis (float), anzahl (int, Standardwert 1) + # Speichere: self.name, self.preis, self.anzahl + # ========================================================================== + def __init__(self, name, preis, anzahl=1): + # Schreibe hier deinen Code für TODO 1: + pass + + # ========================================================================== + # TODO 2: Methode "gesamtpreis(self)" + # Gibt den Preis für diesen Artikel multipliziert mit der Anzahl zurück. + # Beispiel: 3 Äpfel à 0.50€ -> 1.50 + # ========================================================================== + def gesamtpreis(self): + # Schreibe hier deinen Code für TODO 2: + pass + + # ========================================================================== + # TODO 3: Dunder-Methode "__str__(self)" + # Gibt einen String im folgenden Format zurück: + # "{anzahl}x {name} (je {preis:.2f} €) = {gesamtpreis:.2f} €" + # + # Beispiel: + # Bei name="Milch", preis=1.29, anzahl=2: + # -> "2x Milch (je 1.29 €) = 2.58 €" + # ========================================================================== + def __str__(self): + # Schreibe hier deinen Code für TODO 3: + pass + + +class Warenkorb: + # ========================================================================== + # TODO 4: Konstruktor für Warenkorb + # Parameter: self + # Initialisiere: self.artikel_liste als leere Liste [] + # ========================================================================== + def __init__(self): + # Schreibe hier deinen Code für TODO 4: + pass + + # ========================================================================== + # TODO 5: Methode "artikel_hinzufuegen(self, artikel)" + # Fügt das übergebene Artikel-Objekt an 'self.artikel_liste' an (.append). + # ========================================================================== + def artikel_hinzufuegen(self, artikel): + # Schreibe hier deinen Code für TODO 5: + pass + + # ========================================================================== + # TODO 6: Methode "gesamtsumme(self)" + # Berechnet die Summe der Gesamtpreise aller Artikel in self.artikel_liste + # und gibt diese als float zurück. (Tipp: Schleife oder sum()) + # Bei leerem Warenkorb soll 0.0 zurückgegeben werden. + # ========================================================================== + def gesamtsumme(self): + # Schreibe hier deinen Code für TODO 6: + pass + + # ========================================================================== + # TODO 7: Methode "bon_text(self)" + # Erzeugt einen formatierten mehrzeiligen String für den Kassenzettel. + # Format: + # --- KASSENZETTEL --- + # 2x Milch (je 1.29 €) = 2.58 € + # 1x Brot (je 2.49 €) = 2.49 € + # -------------------- + # Gesamtsumme: 5.07 € + # + # Tipp: Baue eine Liste von Zeilen und verbinde sie mit "\n".join(...) + # Die Zeilen für die Artikel erhältst du einfach mit str(artikel)! + # ========================================================================== + def bon_text(self): + # Schreibe hier deinen Code für TODO 7: + pass + + +# ============================================================================== +# Hauptprogramm zum Ausprobieren: +# ============================================================================== +if __name__ == "__main__": + print("🛒 --- SUPERMARKT EINKAUF --- 🛒") + korb = Warenkorb() + + a1 = Artikel("Milch", 1.29, 2) + a2 = Artikel("Brot", 2.49, 1) + a3 = Artikel("Schokolade", 0.99, 3) + + korb.artikel_hinzufuegen(a1) + korb.artikel_hinzufuegen(a2) + korb.artikel_hinzufuegen(a3) + + if korb.artikel_liste: + print(korb.bon_text()) + else: + print("Hinweis: Implementiere die TODOs, um den Kassenbon zu drucken!") diff --git a/04_str_und_darstellung/musterloesung.py b/04_str_und_darstellung/musterloesung.py new file mode 100644 index 0000000..9ffb4d0 --- /dev/null +++ b/04_str_und_darstellung/musterloesung.py @@ -0,0 +1,43 @@ +""" +Kapitel 04: __str__ und Objektlisten – Musterlösung +=================================================== +""" + +class Artikel: + def __init__(self, name, preis, anzahl=1): + self.name = name + self.preis = float(preis) + self.anzahl = int(anzahl) + + def gesamtpreis(self): + return self.preis * self.anzahl + + def __str__(self): + return f"{self.anzahl}x {self.name} (je {self.preis:.2f} €) = {self.gesamtpreis():.2f} €" + + +class Warenkorb: + def __init__(self): + self.artikel_liste = [] + + def artikel_hinzufuegen(self, artikel): + self.artikel_liste.append(artikel) + + def gesamtsumme(self): + return sum(item.gesamtpreis() for item in self.artikel_liste) + + def bon_text(self): + zeilen = ["--- KASSENZETTEL ---"] + for artikel in self.artikel_liste: + zeilen.append(str(artikel)) + zeilen.append("--------------------") + zeilen.append(f"Gesamtsumme: {self.gesamtsumme():.2f} €") + return "\n".join(zeilen) + + +if __name__ == "__main__": + korb = Warenkorb() + korb.artikel_hinzufuegen(Artikel("Milch", 1.29, 2)) + korb.artikel_hinzufuegen(Artikel("Brot", 2.49, 1)) + korb.artikel_hinzufuegen(Artikel("Schokolade", 0.99, 3)) + print(korb.bon_text()) diff --git a/04_str_und_darstellung/test_aufgabe.py b/04_str_und_darstellung/test_aufgabe.py new file mode 100644 index 0000000..c7ff7ef --- /dev/null +++ b/04_str_und_darstellung/test_aufgabe.py @@ -0,0 +1,68 @@ +import unittest +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel04(unittest.TestCase): + + def test_01_artikel_init_und_gesamtpreis(self): + """Prüft Artikel-Konstruktor und gesamtpreis.""" + a = aufgabe.Artikel("Apfel", 0.60, 4) + self.assertEqual(a.name, "Apfel") + self.assertEqual(a.preis, 0.60) + self.assertEqual(a.anzahl, 4) + self.assertAlmostEqual(a.gesamtpreis(), 2.40, places=2) + + def test_02_artikel_str(self): + """Prüft die __str__ Implementierung von Artikel.""" + a = aufgabe.Artikel("Milch", 1.29, 2) + erwartet = "2x Milch (je 1.29 €) = 2.58 €" + self.assertEqual(str(a), erwartet) + + def test_03_warenkorb_init(self): + """Prüft, ob der Warenkorb mit einer leeren Liste startet.""" + korb = aufgabe.Warenkorb() + self.assertTrue(hasattr(korb, "artikel_liste")) + self.assertEqual(korb.artikel_liste, []) + + def test_04_artikel_hinzufuegen_und_gesamtsumme(self): + """Prüft das Hinzufügen von Artikeln und die Gesamtsumme.""" + korb = aufgabe.Warenkorb() + self.assertAlmostEqual(korb.gesamtsumme(), 0.0, places=2) + + a1 = aufgabe.Artikel("Brot", 2.50, 2) # 5.00 € + a2 = aufgabe.Artikel("Butter", 1.80, 1) # 1.80 € + + korb.artikel_hinzufuegen(a1) + korb.artikel_hinzufuegen(a2) + + self.assertEqual(len(korb.artikel_liste), 2) + self.assertAlmostEqual(korb.gesamtsumme(), 6.80, places=2) + + def test_05_bon_text(self): + """Prüft den formatierten Bon-Text.""" + korb = aufgabe.Warenkorb() + korb.artikel_hinzufuegen(aufgabe.Artikel("Kaffee", 4.50, 1)) + korb.artikel_hinzufuegen(aufgabe.Artikel("Keks", 1.20, 3)) + + bon = korb.bon_text() + erwartete_zeilen = [ + "--- KASSENZETTEL ---", + "1x Kaffee (je 4.50 €) = 4.50 €", + "3x Keks (je 1.20 €) = 3.60 €", + "--------------------", + "Gesamtsumme: 8.10 €" + ] + erwartet = "\n".join(erwartete_zeilen) + self.assertEqual(bon.strip(), erwartet.strip()) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/05_objekte_kombinieren/README.md b/05_objekte_kombinieren/README.md new file mode 100644 index 0000000..a5d4b46 --- /dev/null +++ b/05_objekte_kombinieren/README.md @@ -0,0 +1,71 @@ +# Kapitel 05: Komposition – Objekte kombinieren & filtern 🎵 + +In der echten Softwareentwicklung (wie bei Spotify, Instagram oder Netflix) besteht ein System fast immer aus vielen verschiedenen Klassen, die zusammenarbeiten. + +Wenn eine Klasse andere Objekte besitzt oder verwaltet, nennt man das **Komposition** (oder *"hat-ein"-Beziehung*): +- Eine **Playlist** *hat viele* **Songs**. +- Ein **Kino** *hat viele* **Säle** und **Filme**. +- Eine **Schule** *hat viele* **Klassen** und **Schüler**. + +--- + +## 🎶 Das Zusammenspiel: Song & Playlist + +Schauen wir uns an, wie eine Playlist mit Songs arbeiten kann: + +```python +class Song: + def __init__(self, titel, dauer_sekunden): + self.titel = titel + self.dauer_sekunden = dauer_sekunden + +class Playlist: + def __init__(self, name): + self.name = name + self.songs = [] # Hier landen die Song-Objekte + + def hinzufuegen(self, song): + self.songs.append(song) + + def finde_lange_songs(self, min_sekunden): + gefundene = [] + for song in self.songs: + if song.dauer_sekunden >= min_sekunden: + gefundene.append(song) + return gefundene +``` + +--- + +## ⏱️ Zeit-Formatierung: Aus Sekunden werden Minuten & Sekunden + +Um z.B. `215` Sekunden als `"03:35"` darzustellen, teilt man mit Ganzzahldivision `//` und Modulo `%`: +```python +sekunden_gesamt = 215 +minuten = sekunden_gesamt // 60 # 3 +sekunden = sekunden_gesamt % 60 # 35 + +# Mit :02d wird immer zweistellig mit führender Null formatiert: +print(f"{minuten:02d}:{sekunden:02d}") # "03:35" +``` + +--- + +## 🎯 Deine Aufgabe: Der Spotify-Playlist-Manager + +In `aufgabe.py` baust du deine eigene Musikverwaltung: +1. **Klasse `Song`**: + - Attribute: `titel`, `kuenstler`, `dauer_sekunden` + - Methode: `formatierte_dauer()` -> `"MM:SS"` + - `__str__()` -> `"{titel}" von {kuenstler} ({MM:SS})` +2. **Klasse `Playlist`**: + - `song_hinzufuegen(song)` + - `anzahl_songs()` + - `gesamtdauer_sekunden()` + - `finde_songs_von(kuenstler)` (Groß-/Kleinschreibung ignorieren!) + - `laengster_song()` (liefert das Song-Objekt mit der längsten Dauer) + +### Testen: +```bash +python3 test_aufgabe.py +``` diff --git a/05_objekte_kombinieren/aufgabe.py b/05_objekte_kombinieren/aufgabe.py new file mode 100644 index 0000000..c8436d9 --- /dev/null +++ b/05_objekte_kombinieren/aufgabe.py @@ -0,0 +1,123 @@ +""" +Kapitel 05: Komposition – Song & Playlist Manager +================================================= + +In dieser Aufgabe baust du eine Musikverwaltung mit zwei Klassen: +'Song' und 'Playlist'. +""" + +class Song: + # ========================================================================== + # TODO 1: Konstruktor für Song + # Parameter: self, titel (str), kuenstler (str), dauer_sekunden (int) + # Speichere: self.titel, self.kuenstler, self.dauer_sekunden + # ========================================================================== + def __init__(self, titel, kuenstler, dauer_sekunden): + # Schreibe hier deinen Code für TODO 1: + pass + + # ========================================================================== + # TODO 2: Methode "formatierte_dauer(self)" + # Rechnet 'self.dauer_sekunden' in Minuten und Sekunden um und gibt einen + # String im Format "MM:SS" zurück (z.B. 215 -> "03:35", 70 -> "01:10"). + # Tipp: Verwende // 60 und % 60 sowie f"{minuten:02d}:{sekunden:02d}" + # ========================================================================== + def formatierte_dauer(self): + # Schreibe hier deinen Code für TODO 2: + pass + + # ========================================================================== + # TODO 3: Dunder-Methode "__str__(self)" + # Gibt einen String im folgenden Format zurück: + # '"{titel}" von {kuenstler} ({dauer})' + # + # Beispiel: '"Flowers" von Miley Cyrus (03:20)' + # ========================================================================== + def __str__(self): + # Schreibe hier deinen Code für TODO 3: + pass + + +class Playlist: + # ========================================================================== + # TODO 4: Konstruktor für Playlist + # Parameter: self, name (str) + # Speichere: self.name, self.songs (als leere Liste []) + # ========================================================================== + def __init__(self, name): + # Schreibe hier deinen Code für TODO 4: + pass + + # ========================================================================== + # TODO 5: Methode "song_hinzufuegen(self, song)" + # Hängt das übergebene Song-Objekt an 'self.songs' an. + # ========================================================================== + def song_hinzufuegen(self, song): + # Schreibe hier deinen Code für TODO 5: + pass + + # ========================================================================== + # TODO 6: Methode "anzahl_songs(self)" + # Gibt die Anzahl der Songs in der Playlist als Integer zurück. + # ========================================================================== + def anzahl_songs(self): + # Schreibe hier deinen Code für TODO 6: + pass + + # ========================================================================== + # TODO 7: Methode "gesamtdauer_sekunden(self)" + # Gibt die Summe der Sekunden aller Songs in der Playlist zurück. + # Bei leerer Playlist: 0 + # ========================================================================== + def gesamtdauer_sekunden(self): + # Schreibe hier deinen Code für TODO 7: + pass + + # ========================================================================== + # TODO 8: Methode "finde_songs_von(self, kuenstler)" + # Sucht alle Songs in self.songs, bei denen der Künstler übereinstimmt. + # Wichtig: Groß- und Kleinschreibung soll ignoriert werden! (z.B. .lower()) + # Gibt eine Liste von passenden Song-Objekten zurück. + # ========================================================================== + def finde_songs_von(self, kuenstler): + # Schreibe hier deinen Code für TODO 8: + pass + + # ========================================================================== + # TODO 9: Methode "laengster_song(self)" + # Findet den Song mit der längsten Spieldauer in self.songs. + # - Gibt das gefundene Song-Objekt zurück. + # - Gibt None zurück, wenn die Playlist leer ist. + # ========================================================================== + def laengster_song(self): + # Schreibe hier deinen Code für TODO 9: + pass + + +# ============================================================================== +# Hauptprogramm zum Ausprobieren: +# ============================================================================== +if __name__ == "__main__": + print("🎧 --- MEINE PLAYLIST --- 🎧") + meine_hits = Playlist("Sommer Vibes") + + s1 = Song("Flowers", "Miley Cyrus", 200) + s2 = Song("Cruel Summer", "Taylor Swift", 178) + s3 = Song("Anti-Hero", "Taylor Swift", 201) + s4 = Song("As It Was", "Harry Styles", 167) + + meine_hits.song_hinzufuegen(s1) + meine_hits.song_hinzufuegen(s2) + meine_hits.song_hinzufuegen(s3) + meine_hits.song_hinzufuegen(s4) + + print(f"Playlist: {meine_hits.name} ({meine_hits.anzahl_songs()} Songs)") + print(f"Gesamtlaufzeit: {meine_hits.gesamtdauer_sekunden()} Sekunden") + + laengster = meine_hits.laengster_song() + if laengster: + print(f"Längster Song: {laengster}") + + print("\nSongs von Taylor Swift:") + for s in meine_hits.finde_songs_von("taylor swift"): + print(f"- {s}") diff --git a/05_objekte_kombinieren/musterloesung.py b/05_objekte_kombinieren/musterloesung.py new file mode 100644 index 0000000..69ff28c --- /dev/null +++ b/05_objekte_kombinieren/musterloesung.py @@ -0,0 +1,52 @@ +""" +Kapitel 05: Komposition – Song & Playlist Manager – Musterlösung +================================================================ +""" + +class Song: + def __init__(self, titel, kuenstler, dauer_sekunden): + self.titel = titel + self.kuenstler = kuenstler + self.dauer_sekunden = int(dauer_sekunden) + + def formatierte_dauer(self): + minuten = self.dauer_sekunden // 60 + sekunden = self.dauer_sekunden % 60 + return f"{minuten:02d}:{sekunden:02d}" + + def __str__(self): + return f'"{self.titel}" von {self.kuenstler} ({self.formatierte_dauer()})' + + +class Playlist: + def __init__(self, name): + self.name = name + self.songs = [] + + def song_hinzufuegen(self, song): + self.songs.append(song) + + def anzahl_songs(self): + return len(self.songs) + + def gesamtdauer_sekunden(self): + return sum(s.dauer_sekunden for s in self.songs) + + def finde_songs_von(self, kuenstler): + kuenstler_lower = kuenstler.lower() + return [s for s in self.songs if s.kuenstler.lower() == kuenstler_lower] + + def laengster_song(self): + if not self.songs: + return None + return max(self.songs, key=lambda s: s.dauer_sekunden) + + +if __name__ == "__main__": + meine_hits = Playlist("Sommer Vibes") + meine_hits.song_hinzufuegen(Song("Flowers", "Miley Cyrus", 200)) + meine_hits.song_hinzufuegen(Song("Cruel Summer", "Taylor Swift", 178)) + meine_hits.song_hinzufuegen(Song("Anti-Hero", "Taylor Swift", 201)) + + print(f"Playlist: {meine_hits.name} ({meine_hits.anzahl_songs()} Songs)") + print(f"Längster Song: {meine_hits.laengster_song()}") diff --git a/05_objekte_kombinieren/test_aufgabe.py b/05_objekte_kombinieren/test_aufgabe.py new file mode 100644 index 0000000..0b57f13 --- /dev/null +++ b/05_objekte_kombinieren/test_aufgabe.py @@ -0,0 +1,87 @@ +import unittest +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel05(unittest.TestCase): + + def test_01_song_init_und_dauer(self): + """Prüft Song Attribute und Formatierung der Dauer.""" + s = aufgabe.Song("Vampire", "Olivia Rodrigo", 219) + self.assertEqual(s.titel, "Vampire") + self.assertEqual(s.kuenstler, "Olivia Rodrigo") + self.assertEqual(s.dauer_sekunden, 219) + self.assertEqual(s.formatierte_dauer(), "03:39") + + s_kurz = aufgabe.Song("Intro", "Kuenstler", 5) + self.assertEqual(s_kurz.formatierte_dauer(), "00:05") + + def test_02_song_str(self): + """Prüft die __str__ Methode von Song.""" + s = aufgabe.Song("Espresso", "Sabrina Carpenter", 175) + erwartet = '"Espresso" von Sabrina Carpenter (02:55)' + self.assertEqual(str(s), erwartet) + + def test_03_playlist_hinzufuegen_und_dauer(self): + """Prüft Playlist Erstellung, Hinzufügen, Anzahl und Gesamtlaufzeit.""" + p = aufgabe.Playlist("Party") + self.assertEqual(p.anzahl_songs(), 0) + self.assertEqual(p.gesamtdauer_sekunden(), 0) + self.assertIsNone(p.laengster_song()) + + s1 = aufgabe.Song("Song A", "Artist 1", 100) + s2 = aufgabe.Song("Song B", "Artist 2", 150) + p.song_hinzufuegen(s1) + p.song_hinzufuegen(s2) + + self.assertEqual(p.anzahl_songs(), 2) + self.assertEqual(p.gesamtdauer_sekunden(), 250) + + def test_04_finde_songs_von(self): + """Prüft die Filtersuche nach Künstlern (auch case-insensitive).""" + p = aufgabe.Playlist("Mix") + s1 = aufgabe.Song("Anti-Hero", "Taylor Swift", 200) + s2 = aufgabe.Song("Cruel Summer", "Taylor Swift", 180) + s3 = aufgabe.Song("Flowers", "Miley Cyrus", 210) + + p.song_hinzufuegen(s1) + p.song_hinzufuegen(s2) + p.song_hinzufuegen(s3) + + # Exakte Schreibweise + ergebnis = p.finde_songs_von("Taylor Swift") + self.assertEqual(len(ergebnis), 2) + self.assertIn(s1, ergebnis) + self.assertIn(s2, ergebnis) + + # Kleinschreibung testen + ergebnis_lower = p.finde_songs_von("taylor swift") + self.assertEqual(len(ergebnis_lower), 2) + + # Nicht vorhandener Künstler + ergebnis_leer = p.finde_songs_von("Ed Sheeran") + self.assertEqual(len(ergebnis_leer), 0) + + def test_05_laengster_song(self): + """Prüft das Ermitteln des längsten Songs.""" + p = aufgabe.Playlist("Mix") + s1 = aufgabe.Song("Kurz", "A", 120) + s2 = aufgabe.Song("Lang", "B", 300) + s3 = aufgabe.Song("Mittel", "C", 200) + + p.song_hinzufuegen(s1) + p.song_hinzufuegen(s2) + p.song_hinzufuegen(s3) + + self.assertIs(p.laengster_song(), s2) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/06_abschlussprojekt_tamagotchi/README.md b/06_abschlussprojekt_tamagotchi/README.md new file mode 100644 index 0000000..91a5b58 --- /dev/null +++ b/06_abschlussprojekt_tamagotchi/README.md @@ -0,0 +1,48 @@ +# Kapitel 06: Abschlussprojekt – Dein eigenes Tamagotchi! 🥚🐣 + +Herzlichen Glückwunsch! Du hast alle grundlegenden Konzepte der objektorientierten Programmierung gemeistert: +- Klassen & Instanzen +- `__init__` & `self` +- Methoden & Zustandsveränderungen +- `__str__` für ansprechende Textausgaben +- Beziehungen zwischen Objekten + +Jetzt setzen wir alles in einem **vollwertigen, interaktiven Mini-Projekt** zusammen: Einem virtuellen Haustier (Tamagotchi)! + +--- + +## 🎮 Wie funktioniert das Tamagotchi? + +Dein Tamagotchi hat Eigenschaften, die sich durch deine Aktionen (und mit der Zeit) verändern: +- **Hunger** (0 = vollgefressen, 100 = verhungert) +- **Müdigkeit** (0 = voller Energie, 100 = total erschöpft) +- **Glück** (0 = unglücklich, 100 = überglücklich) +- **Alter in Tagen** + +Du kannst mit ihm interagieren: +- 🍎 **Füttern** (`fuettern`): Senkt den Hunger, macht etwas müde und etwas glücklicher. +- ⚽ **Spielen** (`spielen`): Steigert das Glück, macht aber hungrig und müde. Zu müde Haustiere weigern sich zu spielen! +- 💤 **Schlafen** (`schlafen`): Setzt die Müdigkeit auf 0, lässt einen Tag vergehen (`alter_tage += 1`), macht aber etwas hungrig. +- ⏳ **Zeit vergeht** (`zeit_vergeht`): Das Haustier wird hungriger, müder und etwas trauriger. Wenn der Hunger 100 erreicht, schläft es für immer ein... + +--- + +## 🕹️ Das interaktive Spiel + +Sobald du die `Tamagotchi`-Klasse in `aufgabe.py` fertig programmiert hast und alle Tests grün sind, kannst du das Spiel direkt im Terminal starten: + +```bash +python3 tamagotchi_spiel.py +``` +Dort kannst du deinem Haustier einen Namen geben und dich live um es kümmern! + +--- + +## 🎯 Deine Aufgabe + +Öffne `aufgabe.py` und implementiere die `Tamagotchi`-Klasse von TODO 1 bis TODO 6. + +### Tests ausführen: +```bash +python3 test_aufgabe.py +``` diff --git a/06_abschlussprojekt_tamagotchi/aufgabe.py b/06_abschlussprojekt_tamagotchi/aufgabe.py new file mode 100644 index 0000000..00d238a --- /dev/null +++ b/06_abschlussprojekt_tamagotchi/aufgabe.py @@ -0,0 +1,129 @@ +""" +Kapitel 06: Abschlussprojekt – Tamagotchi (Virtuelles Haustier) +============================================================== + +Programmiere hier die Klasse 'Tamagotchi'. +""" + +class Tamagotchi: + # ========================================================================== + # TODO 1: Konstruktor __init__(self, name, tierart="Drache") + # + # Initialisiere die folgenden Attribute: + # - self.name = name + # - self.tierart = tierart + # - self.hunger = 50 (0 = pappsatt, 100 = verhungert) + # - self.muedigkeit = 20 (0 = fit, 100 = erschöpft) + # - self.glueck = 80 (0 = traurig, 100 = super glücklich) + # - self.alter_tage = 0 + # - self.ist_lebendig = True + # ========================================================================== + def __init__(self, name, tierart="Drache"): + # Schreibe hier deinen Code für TODO 1: + pass + + # Hilfsmethode (optional, aber sehr nützlich): + # Hält einen Wert zwischen minimal 0 und maximal 100. + def _begrenze(self, wert): + return max(0, min(100, wert)) + + # ========================================================================== + # TODO 2: Methode "fuettern(self, menge=20)" + # + # Regeln: + # 1. Wenn not self.ist_lebendig: + # - Gib zurück: f"{self.name} reagiert nicht mehr..." + # 2. Wenn lebendig: + # - Verringere 'self.hunger' um 'menge' (darf nicht unter 0 fallen). + # - Erhöhe 'self.glueck' um 5 (darf nicht über 100 steigen). + # - Erhöhe 'self.muedigkeit' um 5 (darf nicht über 100 steigen). + # - Gib zurück: f"{self.name} mampft genüsslich! (Hunger: {self.hunger}/100)" + # ========================================================================== + def fuettern(self, menge=20): + # Schreibe hier deinen Code für TODO 2: + pass + + # ========================================================================== + # TODO 3: Methode "spielen(self, spass=25)" + # + # Regeln: + # 1. Wenn not self.ist_lebendig: + # - Gib zurück: f"{self.name} reagiert nicht mehr..." + # 2. Wenn 'self.muedigkeit' > 80 ist: + # - Zu müde zum Spielen! Ändere keine Werte und gib zurück: + # f"{self.name} ist zu müde zum Spielen und gähnt nur..." + # 3. Wenn fit genug: + # - Erhöhe 'self.glueck' um 'spass' (max 100). + # - Erhöhe 'self.hunger' um 15 (max 100). + # - Erhöhe 'self.muedigkeit' um 20 (max 100). + # - Gib zurück: f"{self.name} hatte riesigen Spaß beim Spielen! (Glück: {self.glueck}/100)" + # ========================================================================== + def spielen(self, spass=25): + # Schreibe hier deinen Code für TODO 3: + pass + + # ========================================================================== + # TODO 4: Methode "schlafen(self)" + # + # Regeln: + # 1. Wenn not self.ist_lebendig: + # - Gib zurück: f"{self.name} reagiert nicht mehr..." + # 2. Wenn lebendig: + # - Setze 'self.muedigkeit' auf 0. + # - Erhöhe 'self.hunger' um 15 (max 100). + # - Erhöhe 'self.alter_tage' um 1. + # - Gib zurück: f"{self.name} hat tief geschlafen und ist ausgeruht! Ein neuer Tag bricht an." + # ========================================================================== + def schlafen(self): + # Schreibe hier deinen Code für TODO 4: + pass + + # ========================================================================== + # TODO 5: Methode "zeit_vergeht(self)" + # Simuliert das Verstreichen von Zeit. + # + # Regeln: + # 1. Wenn not self.ist_lebendig: + # - Mach nichts, gib False zurück. + # 2. Wenn lebendig: + # - Erhöhe 'self.hunger' um 10 (max 100). + # - Erhöhe 'self.muedigkeit' um 10 (max 100). + # - Verringere 'self.glueck' um 10 (min 0). + # - Prüfe, ob das Haustier stirbt: + # Wenn self.hunger >= 100 oder self.glueck <= 0: + # self.ist_lebendig = False + # - Gib self.ist_lebendig zurück (True wenn noch am Leben, False wenn gestorben). + # ========================================================================== + def zeit_vergeht(self): + # Schreibe hier deinen Code für TODO 5: + pass + + # ========================================================================== + # TODO 6: Dunder-Methode "__str__(self)" + # Gibt eine hübsche Statusübersicht als mehrzeiligen String zurück. + # + # Format (wenn lebendig): + # "=== {self.name} ({self.tierart}, {self.alter_tage} Tage alt) ===\n" + # "Hunger: {self.hunger}/100\n" + # "Müdigkeit: {self.muedigkeit}/100\n" + # "Glück: {self.glueck}/100" + # + # Format (wenn tot / not self.ist_lebendig): + # "=== {self.name} ({self.tierart}) - RIP 🪦 ===" + # ========================================================================== + def __str__(self): + # Schreibe hier deinen Code für TODO 6: + pass + + +# ============================================================================== +# Schneller Testlauf: +# ============================================================================== +if __name__ == "__main__": + pet = Tamagotchi("Yoshi", "Dino") + print(pet) + print() + print(pet.fuettern()) + print(pet.spielen()) + print(pet.schlafen()) + print(pet) diff --git a/06_abschlussprojekt_tamagotchi/musterloesung.py b/06_abschlussprojekt_tamagotchi/musterloesung.py new file mode 100644 index 0000000..632c2da --- /dev/null +++ b/06_abschlussprojekt_tamagotchi/musterloesung.py @@ -0,0 +1,81 @@ +""" +Kapitel 06: Abschlussprojekt – Tamagotchi – Musterlösung +======================================================== +""" + +class Tamagotchi: + def __init__(self, name, tierart="Drache"): + self.name = name + self.tierart = tierart + self.hunger = 50 + self.muedigkeit = 20 + self.glueck = 80 + self.alter_tage = 0 + self.ist_lebendig = True + + def _begrenze(self, wert): + return max(0, min(100, wert)) + + def fuettern(self, menge=20): + if not self.ist_lebendig: + return f"{self.name} reagiert nicht mehr..." + + self.hunger = self._begrenze(self.hunger - menge) + self.glueck = self._begrenze(self.glueck + 5) + self.muedigkeit = self._begrenze(self.muedigkeit + 5) + return f"{self.name} mampft genüsslich! (Hunger: {self.hunger}/100)" + + def spielen(self, spass=25): + if not self.ist_lebendig: + return f"{self.name} reagiert nicht mehr..." + + if self.muedigkeit > 80: + return f"{self.name} ist zu müde zum Spielen und gähnt nur..." + + self.glueck = self._begrenze(self.glueck + spass) + self.hunger = self._begrenze(self.hunger + 15) + self.muedigkeit = self._begrenze(self.muedigkeit + 20) + return f"{self.name} hatte riesigen Spaß beim Spielen! (Glück: {self.glueck}/100)" + + def schlafen(self): + if not self.ist_lebendig: + return f"{self.name} reagiert nicht mehr..." + + self.muedigkeit = 0 + self.hunger = self._begrenze(self.hunger + 15) + self.alter_tage += 1 + return f"{self.name} hat tief geschlafen und ist ausgeruht! Ein neuer Tag bricht an." + + def zeit_vergeht(self): + if not self.ist_lebendig: + return False + + self.hunger = self._begrenze(self.hunger + 10) + self.muedigkeit = self._begrenze(self.muedigkeit + 10) + self.glueck = self._begrenze(self.glueck - 10) + + if self.hunger >= 100 or self.glueck <= 0: + self.ist_lebendig = False + return False + return True + + def __str__(self): + if not self.ist_lebendig: + return f"=== {self.name} ({self.tierart}) - RIP 🪦 ===" + + zeilen = [ + f"=== {self.name} ({self.tierart}, {self.alter_tage} Tage alt) ===", + f"Hunger: {self.hunger}/100", + f"Müdigkeit: {self.muedigkeit}/100", + f"Glück: {self.glueck}/100" + ] + return "\n".join(zeilen) + + +if __name__ == "__main__": + pet = Tamagotchi("Yoshi", "Dino") + print(pet) + print(pet.fuettern()) + print(pet.spielen()) + print(pet.schlafen()) + print(pet) diff --git a/06_abschlussprojekt_tamagotchi/tamagotchi_spiel.py b/06_abschlussprojekt_tamagotchi/tamagotchi_spiel.py new file mode 100644 index 0000000..5b5618c --- /dev/null +++ b/06_abschlussprojekt_tamagotchi/tamagotchi_spiel.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +""" +🎮 TAMAGOTCHI TERMINAL SPIEL 🎮 +=============================== +Führe dieses Skript aus, um dein Tamagotchi live im Terminal zu spielen: + python3 tamagotchi_spiel.py +""" + +import sys +import time + +try: + from aufgabe import Tamagotchi +except ImportError: + from musterloesung import Tamagotchi + + +def zeichne_balken(wert, max_wert=100, laenge=15, umgekehrt=False): + """Erzeugt einen visuellen Ladebalken.""" + gefuellt = int((wert / max_wert) * laenge) + ungefuellt = laenge - gefuellt + + if umgekehrt: + # Bei Hunger/Müdigkeit ist weniger besser (grün -> rot) + farbe = "🟢" if wert < 40 else ("🟡" if wert < 75 else "🔴") + else: + # Bei Glück ist mehr besser + farbe = "🟢" if wert > 60 else ("🟡" if wert > 30 else "🔴") + + balken = "█" * gefuellt + "░" * ungefuellt + return f"[{balken}] {wert:3d}% {farbe}" + + +def zeige_tamagotchi_dashboard(pet): + print("\n" + "=" * 45) + print(f" 🥚 VIRTUAL PET: {pet.name.upper()} 🥚") + print(f" Tierart: {pet.tierart} | Alter: {pet.alter_tage} Tag(e)") + print("=" * 45) + + if not pet.ist_lebendig: + print("\n 🪦 R. I. P. 🪦") + print(f" {pet.name} ist leider verstorben...") + print("=" * 45 + "\n") + return + + print(f" 🍖 Hunger: {zeichne_balken(pet.hunger, umgekehrt=True)}") + print(f" 💤 Müdigkeit: {zeichne_balken(pet.muedigkeit, umgekehrt=True)}") + print(f" 💖 Glück: {zeichne_balken(pet.glueck)}") + print("=" * 45) + + +def main(): + print("\n" + "*" * 50) + print(" WILLKOMMEN BEIM TAMAGOTCHI SIMULATOR!") + print("*" * 50) + + name = input("\nWie soll dein Haustier heißen? (z.B. Yoshi): ").strip() + if not name: + name = "Yoshi" + + tierart = input(f"Welche Tierart ist {name}? (z.B. Drache / Katze / Pinguin): ").strip() + if not tierart: + tierart = "Drache" + + pet = Tamagotchi(name, tierart) + print(f"\n🎉 Herzlichen Glückwunsch! {pet.name} das {pet.tierart}-Baby ist geschlüpft!\n") + + while pet.ist_lebendig: + zeige_tamagotchi_dashboard(pet) + print("\nWas möchtest du tun?") + print(" [1] 🍎 Füttern") + print(" [2] ⚽ Spielen") + print(" [3] 💤 Schlafen legen") + print(" [4] ⏳ Nichts tun (Zeit vergeht)") + print(" [q] 🚪 Spiel beenden") + + wahl = input("\nDeine Wahl (1/2/3/4/q): ").strip().lower() + + nachricht = "" + if wahl == "1": + nachricht = pet.fuettern() + elif wahl == "2": + nachricht = pet.spielen() + elif wahl == "3": + nachricht = pet.schlafen() + elif wahl == "4": + nachricht = f"Du schaust {pet.name} eine Weile beim Dösen zu..." + elif wahl == "q": + print(f"\nTschüss! Pass gut auf {pet.name} auf!\n") + break + else: + print("❌ Ungültige Eingabe, bitte wähle 1, 2, 3, 4 oder q.") + continue + + # Wenn eine Aktion ausgeführt wurde, vergeht auch etwas Zeit: + if wahl in ("1", "2", "4"): + pet.zeit_vergeht() + + print("\n👉 " + nachricht) + time.sleep(0.5) + + if not pet.ist_lebendig: + zeige_tamagotchi_dashboard(pet) + print("Game Over! Versuche es beim nächsten Mal besser zu versorgen.") + + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + print("\n\nSpiel abgebrochen. Auf Wiedersehen!") diff --git a/06_abschlussprojekt_tamagotchi/test_aufgabe.py b/06_abschlussprojekt_tamagotchi/test_aufgabe.py new file mode 100644 index 0000000..f838849 --- /dev/null +++ b/06_abschlussprojekt_tamagotchi/test_aufgabe.py @@ -0,0 +1,111 @@ +import unittest +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent)) + +try: + import aufgabe +except ImportError as e: + raise ImportError(f"Konnte 'aufgabe.py' nicht importieren: {e}") + + +class TestKapitel06(unittest.TestCase): + + def test_01_init(self): + """Prüft die Standard-Startwerte eines neuen Tamagotchis.""" + t = aufgabe.Tamagotchi("Pikachu", "Maus") + self.assertEqual(t.name, "Pikachu") + self.assertEqual(t.tierart, "Maus") + self.assertEqual(t.hunger, 50) + self.assertEqual(t.muedigkeit, 20) + self.assertEqual(t.glueck, 80) + self.assertEqual(t.alter_tage, 0) + self.assertTrue(t.ist_lebendig) + + def test_02_fuettern(self): + """Prüft die Auswirkungen des Fütterns.""" + t = aufgabe.Tamagotchi("Yoshi") + t.hunger = 50 + t.muedigkeit = 20 + t.glueck = 80 + + text = t.fuettern(20) + self.assertEqual(t.hunger, 30, "Hunger sollte um 20 sinken!") + self.assertEqual(t.glueck, 85, "Glück sollte um 5 steigen!") + self.assertEqual(t.muedigkeit, 25, "Müdigkeit sollte um 5 steigen!") + self.assertIn("Yoshi", text) + + # Hunger darf nicht negativ werden + t.fuettern(100) + self.assertEqual(t.hunger, 0, "Hunger darf nicht unter 0 fallen!") + + def test_03_spielen(self): + """Prüft die Auswirkungen des Spielens und die Müdigkeitsgrenze.""" + t = aufgabe.Tamagotchi("Sparky") + t.hunger = 30 + t.muedigkeit = 20 + t.glueck = 50 + + text = t.spielen(25) + self.assertEqual(t.glueck, 75) + self.assertEqual(t.hunger, 45) + self.assertEqual(t.muedigkeit, 40) + self.assertIn("Sparky", text) + + # Zu müde zum Spielen (muedigkeit > 80) + t.muedigkeit = 85 + t.glueck = 50 + text_muede = t.spielen(20) + self.assertIn("zu müde", text_muede.lower()) + self.assertEqual(t.glueck, 50, "Werte dürfen sich bei Müdigkeit nicht ändern!") + + def test_04_schlafen(self): + """Prüft Schlafen und Altern.""" + t = aufgabe.Tamagotchi("Luna") + t.muedigkeit = 70 + t.hunger = 20 + t.alter_tage = 2 + + t.schlafen() + self.assertEqual(t.muedigkeit, 0, "Müdigkeit sollte auf 0 zurückgesetzt werden!") + self.assertEqual(t.hunger, 35, "Hunger sollte um 15 steigen!") + self.assertEqual(t.alter_tage, 3, "Alter in Tagen sollte um 1 steigen!") + + def test_05_zeit_vergeht_und_lebenszyklus(self): + """Prüft Verstreichen der Zeit und Ableben bei 100 Hunger oder 0 Glück.""" + t = aufgabe.Tamagotchi("Rex") + t.hunger = 80 + t.muedigkeit = 30 + t.glueck = 20 + + lebendig = t.zeit_vergeht() + self.assertTrue(lebendig) + self.assertEqual(t.hunger, 90) + self.assertEqual(t.muedigkeit, 40) + self.assertEqual(t.glueck, 10) + + # Noch ein Zeitschritt -> Hunger erreicht 100 + lebendig2 = t.zeit_vergeht() + self.assertFalse(lebendig2) + self.assertFalse(t.ist_lebendig) + + # Aktionen an totem Tamagotchi + reaktion = t.fuettern() + self.assertIn("reagiert nicht mehr", reaktion) + + def test_06_str_darstellung(self): + """Prüft die __str__ Repräsentation.""" + t = aufgabe.Tamagotchi("Bobo", "Affe") + str_lebendig = str(t) + self.assertIn("Bobo", str_lebendig) + self.assertIn("Affe", str_lebendig) + self.assertIn("50/100", str_lebendig) + + t.ist_lebendig = False + str_tot = str(t) + self.assertIn("RIP", str_tot) + + +if __name__ == "__main__": + unittest.main(verbosity=2) diff --git a/README.md b/README.md new file mode 100644 index 0000000..8eb160d --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# 🐍 Python OOP Übungsreihe: Der Einstieg in Klassen & Objekte + +Willkommen zu deiner Übungsreihe für **Objektorientierte Programmierung (OOP)** in Python! 🎉 + +Hier lernst du Schritt für Schritt, wie man mit Klassen arbeitet, warum sie die Programmierung übersichtlicher machen und wie du deine eigenen, mächtigen Objekte baust. + +--- + +## 🗺️ Der Lernpfad + +Die Übungen sind so aufgebaut, dass jedes Kapitel auf dem vorherigen aufbaut: + +| Kapitel | Thema | Was du lernst | +| :--- | :--- | :--- | +| **[01. Einstieg in Klassen](./01_einstieg_klassen/)** | Warum Klassen? | Bauplan vs. Objekt, Instanziierung & Attribute | +| **[02. Konstruktor & self](./02_init_und_self/)** | `__init__` & `self` | Objekte sauber mit Startwerten initialisieren | +| **[03. Methoden & Verhalten](./03_methoden_und_verhalten/)** | Methoden & Interaktion | Objekte können handeln & miteinander interagieren | +| **[04. Schöne Textausgabe](./04_str_und_darstellung/)** | `__str__` Dunder-Methode | Wie Objekte lesbar mit `print()` ausgegeben werden | +| **[05. Objekte kombinieren](./05_objekte_kombinieren/)** | Komposition | Listen von Objekten & komplexe Datenstrukturen | +| **[06. Abschlussprojekt](./06_abschlussprojekt_tamagotchi/)** | Mini-Projekt: Tamagotchi | Ein interaktives Konsolenspiel mit allem Gelernten | + +--- + +## 📁 Aufbau eines Kapitels + +In jedem Kapitelordner findest du: +1. **`README.md`**: Eine verständliche Erklärung mit Analogien aus dem Alltag und Codebeispielen. +2. **`aufgabe.py`**: Dein Arbeitsblatt! Hier steht der Starter-Code mit klaren `TODO`-Kommentaren. +3. **`test_aufgabe.py`**: Automatische Tests, mit denen du deine Lösung jederzeit überprüfen kannst. +4. **`musterloesung.py`**: Eine ausführlich kommentierte Beispiellösung (falls du mal nicht weiterkommst oder vergleichen möchtest). + +--- + +## 🚀 Wie du vorgehst + +1. Öffne den Ordner des aktuellen Kapitels (z.B. `01_einstieg_klassen`). +2. Lies dir die Erklärung in der jeweiligen `README.md` durch. +3. Öffne `aufgabe.py` und bearbeite die nummerierten `TODO`s. +4. Führe die Tests aus, um deine Lösung zu prüfen: + ```bash + # Aus dem Hauptverzeichnis: + python3 -m unittest 01_einstieg_klassen/test_aufgabe.py + + # Oder direkt im Ordner: + cd 01_einstieg_klassen + python3 test_aufgabe.py + ``` +5. Wenn alle Tests mit einem grünen `OK` durchlaufen: **Herzlichen Glückwunsch!** Du bist bereit für das nächste Kapitel! 🏆 + +--- + +## 💡 Tipp zum Lernen +Versuche zuerst selbst eine Lösung zu finden. Wenn ein Fehler auftritt, lies die Fehlermeldung der Tests aufmerksam durch – sie gibt dir oft schon den entscheidenden Hinweis! Wenn du gar nicht weiterkommst, wirf einen Blick in `musterloesung.py` und versuche zu verstehen, wie das Problem gelöst wurde. diff --git a/test_all.py b/test_all.py new file mode 100644 index 0000000..fcbb3c8 --- /dev/null +++ b/test_all.py @@ -0,0 +1,73 @@ +#!/usr/bin/env python3 +""" +🧪 Gesamttest-Runner für alle Kapitel +=================================== +Führe dieses Skript aus, um zu sehen, welche Kapitel du bereits erfolgreich gelöst hast: + python3 test_all.py +""" + +import sys +import unittest +from pathlib import Path + +KAPITEL = [ + ("Kapitel 01: Einstieg in Klassen", "01_einstieg_klassen"), + ("Kapitel 02: Konstruktor & self", "02_init_und_self"), + ("Kapitel 03: Methoden & Verhalten", "03_methoden_und_verhalten"), + ("Kapitel 04: __str__ & Darstellung", "04_str_und_darstellung"), + ("Kapitel 05: Objekte kombinieren", "05_objekte_kombinieren"), + ("Kapitel 06: Abschlussprojekt Tamagotchi", "06_abschlussprojekt_tamagotchi"), +] + +def main(): + root = Path(__file__).parent.resolve() + print("=" * 60) + print("🎓 PYTHON OOP ÜBUNGSREIHE – GESAMTFORTSCHRITT") + print("=" * 60) + + gesamt_bestanden = 0 + + for titel, ordner in KAPITEL: + kapitel_pfad = root / ordner + if not kapitel_pfad.exists(): + continue + + sys.path.insert(0, str(kapitel_pfad)) + + # Testsuite laden + loader = unittest.TestLoader() + suite = loader.discover(str(kapitel_pfad), pattern="test_aufgabe.py") + + # Leisen Runner ausführen + runner = unittest.TextTestRunner(stream=open("/dev/null", "w"), verbosity=0) + result = runner.run(suite) + + # Modul aus sys.path und sys.modules entfernen für saubere Isolation + if str(kapitel_pfad) in sys.path: + sys.path.remove(str(kapitel_pfad)) + if "aufgabe" in sys.modules: + del sys.modules["aufgabe"] + if "test_aufgabe" in sys.modules: + del sys.modules["test_aufgabe"] + + anzahl_tests = result.testsRun + erfolgreich = result.wasSuccessful() and anzahl_tests > 0 + + if erfolgreich: + print(f" ✅ {titel:42s} ({anzahl_tests}/{anzahl_tests} Tests OK)") + gesamt_bestanden += 1 + else: + fehlgeschlagen = len(result.failures) + len(result.errors) + print(f" ⏳ {titel:42s} (Noch offen / {fehlgeschlagen} Fehler)") + + print("=" * 60) + print(f"Ergebnis: {gesamt_bestanden} von {len(KAPITEL)} Kapiteln vollständig gelöst!") + if gesamt_bestanden == len(KAPITEL): + print("🏆 PERFEKT! Du hast alle OOP-Aufgaben mit Bravour gemeistert! 🎉") + else: + print("💡 Öffne das nächste offene Kapitel und starte mit aufgabe.py!") + print("=" * 60) + + +if __name__ == "__main__": + main()