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.

Vorab-Check — läuft eure App von letzter Woche noch?
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)
Schritt 1 — Datenmodell lokal anlegen (init_db.py)
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.

  1. In VS Code euer Projekt (das geklonte Repo) öffnen.
  2. Im Datei-Explorer links mit Rechtsklick auf den Ordner app/„New File“ / „Neue Datei“.
  3. Die Datei exakt init_db.py nennen (Endung .py nicht vergessen).
  4. 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).

Schritt 2 — pushen & im GUI prüfen

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.
Schritt 3 — Server: Datenbank anlegen

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.
Schritt 4 — app.py: Räume aus der Datenbank anzeigen

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> &nbsp; ' 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).
Schritt 5 — starten & durchklicken

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.
Schritt 6 — euer Adventure wächst

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

  1. App beenden: Strg + C
  2. Neuen Stand holen: git pull
  3. venv aktivieren (falls nötig): source venv/bin/activate
  4. 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).
  5. 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.

  1. In init_db.py zwei Zeilen ergänzen (unten).
  2. Auf dem Server python3 init_db.py erneut ausführen.
  3. 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 Kür

Wer war am schnellsten aus dem Verlies?

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 Kür

import random richtungen = ["links", "rechts", "hoch"] falle = random.choice(richtungen) # zufälliger Ausgang

6.6   Gestaltung optional

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