diff --git a/app.py b/app.py index d650922..8fb620b 100644 --- a/app.py +++ b/app.py @@ -135,6 +135,9 @@ def adventure_seite(inhalt, akzent="#f59e0b"): border:1px solid #334155; border-radius:8px; padding:6px 12px; } .adv-topbar .link:hover { background:#1e293b; } .adv-topbar .spacer { margin-left:auto; } + .adv-player { max-width:680px; margin:14px auto 0; padding:0 18px; + color:#cbd5e1; font-size:13px; } + .adv-player strong { color:#facc15; } .adv-wrap { max-width:680px; margin:44px auto; padding:0 18px; } .adv-card { background:#1e293b; border:1px solid #334155; border-radius:16px; padding:30px; box-shadow:0 10px 30px rgba(0,0,0,.35); } @@ -182,6 +185,68 @@ def adventure_seite(inhalt, akzent="#f59e0b"): .win-emoji { font-size:54px; text-align:center; margin-bottom:8px; } .muted { color:#94a3b8; font-size:14px; margin-top:20px; } .muted a { color:#93c5fd; } + + /* ── Inventar-Sidebar (U19) ── */ + .inv-panel { position:fixed; top:90px; left:18px; width:190px; z-index:40; + background:#111827; border:1px solid #334155; border-radius:14px; + padding:14px; box-shadow:0 10px 30px rgba(0,0,0,.4); } + .inv-title { font-size:13px; font-weight:800; letter-spacing:1px; + text-transform:uppercase; color:#facc15; margin-bottom:10px; } + .inv-items { display:flex; flex-direction:column; gap:10px; } + .inv-item { display:flex; align-items:center; gap:10px; cursor:grab; + background:#0f172a; border:1px solid #334155; border-radius:10px; + padding:9px 10px; transition:.12s; user-select:none; } + .inv-item:hover { border-color:#facc15; transform:translateY(-1px); } + .inv-item:active { cursor:grabbing; } + .inv-item.dragging { opacity:.4; } + .inv-ico { font-size:22px; line-height:1; } + .inv-lbl { font-size:12px; color:#e5e7eb; line-height:1.25; } + .inv-hint { margin-top:12px; font-size:11px; color:#64748b; line-height:1.4; } + /* ── Drop-Zone ── */ + .dropzone { border:2px dashed #64748b; border-radius:14px; padding:26px 18px; + text-align:center; color:#94a3b8; transition:.15s; + background:#0f172a; margin:6px 0 18px; } + .dropzone .dz-ico { font-size:38px; display:block; margin-bottom:8px; } + .dropzone.over { border-color:#22c55e; background:#0d2818; color:#bbf7d0; + transform:scale(1.02); } + .dropzone.wrong { border-color:#ef4444; background:#2a0d0d; + animation:dzshake .5s; } + @keyframes dzshake { 0%,100%{transform:translateX(0)} 25%{transform:translateX(-8px)} + 75%{transform:translateX(8px)} } + /* ── Scoreboard (U19) ── */ + .sb-wrap { margin-top:26px; } + .sb-box { background:#111827; border:1px solid #334155; border-radius:16px; + padding:20px 22px; margin-bottom:18px; } + .sb-box h3 { font-size:16px; color:#facc15; margin-bottom:12px; + display:flex; align-items:center; gap:8px; } + .sb-table { width:100%; border-collapse:collapse; } + .sb-table td { padding:8px 6px; border-bottom:1px solid #1f2a3a; font-size:15px; } + .sb-table tr:last-child td { border-bottom:none; } + .sb-rank { width:44px; font-weight:800; } + .sb-time { text-align:right; color:#67e8f9; font-variant-numeric:tabular-nums; + font-weight:700; white-space:nowrap; } + .sb-empty { color:#64748b; font-style:italic; } + .sb-stages { display:grid; grid-template-columns:repeat(auto-fill,minmax(200px,1fr)); + gap:12px; } + .sb-stage { background:#0f172a; border:1px solid #334155; border-radius:12px; + padding:12px 14px; } + .sb-stage h4 { font-size:13px; color:#93c5fd; margin-bottom:8px; } + .sb-stage ul { list-style:none; display:flex; flex-direction:column; gap:5px; } + .sb-stage li { font-size:13px; color:#cbd5e1; display:flex; gap:6px; + align-items:baseline; } + .sb-mini-rank { color:#64748b; font-weight:700; } + .sb-mini-time { margin-left:auto; color:#67e8f9; font-variant-numeric:tabular-nums; } + .name-form { display:flex; flex-wrap:wrap; gap:10px; margin-top:14px; } + .name-form input { background:#0f172a; color:#e5e7eb; border:1px solid #334155; + border-radius:10px; padding:12px 14px; font-size:16px; + flex:1; min-width:180px; font-family:inherit; } + .name-form input:focus { outline:none; border-color:#facc15; } + @media (max-width:1100px){ + .inv-panel { position:static; width:auto; margin:0 auto 18px; + max-width:680px; } + .inv-items { flex-direction:row; flex-wrap:wrap; } + .inv-hint { display:none; } + } @@ -192,19 +257,293 @@ def adventure_seite(inhalt, akzent="#f59e0b"):
""".replace("ACCENT", akzent) fuss = """
+ __DRAGDROP_JS__ """ - return kopf + inhalt + fuss + dd_js = """ + """ + sidebar = _inventar_sidebar() + return kopf + _spieler_banner() + sidebar + inhalt + fuss.replace("__DRAGDROP_JS__", dd_js) -@app.route("/") -def start(): - # Fortschritt für einen frischen Durchlauf zurücksetzen +# ───────────────────────────────────────────────────────────────────────────── +# 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("fs40")]: + or k.startswith("stg_") or k.startswith("stgdone_") + or k.startswith("fs40") + or k in ("inv", "run_start", "vault_unlocked")]: session.pop(key, None) - inhalt = """ + 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 🧱

    @@ -213,20 +552,97 @@ def start(): 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 →

    +

    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/") @@ -243,6 +659,8 @@ def _clear_challenge(rid): 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 = ("""
    🎉🧱🎉
    @@ -250,21 +668,22 @@ def sieg_screen(text=None):

    Befreit!

    %s

    „Everything is awesome!“ 🎵

    - Nochmal spielen 🔁 - Sieg in den Kommentaren feiern 💬 -
    """ % esc(body)) + 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

    - Nochmal von vorn ▶ - Frust in den Kommentaren lassen 💬 + Erneut versuchen ▶ + 🏆 Scoreboard ansehen
    """ % esc(grund)) return adventure_seite(inhalt, akzent="#f87171") @@ -297,6 +716,7 @@ def challenge_raum(db, r): 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") @@ -324,6 +744,7 @@ def challenge_raum(db, r): # 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.") @@ -337,19 +758,27 @@ def challenge_raum(db, r): 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), erfolg_raum)) + """ % (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.") @@ -443,6 +872,30 @@ 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 = """ @@ -487,7 +940,11 @@ def _blaulicht_intro():

    🚦 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 📝 +
    + 🪪 + Zieh deinen Führerschein aus dem Inventar (links) hierher, + um ihn den Beamten vorzuzeigen. +
    """ return adventure_seite(inhalt, akzent="#1e50ff") @@ -610,6 +1067,7 @@ def fuehrerschein_kontrolle(): 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) @@ -627,6 +1085,20 @@ def raum(id): '

    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) ═══ @@ -635,10 +1107,27 @@ def raum(id): # ═══ 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() + # ── Finale (Raum 40): Tresor-Schloss → Polizei-Kontrolle → Frage ── + if typ == "ziel" and id == 40: + # Zugangsschlüssel einsetzen (per Drag&Drop ausgelöst) + 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() + if not session.get(FS_DONE_KEY): + _stage_start(40) # Polizei-Kontrolle Zeitmessung + db.close() + return fuehrerschein_kontrolle() + # ── 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() @@ -694,19 +1183,14 @@ def raum(id): # 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) + 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' @@ -720,7 +1204,8 @@ def raum(id):

    %s

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

    %s - """ % (esc(r["name"]), esc(r["beschreibung"]), weiter_btn)) + %s + """ % (esc(r["name"]), esc(r["beschreibung"]), key_hinweis, weiter_btn)) return adventure_seite(inhalt) # ═══ Gang / Start: Beschreibung + Wege (Navigations-Entscheidung) ═══ diff --git a/init_db.py b/init_db.py index 4f0e35e..2dc9f83 100644 --- a/init_db.py +++ b/init_db.py @@ -71,6 +71,50 @@ CREATE TABLE fragen ( ); """) +# ─── Persistente Tabellen (U19) ────────────────────────────────────────────── +# Bewusst OHNE DROP und mit IF NOT EXISTS: Wenn init_db.py erneut läuft (z. B. +# nach neuen Räumen), werden Räume/Ausgänge/Fragen neu aufgebaut – aber alle +# Spielstände, Bestzeiten und Inventare BLEIBEN erhalten. +db.executescript(""" +CREATE TABLE IF NOT EXISTS spielstand ( + spieler TEXT PRIMARY KEY, -- jeder Name kommt genau einmal vor + raum_id INTEGER, -- zuletzt besuchter Raum + start_zeit REAL, -- Start des aktuellen Laufs (time.time()) + fertig_zeit REAL -- Zeitpunkt des Sieges (NULL solange offen) +); + +CREATE TABLE IF NOT EXISTS laeufe ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + spieler TEXT, + start_zeit REAL, + fertig_zeit REAL, + dauer REAL, -- Gesamtdauer in Sekunden + status TEXT -- 'gewonnen' | 'gescheitert' +); + +CREATE TABLE IF NOT EXISTS stage_zeiten ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + spieler TEXT, + stage TEXT, -- z. B. 'Hai-Fragen (Prüf-Weg)' + dauer REAL, -- Dauer dieses Einzelabschnitts + zeit REAL -- Zeitpunkt der Eintragung +); + +CREATE TABLE IF NOT EXISTS inventar ( + spieler TEXT, + gegenstand TEXT -- Item-ID, z. B. 'laserhai_key' +); +""") + +# Falls eine ältere spielstand-Tabelle bereits existiert (z. B. nur mit +# spieler/raum_id), fehlende U19-Spalten nachziehen statt Daten zu verlieren. +cols = {r[1] for r in db.execute("PRAGMA table_info(spielstand)").fetchall()} +if "start_zeit" not in cols: + db.execute("ALTER TABLE spielstand ADD COLUMN start_zeit REAL") +if "fertig_zeit" not in cols: + db.execute("ALTER TABLE spielstand ADD COLUMN fertig_zeit REAL") + + # ─── Räume ─────────────────────────────────────────────────────────────────── # IDs bewusst gruppiert: 1 = Lobby, 10er = Etage 1, 20er = Etage 2, 30er = Etage 3. raeume = [ @@ -304,3 +348,4 @@ print("Datenbank adventure.db angelegt:", DB_PATH) print(" Räume:", len(raeume), "| Ausgänge:", len(ausgaenge), "| Fragen:", len(fragen)) +print(" Persistente Tabellen: spielstand, laeufe, stage_zeiten, inventar (erhalten)")