Krasses 3-Stufen-Rätsel im KI-Kern, persistenter Fortschritt, Terminal-Log-UI
This commit is contained in:
parent
92c485d86f
commit
fa4521a684
2 changed files with 217 additions and 32 deletions
226
app/app.py
226
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",
|
||||
'<h1>Notsignal erfolgreich aktiviert</h1><p>Rettung ist unterwegs.</p>'
|
||||
'<div class="actions"><a href="/app/">Zurück zum Start</a></div>',
|
||||
eyebrow="MISSION ERFÜLLT"
|
||||
)
|
||||
db.close()
|
||||
return render_page(
|
||||
"Falscher Code",
|
||||
'<h1>Falscher Code</h1><p>Der KI-Kern reagiert nicht.</p>'
|
||||
'<div class="actions"><a class="secondary" href="/app/raum/' + str(id) + '">nochmal versuchen</a></div>',
|
||||
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'<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"""
|
||||
<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 class="frage">{html.escape(frage)}</p>
|
||||
{deny_html}
|
||||
<form method="post">
|
||||
<input id="antwort" name="antwort" placeholder="Antwort eingeben" autocomplete="off">
|
||||
<button type="submit">Senden</button>
|
||||
<button type="submit">Prüfen</button>
|
||||
</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(
|
||||
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?", (id,)
|
||||
|
|
@ -380,7 +530,35 @@ def raum(id):
|
|||
for a in ausgaenge:
|
||||
body += f'<a class="secondary" href="/app/raum/{a["nach_raum"]}">{html.escape(a["richtung"])}</a>'
|
||||
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")
|
||||
|
|
|
|||
|
|
@ -1,8 +1,5 @@
|
|||
import sqlite3
|
||||
|
||||
# Die Datenbank entsteht auf dem Server beim einmaligen Aufruf
|
||||
# und bleibt dort unverändert erhalten.
|
||||
|
||||
db = sqlite3.connect("adventure.db")
|
||||
|
||||
db.executescript("""
|
||||
|
|
@ -27,13 +24,23 @@ CREATE TABLE IF NOT EXISTS spielstand (
|
|||
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
|
||||
(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),
|
||||
(3, 'Maschinenraum', 'Überall verlaufen Rohre und Kabel. Die Hauptenergie scheint instabil zu sein.', NULL, NULL),
|
||||
(4, 'Forschungslabor', 'Verlassene Arbeitsplätze und zerbrochene Glasbehälter lassen nichts Gutes vermuten.', 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. 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. 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),
|
||||
(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
|
||||
(1, 'Zur Brücke', 2),
|
||||
|
|
@ -52,4 +59,4 @@ INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES
|
|||
|
||||
db.commit()
|
||||
db.close()
|
||||
print("Datenbank adventure.db angelegt.")
|
||||
print("Datenbank adventure.db angelegt.")
|
||||
Loading…
Reference in a new issue