U18 — Klick-Adventure mit SQLite
Ziel dieser Übung
Ihre Flask-App bekommt eine Datenbank. Aus der In-Memory-Pinnwand (die beim Neustart alles vergisst) wird ein Klick-Adventure, dessen Räume dauerhaft in einer SQLite-Datenbank liegen. Sie klicken sich von Raum zu Raum — die Daten kommen aus der DB.
Leitprinzip (wie in u17):
lokal Dateien werden auf dem Laptop geschrieben und gepusht.
server Ausgeführt — und die Datenbank angelegt — wird auf dem Server.
Voraussetzungen: u17 abgeschlossen — die venv im Ordner public_html/app/ steht bereits. SQLite ist in Python eingebaut — es muss nichts installiert werden.
Bevor wir die Datenbank anbauen: Bringt zuerst eure Flask-App aus u17 wieder zum Laufen und prüft, dass alles funktioniert.
server App starten & im Browser prüfen
1. In den App-Ordner wechseln:
cd ~/public_html/app
2. Virtuelle Umgebung aktivieren (danach steht (venv) vorne):
source venv/bin/activate
3. App starten:
python3 app.py
Es erscheint Running on http://0.0.0.0:90## — Terminal offen lassen. Dann im Browser https://isa##.edumake.de/app/ aufrufen (Hard Reload Strg/Cmd + Shift + R): Erscheint eure Seite/Route aus u17?
Geht nicht? Kurz-Checkliste (aus dem Tandem Lab): Steht (venv) vorne? Seid ihr im Ordner ~/public_html/app? Ist das Terminal noch offen? Im Browser Hard Reload gemacht?
Schritte im Überblick
1init_db.py lokal anlegen (Tabellen + Starträume)
2pushen & im Forgejo-GUI prüfen
3Server: init_db.py ausführen → adventure.db
4app.py: Räume aus der DB anzeigen
5starten & durchklicken
6erweitern (Pflicht + Kür)
Zwei Tabellen genügen für ein Adventure: raeume (welche Räume gibt es?) und ausgaenge (welcher Ausgang führt wohin?).
lokal Datei init_db.py in public_html/app/ anlegen
Dieses Skript legt die Tabellen an und füllt drei Starträume ein. Es wird später einmal auf dem Server ausgeführt.
- In VS Code euer Projekt (das geklonte Repo) öffnen.
- Im Datei-Explorer links mit Rechtsklick auf den Ordner
app/ → „New File“ / „Neue Datei“.
- Die Datei exakt
init_db.py nennen (Endung .py nicht vergessen).
- Den folgenden Code hineinkopieren:
import sqlite3
db = sqlite3.connect("adventure.db") # Datei wird angelegt, falls nicht da
db.executescript("""
CREATE TABLE IF NOT EXISTS raeume (
id INTEGER PRIMARY KEY,
name TEXT,
beschreibung TEXT
);
CREATE TABLE IF NOT EXISTS ausgaenge (
von_raum INTEGER,
richtung TEXT,
nach_raum INTEGER
);
DELETE FROM raeume;
DELETE FROM ausgaenge;
INSERT INTO raeume (id, name, beschreibung) VALUES
(1, 'Eingangshalle', 'Eine schwere Tür fällt hinter dir zu. Zwei Gänge führen ins Dunkel.'),
(2, 'Krypta', 'Kaltes Kerzenlicht flackert an den Wänden.'),
(3, 'Bibliothek', 'Staubige Bücher stapeln sich bis zur Decke.');
INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES
(1, 'links', 2),
(1, 'rechts', 3),
(2, 'zurück', 1),
(3, 'zurück', 1);
""")
db.commit()
db.close()
print("Datenbank adventure.db angelegt.")
Datei speichern (Strg/Cmd + S).
lokal init_db.py committen
init_db.py → Stage → Commit (Datenbank-Setup) → Sync.
git im GUI prüfen
Auf git.md-phw.de: Ist init_db.py im Repo angekommen?
Und die Datenbank-Datei? Die muss nicht in die .gitignore: adventure.db entsteht erst auf dem Server, und der Server pullt nur (Auto-Deploy), er pusht nie. Die Datei ist dort untracked und bleibt erhalten.
Sicherheitsnetz (optional): Wer init_db.py auch mal lokal ausprobiert, schreibt *.db in die .gitignore — sonst könnte eine lokal committete .db die Server-Datenbank beim Deploy überschreiben.
server init_db.py einmalig ausführen
Verbindet euch per SSH und geht die Befehle einzeln durch:
1. In den App-Ordner wechseln (dort liegt init_db.py — und dort entsteht gleich adventure.db):
cd ~/public_html/app
2. Nachsehen, ob init_db.py per Auto-Deploy angekommen ist:
ls
app.py init_db.py venv
3. Die virtuelle Umgebung aktivieren (danach steht (venv) vorne):
source venv/bin/activate
4. Das Skript ausführen — jetzt wird die Datenbank angelegt:
python3 init_db.py
Datenbank adventure.db angelegt.
Kontrolle: Liegt die Datei jetzt wirklich da?
ls
adventure.db app.py init_db.py venv
Wichtig: adventure.db entsteht hier auf dem Server (durch init_db.py) — nicht lokal. Sie ist untracked, und Auto-Deploy (git reset --hard) lässt untrackte Dateien in Ruhe — die Datenbank bleibt also bei jedem Deploy erhalten.
lokal app.py ersetzen
Die App liest jetzt Raum und Ausgänge aus der Datenbank und baut daraus Klick-Links. Ersetzen Sie ## durch Ihre Raumnummer.
from flask import Flask
import sqlite3
app = Flask(__name__)
def get_db():
db = sqlite3.connect("adventure.db")
db.row_factory = sqlite3.Row # Spalten per Name ansprechen
return db
@app.route("/")
def start():
return '<h1>Verlies</h1><a href="/app/raum/1">Abenteuer starten</a>'
@app.route("/raum/<int:id>")
def raum(id):
db = get_db()
r = db.execute("SELECT name, beschreibung FROM raeume WHERE id=?", (id,)).fetchone()
ausgaenge = db.execute(
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,)
).fetchall()
db.close()
html = "<h1>" + r["name"] + "</h1>"
html = html + "<p>" + r["beschreibung"] + "</p>"
for a in ausgaenge:
html = html + '<a href="/app/raum/' + str(a["nach_raum"]) + '">' \
+ a["richtung"] + '</a> '
return html
if __name__ == "__main__":
app.run(host="0.0.0.0", port=90##)
Stage → Commit (Adventure aus DB) → Sync.
Der /app/-Trick: Der Server leitet alle /app/-Adressen an eure Flask-App weiter. Deshalb müssen die Links im HTML mit /app/raum/… beginnen — sonst landet der Klick auf der statischen Seite.
? in WHERE id=? setzt den Wert sicher ein (kein Zusammenkleben von SQL — schützt vor Fehlern & Angriffen).
server App neu starten
cd ~/public_html/app
git pull
source venv/bin/activate
python3 app.py
Im Browser (Hard Reload Strg/Cmd + Shift + R):
https://isa##.edumake.de/app/
„Abenteuer starten“ → ihr steht in der Eingangshalle → links / rechts anklicken → ihr wechselt die Räume. Die Inhalte kommen alle aus der Datenbank.
Geschafft: Ihr habt ein funktionierendes, datenbankgestütztes Klick-Adventure. Startet die App neu — die Räume sind alle noch da (anders als die Pinnwand!). Das ist der Sinn einer Datenbank.
Alles Weitere baut auf dem funktionierenden Grundgerüst auf. Wenn ihr etwas ändert (lokal schreiben → pushen), lauft auf dem Server immer diese Reihenfolge durch — App zuerst stoppen, zuletzt wieder starten:
server Sauberer Ablauf bei jeder Änderung
- App beenden:
Strg + C
- Neuen Stand holen:
git pull
- venv aktivieren (falls nötig):
source venv/bin/activate
- Nur wenn
init_db.py geändert: python3 init_db.py — die Datenbank neu aufbauen. Das geht nur sauber, solange die App gestoppt ist (sonst ist die Datenbank gesperrt).
- App zuletzt wieder starten:
python3 app.py
Warum diese Reihenfolge? Die laufende App hält die Datenbank offen. Ein DROP/DELETE währenddessen führt zu „database is locked“ oder halbfertigem Zustand. Erst stoppen → umbauen → einmal sauber starten.
6.1 Eigener Raum Pflicht
Erweitert das Verlies um einen vierten Raum mit mindestens einem Ausgang dorthin.
- In
init_db.py zwei Zeilen ergänzen (unten).
- Auf dem Server
python3 init_db.py erneut ausführen.
- Im Browser zum neuen Raum durchklicken.
INSERT INTO raeume (id, name, beschreibung) VALUES
(4, 'Schatzkammer', 'Eine verschlossene Truhe glänzt im Halbdunkel.');
INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES
(2, 'vor', 4);
6.2 Mehr als zwei Wege Pflicht
Ein Raum darf beliebig viele Ausgänge haben. Gebt mindestens einem Raum drei Ausgänge — einfach weitere Zeilen in ausgaenge:
INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES
(2, 'vor', 4),
(2, 'zurück', 1),
(2, 'hoch', 3);
Kein Eingriff in app.py nötig — die App baut für jeden gefundenen Ausgang automatisch einen Button.
6.3 Ein echtes Rätsel im Zielraum Pflicht
Der Zielraum stellt eine Frage. Nur die richtige Antwort führt zu „Befreit!“.
a) In init_db.py: zwei Spalten ergänzen und im Zielraum eine Frage hinterlegen. Damit die neue Struktur greift, die Tabellen am Anfang neu aufbauen (DROP statt DELETE):
db.executescript("""
DROP TABLE IF EXISTS raeume;
DROP TABLE IF EXISTS ausgaenge;
CREATE TABLE raeume (
id INTEGER PRIMARY KEY,
name TEXT,
beschreibung TEXT,
raetsel_frage TEXT,
raetsel_antwort TEXT
);
CREATE TABLE ausgaenge (von_raum INTEGER, richtung TEXT, nach_raum INTEGER);
INSERT INTO raeume (id, name, beschreibung, raetsel_frage, raetsel_antwort) VALUES
(1, 'Eingangshalle', 'Eine schwere Tür fällt hinter dir zu.', NULL, NULL),
(2, 'Krypta', 'Kaltes Kerzenlicht flackert.', NULL, NULL),
(3, 'Bibliothek', 'Staubige Bücher bis zur Decke.', NULL, NULL),
(4, 'Schatzkammer', 'Eine verschlossene Truhe.', 'Wie viele Beine hat eine Spinne?', '8');
""")
b) In app.py: die Route um POST erweitern und im Rätselraum ein Formular zeigen.
from flask import Flask, request # request nicht vergessen
@app.route("/raum/<int:id>", methods=["GET", "POST"])
def raum(id):
db = get_db()
r = db.execute("SELECT * FROM raeume WHERE id=?", (id,)).fetchone()
if r["raetsel_frage"]: # Rätselraum?
if request.method == "POST":
if request.form["antwort"].strip() == r["raetsel_antwort"]:
return "<h1>Befreit!</h1>"
return 'Leider falsch. <a href="/app/raum/' + str(id) + '">nochmal</a>'
return "<h1>" + r["name"] + "</h1><p>" + r["beschreibung"] + "</p>" \
+ '<form method="post">' + r["raetsel_frage"] \
+ ' <input name="antwort"><button>OK</button></form>'
# sonst: normaler Raum mit Ausgängen (wie bisher)
ausgaenge = db.execute("SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,)).fetchall()
...
Danach: python3 init_db.py → App neu starten → im Zielraum die Frage beantworten.
6.4 Spielername, Zeit & Highscore
Wer war am schnellsten aus dem Verlies?
- Neue Tabelle
spieler (id, name, start_zeit, fertig_zeit).
- Startseite fragt den Namen ab; beim Start
start_zeit speichern (import time → time.time()), im Zielraum fertig_zeit.
- Den aktuellen Spieler merkt ihr euch mit
session (Flask, braucht app.secret_key).
Highscore-Abfrage (schnellste zuerst):
SELECT name, fertig_zeit - start_zeit AS dauer
FROM spieler
WHERE fertig_zeit IS NOT NULL
ORDER BY dauer
LIMIT 10;
6.5 Für Schnelle: Zufall & Storytelling
- Mit
import random und random.choice(...) einen Ausgang auslosen (Falltür!).
- Baut die Beschreibungen zu einer echten Geschichte aus — Stimmung, Hinweise, ein Ziel.
import random
richtungen = ["links", "rechts", "hoch"]
falle = random.choice(richtungen) # zufälliger Ausgang
6.6 Gestaltung
Gestaltet die Ausgabe modern & minimalistisch: dunkle Fläche, klare moderne Schrift, dezenter Akzent, Ausgänge als Buttons. Kein 3D, keine Spielereien. Eigene Ideen & Farben ausdrücklich erwünscht. Mini-Start — einfach vor euer html setzen:
return """<style>
body{background:#0f172a;color:#e5e7eb;font-family:system-ui,sans-serif;
max-width:640px;margin:3rem auto;padding:0 1rem;line-height:1.6}
a{display:inline-block;border:1px solid #334155;border-radius:8px;
padding:.4em 1em;margin:.3em .3em 0 0;color:#e5e7eb;text-decoration:none}
</style>""" + html