# Schiffe versenken mit Python und Pygame

Martin Schmid mit Claude·
2026-08-13

**Voraussetzungen:** Python-Grundlagen (Variablen, Schleifen, Listen, einfache Funktionen) **Technisch:** Python 3.12 und pygame, eingerichtet nach dem Dokument „Python, uv und einen Editor einrichten“ — dort ist auch erklärt, wo man welchen Befehl eintippt **Dauer:** ein Arbeitstag (ca. 6–7 h netto) **Ziel am Abend:** ein spielbares, grafisches Schiffe-versenken mit Gegner-KI – und das Verständnis, *warum* man Objekte benutzt

## Fahrplan

| Zeit | Block | Was passiert | Ergebnis |
|---|---|---|---|
| **vorab** | **Systemeinrichtung** | **uv, Python 3.12, Editor — separates Dokument** | **läuft, bevor der Tag beginnt** |
| 09:00–09:20 | Meilenstein 0 | Projektordner, Fenster geht auf | „Es lebt!“ |
| 09:20–10:00 | Kapitel A + B | Typen-Auffrischung, Objekt-Aha | Verständnis |
| 10:00–10:45 | Meilenstein 1 | Klasse `Schiff` | erste eigene Klasse |
| 10:45–11:45 | Meilenstein 2 | Klasse `Spielfeld` | **im Terminal spielbar** |
| 11:45–12:30 | Mittag |  |  |
| 12:30–13:15 | Meilenstein 3 | Pygame-Fenster + Game Loop | Fenster mit Raster |
| 13:15–14:00 | Meilenstein 4 | Koordinaten-Mathematik | Klick → Feld |
| 14:00–15:00 | Meilenstein 5 | Schüsse, Treffer, Grafik | **grafisch spielbar** |
| 15:00–15:45 | Meilenstein 6 | Schiffe platzieren | vollständige Runde |
| 15:45–16:45 | Meilenstein 7 | Gegner-KI in 3 Stufen | der Wow-Moment |
| 16:45–17:00 | Meilenstein 8 | Politur, Demo | Vorführung |

**Hinweis zur Kursleitung:** Nach jedem Meilenstein steht ein **Code-Review-Checkpoint** (unten jeweils markiert). Nicht selbst tippen, nicht die Lösung verraten – nur Fragen stellen. Die Fragen stehen dabei.

**Die drei Hausregeln**, die den ganzen Tag an der Wand kleben:

> Keine globalen Variablen. Zustand lebt in Objekten. Die Spiellogik kennt Pygame nicht. Kein pygame. in schiff.py oder spielfeld.py. Eine Methode macht eine Sache. Passt sie nicht auf den Bildschirm, ist sie zu lang.

Regel 2 ist die wichtigste – sie ist das direkte Gegengift gegen Spaghetticode und wird in Meilenstein 2 belohnt: Das Spiel ist im Terminal *fertig spielbar*, bevor die erste Grafik existiert.

## So liest du die Code-Blöcke

Über jedem Block steht, **wo** er hingehört. Das ist keine Deko — die drei Orte sehen ähnlich aus und verhalten sich völlig verschieden:

| Markierung | Ort | Prompt | Wofür |
|---|---|---|---|
| 🖥️ **Terminal** | PowerShell / Terminal | `PS C:\…>` bzw. `… %` | starten, installieren |
| 🐍 **Python-Konsole** | gestartet mit `uv run python` | `>>>` | ausprobieren, testen |
| 📄 **Datei** | Editor, dann `uv run datei.py` | keiner | das eigentliche Programm |

Steht bei einem Block `>>>` davor, ist das Zeichen schon da — du tippst es nicht mit.

## Meilenstein 0 — Setup (20 min)

**Ziel:** Ein schwarzes Fenster geht auf und lässt sich schließen.

> 📘 Voraussetzung: Die Einrichtung aus dem Dokument „Python, uv und einen Editor einrichten“ ist einmal durchgelaufen — uv installiert, Python 3.12 vorhanden, Editor eingerichtet. Falls nicht: erst dort das Kapitel für deinen Rechner abarbeiten — Windows, macOS oder Linux — das dauert 20 Minuten, danach das Kapitel „Der Editor: VSCodium“. Das Kapitel „Wo tippe ich das eigentlich ein?“ solltest du gelesen haben, auch wenn der Rechner schon fertig eingerichtet ist — sonst kosten dich die ersten Fehlermeldungen heute unnötig Zeit. Wer noch nie mit einem Terminal gearbeitet hat, nimmt zusätzlich das Kapitel „Die Shell: das Minimum, das trägt“ mit.

**Warum Python 3.12 und nicht die neueste Version?** Weil es für Python 3.14 und neuer kein fertiges pygame-Paket gibt. Die Installation versucht dann, pygame selbst zu übersetzen, und scheitert. Deshalb steht in den Befehlen unten überall `--python 3.12`.

🖥️ **Terminal** (PowerShell unter Windows, Terminal unter Mac)

```
mkdir schiffe
cd schiffe
uv venv --python 3.12
uv pip install pygame
```

**Kontrolle, bevor es weitergeht:**

🖥️ **Terminal**

```
uv run python -c "import pygame; print(pygame.version.ver)"
```

Erscheint eine Versionsnummer, ist alles bereit. Erscheint eine Fehlermeldung, lohnt der Blick ins Kapitel „Erste Hilfe“ der Systemeinrichtung — dort stehen die vier häufigsten mit Lösung.

📄 **Datei** `test.py` — im Editor anlegen, im Ordner `schiffe` speichern:

```
import pygame

pygame.init()
fenster = pygame.display.set_mode((800, 600))
pygame.display.set_caption("Schiffe versenken")

laeuft = True
while laeuft:
    for ereignis in pygame.event.get():
        if ereignis.type == pygame.QUIT:
            laeuft = False
    fenster.fill((10, 20, 40))
    pygame.display.flip()

pygame.quit()
```

**Starten** — Datei speichern (`Strg`+`S`), dann:

🖥️ **Terminal**

```
uv run test.py
```

Nicht `python test.py` — sonst läuft ein anderes Python, in dem pygame fehlt, und du bekommst `ModuleNotFoundError`. Das Fenster schließt du übers Kreuz oder mit `Strg`+`C` im Terminal.

**Aufgabe:** Ändere die Fenstergröße und die Hintergrundfarbe. Finde heraus, was `(10, 20, 40)` bedeutet.

> 💡 Farben sind Vektoren. RGB ist ein 3-Tupel im Wertebereich 0–255. (255, 0, 0) ist knallrot. Du kannst später zwischen zwei Farben interpolieren – genau wie eine lineare Interpolation in Mathe: farbe = a + t * (b - a) mit t zwischen 0 und 1. Damit machst du einen Treffer-Blitz, der sanft ausblendet.

## Kapitel A — Typen, kurz und schmerzhaft ehrlich (20 min)

Kein Frontalunterricht: Dieses Kapitel wird **getippt**, nicht gelesen.

Dafür brauchst du die **Python-Konsole** — den Ort, an dem du eine Zeile Python eingibst und sofort die Antwort bekommst, ohne etwas zu speichern. Sie startet aus dem Terminal heraus:

🖥️ **Terminal**

```
uv run python
```

Der Prompt wechselt daraufhin auf `>>>`. **Das ist das Zeichen, dass du jetzt in Python bist** und nicht mehr im Terminal:

```
Python 3.12.11 (main, …)
Type "help", "copyright", "credits" or "license" for more information.
>>>
```

Alles, was in diesem Kapitel mit 🐍 markiert ist, gehört hinter dieses `>>>`. Das Zeichen selbst tippst du nicht mit — es steht schon da. Die Antwort erscheint direkt darunter, ganz ohne `print()`:

🐍 **Python-Konsole**

```
>>> 2 + 3
5
>>> len("Kreuzer")
7
```

**Wieder heraus** kommst du mit `exit()`. Und wenn eine Fehlermeldung sagt, dein Befehl sei kein Cmdlet (Windows) oder `zsh: number expected` (Mac), dann hast du Python-Code ins Terminal getippt — Prompt anschauen, `uv run python`, nochmal.

> 💡 Diese Konsole ist kein Anfängerwerkzeug, das man später ablegt. Auch nach dreißig Jahren ist sie der schnellste Weg, eine Frage wie „was gibt sorted() bei einem Set zurück?“ in vier Sekunden zu beantworten, statt drei Minuten zu suchen.

### Die Typen, die du heute brauchst

| Typ | Beispiel | Änderbar? | Wofür heute |
|---|---|---|---|
| `int` | `7` | – | Zeilen, Spalten, Pixel |
| `float` | `3.5` | – | Animationen, Zeit |
| `bool` | `True` | – | „getroffen?“ |
| `str` | `"Kreuzer"` | nein | Namen, Anzeigen |
| `tuple` | `(3, 5)` | **nein** | eine Koordinate |
| `list` | `[(3,5), (3,6)]` | **ja** | Liste von Koordinaten |
| `dict` | `{"C4": "Treffer"}` | ja | Nachschlagetabellen |
| `set` | `{(3,5), (1,2)}` | ja | „schon beschossen?“ |
| `None` | `None` | – | „hier ist nichts“ |

### Die drei Erkenntnisse, die wirklich zählen

**1. Tupel vs. Liste ist keine Geschmacksfrage.**

🐍 **Python-Konsole**

```
position = (3, 5)        # eine Koordinate: ein Ding, unveränderlich
treffer  = [(3, 5)]      # eine Sammlung: wächst
```

Eine Koordinate ist wie ein Punkt im ℝ² – der Punkt (3,5) *ist* (3,5), man ändert ihn nicht, man nimmt einen anderen. Deshalb Tupel. Bonus: nur Tupel dürfen in ein `set` oder als `dict`-Schlüssel.

**2. set ist eine Superkraft.**

🐍 **Python-Konsole**

```
beschossen = set()
beschossen.add((3, 5))
(3, 5) in beschossen     # True — und das in O(1), egal wie groß die Menge ist
```

Bei einer Liste muss Python jedes Element durchgehen. Beim Set rechnet es die Position direkt aus (Hashing). Für „hab ich hier schon geschossen?“ ist das der richtige Typ.

**3. Typannotationen sind Definitionsbereiche.**

🐍 **Python-Konsole**

```
def entfernung(a: tuple[int, int], b: tuple[int, int]) -> float:
    ...
```

Das ist nichts anderes als `f: ℤ² × ℤ² → ℝ` aus der Mathe. Python erzwingt es nicht, aber dein Editor warnt dich und dein zukünftiges Ich versteht den Code noch. **Ab heute schreibst du sie überall hin.**

### Mini-Übungen (5 min)

🐍 **Python-Konsole** — Zeile für Zeile eintippen und jeweils die Antwort ansehen:

```
# 1. Was passiert hier? Erkläre es.
a = [1, 2, 3]
b = a
b.append(4)
print(a)

# 2. Und warum hier nicht?
x = (1, 2, 3)
y = x
# y.append(4)   ← probier's aus, lies die Fehlermeldung

# 3. Baue ein set mit allen 100 Feldern eines 10x10-Bretts – in EINER Zeile.
```

Tipp zu 3

Comprehension mit zwei `for`: `{(z, s) for z in range(10) for s in range(10)}`

## Kapitel B — Der Aha-Moment: Warum Objekte? (20 min)

**Erst der Schmerz.** So sieht Schiffe versenken sequenziell aus:

```
feld = [[0]*10 for _ in range(10)]
schiff1_zeilen = [2, 3, 4]
schiff1_spalte = 5
schiff1_treffer = 0
schiff2_zeilen = [7]
schiff2_spalte = 1
schiff2_treffer = 0
# ... und jetzt kommt Schiff 3, 4, 5 ...
```

**Frage:** „Wir bekommen jetzt 5 Schiffe pro Spieler und 2 Spieler. Wie viele Variablen sind das? Und was passiert, wenn du die Schiffslänge nachträglich änderst?“

Spätestens beim Durchzählen wird klar, dass das nicht skaliert. Dann kommt die Auflösung:

> Ein Objekt ist ein Substantiv, eine Methode ist ein Verb. Schreib dein Spiel als Satz auf: „Das Spielfeld hat Schiffe. Ein Schiff wird getroffen und ist irgendwann versenkt.“ Substantive → Klassen. Verben → Methoden. Fertig ist der Entwurf.

### Die Klassen-Mechanik in 10 Zeilen

🐍 **Python-Konsole**

```
class Katze:
    def __init__(self, name: str):   # Konstruktor: läuft beim Erzeugen
        self.name = name             # Attribut: gehört DIESEM Objekt
        self.satt = False

    def fressen(self) -> None:       # Methode: Verb
        self.satt = True
        print(f"{self.name} ist satt.")

mia = Katze("Mia")     # Instanz erzeugen
tom = Katze("Tom")     # zweite Instanz — eigene Attribute!
mia.fressen()
print(tom.satt)        # False — Tom hat nichts abbekommen
```

**Was ist self?** Die Klasse ist der Bauplan, das Objekt das gebaute Haus. `self` ist „dieses konkrete Haus hier“. Python schiebt es bei jedem Methodenaufruf automatisch als erstes Argument rein – `mia.fressen()` wird intern zu `Katze.fressen(mia)`. Deshalb steht es in der Definition, aber nicht im Aufruf.

**Die häufigste Anfängerfalle:**

```
def fressen(self):
    satt = True          # ❌ lokale Variable, verschwindet sofort
    self.satt = True     # ✅ Attribut des Objekts, bleibt
```

**Kein self = kein Gedächtnis.** Das ist der ganze Unterschied.

## Meilenstein 1 — Die Klasse `Schiff` (45 min)

**Ziel:** Ein Schiff weiß, wo es liegt, ob es getroffen wurde und ob es versenkt ist. Ganz ohne Grafik.

Datei `schiff.py` – das ist das Gerüst, die Logik schreibst du:

📄 **Datei**

```
class Schiff:
    """Ein Schiff auf dem Spielfeld.

    Attribute:
        name:      z.B. "Zerstörer"
        felder:    Liste der belegten Koordinaten, z.B. [(2,5), (3,5), (4,5)]
        treffer:   Menge der bereits getroffenen Koordinaten
    """

    def __init__(self, name: str, felder: list[tuple[int, int]]):
        self.name = name
        self.felder = felder
        self.treffer: set[tuple[int, int]] = set()

    def laenge(self) -> int:
        """Wie viele Felder belegt das Schiff?"""
        # TODO
        ...

    def belegt(self, feld: tuple[int, int]) -> bool:
        """Liegt dieses Feld auf dem Schiff?"""
        # TODO
        ...

    def beschiessen(self, feld: tuple[int, int]) -> bool:
        """Trägt einen Treffer ein. Gibt True zurück, wenn getroffen wurde."""
        # TODO
        ...

    def versenkt(self) -> bool:
        """Sind alle Felder getroffen?"""
        # TODO
        ...

    def __repr__(self) -> str:
        """Was print() anzeigt — Gold wert beim Debuggen."""
        return f""
```

**Testen — nicht raten, ausprobieren!**

Deine Klasse steht in einer Datei, z. B. `schiff.py`. Zum Ausprobieren startest du die Python-Konsole **im selben Ordner** und importierst sie:

🖥️ **Terminal**

```
uv run python
```

🐍 **Python-Konsole**

```
>>> from schiff import Schiff
>>> s = Schiff("Zerstörer", [(2,5), (3,5), (4,5)])
>>> s.laenge()
3
```

Der Import holt deine Klasse aus der Datei in die Konsole. Ändert sich die Datei, musst du die Konsole neu starten (`exit()`, dann wieder `uv run python`) — ein Import wird nicht automatisch aufgefrischt. Das ist der häufigste Grund für „aber ich hab's doch repariert, es kommt trotzdem der alte Fehler“.

Die ganze Prüfreihe:

🐍 **Python-Konsole**

```
s = Schiff("Zerstörer", [(2,5), (3,5), (4,5)])
print(s.laenge())              # 3
print(s.beschiessen((3,5)))    # True
print(s.beschiessen((9,9)))    # False
print(s.versenkt())            # False
s.beschiessen((2,5)); s.beschiessen((4,5))
print(s.versenkt())            # True
print(s)                       # nutzt __repr__
```

**Extra für Schnelle:** Schreib die Klasse als `@dataclass` neu (`from dataclasses import dataclass, field`). Sie spart `__init__` und `__repr__`. Frage: Warum braucht man für `treffer` dann `field(default_factory=set)` und nicht `= set()`?

> ✅ Checkpoint 1 — Prüffragen: - Warum ist treffer ein set und keine list? - Was passiert, wenn man zweimal auf dasselbe Feld schießt? - Wo im Code steht die Regel „ein Schiff ist versenkt, wenn alle Felder getroffen sind“? Genau einmal – oder mehrfach?

## Meilenstein 2 — Die Klasse `Spielfeld` (60 min)

**Das ist der wichtigste Meilenstein des Tages.** Am Ende ist das Spiel im Terminal komplett spielbar – und zwar ohne eine einzige Zeile Pygame. Das beweist Hausregel 2.

Datei `spielfeld.py`:

📄 **Datei**

```
from enum import Enum
from schiff import Schiff

class Ergebnis(Enum):
    """Was ein Schuss bewirkt hat."""
    DANEBEN = "daneben"
    TREFFER = "Treffer"
    VERSENKT = "versenkt"
    UNGUELTIG = "ungültig"     # außerhalb oder schon beschossen

class Spielfeld:
    def __init__(self, groesse: int = 10):
        self.groesse = groesse
        self.schiffe: list[Schiff] = []
        self.beschossen: set[tuple[int, int]] = set()

    # --- Aufbau ---------------------------------------------------------
    def passt(self, felder: list[tuple[int, int]]) -> bool:
        """Prüft: alle Felder im Brett UND kein anderes Schiff im Weg."""
        # TODO
        ...

    def setze_schiff(self, schiff: Schiff) -> bool:
        """Fügt das Schiff hinzu, wenn es passt. Gibt Erfolg zurück."""
        # TODO
        ...

    # --- Spielablauf ----------------------------------------------------
    def schuss(self, feld: tuple[int, int]) -> Ergebnis:
        """Der Kern des Spiels. Genau EINE Methode ändert den Zustand."""
        # TODO: 1) gültig?  2) merken  3) Schiff suchen  4) Ergebnis
        ...

    def alle_versenkt(self) -> bool:
        # TODO
        ...

    def zeige(self) -> str:
        """Das Brett als Text — zum Testen und Debuggen."""
        # TODO: Kopfzeile A-J, dann Zeilen mit ~ (Wasser), X (Treffer), o (daneben)
        ...
```

**Warum ein Enum und keine Strings?** Weil `"Treffer"` vs. `"treffer"` vs. `"Treffe"` stumme Fehler sind, die erst um 16 Uhr auffallen. `Ergebnis.TREFFER` gibt es entweder – oder Python meckert sofort.

**Aufgabe: Bau daraus ein Terminal-Spiel** (`konsole.py`, ca. 20 Zeilen):

```
A B C D E F G H I J
 1 ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
 2 ~ ~ o ~ ~ ~ ~ ~ ~ ~
 3 ~ ~ ~ ~ X ~ ~ ~ ~ ~
Dein Schuss: E3
>>> Treffer!
```

Eingabe `"E3"` → Tupel `(2, 4)` umrechnen. Achtung: Menschen zählen ab 1, Python ab 0.

📄 **Datei** `konsole.py` — gestartet wird sie so:

🖥️ **Terminal**

```
uv run konsole.py
```

Ab hier läuft dein Programm und fragt dich nach Eingaben. Das ist **kein** dritter Ort: Du bist im Terminal, aber dein eigenes Programm stellt die Fragen. Mit `Strg`+`C` brichst du ab.

> ✅ Checkpoint 2 — Prüffragen: - Steht irgendwo import pygame in diesen beiden Dateien? (Nein? Perfekt.) - Wenn ich das Brett auf 15×15 vergrößern will – wie viele Stellen muss ich anfassen? - schuss() ist die einzige Methode, die etwas verändert. Warum ist das gut? Der Motivations-Moment: Das Spiel ist fertig und funktioniert. Alles Weitere ist nur noch eine hübsche Oberfläche für Code, der bereits läuft.

## Meilenstein 3 — Pygame und die Game Loop (45 min)

**Ziel:** Fenster mit Raster, sauber in einer Klasse gekapselt.

### Das Herzstück jedes Spiels

Jedes Spiel der Welt – Tetris, GTA, Minecraft – macht 60-mal pro Sekunde dasselbe:

```
┌─────────────────────────────────────────┐
│  1. EREIGNISSE    (Was tut der Mensch?) │
│  2. AKTUALISIEREN (Was folgt daraus?)   │
│  3. ZEICHNEN      (Wie sieht es aus?)   │
└─────────────────────────────────────────┘
        ↑ und wieder von vorn
```

> 🔬 Physik-Bezug: Das ist numerische Integration in Zeitschritten Δt. Wer schon mal eine Wurfparabel simuliert hat, kennt das Muster: Zustand → kleiner Zeitschritt → neuer Zustand. Ein Spiel ist ein Physiksimulator, der auf Mausklicks hört.

Datei `spiel.py`:

📄 **Datei**

```
import pygame
from spielfeld import Spielfeld, Ergebnis

ZELLE = 40           # Pixel pro Kästchen
RAND = 60            # Abstand vom Fensterrand
WASSER = (12, 34, 68)
LINIE = (60, 90, 130)

class Spiel:
    def __init__(self, groesse: int = 10):
        pygame.init()
        self.feld = Spielfeld(groesse)
        breite = 2 * RAND + groesse * ZELLE
        self.fenster = pygame.display.set_mode((breite, breite))
        self.uhr = pygame.time.Clock()
        self.laeuft = True

    def ereignisse(self) -> None:
        for e in pygame.event.get():
            if e.type == pygame.QUIT:
                self.laeuft = False
            # TODO später: Mausklick

    def aktualisieren(self) -> None:
        # TODO später: Animationen
        ...

    def zeichnen(self) -> None:
        self.fenster.fill(WASSER)
        self._raster_zeichnen()
        pygame.display.flip()

    def _raster_zeichnen(self) -> None:
        """Der Unterstrich heißt: interne Methode, von außen nicht anfassen."""
        # TODO: groesse+1 senkrechte und waagerechte Linien
        # pygame.draw.line(flaeche, farbe, start, ende)
        ...

    def start(self) -> None:
        while self.laeuft:
            self.ereignisse()
            self.aktualisieren()
            self.zeichnen()
            self.uhr.tick(60)      # max. 60 Bilder pro Sekunde
        pygame.quit()

if __name__ == "__main__":
    Spiel().start()
```

**Frage zum Nachdenken:** Was passiert, wenn du `self.uhr.tick(60)` weglässt? (Antwort: Der Lüfter dreht auf. Die Schleife rennt so schnell sie kann.)

**Was bedeutet if __name__ == "__main__":?** Diese Zeile läuft nur, wenn *diese* Datei direkt gestartet wird – nicht beim Import. Deshalb kannst du `spielfeld.py` importieren, ohne dass ein Fenster aufgeht.

## Meilenstein 4 — Koordinaten-Mathematik (45 min)

**Ziel:** Ein Mausklick bei Pixel (327, 214) landet im richtigen Kästchen.

Das ist reine Mathematik und der leichteste Teil des Tages. Zwei Abbildungen, die zueinander invers sind:

```
Gitter → Pixel:   px = RAND + spalte * ZELLE
                  py = RAND + zeile  * ZELLE

Pixel → Gitter:   spalte = (mx - RAND) // ZELLE
                  zeile  = (my - RAND) // ZELLE
```

`//` ist Ganzzahldivision (abrunden) – genau das Richtige, denn alles zwischen Pixel 60 und 99 gehört zu Spalte 0.

📄 **Datei** — in `spiel.py` ergänzen

```
def gitter_zu_pixel(self, feld: tuple[int, int]) -> tuple[int, int]:
    """(zeile, spalte) -> linke obere Ecke in Pixeln."""
    # TODO
    ...

def pixel_zu_gitter(self, pos: tuple[int, int]) -> tuple[int, int] | None:
    """Maus-Pixel -> (zeile, spalte), oder None wenn außerhalb des Bretts."""
    # TODO: erst umrechnen, DANN prüfen ob 0 <= wert < groesse
    ...
```

**Die Falle:** `(-5) // 40` ergibt in Python `-1`, nicht `0`. Klickt man links neben das Brett, kommt Spalte `-1` heraus – und `liste[-1]` ist in Python das *letzte* Element. Der Klick daneben trifft also stillschweigend die rechte Bretthälfte. Deshalb **immer die Bereichsprüfung nach der Division.** Genau dafür gibt es den Rückgabewert `None`.

**Aufgabe:** Zeichne das Kästchen unter dem Mauszeiger hell ein (`pygame.mouse.get_pos()`, `pygame.draw.rect`). Wenn das flüssig mitläuft, sind beide Richtungen korrekt.

> ✅ Checkpoint 3: Klick in alle vier Ecken und knapp daneben. Kein Absturz, kein falsches Feld? Dann weiter.

## Meilenstein 5 — Schießen und Zeichnen (60 min)

Jetzt kommt beides zusammen. In `ereignisse()`:

📄 **Datei** — in `spiel.py` ergänzen

```
if e.type == pygame.MOUSEBUTTONDOWN and e.button == 1:
    feld = self.pixel_zu_gitter(e.pos)
    if feld is not None:
        ergebnis = self.feld.schuss(feld)      # Logik aus Meilenstein 2!
        # TODO: Ergebnis auswerten (Sound? Animation? Text?)
```

**Achte darauf:** Hier steht keine Spielregel. Die Frage „war das ein Treffer?“ beantwortet `Spielfeld`. Die Oberfläche fragt nur und malt das Ergebnis. Das ist Hausregel 2 in Aktion.

**Zeichnen:**

| Zustand | Darstellung |
|---|---|
| unbeschossen | Wasser |
| daneben | weißer Punkt |
| Treffer | rotes X oder Flammen-Emoji |
| versenktes Schiff | ganzes Schiff grau/schwarz durchgestrichen |

**Text anzeigen:**

📄 **Datei** — in `spiel.py` ergänzen

```
self.schrift = pygame.font.SysFont("Arial", 22)     # einmal in __init__!
bild = self.schrift.render("Treffer!", True, (255, 220, 100))
self.fenster.blit(bild, (RAND, 20))
```

⚠️ `SysFont` in der Zeichenschleife anzulegen kostet 60-mal pro Sekunde eine Schriftart. Merksatz: **Was sich nicht ändert, gehört in __init__.**

> ✅ Checkpoint 4: Grafisch spielbar. Guter Zeitpunkt für eine Pause.

## Meilenstein 6 — Schiffe platzieren (45 min)

Klassische Flotte: 1×5, 1×4, 2×3, 1×2.

📄 **Datei**

```
import random

def platziere_zufaellig(feld: Spielfeld, flotte: dict[str, list[int]]) -> None:
    """Setzt alle Schiffe zufällig, ohne Überlappung."""
    # TODO:
    #   für jedes Schiff:
    #     wiederhole:
    #       zufällige Startzelle + zufällige Richtung ((0,1) oder (1,0))
    #       Felder ausrechnen
    #       wenn feld.passt(...): setzen, fertig
    ...
```

**Die Denkfalle:** Eine `while True`-Schleife, die immer wieder würfelt, hängt sich auf, wenn kein Platz mehr da ist. Bau einen Zähler ein (max. 500 Versuche) und beginne notfalls das ganze Brett neu.

> 🎲 Zahlenspiel: Wie viele legale Positionen gibt es für ein 5er-Schiff auf einem leeren 10×10-Brett? Waagerecht: 6 pro Zeile × 10 Zeilen = 60. Senkrecht ebenso. Macht 120. Diese Zahl brauchst du in Meilenstein 7 wieder – merk sie dir.

**Ausbaustufe (optional):** Der Mensch platziert selbst per Maus, `R` dreht das Schiff, Vorschau in Grün/Rot je nach `passt()`.

## Meilenstein 7 — Der Gegner in drei Stufen (60 min)

**Das ist der Höhepunkt des Tages.** Drei KI-Stufen, jede eine eigene Klasse mit derselben Methode `naechster_schuss()`. Beim Start wählt man aus – und sieht sofort, wie viel schlauer die nächste Stufe ist.

📄 **Datei** `ki.py`

```
class KI:
    """Basisklasse — legt fest, was jede KI können muss."""
    def naechster_schuss(self, gegner: Spielfeld) -> tuple[int, int]:
        raise NotImplementedError
```

**Stufe 1 – Zufall.** Würfelt ein Feld, das noch nicht beschossen ist. → Braucht im Schnitt **~96 Schüsse** für 100 Felder.

**Stufe 2 – Jäger.** Zwei Modi: Solange nichts getroffen wurde, zufällig schießen. Nach einem Treffer die vier Nachbarfelder abarbeiten. Nach zwei Treffern in einer Linie: in dieser Richtung weiter. → **~65 Schüsse.**

**Stufe 3 – Wahrscheinlichkeitskarte.** Für jedes noch nicht versenkte Schiff werden *alle* legalen Positionen durchgezählt. Jedes Feld bekommt einen Zähler: „auf wie vielen möglichen Schiffspositionen liege ich?“ Dann wird auf das Maximum geschossen.

📄 **Datei** `ki.py`

```
def wahrscheinlichkeitskarte(gegner, restliche_laengen: list[int]):
    karte = [[0] * gegner.groesse for _ in range(gegner.groesse)]
    # TODO für jede Länge, jede Startzelle, jede Richtung:
    #   passt die Position noch zu allem, was wir wissen?
    #   wenn ja: +1 auf alle ihre Felder
    return karte
```

→ **~45 Schüsse.** Und das Schönste: **Man kann die Karte einfärben.** Zeichne sie halbtransparent über das Brett – dunkelblau = unwahrscheinlich, hellgelb = heiß. Man sieht dem Computer beim Denken zu.

> 🔥 Das ist der Moment, in dem es Klick macht. Kein „ich habe Code abgetippt“, sondern: Ich habe eine Idee aus der Stochastik in Code übersetzt, und der Computer wurde dadurch messbar besser. Genau das ist Informatik. Und hier zahlt sich das Objektdesign aus: Drei völlig verschiedene Gegner, ein Austausch einer einzigen Zeile (self.ki = Stufe3()). Im Spaghetticode hätte das ein if-Monster mit 40 Zeilen gebraucht.

**Wettbewerb zum Abschluss:** Lass alle drei KIs je 100 Spiele gegen zufällig platzierte Flotten laufen (ohne Grafik – geht in Sekunden, weil die Logik pygame-frei ist!) und trage den Schnitt in eine Tabelle ein. Das ist eine echte Messreihe, wie im Physikpraktikum.

## Meilenstein 8 — Politur (Rest des Tages)

Freie Auswahl, alles optional:

- **Sound:** `pygame.mixer.Sound("treffer.wav").play()` – Explosion, Platsch, Sieges-Fanfare
- **Animation:** Treffer-Blitz, der über 0,3 s ausblendet (Farbinterpolation aus Meilenstein 0)
- **Endbildschirm:** „Du hast gewonnen – in 43 Schüssen!“
- **Eigenes Thema:** Raumschiffe statt Boote, Weltraum statt Meer. Kostet nur Farben und Beschriftungen – weil die Logik davon nichts weiß.
- **Statistik:** Trefferquote in Prozent, live oben rechts

## Anhang A — Fehlermeldungen lesen (5 min, aber wichtig)

Fehlermeldungen sind keine Beleidigung, sondern eine Wegbeschreibung. **Immer von unten nach oben lesen:** Die letzte Zeile sagt *was*, die vorletzte *wo*.

| Meldung | Bedeutet meistens |
|---|---|
| `AttributeError: 'Schiff' object has no attribute 'x'` | Tippfehler oder `self.x` nie gesetzt |
| `TypeError: ... missing 1 required positional argument: 'self'` | Methode über die Klasse statt über das Objekt aufgerufen |
| `IndexError: list index out of range` | Bereichsprüfung vergessen (siehe Meilenstein 4!) |
| `TypeError: unhashable type: 'list'` | Liste statt Tupel als Koordinate benutzt |
| `NameError: name 'x' is not defined` | Vertippt, oder Variable lebt in einem anderen Block |

**Die zweite Sorte Fehler** kommt nicht aus deinem Code, sondern daraus, dass etwas am falschen Ort steht:

| Meldung | Bedeutet |
|---|---|
| `ModuleNotFoundError: No module named 'pygame'` | mit `python …` statt `uv run …` gestartet |
| `SyntaxError` bei einem `uv`- oder `cd`-Befehl | Terminal-Befehl in der Python-Konsole → `exit()` |
| `… nicht als Name eines Cmdlets erkannt` / `zsh: number expected` | Python-Code im Terminal → `uv run python` |
| `ModuleNotFoundError: No module named 'schiff'` | Python-Konsole läuft in einem anderen Ordner als die Datei |
| Änderung wirkt nicht, alter Fehler bleibt | Konsole neu starten — ein Import wird nicht aufgefrischt |

Ausführlich stehen die im Kapitel „Erste Hilfe“ der **Systemeinrichtung**.

**Wenn gar nichts geht:** `print()` ist kein Zeichen von Schwäche. Drei gut gesetzte `print()` schlagen zwanzig Minuten Starren. Und `print(self.feld)` verrät dir alles – wenn du `__repr__` geschrieben hast.

## Anhang B — Spickzettel

```
# Klasse
class Ding:
    def __init__(self, wert):
        self.wert = wert            # Attribut
    def machen(self):               # Methode
        return self.wert * 2

d = Ding(21)
d.machen()                          # 42

# Listen & Mengen
felder = [(0,0), (0,1)]
felder.append((0,2))
menge = set(felder)
(0,1) in menge                      # schnelle Prüfung

# Comprehension
quadrate = [x**2 for x in range(5)]           # [0,1,4,9,16]
brett = [[0]*10 for _ in range(10)]           # 10x10 (NICHT [[0]*10]*10 !)

# f-String
print(f"{name}: {treffer}/{gesamt} ({treffer/gesamt:.1%})")

# Pygame
pygame.draw.rect(flaeche, farbe, (x, y, breite, hoehe))
pygame.draw.line(flaeche, farbe, (x1,y1), (x2,y2), dicke)
pygame.draw.circle(flaeche, farbe, (x,y), radius)
```

⚠️ **[[0]*10]*10 ist die fieseste Falle in Python.** Das erzeugt zehnmal *dieselbe* Zeile. Änderst du `brett[0][3]`, ändern sich alle zehn Zeilen. Immer die Comprehension nehmen.

## Anhang C — Wenn es hakt: die Hilfeleiter

Nicht die Lösung geben. In dieser Reihenfolge helfen:

1. „Lies die Fehlermeldung laut vor. Welche Zeile?“
2. „Was *erwartest* du, dass hier drinsteht? Setz ein `print()` davor.“
3. „Erklär mir die Methode Satz für Satz.“ (Rubber-Duck-Debugging – funktioniert erschreckend oft schon bei Satz zwei)
4. „Welche der drei Hausregeln verletzt der Code hier gerade?“
5. Erst jetzt: konkreter Hinweis auf die Zeile – nie die fertige Zeile selbst.

## Abschluss (15 min)

Das Spiel wird vorgeführt und in drei Sätzen erklärt: Was macht `Schiff`, was macht `Spielfeld`, was macht `Spiel`? Wer das beantworten kann, hat an einem Tag verstanden, wofür manche ein Semester brauchen: **Ein Programm ist kein Ablauf von oben nach unten. Es ist ein Ensemble von Dingen, die zusammenspielen.**

Und zum Schluss die Frage, die hängen bleibt: *„Deine Stufe-3-KI braucht 45 statt 96 Schüsse. Du hast den Computer heute doppelt so schlau gemacht — mit Mathe. Willst du wissen, was passiert, wenn man das auf echte Probleme loslässt?“*
