import os import time import sqlite3 from flask import Flask, request, redirect, session from datetime import datetime, timezone, timedelta app = Flask(__name__) app.secret_key = "octan-tower-kragle-everything-is-awesome-2026" # für session (Adventure-Fortschritt) # ─── SQLite-Datenbank (U18) ────────────────────────────────────────────────── BASE_DIR = os.path.dirname(os.path.abspath(__file__)) DB_PATH = os.path.join(BASE_DIR, "adventure.db") def get_db(): db = sqlite3.connect(DB_PATH) db.row_factory = sqlite3.Row # Spalten per Name ansprechen return db # ─── Zeitzone: CEST (UTC+2) ────────────────────────────────────────────────── BERLIN = timezone(timedelta(hours=2)) def jetzt(): return datetime.now(BERLIN).strftime("%d.%m.%Y, %H:%M Uhr") # ─── Kommentare im Arbeitsspeicher ─────────────────────────────────────────── # Schema: {"name": str, "text": str, "zeit": str, # "antworten": [{"name": str, "text": str, "zeit": str}]} eintraege = [] # ───────────────────────────────────────────────────────────────────────────── # Hilfsfunktion: gemeinsames HTML-Gerüst # ───────────────────────────────────────────────────────────────────────────── def seite(inhalt): return """ Flask-App – David & Karo
← Zurück zur Website
""" + inhalt + """
""" # ───────────────────────────────────────────────────────────────────────────── # U18 – Klick-Adventure "Einbruch im Octan-Tower" (The-Lego-Movie-Style) # Räume, Ausgänge und Trivia-Fragen kommen aus der SQLite-Datenbank adventure.db # ───────────────────────────────────────────────────────────────────────────── def adventure_seite(inhalt, akzent="#f59e0b"): kopf = """ Einbruch im Octan-Tower – Klick-Adventure
💬 Kommentare ← Website
""".replace("ACCENT", akzent) fuss = """
""" return kopf + inhalt + fuss @app.route("/") def start(): # Fortschritt für einen frischen Durchlauf zurücksetzen for key in [k for k in list(session.keys()) if k.startswith("raum_") or k.startswith("chal_") or k.startswith("fs40")]: session.pop(key, None) inhalt = """
LEGO • Geheimmission

Einbruch im Octan-Tower 🧱

Präsident Business hortet ganz oben in seinem Wolkenkratzer das Kragle – und will damit die ganze Welt für immer festkleben. Nur du kannst ihn stoppen! Kämpf dich Etage für Etage nach oben: vorbei an Haien 🦈, Lasern 🔴 und – natürlich – Laserhaien 🦈⚡.

Jede Etage ist ein Gang, der sich gabelt. Lies die Hinweise genau! Der sichere Prüf-Weg stellt dir ein paar thematische Fragen und führt gemütlich eine Etage höher. Der andere Weg ist eine riskante Abkürzung: Dort wartet jemand mit einem Deal – schaffst du seine Fragen gegen die Uhr, springst du weit nach oben. Ein Fehler oder zu langsam? Mission vorbei. Ganz oben wartet die letzte Frage zu Präsident Business.

Abenteuer starten ▶

Lieber quatschen statt einbrechen? Zu den Kommentaren →

""" return adventure_seite(inhalt) @app.route("/frage") def frage(): return redirect("/app/") # ─── Adventure-Helfer: Sieg / Game Over / Challenge ────────────────────────── def _clear_challenge(rid): """Timer- und Fortschritts-Keys einer Challenge aus der Session entfernen.""" session.pop("chal_%d_deadline" % rid, None) session.pop("chal_%d_idx" % rid, None) session.modified = True def sieg_screen(text=None): body = text or ("Du hast Präsident Business gestoppt und das Kragle gesichert. " "Die Welt bleibt frei – und kreativ!") inhalt = ("""
🎉🧱🎉
Mission erfüllt

Befreit!

%s

„Everything is awesome!“ 🎵

Nochmal spielen 🔁 Sieg in den Kommentaren feiern 💬
""" % esc(body)) return adventure_seite(inhalt) def gameover_screen(grund): inhalt = ("""
💥🤖💥
Mission gescheitert

Erwischt!

%s

Nochmal von vorn ▶ Frust in den Kommentaren lassen 💬
""" % esc(grund)) return adventure_seite(inhalt, akzent="#f87171") def challenge_raum(db, r): """Risk/Reward-Abkürzung mit Zeitlimit (Biologe, Elektriker, Cyborgs).""" rid = r["id"] limit = int(r["zeit_limit"] or 40) ziel = int(r["ziel_anzahl"] or 10) erfolg_raum = r["erfolg_raum"] or 0 erfolg_text = r["erfolg_text"] or "" dl_key = "chal_%d_deadline" % rid idx_key = "chal_%d_idx" % rid fragen = db.execute( "SELECT * FROM fragen WHERE raum_id=? ORDER BY id", (rid,) ).fetchall() back = db.execute( "SELECT nach_raum FROM ausgaenge WHERE von_raum=? ORDER BY rowid LIMIT 1", (rid,) ).fetchone() db.close() back_raum = back["nach_raum"] if back else 1 gesamt = min(ziel, len(fragen)) if fragen else ziel now = time.time() # ── Noch nicht gestartet → Intro (die Entscheidung: Risk vs. Reward) ── if dl_key not in session: if request.args.get("start") == "1" and fragen: session[dl_key] = now + limit session[idx_key] = 0 session.modified = True return redirect("/app/raum/%d" % rid) reward = ("🏆 Bei Erfolg: sofort gewonnen!" if not erfolg_raum else "🏆 Bei Erfolg: Express ganz nach oben") inhalt = ("""
⚠️ Risiko-Abkürzung · Etage %d

%s

%s

🎯 %d Fragen richtig ⏱️ %d Sekunden %s 💀 Ein Fehler = Aus
Herausforderung annehmen ⏱️ Lieber den sicheren Prüf-Weg
""" % (r["etage"] or 0, esc(r["name"]), esc(r["beschreibung"]), gesamt, limit, reward, rid, back_raum)) return adventure_seite(inhalt, akzent="#f87171") # ── Challenge läuft ── deadline = session.get(dl_key, 0) idx = session.get(idx_key, 0) # Zeit abgelaufen? (Server ist die Wahrheit; JS meldet Timeout per ?timeout=1) if now > deadline or request.args.get("timeout") == "1": _clear_challenge(rid) return gameover_screen( "⏱️ Zeit abgelaufen! Die Wachen kommen näher – du musst fliehen. " "Der riskante Weg hat dich diesmal erwischt.") # Antwort verarbeiten if request.method == "POST": gewaehlt = request.form.get("antwort", "").strip() if idx < len(fragen) and gewaehlt.lower() == fragen[idx]["antwort"].strip().lower(): idx += 1 session[idx_key] = idx session.modified = True if idx >= gesamt: _clear_challenge(rid) if erfolg_raum and erfolg_raum > 0: inhalt = ("""
✅ Abkürzung geschafft!

Geschafft – in letzter Sekunde!

%s

Weiter ▶
""" % (esc(erfolg_text), erfolg_raum)) return adventure_seite(inhalt) return sieg_screen(erfolg_text) return redirect("/app/raum/%d" % rid) # falsche Antwort → sofortiges Game Over _clear_challenge(rid) return gameover_screen( "❌ Falsch! Dein Gegenüber durchschaut dich sofort und schlägt Alarm. " "So knapp – aber der riskante Weg verzeiht keinen einzigen Fehler.") # GET → aktuelle Frage mit Countdown zeigen if idx >= len(fragen): _clear_challenge(rid) return sieg_screen(erfolg_text) aktuelle = fragen[idx] remaining = deadline - now buttons = "" for opt in aktuelle["optionen"].split("|"): buttons += ('' % (esc(opt), esc(opt))) tpl = """
⏱️ Risiko-Challenge · __NAME__

__NAME__

__REMAIN__ s

Frage __NR__ von __GES__ · schon richtig: __IDX__

__FRAGE__

__BTN__
""" html = (tpl .replace("__NAME__", esc(r["name"])) .replace("__REMAINMS__", str(int(max(0.0, remaining) * 1000))) .replace("__REMAIN__", "%.1f" % max(0.0, remaining)) .replace("__LIMIT__", str(limit)) .replace("__NR__", str(idx + 1)) .replace("__GES__", str(gesamt)) .replace("__IDX__", str(idx)) .replace("__FRAGE__", esc(aktuelle["frage"])) .replace("__BTN__", buttons) .replace("__ID__", str(rid))) return adventure_seite(html, akzent="#f87171") # ─── Polizei-Führerschein-Kontrolle vor dem Lord-Business-Finale (U18) ──────── # Die drei Aufgaben stammen aus der deutschen Führerschein-Theorieprüfung. # "richtig" = Menge der (1-basierten) anzukreuzenden Optionen. Mehrfachauswahl! FS_FRAGEN = [ { "frage": "Welches Verhalten ist richtig?", "bild": "/fs_frage1.png", "optionen": ["erst nach dem Bus abbiegen", "vor dem Motorrad abbiegen", "als Erster abbiegen"], "richtig": {3}, }, { "frage": "Wann kann es hier zu einer gefährlichen Situation kommen?", "bild": "/fs_frage2.png", "optionen": ["Wenn der Radfahrer am Ende des Radweges anhält", "Wenn ich unvermindert weiterfahre", "Wenn der Radfahrer auf meine Fahrbahn wechselt"], "richtig": {2, 3}, }, { "frage": "Warum ist man nach dem Konsum der Droge „Crystal Meth“ fahruntauglich?", "bild": None, "optionen": ["Weil plötzliche Erschöpfung eintreten kann", "Weil Wahnvorstellungen auftreten können", "Weil die eigene Leistungsfähigkeit überschätzt werden kann"], "richtig": {1, 2, 3}, }, ] FS_IDX_KEY = "fs40_idx" FS_DONE_KEY = "fs40_done" def _blaulicht_intro(): """Grelles deutsches Blaulicht + Polizei-Ansage vor der Kontrolle.""" inhalt = """
🚨
🚔 POLIZEI · VERKEHRSKONTROLLE

Anhalten – Fahrzeugkontrolle!

Kurz vor dem Kragle-Tresor stoppt dich eine Streife. Die Beamten mustern deinen Führerschein misstrauisch: „Diesen Lappen haben Sie doch nie im Leben ehrlich bestanden! Wenn er echt ist, beweisen Sie es: Lösen Sie diese drei Aufgaben aus der Theorieprüfung – dann dürfen Sie weiter.“

🚦 Achtung: Bei manchen Fragen ist mehr als eine Antwort richtig. Kreuze alle zutreffenden an – ein falsches Kreuz und du fällst durch.

Führerschein beweisen 📝
""" return adventure_seite(inhalt, akzent="#1e50ff") def _fs_frage_screen(idx, feedback=""): """Eine Führerschein-Frage im (bewusst hässlichen) grünen Prüfungs-Stil.""" f = FS_FRAGEN[idx] bild_html = "" if f["bild"]: bild_html = ('
Verkehrssituation
' % f["bild"]) checks = "" for i, opt in enumerate(f["optionen"], start=1): checks += (""" """ % (i, esc(opt))) fb = "" if feedback: fb = '
%s
' % esc(feedback) inhalt = """
🚔 THEORIEPRÜFUNG · POLIZEIKONTROLLE Frage __NR__ / __GES__
__FB__

__FRAGE__

__BILD__
__CHECKS__

Mehrfachauswahl möglich – kreuze alle richtigen Antworten an.

""" html = (inhalt .replace("__NR__", str(idx + 1)) .replace("__GES__", str(len(FS_FRAGEN))) .replace("__FB__", fb) .replace("__FRAGE__", esc(f["frage"])) .replace("__BILD__", bild_html) .replace("__CHECKS__", checks)) return adventure_seite(html, akzent="#6b8e23") def fuehrerschein_kontrolle(): """Steuert die dreiteilige Polizei-Führerschein-Kontrolle vor dem Finale.""" # Noch nicht gestartet → Blaulicht-Intro (oder Start per ?fs=1) if FS_IDX_KEY not in session: if request.args.get("fs") == "1": session[FS_IDX_KEY] = 0 session.modified = True return redirect("/app/raum/40") return _blaulicht_intro() idx = session.get(FS_IDX_KEY, 0) feedback = "" if request.method == "POST" and idx < len(FS_FRAGEN): gewaehlt = set() for i in range(1, len(FS_FRAGEN[idx]["optionen"]) + 1): if request.form.get("opt%d" % i): gewaehlt.add(i) if gewaehlt == FS_FRAGEN[idx]["richtig"]: idx += 1 session[FS_IDX_KEY] = idx session.modified = True elif not gewaehlt: feedback = ("🚨 Kein Kreuz gesetzt! „So kommen Sie hier nicht durch.“ " "Kreuze alle richtigen Antworten an.") else: feedback = ("🚨 Durchgefallen bei dieser Aufgabe! Die Beamten schütteln " "den Kopf: „Nochmal – und diesmal richtig.“") if idx >= len(FS_FRAGEN): session[FS_DONE_KEY] = True session.modified = True return redirect("/app/raum/40") return _fs_frage_screen(idx, feedback) @app.route("/raum/", methods=["GET", "POST"]) def raum(id): db = get_db() r = db.execute("SELECT * FROM raeume WHERE id=?", (id,)).fetchone() if r is None: db.close() return adventure_seite( '
Fehler
' '

Diese Etage existiert nicht 🤖

' '

Da hat sich wohl ein Bauplan verheddert.

' 'Zurück zum Start
') typ = r["typ"] # ═══ Risk/Reward-Abkürzung mit Timer (Biologe / Elektriker / Cyborgs) ═══ if typ == "challenge": return challenge_raum(db, r) # ═══ Räume mit Fragen: thema (Prüf-Weg) · ziel (Finale) ═══ if typ in ("thema", "ziel"): # ── Polizei-Führerschein-Kontrolle vor dem Lord-Business-Finale ── if typ == "ziel" and id == 40 and not session.get(FS_DONE_KEY): db.close() return fuehrerschein_kontrolle() fragen = db.execute( "SELECT * FROM fragen WHERE raum_id=? ORDER BY id", (id,) ).fetchall() prog_key = "raum_%d_idx" % id idx = session.get(prog_key, 0) # ── Noch offene Fragen → nächste Frage zeigen ── if fragen and idx < len(fragen): aktuelle = fragen[idx] feedback = "" if request.method == "POST": gewaehlt = request.form.get("antwort", "").strip() if gewaehlt.lower() == aktuelle["antwort"].strip().lower(): session[prog_key] = idx + 1 session.modified = True db.close() return redirect("/app/raum/%d" % id) feedback = ('
🚨 Alarm! Falsche Antwort – ' 'die Wächter werden fast auf dich aufmerksam. Nochmal!
') optionen = aktuelle["optionen"].split("|") db.close() buttons = "" for opt in optionen: buttons += ('' % (esc(opt), esc(opt))) if typ == "ziel": label = "🧱 FINALE · %s" % esc(r["name"]) else: label = "🔐 Sicherheitsprüfung · %s" % esc(r["name"]) inhalt = ("""
%s

%s

%s

Frage %d von %d

%s

%s

%s
""" % (label, esc(r["name"]), esc(r["beschreibung"]), idx + 1, len(fragen), feedback, esc(aktuelle["frage"]), buttons)) return adventure_seite(inhalt) # ── Alle Fragen dieses Raums beantwortet ── ausgaenge = db.execute( "SELECT richtung, nach_raum, hinweis FROM ausgaenge " "WHERE von_raum=? ORDER BY rowid", (id,) ).fetchall() db.close() # ZIEL erreicht → Sieg if typ == "ziel": inhalt = ("""
🎉🧱🎉
Mission erfüllt

Befreit!

%s

„Everything is awesome!“ 🎵

Nochmal spielen 🔁 Sieg in den Kommentaren feiern 💬
""" % esc(r["beschreibung"])) return adventure_seite(inhalt) # THEMA gelöst → Erfolg + Ausgang zur nächsten Etage weiter = ausgaenge[0] if ausgaenge else None weiter_btn = ( '%s' % (weiter["nach_raum"], esc(weiter["richtung"])) if weiter else 'Zurück zum Start') inhalt = ("""
✅ Richtiger Weg

%s

%s

✅ Alle Fragen geknackt – der Weg nach oben ist frei!

%s
""" % (esc(r["name"]), esc(r["beschreibung"]), weiter_btn)) return adventure_seite(inhalt) # ═══ Gang / Start: Beschreibung + Wege (Navigations-Entscheidung) ═══ ausgaenge = db.execute( "SELECT richtung, nach_raum, hinweis FROM ausgaenge " "WHERE von_raum=? ORDER BY rowid", (id,) ).fetchall() db.close() wege_html = "" for a in ausgaenge: wege_html += ("""
%s %s
""" % (a["nach_raum"], esc(a["richtung"]), esc(a["hinweis"] or ""))) if not ausgaenge: wege_html = 'Zurück zum Start' if r["etage"] and r["etage"] > 0: etage_label = "🏢 Etage %d" % r["etage"] else: etage_label = "🚪 Eingang" inhalt = ("""
%s

%s

%s

%s
""" % (etage_label, esc(r["name"]), esc(r["beschreibung"]), wege_html)) return adventure_seite(inhalt) # ───────────────────────────────────────────────────────────────────────────── # Route 3: Pinnwand – YouTube-Kommentar-Style mit Antwort-Funktion # ───────────────────────────────────────────────────────────────────────────── PINNWAND_CSS = """ """ def esc(s): return (s.replace("&", "&") .replace("<", "<") .replace(">", ">") .replace('"', """)) def avatar_farbe(name): farben = ["#e53935", "#8e24aa", "#1e88e5", "#00897b", "#43a047", "#fb8c00", "#6d4c41", "#546e7a"] idx = sum(ord(c) for c in name) % len(farben) return farben[idx] def render_antworten(antworten): html = "" for a in antworten: farbe = avatar_farbe(a["name"]) initial = a["name"][0].upper() html += f"""
{initial}
{esc(a['name'])} {esc(a['zeit'])}
{esc(a['text'])}
""" return html @app.route("/pinnwand", methods=["GET", "POST"]) def pinnwand(): if request.method == "POST": name = request.form.get("name", "").strip() or "Anonym" text = request.form.get("nachricht", "").strip() parent_raw = request.form.get("parent_id", "").strip() if text: if parent_raw.isdigit(): idx = int(parent_raw) if 0 <= idx < len(eintraege): eintraege[idx]["antworten"].append( {"name": name, "text": text, "zeit": jetzt()} ) else: eintraege.append( {"name": name, "text": text, "zeit": jetzt(), "antworten": []} ) return redirect("/app/pinnwand") # ── Kommentare rendern (neueste zuerst, echter Index für parent_id) ── komm_html = "" for real_idx, e in reversed(list(enumerate(eintraege))): farbe = avatar_farbe(e["name"]) initial = e["name"][0].upper() n_ant = len(e["antworten"]) # Block: Antworten anzeigen/verbergen antworten_block = "" if n_ant > 0: label = f"{n_ant} Antwort{'en' if n_ant != 1 else ''}" antworten_block = f""" """ komm_html += f"""
{initial}
{esc(e['name'])} {esc(e['zeit'])}
{esc(e['text'])}
{antworten_block}
""" if not eintraege: komm_html = '

Noch keine Kommentare. Sei der Erste! 💬

' n = len(eintraege) anzahl_text = f"{n} Kommentar{'e' if n != 1 else ''}" return seite(PINNWAND_CSS + f"""
Kommentare
👤

{anzahl_text}

{komm_html}
""") # ───────────────────────────────────────────────────────────────────────────── if __name__ == "__main__": app.run(host="0.0.0.0", port=9002) # isa2 → Port 9002