Krasses 3-Stufen-Rätsel im KI-Kern, persistenter Fortschritt, Terminal-Log-UI

This commit is contained in:
DylanBarnes 2026-07-15 15:58:48 +00:00
parent 92c485d86f
commit fa4521a684
2 changed files with 217 additions and 32 deletions

View file

@ -1,10 +1,11 @@
from flask import Flask, request, session, redirect from flask import Flask, request, session, redirect
import html import html
import random
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
app = Flask(__name__) 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") DB_PATH = Path(__file__).with_name("adventure.db")
@ -14,6 +15,52 @@ def get_db():
return 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 = """ 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'); @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; overflow-x: hidden;
} }
/* feine Scanlines über der gesamten Seite */
body::before { body::before {
content: ""; content: "";
position: fixed; position: fixed;
@ -61,7 +107,6 @@ body::before {
mix-blend-mode: overlay; mix-blend-mode: overlay;
} }
/* Vignette */
body::after { body::after {
content: ""; content: "";
position: fixed; position: fixed;
@ -224,8 +269,86 @@ input:focus {
border-top: 1px dashed var(--line); 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) { @media (prefers-reduced-motion: reduce) {
.card h1::after { animation: none; opacity: 1; } .card h1::after { animation: none; opacity: 1; }
.danger-btn { animation: none; }
.denied { animation: none; }
} }
""" """
@ -336,36 +459,63 @@ def raum(id):
eyebrow="FEHLER" eyebrow="FEHLER"
) )
eyebrow = f"SEKTOR {id:02d}" eyebrow_basis = f"SEKTOR {id:02d}"
if r["raetsel_frage"]: if r["raetsel_frage"]:
if request.method == "POST": stufen = parse_stufen(r)
if request.form.get("antwort", "").strip() == r["raetsel_antwort"]: spieler = session.get("spieler")
db.close() stufe, fehler = get_fortschritt(db, spieler, id)
return render_page(
"Notsignal aktiviert", deny_msg = None
'<h1>Notsignal erfolgreich aktiviert</h1><p>Rettung ist unterwegs.</p>' if request.method == "POST" and stufe < len(stufen):
'<div class="actions"><a href="/app/">Zurück zum Start</a></div>', antwort = request.form.get("antwort", "").strip()
eyebrow="MISSION ERFÜLLT" erwartet = stufen[stufe][1]
) if antwort.upper() == erwartet.upper():
db.close() stufe += 1
return render_page( fehler = 0
"Falscher Code", else:
'<h1>Falscher Code</h1><p>Der KI-Kern reagiert nicht.</p>' fehler += 1
'<div class="actions"><a class="secondary" href="/app/raum/' + str(id) + '">nochmal versuchen</a></div>', deny_msg = random.choice(DENY_MESSAGES)
eyebrow=eyebrow set_fortschritt(db, spieler, id, stufe, fehler)
)
db.close() db.close()
log_html = "".join(
f'<div class="log-line done">✓ Stufe {i+1}: {html.escape(f)}</div>'
for i, (f, a) in enumerate(stufen[:stufe])
)
if stufe >= len(stufen):
body = f"""
<div class="terminal-log">{log_html}</div>
<p class="verify-ok">IDENTITÄT BESTÄTIGT ALLE SYSTEME GRÜN</p>
<p>{html.escape(r['beschreibung'])}</p>
<div class="actions">
<a class="danger-btn" href="/app/raum/{id}/aktivieren">Notsignal auslösen</a>
</div>
"""
return render_page(html.escape(r["name"]), body, eyebrow=f"{eyebrow_basis} · VERIFIZIERT")
frage = stufen[stufe][0]
hinweis_html = f'<p class="hint">{html.escape(STUFE_HINWEISE[stufe])}</p>' if fehler >= 2 else ""
deny_html = f'<p class="denied">{deny_msg}</p>' if deny_msg else ""
dots = "".join(
f'<span class="dot-step{" filled" if i < stufe else ""}"></span>' for i in range(len(stufen))
)
body = f""" body = f"""
<h1>{html.escape(r['name'])}</h1> <div class="terminal-log">{log_html}</div>
<p class="progress-dots">{dots}</p>
<p>{html.escape(r['beschreibung'])}</p> <p>{html.escape(r['beschreibung'])}</p>
<p class="frage">{html.escape(frage)}</p>
{deny_html}
<form method="post"> <form method="post">
<input id="antwort" name="antwort" placeholder="Antwort eingeben" autocomplete="off"> <input id="antwort" name="antwort" placeholder="Antwort eingeben" autocomplete="off">
<button type="submit">Senden</button> <button type="submit">Prüfen</button>
</form> </form>
{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( ausgaenge = db.execute(
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,) "SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,)
@ -380,7 +530,35 @@ def raum(id):
for a in ausgaenge: for a in ausgaenge:
body += f'<a class="secondary" href="/app/raum/{a["nach_raum"]}">{html.escape(a["richtung"])}</a>' body += f'<a class="secondary" href="/app/raum/{a["nach_raum"]}">{html.escape(a["richtung"])}</a>'
body += "</div>" body += "</div>"
return render_page(html.escape(r["name"]), body, eyebrow=eyebrow) return render_page(html.escape(r["name"]), body, eyebrow=eyebrow_basis)
@app.route("/raum/<int:id>/aktivieren")
@app.route("/app/raum/<int:id>/aktivieren")
def aktivieren(id):
db = get_db()
r = db.execute("SELECT * FROM raeume WHERE id=?", (id,)).fetchone()
if not r:
db.close()
return redirect("/app/")
stufen = parse_stufen(r)
spieler = session.get("spieler")
stufe, _ = get_fortschritt(db, spieler, id)
db.close()
if stufe < len(stufen):
return redirect(f"/app/raum/{id}")
body = """
<h1>Notsignal aktiviert</h1>
<p class="verify-ok"> ALLE DREI SYSTEME VERIFIZIERT</p>
<p>Ein greller Alarmton hallt durch die Gänge. Auf dem Hauptschirm leuchtet: NOTSIGNAL GESENDET RETTUNG UNTERWEGS.</p>
<div class="actions">
<a href="/app/">Zurück zum Start</a>
</div>
"""
return render_page("Notsignal aktiviert", body, eyebrow="MISSION ERFÜLLT")
@app.route("/reset") @app.route("/reset")

View file

@ -1,8 +1,5 @@
import sqlite3 import sqlite3
# Die Datenbank entsteht auf dem Server beim einmaligen Aufruf
# und bleibt dort unverändert erhalten.
db = sqlite3.connect("adventure.db") db = sqlite3.connect("adventure.db")
db.executescript(""" db.executescript("""
@ -27,13 +24,23 @@ CREATE TABLE IF NOT EXISTS spielstand (
raum_id INTEGER raum_id INTEGER
); );
CREATE TABLE IF NOT EXISTS raetsel_fortschritt (
spieler TEXT,
raum_id INTEGER,
stufe INTEGER DEFAULT 0,
fehler INTEGER DEFAULT 0,
PRIMARY KEY (spieler, raum_id)
);
INSERT INTO raeume (id, name, beschreibung, raetsel_frage, raetsel_antwort) VALUES INSERT INTO raeume (id, name, beschreibung, raetsel_frage, raetsel_antwort) VALUES
(1, 'Schlafkapsel', 'Du erwachst aus dem Kryoschlaf. Die Notbeleuchtung flackert und das Schiff wirkt verlassen.', NULL, NULL), (1, 'Schlafkapsel', 'Du erwachst aus dem Kryoschlaf. Die Notbeleuchtung flackert und das Schiff wirkt verlassen.', NULL, NULL),
(2, 'Brücke', 'Die Steuerzentrale ist dunkel. Mehrere Monitore zeigen Fehlermeldungen.', NULL, NULL), (2, 'Brücke', 'Die Steuerzentrale ist dunkel. Ein einzelner Bildschirm flackert und zeigt: „NAV-VERIFIKATION — LETZTER KURS: SEKTOR 7-4-2“. Mehrere andere Monitore zeigen nur Fehlermeldungen.', NULL, NULL),
(3, 'Maschinenraum', 'Überall verlaufen Rohre und Kabel. Die Hauptenergie scheint instabil zu sein.', NULL, NULL), (3, 'Maschinenraum', 'Überall verlaufen Rohre und Kabel. Ein Typenschild am Hauptreaktor liest: „ENERGIESIGNATUR OMEGA-09“. Die Hauptenergie scheint instabil zu sein.', NULL, NULL),
(4, 'Forschungslabor', 'Verlassene Arbeitsplätze und zerbrochene Glasbehälter lassen nichts Gutes vermuten.', NULL, NULL), (4, 'Forschungslabor', 'Verlassene Arbeitsplätze und zerbrochene Glasbehälter. Auf einem Klemmbrett steht handschriftlich: „6 Kryokapseln installiert, 4 havariert, 2 stabil.“', NULL, NULL),
(5, 'Hangar', 'Die Landebucht ist leer. Nur ein beschädigtes Shuttle steht noch dort.', NULL, NULL), (5, 'Hangar', 'Die Landebucht ist leer. Nur ein beschädigtes Shuttle steht noch dort.', NULL, NULL),
(6, 'KI-Kern', 'Vor dir befindet sich der zentrale Computer des Schiffs. Nur hier kann das Notsignal aktiviert werden.', 'Gib den Notcode ein, um das Signal zu senden.', 'SOS'); (6, 'KI-Kern', 'Vor dir befindet sich der zentrale Computer des Schiffs. Ein Drei-Faktor-Protokoll blockiert das Notsignal. Ohne die Daten aus Brücke, Maschinenraum und Forschungslabor kommst du hier nicht durch.',
'Nenne den Sektor-Code aus dem Navigationslog der Brücke (nur Ziffern).|Wie lautet die Energiesignatur des Reaktors im Maschinenraum?|Wie viele Kryokapseln sind laut den Labornotizen noch stabil?',
'742|OMEGA-09|2');
INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES
(1, 'Zur Brücke', 2), (1, 'Zur Brücke', 2),
@ -52,4 +59,4 @@ INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES
db.commit() db.commit()
db.close() db.close()
print("Datenbank adventure.db angelegt.") print("Datenbank adventure.db angelegt.")