diff --git a/app/app.py b/app/app.py index 02c3fb7..fe6fd92 100644 --- a/app/app.py +++ b/app/app.py @@ -1,10 +1,11 @@ from flask import Flask, request, session, redirect import html +import random import sqlite3 from pathlib import Path app = Flask(__name__) -app.secret_key = "hier-eigenen-geheimen-text" # für session nötig +app.secret_key = "hier-eigenen-geheimen-text" DB_PATH = Path(__file__).with_name("adventure.db") @@ -14,6 +15,52 @@ def get_db(): return db +DENY_MESSAGES = [ + "ZUGRIFF VERWEIGERT.", + "AUTHENTIFIZIERUNG FEHLGESCHLAGEN.", + "CODE UNGÜLTIG — VERSUCH PROTOKOLLIERT.", +] + +STUFE_HINWEISE = [ + "Tipp: Sichte das Navigationslog auf der Brücke noch einmal genau.", + "Tipp: Der Reaktor im Maschinenraum trägt ein Typenschild mit einer Signatur.", + "Tipp: Das Klemmbrett im Forschungslabor listet den Zustand der Kryokapseln.", +] + + +def parse_stufen(r): + if not r["raetsel_frage"]: + return [] + fragen = r["raetsel_frage"].split("|") + antworten = r["raetsel_antwort"].split("|") + return list(zip(fragen, antworten)) + + +def get_fortschritt(db, spieler, raum_id): + if not spieler: + return session.get(f"stufe_r{raum_id}", 0), session.get(f"fehler_r{raum_id}", 0) + row = db.execute( + "SELECT stufe, fehler FROM raetsel_fortschritt WHERE spieler=? AND raum_id=?", + (spieler, raum_id) + ).fetchone() + if row: + return row["stufe"], row["fehler"] + return 0, 0 + + +def set_fortschritt(db, spieler, raum_id, stufe, fehler): + if not spieler: + session[f"stufe_r{raum_id}"] = stufe + session[f"fehler_r{raum_id}"] = fehler + return + db.execute( + "INSERT INTO raetsel_fortschritt (spieler, raum_id, stufe, fehler) VALUES (?,?,?,?) " + "ON CONFLICT(spieler, raum_id) DO UPDATE SET stufe=excluded.stufe, fehler=excluded.fehler", + (spieler, raum_id, stufe, fehler) + ) + db.commit() + + BASE_CSS = """ @import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600;700&family=IBM+Plex+Sans:wght@400;500&display=swap'); @@ -44,7 +91,6 @@ body { overflow-x: hidden; } -/* feine Scanlines über der gesamten Seite */ body::before { content: ""; position: fixed; @@ -61,7 +107,6 @@ body::before { mix-blend-mode: overlay; } -/* Vignette */ body::after { content: ""; position: fixed; @@ -224,8 +269,86 @@ input:focus { border-top: 1px dashed var(--line); } +.terminal-log { + font-family: 'IBM Plex Mono', monospace; + font-size: 0.78rem; + margin-bottom: 1rem; +} +.log-line { margin: 0.15rem 0; } +.log-line.done { color: var(--success); } + +.progress-dots { display: flex; gap: 0.4rem; margin: 0 0 1.2rem; } +.dot-step { + width: 22px; height: 6px; border-radius: 3px; + background: rgba(127,219,182,0.15); + border: 1px solid var(--line); +} +.dot-step.filled { + background: var(--success); + border-color: var(--success); + box-shadow: 0 0 8px var(--success); +} + +.frage { + font-family: 'IBM Plex Mono', monospace; + color: var(--amber); + font-size: 0.95rem; + margin: 0 0 0.4rem; +} + +.denied { + font-family: 'IBM Plex Mono', monospace; + color: var(--danger); + font-size: 0.85rem; + animation: flash 0.4s ease; + border-left: 2px solid var(--danger); + padding-left: 0.6rem; +} +@keyframes flash { + 0% { opacity: 0; transform: translateX(-4px); } + 100% { opacity: 1; transform: translateX(0); } +} + +.hint { + font-family: 'IBM Plex Mono', monospace; + font-size: 0.78rem; + color: var(--text-dim); + border-left: 2px solid var(--amber-dim); + padding-left: 0.6rem; + margin-top: 0.8rem; +} + +.verify-ok { + font-family: 'IBM Plex Mono', monospace; + color: var(--success); + letter-spacing: 0.06em; + font-size: 0.85rem; +} + +.danger-btn { + font-family: 'IBM Plex Mono', monospace; + text-transform: uppercase; + color: #ffe1c9; + background: linear-gradient(135deg, rgba(255,107,94,0.15), rgba(255,180,84,0.15)); + border: 1px solid var(--danger); + border-radius: 6px; + padding: 0.9rem 1.4rem; + text-decoration: none; + display: inline-flex; + align-items: center; + gap: 0.5em; + animation: pulse-glow 1.6s ease-in-out infinite; +} +.danger-btn::before { content: "⚠"; } +@keyframes pulse-glow { + 0%, 100% { box-shadow: 0 0 10px rgba(255,107,94,0.25); } + 50% { box-shadow: 0 0 22px rgba(255,107,94,0.55); } +} + @media (prefers-reduced-motion: reduce) { .card h1::after { animation: none; opacity: 1; } + .danger-btn { animation: none; } + .denied { animation: none; } } """ @@ -336,36 +459,63 @@ def raum(id): eyebrow="FEHLER" ) - eyebrow = f"SEKTOR {id:02d}" + eyebrow_basis = f"SEKTOR {id:02d}" if r["raetsel_frage"]: - if request.method == "POST": - if request.form.get("antwort", "").strip() == r["raetsel_antwort"]: - db.close() - return render_page( - "Notsignal aktiviert", - '
Rettung ist unterwegs.
' - '', - eyebrow="MISSION ERFÜLLT" - ) - db.close() - return render_page( - "Falscher Code", - 'Der KI-Kern reagiert nicht.
' - '', - eyebrow=eyebrow - ) + stufen = parse_stufen(r) + spieler = session.get("spieler") + stufe, fehler = get_fortschritt(db, spieler, id) + + deny_msg = None + if request.method == "POST" and stufe < len(stufen): + antwort = request.form.get("antwort", "").strip() + erwartet = stufen[stufe][1] + if antwort.upper() == erwartet.upper(): + stufe += 1 + fehler = 0 + else: + fehler += 1 + deny_msg = random.choice(DENY_MESSAGES) + set_fortschritt(db, spieler, id, stufe, fehler) db.close() + + log_html = "".join( + f'IDENTITÄT BESTÄTIGT — ALLE SYSTEME GRÜN
+{html.escape(r['beschreibung'])}
+ + """ + return render_page(html.escape(r["name"]), body, eyebrow=f"{eyebrow_basis} · VERIFIZIERT") + + frage = stufen[stufe][0] + hinweis_html = f'{html.escape(STUFE_HINWEISE[stufe])}
' if fehler >= 2 else "" + deny_html = f'{deny_msg}
' if deny_msg else "" + dots = "".join( + f'' for i in range(len(stufen)) + ) + body = f""" -{dots}
{html.escape(r['beschreibung'])}
+{html.escape(frage)}
+ {deny_html} + {hinweis_html} """ - return render_page(html.escape(r["name"]), body, eyebrow=eyebrow) + return render_page(html.escape(r["name"]), body, eyebrow=f"{eyebrow_basis} · STUFE {stufe+1}/{len(stufen)}") ausgaenge = db.execute( "SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,) @@ -380,7 +530,35 @@ def raum(id): for a in ausgaenge: body += f'{html.escape(a["richtung"])}' body += "" - return render_page(html.escape(r["name"]), body, eyebrow=eyebrow) + return render_page(html.escape(r["name"]), body, eyebrow=eyebrow_basis) + + +@app.route("/raum/✓ ALLE DREI SYSTEME VERIFIZIERT
+Ein greller Alarmton hallt durch die Gänge. Auf dem Hauptschirm leuchtet: „NOTSIGNAL GESENDET — RETTUNG UNTERWEGS.“
+