Skip to content

Commit

Permalink
Merge pull request #41 from SilasBerger/feature/game-projekt-gbsl
Browse files Browse the repository at this point in the history
Text Adventure Tutorial
  • Loading branch information
SilasBerger authored Apr 13, 2024
2 parents c2fed5f + ee90d41 commit 3a33fed
Show file tree
Hide file tree
Showing 17 changed files with 586 additions and 6 deletions.
14 changes: 14 additions & 0 deletions config/scriptsConfigs/drafts.scriptsConfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,17 @@ default:
material: structure/script-landing-pages/drafts.mdx
- section: 01-Components
material: Components-Gallery
- section: 02-Game-Projekt/01-Pygame/index.mdx
material: Pygame/index.mdx
- section: 02-Game-Projekt/01-Pygame/img
material: Pygame/img
- section: 02-Game-Projekt/01-Pygame/01-Pygame-Einrichten.mdx
material: Pygame/Misc/Pygame-Einrichten.mdx
- section: 02-Game-Projekt/01-Pygame/img
material: Pygame/Misc/img
- section: 02-Game-Projekt/01-Pygame/02-Breakout-Tutorial
material: Pygame/Breakout-Basics
- section: 02-Game-Projekt/02-Text-Adventure
material: Programmieren/Text-Adventure-GYM2
- section: 02-Game-Projekt/99-Advanced-Python
material: Programmieren/Advanced-Python
16 changes: 10 additions & 6 deletions config/scriptsConfigs/teach.scriptsConfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ gymnasium/informatik:
material: IT-Basics
- section: 02-Programmieren/01-Thonny
material: Programmieren/Thonny
- section: 02-Programmieren/99-Advanced-Python
material: Programmieren/Advanced-Python
- section: 03-Textverarbeitung-und-Style/_category_.json
material: structure/categories/textverarbeitung-und-style.json
- section: 03-Textverarbeitung-und-Style/01-Word
Expand All @@ -23,18 +25,20 @@ gymnasium/informatik:
material: Netzwerke
- section: 06-Kryptologie
material: Kryptologie
- section: 07-Pygame/index.mdx
- section: 07-Game-Development/01-Pygame/index.mdx
material: Pygame/index.mdx
- section: 07-Pygame/img
- section: 07-Game-Development/01-Pygame/img
material: Pygame/img
- section: 07-Pygame/img
- section: 07-Game-Development/01-Pygame/img
material: Pygame/Misc/img
- section: 07-Pygame/01-Pygame-Einrichten.mdx
- section: 07-Game-Development/01-Pygame/01-Pygame-Einrichten.mdx
material: Pygame/Misc/Pygame-Einrichten.mdx
- section: 07-Pygame/02-Breakout-Tutorial
- section: 07-Game-Development/01-Pygame/02-Breakout-Tutorial
material: Pygame/Breakout-Basics
- section: 07-Pygame/99-Advanced-Python
- section: 07-Game-Development/01-Pygame/99-Advanced-Python
material: Programmieren/Advanced-Python
- section: 07-Game-Development/02-Text-Adventure
material: Programmieren/Text-Adventure-GYM2
- section: 08-Microbit/img
material: Microbit/img
- section: 08-Microbit/index.mdx
Expand Down
73 changes: 73 additions & 0 deletions content/material/Programmieren/Advanced-Python/02-Random.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Zufall
Mit der Funktion `randint()` aus dem Modul `random` können wir ganz einfach Zufallszahlen erzeugen.

Dazu müssen wir das Modul zuerst importieren, damit es in unserem Programm verfügbar wird[^1]. Das `random`-Modul enthält eine Vielzahl verschiedener Funktionen, wovon wir hier aber nur die Funktion `randint()` benötigen. Den entsprechenden Import formulieren wir also folgendermassen:
```python showLineNumbers
from random import randint
```

Mit der Funktion `randint(x, y)` können wir nun zufällige Ganzzahlen im Bereich `x` bis `y` generieren. Zum Beispiel generiert folgender Code eine zufällige Ganzzahl von `0` bis `10` und gibt sie in der Kommandozeile aus:

```python showLineNumbers
from random import randint

zufallszahl = randint(0, 10)
print(zufallszahl)
```

## Anwendungsmöglichkeiten
Damit lassen sich bereits einige nützliche Zufallsprogramme schreiben. Eine kleine Auswahl an Ideen finden Sie hier.

### Würfel
```python showLineNumbers
from random import randint

augenzahl = randint(1, 6)
print("Gewürfelt:", augenzahl)
```

### Münzwurf-Generator
```python showLineNumbers
from random import randint

wurf = randint(0, 1)
if wurf == 0:
print("Kopf")
else:
print("Zahl")
```

### Zufällige Spielkarte
_Hinweis: In diesem Beispiel werden zur Erläuterung [Kommentare](Kommentare) verwendet: Alles, was auf einer Zeile nach einem `#` steht, wird von Python ignoriert._

```python showLineNumbers
from random import randint

# Zufallszahl zwischen 0 und 3 für vier Farben
farbe = randint(0, 3)
if farbe == 0:
farbe_name = "Karo"
elif farbe == 1:
farbe_name = "Herz"
elif farbe == 2:
farbe_name = "Pik"
else:
farbe_name = "Kreuz"

# Zahlenwerte 2-10, Bube=11, Dame=12, König=13, As=14
wert = randint(2, 14)
if wert == 11:
wert_name = "Bube"
elif wert == 12:
wert_name = "Dame"
elif wert == 13:
wert_name = "König"
elif wert == 14:
wert_name = "As"
else:
wert_name = wert

print(farbe_name, wert_name)
```

[^1]: Das _Importieren_ kennen Sie bereits aus der Turtle-Zeit: `from turtle import *`.
102 changes: 102 additions & 0 deletions content/material/Programmieren/Advanced-Python/06-Module.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Module
Je komplexer unsere Applikation wird, desto grösser wird auch die entsprechende Python-Datei. Irgendwann steht dort so viel Code drin, dass wir uns kaum mehr zurechtfinden.

Da kommen **Module** ins Spiel: Sie erlauben es uns, ein Programm auf mehrere Python-Dateien zu verteilen.

Für dieses Beispiel gehen wir davon aus, dass wir eine Datei `rechner.py` mit folgendem Code haben:
```python showLineNumbers
def plus(a, b):
print(a + b)

def minus(a, b):
print(a - b)

def mal(a, b):
print(a * b)

def durch(a, b):
print(a / b)

plus(10, 5)
minus(10, 5)
durch(10, 5)
```

## Module erstellen
Um unser Programm übersichtlicher zu gestalten, könnten wir nun beispielsweise die Funktionen `plus()` und `minus()` in eine Datei `strich_operationen.py` und die Funktionen `mal()` und `durch()` in eine Datei `punkt_operationen.py` verschieben. Diese beiden Dateien sehen also folgendermassen aus:

**strich_operationen.py:**
```python showLineNumbers
def plus(a, b):
print(a + b)

def minus(a, b):
print(a - b)
```

**punkt_operationen.py:**
```python showLineNumbers
def mal(a, b):
print(a * b)

def durch(a, b):
print(a / b)
```

Damit haben wir zwei neue Module erstellt: `strich_operationen` und `punkt_operationen`.

:::key[Module und Modulnamen]
Jede Python-Datei ist auch ein Modul. Ihr Modulname entspricht ihrem Dateinamen ohne die `.py`-Endung.

Sprich: Die Datei `strich_operationen.py` ist zugleich ein Modul namens `strich_operationen`, `punkt_operationen.py` ist zugleich ein Modul namens `punkt_operationen`.
:::

## Module verwenden
Unser ursprüngliches Programm können wir nun wie folgt vereinfachen:

**rechner.py:**
```python showLineNumbers
plus(10, 5)
minus(10, 5)
durch(10, 5)
```

Doch etwas fehlt noch: Wir müssen Python noch mitteilen, dass wir die Funktionen aus den beiden neuen Modulen verwenden wollen. Das machen wir mit einem **Import**.

Den _Import_ kennen Sie bereits aus der Turtle-Zeit: `from turtle import *`. Damit sagen wir Python, dass wir aus dem Turtle-Modul (`from turtle`) sämtliche Funktionen und Variablen importieren wollen (`import *`[^1]). Analog importieren wir die beiden Module `strich_operationen` und `punkt_operationen`:

**rechner.py:**
```python showLineNumbers
from strich_operationen import *
from punkt_operationen import *

plus(10, 5)
minus(10, 5)
durch(10, 5)
```

:::warning[Speicherort]
Damit dieser Import funktioniert, müssen alle drei Dateien nebeneinander liegen (also im gleichen Ordner gespeichert sein)!
:::

Nun funktioniert unser Rechner auch wieder korrekt und verwendet die Funktionen aus unseren neuen Modulen.

### Import optimieren
Tatsächlich verwenden wir aus dem modul `punkt_operationen` eigentlich nur die Funktion `durch()`. Mit dem aktuellen Import auf Zeile `2` im vorherigen Code-Ausschnitt importieren wir aber alle Funktionen aus `punkt_operationen` — also auch die Funktion `mal()`, welche wir momentan gar nicht brauchen.

In Python gehört es zur _Best Practice_ (empfohlene Vorgehensweise), dass wir nur die Dinge importieren, die wir auch wirklich benötigen. Das machen wir, indem wir den import nicht überall mit `import *` spezifizieren, sondern dort genau diejenigen Dinge auflisten, die wir benötigen.

```python showLineNumbers
from strich_operationen import plus, minus
from punkt_operationen import durch

plus(10, 5)
minus(10, 5)
durch(10, 5)
```

:::insight[Äquivalenz]
Da wir aus dem Modul `strich_operationen` alle Funktionen benötigen, ist der Import auf Zeile `1` in diesem spezifischen Fall gleichwertig mit `from strich_operationen import *`.
:::

[^1]: Das `*`-Zeichen bezeichnen wir in diesem Zusammenhang als _Wildcard_. Es bedeutet so viel wie "alles".
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
# Sanitization: Benutzereingaben reinigen
Schauen Sie sich folgendes Beispielprogramm an:

```python showLineNumbers
entscheidung = input("Möchten Sie Ihre Änderungen speichern? (ja/nein) ")

if entscheidung == "ja":
print("✅ Änderungen gespeichert")
else:
print("🚮 Alle Änderungen wurden verworfen")
```

Das Programm funktioniert zwar, doch es gibt ein Problem in der Anwendung:

```
Möchten Sie Ihre Änderungen speichern? (ja/nein) Ja
🚮 Alle Änderungen wurden verworfen
```

oder

```
Möchten Sie Ihre Änderungen speichern? (ja/nein) ja
🚮 Alle Änderungen wurden verworfen
```

:::insight[Was ist hier passiert?]
Der Wert der Variable `entscheidung` entspricht in diesen Fällen nicht genau dem String `"ja"`. Somit ist die Bedingung auf Zeile `3` `False` und wir führen den `else`-Fall auf Zeile `6` aus.
:::

Um dieses Problem zu umgehen und unser Programm benutzerfreundlicher zu machen, gibt es zwei Arten, auf die wir solche Benutzereingaben meistens zuerst anpassen, bevor wir sie mit einem erwarteten Wert vergleichen:
1. **Die Eingabe wird zu Kleinbuchstaben verwandelt.** Wenn wir in den Vergleichswerten (hier Zeile `3`) ebenfalls nur Kleinbuchstaben verwenden, erreichen wir so den Effekt, dass die Gross- und Kleinschreibung in der Benutzereingabe "ignoriert" wird.
2. **Die Eingabe wird _getrimmt_**. Alle Leerzeichen am Anfang und am Ende des Strings werden entfernt.

Diesen Prozess des _Standardisierens_ von Benutzereingaben nennt man **Sanitization** (engl.: _to sanitize_, reinigen, desinfizieren).

## String zu Kleinbuchstaben verwandeln
Dazu rufen wir auf einem beliebigen String die Funktion[^1] `.lower()` auf. Der Rückgabewert[^2] ist ein identischer String mit nur Kleinbuchstaben:

```python showLineNumbers
wert = "HaLLo"
wert = wert.lower()
print(wert)
```

produziert in der Kommandozeile die Ausgabe

```
hallo
```

## Leerzeichen am Anfang und am Ende entfernen
Dazu rufen wir auf einem beliebigen String die Funktion[^1] `.strip()` auf. Der Rückgabewert[^2] ist ein identischer String ohne Leerzeichen am Anfang und am Ende:

```python showLineNumbers
wert = " hallo "
wert = wert.strip()
print(wert)
```

produziert in der Kommandozeile die Ausgabe

```
hallo
```

## Das verbesserte Beispielprogramm
Mit diesen beiden Veränderungen können wir unser Beispielprogramm nun benutzerfreundlicher machen:
```python showLineNumbers
entscheidung = input("Möchten Sie Ihre Änderungen speichern? (ja/nein) ")
entscheidung = entscheidung.lower()
entscheidung = entscheidung.strip()

if entscheidung == "ja":
print("✅ Änderungen gespeichert")
else:
print("🚮 Alle Änderungen wurden verworfen")
```

Wichtig ist dabei nur, dass der Vergleichswert (der String `ja` auf Zeile `5`) selbst ebenfalls nur Kleinbuchstaben und keine Leerzeichen am Anfang und am Ende hat.

```
Möchten Sie Ihre Änderungen speichern? (ja/nein) Ja
✅ Änderungen gespeichert
```

[^1]: Genaugenommen sprechen wir hier von einer Methode; siehe [OOP](OOP).
[^2]: siehe [Rückgabewerte](Return).
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Eine erste Entscheidung
Unser Text Adventure beginnt ganz einfach: Wir heissen den Spieler willkommen und erklären ihm das Setting (Zeilen `1`-`3`). Bei Ihrem eigenen Spiel dürfen Sie hier ruhig etwas kreativer und ausführlicher werden. Mit gelungenem Storytelling versetzen Sie den Spieler im Handumdrehen mitten ins Geschehen Ihrer Spielwelt!

Auf Zeile `5` setzen wir den Spieler anschliessend vor eine erste Entscheidung: Wohin willst du gehen? Links, rechts, oder geradeaus? Die entsprechende Texteingabe speichern wir in der Variable `auswahl`.

Basierend auf dem Wert der Variable `auswahl` entscheiden wir anschliessend in der `if-elif-else`-Bedingung auf den Zeilen `6`-`13`, wie das Spiel weitergehen soll. Hat der Spieler beispielsweise `links` eingegeben, so bedeutet das für uns, dass er in der Spielwelt nach links gelaufen ist. Dort lassen wir ihn auf einen Fluss treffen (Zeilen `6`-`7`). Rechts findet er ein Rudel Wölfe, während geradeaus eine Riesenspinne auf ihn wartet. Gibt der Benutzer etwas anderes als genau eines der Worte `links`, `rechts` oder `geradeaus` ein, so lassen wir ihn wissen, dass diese Eingabe ungültig ist (Zeilen `12`-`13`).

```python showLineNumbers
print("Willkommen beim Schatzsuche-Spiel!")
print("Du befindest dich in einem Wald. Du musst den versteckten Schatz finden.")
print("Du stehst an einer Wegkreuzung. Du kannst nach links, rechts oder geradeaus gehen.")

auswahl = input("In welche Richtung möchtest du gehen? (links/rechts/geradeaus): ")
if auswahl == "links":
print("Du stösst auf einen Fluss. Du kannst schwimmen oder eine Brücke suchen.")
elif auswahl == "rechts":
print("Du begegnest einem Rudel Wölfe. Du kannst entweder rennen oder versuchen, sie zu zähmen.")
elif auswahl == "geradeaus":
print("Du gehst geradeaus und triffst auf eine Riesenspinne.")
else:
print("Ungültige Wahl! Spiel vorbei!")
```

Nach dieser ersten Entscheidung und dem Ausgeben der entsprechenden Konsequenz ist das Spiel fürs Erste auch bereits wieder vorbei. In den nächsten paar Schritten werden wir die Reise unseres Spielers aber noch etwas länger und spannender gestalten. Bleiben Sie also dran!

---
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Was passiert am Fluss?
Wenn der Spieler bei dieser ersten Entscheidung aus dem vorherigen Kapitel nach links geht, so trifft er auf einen Fluss. Aber was passiert denn nun an diesem Fluss? Das entscheiden wir in diesem Kapitel.

Doch bevor wir zu sehr in die Story unseres Abenteurers eintauchen, sollten wir uns ein paar Gedanken zu unserem Code machen: Der wird jetzt nämlich langsam länger, und damit leider auch irgendwann unübersichtlich. Um das zu verhindern, definieren wir folgende Regel: für jede Szene gibt es eine eigene Funktion, die wir möglichst der Szene entsprechend benennen. Da wir in diesem Kapitel die Szene am Fluss beschreiben wollen, erstellen wir also eine neue Funktion `fluss()` (ab Zeile `1`).

Innerhalb dieser neuen `fluss()`-Funktion machen wir nun wieder genau das gleiche wie in der Anfangsszene: Wir erzählen dem Spieler ein Stück der Geschichte (den Satz von wegen `Du stösst auf einen Fluss...` haben wir einfach in die neue Funktion verschoben), stellen ihn vor eine nächste Entscheidung, und setzen den Spielverlauf entsprechend fort (Zeilen `2-10`).

Für die Entscheidung `schwimmen` definieren wir auf Zeile `5` ein erstes reguläres Spielende. Der Spieler wurde von einem Krokodil gefressen — das Spiel ist vorbei. Hier folgt also keine nächste Szene.

Auf Zeile `10` behandeln wir wiederum den Fall, dass der Spieler eine Entscheidung eingetippt hat, die es gar nicht gibt. Das haben wir auch bereits bei der Anfangsszene gemacht, auf Zeile `24`.

Die einzige Entscheidung, die den Spieler vorwärtsbringt, ist `brücke` — hier lassen wir ihn tatsächlich eine versteckte Brücke finden. Mit dem Satz auf Zeile `7` beginnt also eine nächste Szene. Gemäss unserer "Pro Szene eine Funktion"-Regel müsste dieser Satz also auch bereits wieder in eine eigene neue Funktion verschoben werden. Das erledigen wir aber in einem nächsten Schritt.

:::insight[Iterative Entwicklung]
In der Softwareentwicklung gibt es ein wichtiges Arbeitsprinzip: _First do it, then do it right._

Das Programmieren braucht viel mentale Kapazität. Erfahrene Programmiererinnen und Programmierer gehen deshalb am liebsten Schrittweise vor. Wir schreiben unseren Code zuerst einfach mal so, dass er läuft. In einem zweiten Schritt kümmern wir uns dann um die "Code-Qualität" — also um solche Dinge wie die "Pro Szene eine Funktion"-Regel.

Deshalb lassen wir den Satz auf Zeile `7` vorerst einfach mal dort stehen, auch wenn der später dann noch in seine eigene Funktion verschoben werden muss.
:::

```python showLineNumbers {1-10,18}
def fluss():
print("Du stösst auf einen Fluss. Du kannst schwimmen oder eine Brücke suchen.")
auswahl = input("Was möchtest du tun? (schwimmen/brücke): ")
if auswahl == "schwimmen":
print("Du wurdest von einem Krokodil angegriffen! Spiel vorbei!")
elif auswahl == "brücke":
print("Du hast eine versteckte Brücke gefunden und den Fluss sicher überquert.")
print("Du siehst eine Höhle vor dir.")
else:
print("Ungültige Wahl! Spiel vorbei!")

print("Willkommen beim Schatzsuche-Spiel!")
print("Du befindest dich in einem Wald. Du musst den versteckten Schatz finden.")
print("Du stehst an einer Wegkreuzung. Du kannst nach links, rechts oder geradeaus gehen.")

auswahl = input("In welche Richtung möchtest du gehen? (links/rechts/geradeaus): ")
if auswahl == "links":
fluss()
elif auswahl == "rechts":
print("Du begegnest einem Rudel Wölfe. Du kannst entweder rennen oder versuchen, sie zu zähmen.")
elif auswahl == "geradeaus":
print("Du gehst geradeaus und triffst auf eine Riesenspinne.")
else:
print("Ungültige Wahl! Spiel vorbei!")
```

Damit haben Sie bereits das ganze Kernprinzip kennengelernt, nach welchen wir unser Text Adventure entwickeln. In den nächsten Schritten machen wir noch ein paar kleine Anpassungen und bauen die restlichen Szenen weiter aus.

---
Loading

0 comments on commit 3a33fed

Please sign in to comment.