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 = """
__DRAGDROP_JS__ """ dd_js = """ """ sidebar = _inventar_sidebar() return kopf + _spieler_banner() + sidebar + inhalt + fuss.replace("__DRAGDROP_JS__", dd_js) # ───────────────────────────────────────────────────────────────────────────── # U19 – Spielstand, Inventar, Zeitmessung & Scoreboards # ───────────────────────────────────────────────────────────────────────────── # Der Führerschein liegt von Anfang an im Inventar (Polizei-Kontrolle im Finale). FUEHRERSCHEIN = {"id": "fuehrerschein", "name": "Führerschein", "icon": "🪪"} # Jeder Einzelabschnitt, der zeitlich gemessen wird → eigener Scoreboard-Bereich. STAGE_NAMES = { 11: "Hai-Fragen (Prüf-Weg)", 21: "Laser-Fragen (Prüf-Weg)", 31: "Laserhai-Fragen (Prüf-Weg)", 12: "Abkürzung Biologe", 22: "Abkürzung Elektriker", 32: "Abkürzung Cyborgs", 40: "Polizei-Kontrolle", } # Schlüssel/Zugangs-Items, die man beim Abschließen eines Abschnitts erhält. STAGE_KEYS = { 31: {"id": "laserhai_key", "name": "Laserhai-Schlüssel", "icon": "🦈"}, 12: {"id": "bio_card", "name": "Zugangskarte vom Biologen", "icon": "🔬"}, 22: {"id": "elec_key", "name": "Sicherungsschlüssel des Elektrikers", "icon": "⚡"}, 32: {"id": "cyborg_chip", "name": "Cyborg-Zugangschip", "icon": "🤖"}, } # Alle Items, die den Kragle-Tresor entsperren können. ACCESS_KEYS = ["laserhai_key", "bio_card", "elec_key", "cyborg_chip"] # ── Inventar (Session ist Spielwahrheit, DB spiegelt für Persistenz) ────────── def _inv_list(): return session.get("inv", []) def _inv_has(item_id): return any(x["id"] == item_id for x in session.get("inv", [])) def _inv_add(item): inv = session.get("inv", []) if not any(x["id"] == item["id"] for x in inv): session["inv"] = inv + [item] session.modified = True sp = session.get("spieler") if sp: db = get_db() db.execute("INSERT INTO inventar (spieler, gegenstand) VALUES (?,?)", (sp, item["id"])) db.commit(); db.close() def _inv_remove(item_id): session["inv"] = [x for x in session.get("inv", []) if x["id"] != item_id] session.modified = True sp = session.get("spieler") if sp: db = get_db() db.execute("DELETE FROM inventar WHERE spieler=? AND gegenstand=?", (sp, item_id)) db.commit(); db.close() def _inv_hydrate_from_db(spieler): """Inventar aus der DB in die Session laden (z. B. bei App-Neustart).""" item_map = { "fuehrerschein": dict(FUEHRERSCHEIN), "laserhai_key": STAGE_KEYS[31], "bio_card": STAGE_KEYS[12], "elec_key": STAGE_KEYS[22], "cyborg_chip": STAGE_KEYS[32], } db = get_db() rows = db.execute("SELECT gegenstand FROM inventar WHERE spieler=?", (spieler,)).fetchall() db.close() inv = [] for r in rows: if r["gegenstand"] in item_map: inv.append(dict(item_map[r["gegenstand"]])) session["inv"] = inv session.modified = True def _spieler_banner(): if not session.get("spieler"): return "" return ('
Unterwegs als: %s
' % esc(session["spieler"])) def _inventar_sidebar(): """Fixe Inventar-Leiste links – nur für angemeldete Spieler mit Items.""" inv = session.get("inv", []) if not session.get("spieler") or not inv: return "" items = "" for x in inv: items += ('
%s' '%s
' % (esc(x["id"]), esc(x["name"]), x["icon"], esc(x["name"]))) return ('' % items) # ── Zeitmessung: Einzelabschnitte (Stages) ─────────────────────────────────── def _stage_start(rid): key = "stg_%d" % rid if key not in session and not session.get("stgdone_%d" % rid): session[key] = time.time() session.modified = True def _stage_finish(rid, grant_key=True): """Dauer des Abschnitts speichern; optional zugehörigen Schlüssel vergeben.""" key = "stg_%d" % rid done_key = "stgdone_%d" % rid sp = session.get("spieler") if sp and key in session and not session.get(done_key): dauer = time.time() - session[key] db = get_db() db.execute("INSERT INTO stage_zeiten (spieler, stage, dauer, zeit) " "VALUES (?,?,?,?)", (sp, STAGE_NAMES.get(rid, "Stage %d" % rid), dauer, time.time())) db.commit(); db.close() session[done_key] = True session.pop(key, None) session.modified = True k = STAGE_KEYS.get(rid) if grant_key and k: _inv_add(k) # ── Gesamtlauf protokollieren (Sieg/Niederlage) ────────────────────────────── def _record_run(status): sp = session.get("spieler"); st = session.get("run_start") if sp and st: now = time.time(); dauer = now - st db = get_db() db.execute("INSERT INTO laeufe (spieler, start_zeit, fertig_zeit, dauer, status) " "VALUES (?,?,?,?,?)", (sp, st, now, dauer, status)) if status == "gewonnen": db.execute("UPDATE spielstand SET fertig_zeit=? WHERE spieler=?", (now, sp)) # start_zeit leeren, damit der Lauf nicht erneut (re-hydratisiert) gezählt wird db.execute("UPDATE spielstand SET start_zeit=NULL WHERE spieler=?", (sp,)) db.commit(); db.close() session.pop("run_start", None) session.modified = True def _run_reset_keys(): """Alle lauf-spezifischen Session-Keys entfernen (Name bleibt erhalten).""" for key in [k for k in list(session.keys()) if k.startswith("raum_") or k.startswith("chal_") or k.startswith("stg_") or k.startswith("stgdone_") or k.startswith("fs40") or k in ("inv", "run_start", "vault_unlocked")]: session.pop(key, None) session.modified = True # ── Zeit-Formatierung & Scoreboards ────────────────────────────────────────── def _fmt_zeit(sek): if sek is None: return "–" sek = float(sek) if sek < 60: return "%.1f s" % sek m = int(sek // 60); s = sek - m * 60 return "%d:%04.1f min" % (m, s) def _sb_gesamt(limit=10): db = get_db() try: rows = db.execute( "SELECT spieler, dauer AS best FROM laeufe " "WHERE status='gewonnen' ORDER BY dauer LIMIT ?", (limit,) ).fetchall() except sqlite3.OperationalError: rows = [] db.close(); return rows def _sb_stage(stage, limit=3): db = get_db() try: rows = db.execute( "SELECT spieler, dauer AS best FROM stage_zeiten " "WHERE stage=? ORDER BY dauer LIMIT ?", (stage, limit) ).fetchall() except sqlite3.OperationalError: rows = [] db.close(); return rows def _scoreboard_html(): ges = _sb_gesamt(10) rows = "" if ges: for i, r in enumerate(ges, 1): medal = ["🥇", "🥈", "🥉"][i - 1] if i <= 3 else ("%d." % i) rows += ('%s%s' '%s' % (medal, esc(r["spieler"]), _fmt_zeit(r["best"]))) else: rows = ('Noch keine Zeiten – ' 'sei die/der Erste, die/der entkommt!') stage_blocks = "" for rid in sorted(STAGE_NAMES): name = STAGE_NAMES[rid] st = _sb_stage(name, 3) items = "" if st: for i, r in enumerate(st, 1): items += ('
  • %d. %s' '%s
  • ' % (i, esc(r["spieler"]), _fmt_zeit(r["best"]))) else: items = '
  • — noch leer —
  • ' stage_blocks += ('

    %s

    ' % (esc(name), items)) return ("""

    🏁 Bestenliste – schnellster Einbruch

    %s

    ⏱️ Bestzeiten pro Abschnitt

    %s
    """ % (rows, stage_blocks)) @app.route("/", methods=["GET", "POST"]) def start(): # ── Formular abgeschickt: Name → INSERT + neuen Lauf starten ── if request.method == "POST": name = request.form.get("name", "").strip() if name: _run_reset_keys() # evtl. alten Lauf verwerfen session["spieler"] = name # App merkt sich: das bist du session["run_start"] = time.time() # Gesamtzeit-Messung startet session["inv"] = [dict(FUEHRERSCHEIN)] # Führerschein von Anfang an session.modified = True db = get_db() db.execute( "INSERT OR IGNORE INTO spielstand (spieler, raum_id, start_zeit) " "VALUES (?, 1, ?)", (name, session["run_start"])) db.execute("UPDATE spielstand SET raum_id=1, start_zeit=?, fertig_zeit=NULL " "WHERE spieler=?", (session["run_start"], name)) db.execute("DELETE FROM inventar WHERE spieler=?", (name,)) db.execute("INSERT INTO inventar (spieler, gegenstand) VALUES (?,?)", (name, FUEHRERSCHEIN["id"])) db.commit(); db.close() return redirect("/app/raum/1") intro = """
    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 gabelt sich: Der sichere Prüf-Weg stellt thematische Fragen, die riskante Abkürzung läuft gegen die Uhr. Unterwegs sammelst du Schlüssel ins Inventar – ganz oben brauchst du einen davon, um den Kragle-Tresor zu öffnen. Deine Zeit läuft mit und landet in der Bestenliste!

    __MITTE__
    """ # ── Angemeldet? Dann „Willkommen zurück“ + Weiterspielen ── mitte = "" if session.get("spieler"): sp = session["spieler"] db = get_db() s = db.execute("SELECT raum_id, start_zeit FROM spielstand WHERE spieler=?", (sp,)).fetchone() anzahl = db.execute("SELECT COUNT(*) AS n FROM spielstand").fetchone() db.close() if not session.get("inv"): _inv_hydrate_from_db(sp) if s and s["start_zeit"] and not session.get("run_start"): session["run_start"] = float(s["start_zeit"]) session.modified = True weiter_raum = s["raum_id"] if s and s["raum_id"] else 1 mitte = ( '

    Willkommen zurück, %s! 🎉

    ' 'Weiterspielen ▶' 'Neuer Versuch 🔁' 'Anderer Name 🚪' '

    %d Abenteurer:innen waren schon hier.

    ' % (esc(sp), weiter_raum, anzahl["n"])) else: db = get_db() anzahl = db.execute("SELECT COUNT(*) AS n FROM spielstand").fetchone() db.close() mitte = ( '
    ' '' '' '
    ' '

    %d Abenteurer:innen waren schon hier. ' 'Lieber quatschen? Zu den Kommentaren →

    ' % anzahl["n"]) inhalt = intro.replace("__MITTE__", mitte) + _scoreboard_html() return adventure_seite(inhalt) @app.route("/neustart") def neustart(): """Gleicher Name, neuer Lauf – Fortschritt & Zeit zurücksetzen.""" _run_reset_keys() sp = session.get("spieler") if not sp: return redirect("/app/") session["run_start"] = time.time() session["inv"] = [dict(FUEHRERSCHEIN)] session.modified = True db = get_db() db.execute("UPDATE spielstand SET raum_id=1, start_zeit=?, fertig_zeit=NULL " "WHERE spieler=?", (session["run_start"], sp)) db.execute("DELETE FROM inventar WHERE spieler=?", (sp,)) db.execute("INSERT INTO inventar (spieler, gegenstand) VALUES (?,?)", (sp, FUEHRERSCHEIN["id"])) db.commit(); db.close() return redirect("/app/raum/1") @app.route("/reset") def reset(): """Anderer Name – Session vergessen (Datenbank/Bestzeiten bleiben!).""" _run_reset_keys() session.pop("spieler", None) session.modified = True return redirect("/app/") @app.route("/scoreboard") def scoreboard(): zurueck = ('Weiterspielen ▶' if session.get("spieler") else "") inhalt = ("""
    🏆 Bestenlisten

    Wer entkommt am schnellsten?

    Gesamtzeit vom Start bis zum Kragle-Tresor – plus die Bestzeiten für jeden einzelnen Abschnitt.

    Zur Startseite ▶%s
    %s""" % (zurueck, _scoreboard_html())) 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!") _record_run("gewonnen") zeit_html = "" inhalt = ("""
    🎉🧱🎉
    Mission erfüllt

    Befreit!

    %s

    „Everything is awesome!“ 🎵

    Nochmal spielen 🔁 🏆 Scoreboard ansehen
    %s""" % (esc(body), zeit_html)) return adventure_seite(inhalt) def gameover_screen(grund): _record_run("gescheitert") inhalt = ("""
    💥🤖💥
    Mission gescheitert

    Erwischt!

    %s

    Erneut versuchen ▶ 🏆 Scoreboard ansehen
    """ % 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 _stage_start(rid) # Zeitmessung dieses Abschnitts 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) _stage_finish(rid, grant_key=False) 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) _stage_finish(rid) # Zeit speichern + Schlüssel vergeben if erfolg_raum and erfolg_raum > 0: k = STAGE_KEYS.get(rid) key_hinweis = ("" if not k else '

    🎒 Erhalten: %s ' '%s – damit öffnest du oben den Kragle-Tresor.

    ' % (esc(k["name"]), k["icon"])) inhalt = ("""
    ✅ Abkürzung geschafft!

    Geschafft – in letzter Sekunde!

    %s

    %s Weiter ▶
    """ % (esc(erfolg_text), key_hinweis, 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) _stage_finish(rid, grant_key=False) 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 hier richtig?", "bild": "/fs_frage1.png", "optionen": ["Ich muss den roten Bus durchfahren lassen", "Ich darf vor dem blauen Pkw fahren", "Ich muss den blauen Pkw durchfahren lassen"], "richtig": {1, 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 _tresor_schloss(): """Finale-Gate: Der Kragle-Tresor lässt nur mit Zugangsschlüssel öffnen.""" have = [k for k in ACCESS_KEYS if _inv_has(k)] accept = ",".join(have) if have else "none" namen = {"laserhai_key": "Laserhai-Schlüssel", "bio_card": "Zugangskarte vom Biologen", "elec_key": "Sicherungsschlüssel des Elektrikers", "cyborg_chip": "Cyborg-Chip"} besitz = (", ".join(namen[k] for k in have) if have else "leider kein passender Schlüssel") inhalt = ("""
    🔒 Kragle-Tresor · Zugang gesperrt

    Der Tresor ist verriegelt

    Ganz oben – doch ein schweres Sicherheitsschloss versperrt den letzten Schritt. Setze deinen Zugangsschlüssel ein: Zieh ihn aus dem Inventar (links) auf das Schloss.

    🔒 Schlüssel hierher ziehen, um den Tresor zu öffnen

    🎒 In deinem Besitz: %s

    """ % (accept, esc(besitz))) return adventure_seite(inhalt, akzent="#facc15") 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. Ein Beamter tritt ans Fenster: „Guten Tag. Fahrzeugkontrolle – Ihren Führerschein bitte!“

    🪪 Zieh deinen Führerschein aus dem Inventar (links) hierher, um ihn den Beamten vorzuzeigen.
    """ return adventure_seite(inhalt, akzent="#1e50ff") def _fs_ansage(): """Nach dem Vorzeigen des Führerscheins: misstrauische Ansage der Beamten.""" inhalt = """
    🚨
    🚔 POLIZEI · FÜHRERSCHEIN-PRÜFUNG

    „Der ist doch nie echt!“

    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.

    Theorieprüfung starten ▶
    """ 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.""" # Ablauf: Blaulicht + Führerschein droppen → misstrauische Ansage → 3 Fragen 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") if request.args.get("fs") == "gezeigt": return _fs_ansage() # Führerschein vorgezeigt → Ansage return _blaulicht_intro() # Kontrolle: Führerschein hierher ziehen 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 _stage_finish(40) # Zeit der Polizei-Kontrolle speichern 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
    ') # Spielstand fortschreiben: Wo war der Spieler zuletzt? (UPDATE) if session.get("spieler"): db.execute("UPDATE spielstand SET raum_id=? WHERE spieler=?", (id, session["spieler"])) if not session.get("inv"): _inv_hydrate_from_db(session["spieler"]) if not session.get("run_start"): st = db.execute("SELECT start_zeit FROM spielstand WHERE spieler=?", (session["spieler"],)).fetchone() if st and st["start_zeit"]: session["run_start"] = float(st["start_zeit"]) session.modified = True db.commit() 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"): # ── Finale (Raum 40): erst Polizei-Kontrolle, dann Tresor-Schloss ── if typ == "ziel" and id == 40: # 1) Polizei-Führerschein-Kontrolle zuerst if not session.get(FS_DONE_KEY): _stage_start(40) # Polizei-Kontrolle Zeitmessung db.close() return fuehrerschein_kontrolle() # 2) Danach: Zugangsschlüssel in den Kragle-Tresor (per Drag&Drop) if request.args.get("unlock") == "1" and not session.get("vault_unlocked"): used = next((k for k in ACCESS_KEYS if _inv_has(k)), None) if used: _inv_remove(used) session["vault_unlocked"] = True session.modified = True db.close() return redirect("/app/raum/40") if not session.get("vault_unlocked"): db.close() return _tresor_schloss() # ── Prüf-Weg-Räume: Zeitmessung dieses Abschnitts startet ── if typ == "thema": _stage_start(id) 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": return sieg_screen(r["beschreibung"]) # THEMA gelöst → Erfolg + Ausgang zur nächsten Etage _stage_finish(id) # Zeit speichern (+ ggf. Schlüssel) k = STAGE_KEYS.get(id) key_hinweis = ("" if not k else '

    🎒 Erhalten: %s %s – ' 'damit öffnest du oben den Kragle-Tresor.

    ' % (esc(k["name"]), k["icon"])) 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 %s
    """ % (esc(r["name"]), esc(r["beschreibung"]), key_hinweis, 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