U19 — Spielstand & Formulare

Ziel dieser Übung

Euer Adventure merkt sich euch. Bisher hat die App nur aus der Datenbank gelesen — jetzt schreibt sie hinein: Über ein Formular gebt ihr euren Namen ein, die App speichert per INSERT und UPDATE euren Spielstand. App-Neustart? Ihr macht genau dort weiter, wo ihr wart.

Leitprinzip (wie in u18):
lokal Dateien werden auf dem Laptop geschrieben und gepusht.
server Ausgeführt — und die Datenbank umgebaut — wird auf dem Server.

Voraussetzungen: u18 abgeschlossen — euer Klick-Adventure läuft, das Rätsel (6.3) hat schon ein erstes POST-Formular. Genau das bauen wir jetzt systematisch aus. Es muss nichts installiert werden.

Vorab-Check — läuft euer Adventure aus u18?
Bevor wir den Spielstand anbauen: Bringt euer Klick-Adventure aus u18 zum Laufen und klickt einmal durch die Räume.

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

Dann im Browser https://isa##.edumake.de/app/ aufrufen (Hard Reload Strg/Cmd + Shift + R): Kommt ihr von der Startseite bis ins Rätsel?

Geht nicht? Kurz-Checkliste: Steht (venv) vorne? Seid ihr im Ordner ~/public_html/app? Liegt adventure.db da (ls)? Falls nicht: python3 init_db.py. Im Browser Hard Reload gemacht?

Schritte im Überblick

1init_db.py: Tabelle spielstand ergänzen
2pushen & im Forgejo-GUI prüfen
3Server: App stoppen → init_db.py ausführen
4Startseite wird Formular: Name → INSERT
5jeder Raumwechsel: UPDATE + Weiterspielen
6erweitern (Pflicht + Kür)
Schritt 1 — Tabelle spielstand anlegen (init_db.py)
Eine dritte Tabelle genügt: spielstand merkt sich pro Spieler, in welchem Raum er zuletzt war. spieler ist der Primärschlüssel — jeder Name kommt genau einmal vor.

lokal init_db.py ergänzen

Öffnet init_db.py in VS Code und fügt innerhalb von db.executescript("""…""") — am besten direkt nach den CREATE TABLE-Blöcken — diesen Block ein:

CREATE TABLE IF NOT EXISTS spielstand ( spieler TEXT PRIMARY KEY, raum_id INTEGER );

Datei speichern (Strg/Cmd + S).

Absichtlich anders als bei den Räumen: Für spielstand gibt es kein DROP und kein DELETE. Wenn ihr später neue Räume einbaut und init_db.py erneut lauft, werden die Räume neu aufgebaut — die Spielstände bleiben erhalten.
Schritt 2 — pushen & im GUI prüfen

lokal init_db.py committen

init_db.py → Stage → Commit (Tabelle spielstand) → Sync.

git im GUI prüfen

Auf git.md-phw.de: Ist die neue Tabelle in init_db.py zu sehen? (Datei anklicken → Inhalt prüfen)

Schritt 3 — Server: Datenbank umbauen

server App stoppen, dann init_db.py ausführen

Ihr kennt die Reihenfolge aus u18 — App zuerst stoppen, sonst ist die Datenbank gesperrt:

1. Laufende App beenden:

Strg + C

2. Neuen Stand holen (kommt init_db.py geändert an?):

git pull

3. venv aktivieren (falls nötig) und Skript ausführen:

source venv/bin/activate python3 init_db.py
Datenbank adventure.db angelegt.

Kontrolle: Gibt es die neue Tabelle wirklich? Einmal kurz nachsehen:

python3 -c "import sqlite3; print(sqlite3.connect('adventure.db').execute('SELECT name FROM sqlite_master WHERE type=\"table\"').fetchall())"
[('raeume',), ('ausgaenge',), ('spielstand',)]
Schritt 4 — Startseite wird Formular: Name → INSERT
Neu heute: Bisher hat der Browser Daten nur geholt (GET). Ein Formular mit method="post" schickt Daten an die App — dort landen sie in request.form. Und damit die App sich merkt, wer ihr seid, nutzen wir session (ein Cookie im Browser).

lokal app.py: Import-Zeile und Startroute ersetzen

1. Die erste Zeile und den Kopf der App erweitern — session braucht einen secret_key (eigenen Fantasietext wählen):

from flask import Flask, request, session, redirect import sqlite3 app = Flask(__name__) app.secret_key = "hier-eigenen-geheimen-text" # für session nötig

2. Die Startroute (@app.route("/") samt Funktion) komplett ersetzen:

@app.route("/", methods=["GET", "POST"]) def start(): if request.method == "POST": # Formular abgeschickt? name = request.form["name"].strip() if name != "": session["spieler"] = name # App merkt sich: das bist du db = get_db() db.execute( "INSERT OR IGNORE INTO spielstand (spieler, raum_id) VALUES (?, 1)", (name,) ) db.commit() db.close() return redirect("/app/raum/1") return '<h1>Verlies</h1>' \ + '<form method="post">Dein Name: ' \ + '<input name="name"> <button>Abenteuer starten</button></form>'

Stage → Commit (Startseite mit Formular) → Sync.

Drei neue Bausteine auf einen Blick:
request.form["name"] — was im Eingabefeld <input name="name"> stand.
session["spieler"] — die App merkt sich euren Namen (Cookie), bis der Browser ihn vergisst.
INSERT OR IGNORE — legt den Spieler an; gibt es ihn schon (gleicher Name = Primärschlüssel), passiert nichts — sein Spielstand bleibt.
Das Formular hat kein action — es schickt an dieselbe Adresse. Der /app/-Trick aus u18 funktioniert so einfach weiter.

server testen

git pull python3 app.py

Im Browser https://isa##.edumake.de/app/ (Hard Reload): Namen eingeben → Abenteuer starten → ihr landet in der Eingangshalle. Der INSERT ist passiert — sehen werdet ihr ihn im nächsten Schritt.

Schritt 5 — jeder Raumwechsel speichert: UPDATE + Weiterspielen

lokal app.py: zwei kleine Ergänzungen

1. In der Funktion raum(id)direkt nach db = get_db() — den Spielstand fortschreiben:

if "spieler" in session: # angemeldet? db.execute( "UPDATE spielstand SET raum_id=? WHERE spieler=?", (id, session["spieler"]) ) db.commit()

2. In der Startroute start() die letzte return-Zeile durch diesen Block ersetzen — wer schon einen Spielstand hat, bekommt einen Weiterspielen-Link:

html = '<h1>Verlies</h1>' if "spieler" in session: db = get_db() s = db.execute( "SELECT raum_id FROM spielstand WHERE spieler=?", (session["spieler"],) ).fetchone() db.close() if s: html = html + '<p>Willkommen zurück, ' + session["spieler"] + '!</p>' \ + '<a href="/app/raum/' + str(s["raum_id"]) + '">Weiterspielen</a><hr>' return html + '<form method="post">Dein Name: ' \ + '<input name="name"> <button>Abenteuer starten</button></form>'

Stage → Commit (Spielstand speichern) → Sync.

server der große Test: überlebt der Spielstand einen Neustart?

  1. App neu starten: Strg + Cgit pullpython3 app.py
  2. Im Browser: Name eingeben → zwei, drei Räume weiterklicken
  3. App beenden (Strg + C) und wieder starten (python3 app.py)
  4. Startseite neu laden: „Willkommen zurück…“Weiterspielen → ihr steht im letzten Raum!
Geschafft: Eure App liest und schreibt jetzt. Formular → request.formINSERT/UPDATE → Datenbank. Genau dieses Muster steckt hinter Logins, Warenkörben, Gästebüchern — hinter fast jeder echten Webanwendung.
Warum ? statt Zusammenkleben? Alles, was aus request.form kommt, hat ein Mensch getippt — auch Böswilliges. Mit WHERE spieler=? setzt SQLite den Wert sicher ein. SQL selbst zusammenzukleben ("…" + name + "…") öffnet die Tür für SQL-Injection. Merksatz: Nutzereingaben nie in SQL kleben — immer ?.
Schritt 6 — euer Adventure wächst weiter

Wie immer gilt auf dem Server die Reihenfolge aus u18: App stoppengit pull → (nur bei geändertem init_db.py: python3 init_db.py) → App starten.

6.1   Persönliche Begrüßung im Raum Pflicht

Zeigt in jedem Raum an, wer gerade unterwegs ist. In raum(id) beim Zusammenbauen des HTML:

if "spieler" in session: html = html + '<p><small>Unterwegs als: ' + session["spieler"] + '</small></p>'

6.2   Wer ist alles im Verlies? Pflicht

Die Startseite zeigt, wie viele Abenteurer es schon gibt — eure erste COUNT-Abfrage:

anzahl = db.execute("SELECT COUNT(*) AS n FROM spielstand").fetchone() html = html + '<p>' + str(anzahl["n"]) + ' Abenteurer waren schon hier.</p>'

Wo genau der Code hin muss, überlegt ihr selbst: Er braucht eine offene DB-Verbindung (db = get_db()db.close()).

6.3   Neues Abenteuer Pflicht

Ein Link/Button „Neues Abenteuer“ meldet den Spieler ab. Neue Route:

@app.route("/reset") def reset(): session.pop("spieler", None) # Session vergessen (DB bleibt!) return redirect("/app/")

Link auf der Startseite: <a href="/app/reset">Neues Abenteuer</a>. Was passiert, wenn sich derselbe Name danach wieder anmeldet — und warum? (Tipp: INSERT OR IGNORE)

6.4   Highscore: Wer entkommt am schnellsten? Kür

Jetzt habt ihr alles dafür zusammen (in u18 war das noch Zukunftsmusik):

SELECT spieler, fertig_zeit - start_zeit AS dauer FROM spielstand WHERE fertig_zeit IS NOT NULL ORDER BY dauer LIMIT 10;

6.5   Inventar & Schlüssel Kür

6.6   Gestaltung optional

Gestaltet auch das Formular modern & minimalistisch (passend zu eurem Stil aus u18): dunkle Fläche, Eingabefeld und Button mit abgerundeten Ecken, dezenter Akzent. Mini-Start:

input, button { background:#1e293b; color:#e5e7eb; border:1px solid #334155; border-radius:8px; padding:.4em .8em; font-size:1em; } button:hover { border-color:#818cf8; cursor:pointer; }