U18: SQLite Klick-Adventure (Octan-Tower, Lego Movie) neben Kommentar-Funktion + Nav-Links

This commit is contained in:
David Kertzscher 2026-07-01 13:02:43 +00:00
parent b6b8142707
commit fc49784d66
15 changed files with 354 additions and 9 deletions

4
.gitignore vendored
View file

@ -6,3 +6,7 @@ venv/
__pycache__/
*.pyc
# SQLite-Datenbank entsteht auf dem Server (untracked) nie committen,
# sonst würde ein Deploy die Server-Datenbank überschreiben
*.db

View file

@ -45,6 +45,7 @@
<a href="api.html" class="active">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

203
app.py
View file

@ -1,7 +1,19 @@
from flask import Flask, request, redirect
import os
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))
@ -87,6 +99,7 @@ def seite(inhalt):
<div class="topbar">
<a href="/app/" class="topbar-logo">&#9654; Flask-App</a>
<nav>
<a href="/app/">🧱 Adventure</a>
<a href="/app/pinnwand" class="aktiv">Kommentare</a>
</nav>
<a href="https://isa2.edumake.de" class="topbar-back">&#8592; Zurück zur Website</a>
@ -98,19 +111,191 @@ def seite(inhalt):
# ─────────────────────────────────────────────────────────────────────────────
# Route 1: Startseite
# 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 = """<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Einbruch im Octan-Tower Klick-Adventure</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body { background:#0f172a; color:#e5e7eb;
font-family: system-ui, "Segoe UI", Arial, sans-serif;
min-height:100vh; line-height:1.6; }
.adv-topbar { display:flex; align-items:center; gap:10px; padding:12px 20px;
background:#111827; border-bottom:2px solid ACCENT; }
.adv-logo { font-weight:800; letter-spacing:-.5px; color:ACCENT;
text-decoration:none; font-size:18px; }
.adv-topbar .link { color:#93c5fd; text-decoration:none; font-size:14px;
border:1px solid #334155; border-radius:8px; padding:6px 12px; }
.adv-topbar .link:hover { background:#1e293b; }
.adv-topbar .spacer { margin-left:auto; }
.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); }
.adv-card h1 { font-size:26px; margin-bottom:6px; }
.adv-floor { font-size:12px; text-transform:uppercase; letter-spacing:2px;
color:ACCENT; font-weight:700; }
.adv-desc { color:#cbd5e1; margin:14px 0 22px; }
.adv-progress { font-size:13px; color:#94a3b8; margin-bottom:16px; }
.adv-frage { font-size:19px; font-weight:600; margin-bottom:18px; }
.opt-form { display:flex; flex-direction:column; gap:12px; }
.opt-btn { display:block; width:100%; text-align:left; background:#0f172a;
color:#e5e7eb; border:1px solid #334155; border-radius:10px;
padding:14px 16px; font-size:16px; cursor:pointer;
transition:.15s; font-family:inherit; }
.opt-btn:hover { border-color:ACCENT; background:#172033; transform:translateY(-1px); }
.exit-link { display:inline-block; background:ACCENT; color:#111827; font-weight:700;
text-decoration:none; border-radius:10px; padding:12px 18px;
margin:6px 8px 0 0; }
.exit-link:hover { filter:brightness(1.08); }
.ghost-link { display:inline-block; color:#93c5fd; text-decoration:none;
border:1px solid #334155; border-radius:10px; padding:12px 18px;
margin:6px 8px 0 0; }
.ghost-link:hover { background:#0f172a; }
.feedback-bad { background:#7f1d1d; border:1px solid #b91c1c; color:#fecaca;
border-radius:10px; padding:12px 14px; margin-bottom:16px; font-weight:600; }
.win-emoji { font-size:54px; text-align:center; margin-bottom:8px; }
.muted { color:#94a3b8; font-size:14px; margin-top:20px; }
.muted a { color:#93c5fd; }
</style>
</head>
<body>
<div class="adv-topbar">
<a href="/app/" class="adv-logo">🧱 Octan-Tower</a>
<a href="/app/pinnwand" class="link spacer">💬 Kommentare</a>
<a href="https://isa2.edumake.de" class="link"> Website</a>
</div>
<div class="adv-wrap">""".replace("ACCENT", akzent)
fuss = """</div>
</body>
</html>"""
return kopf + inhalt + fuss
@app.route("/")
def hello():
return redirect("/app/pinnwand")
def start():
# Fortschritt für einen frischen Durchlauf zurücksetzen
for key in [k for k in session.keys() if k.startswith("raum_")]:
session.pop(key, None)
inhalt = """
<div class="adv-card">
<div class="adv-floor">LEGO Geheimmission</div>
<h1>Einbruch im Octan-Tower 🧱</h1>
<p class="adv-desc">Präsident Business hortet ganz oben in seinem Wolkenkratzer
das <strong>Kragle</strong> 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
<strong>Haien</strong> 🦈, <strong>Lasern</strong> 🔴 und natürlich
<strong>Laserhaien</strong> 🦈. Auf jeder Etage prüfen dich die Sicherheits­systeme
mit Trivia-Fragen. Nur wer alle knackt, kommt weiter.</p>
<a href="/app/raum/1" class="exit-link">Abenteuer starten </a>
<p class="muted">Lieber quatschen statt einbrechen?
<a href="/app/pinnwand">Zu den Kommentaren </a></p>
</div>"""
return adventure_seite(inhalt)
# ─────────────────────────────────────────────────────────────────────────────
# Route 2: Frage-Formular (GET + POST)
# ─────────────────────────────────────────────────────────────────────────────
@app.route("/frage", methods=["GET", "POST"])
@app.route("/frage")
def frage():
return redirect("/app/pinnwand")
return redirect("/app/")
@app.route("/raum/<int:id>", 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(
'<div class="adv-card"><div class="adv-floor">Fehler</div>'
'<h1>Diese Etage existiert nicht 🤖</h1>'
'<p class="adv-desc">Da hat sich wohl ein Bauplan verheddert.</p>'
'<a href="/app/" class="exit-link">Zurück zum Start</a></div>')
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)
# ── Es sind noch Sicherheitsfragen offen ──
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 = ('<div class="feedback-bad">🚨 Alarm! Falsche Antwort '
'die Wächter wurden fast auf dich aufmerksam. Versuch es nochmal.</div>')
optionen = aktuelle["optionen"].split("|")
db.close()
buttons = ""
for opt in optionen:
buttons += ('<button class="opt-btn" name="antwort" value="%s">%s</button>'
% (esc(opt), esc(opt)))
inhalt = ("""
<div class="adv-card">
<div class="adv-floor">🔐 Sicherheitsprüfung · %s</div>
<h1>%s</h1>
<p class="adv-progress">Frage %d von %d</p>
%s
<p class="adv-frage">%s</p>
<form method="post" class="opt-form">%s</form>
</div>""" % (esc(r["name"]), esc(r["name"]), idx + 1, len(fragen),
feedback, esc(aktuelle["frage"]), buttons))
return adventure_seite(inhalt)
# ── Alle Fragen gelöst (oder keine) → Beschreibung + Ausgänge / Sieg ──
ausgaenge = db.execute(
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=? ORDER BY rowid", (id,)
).fetchall()
db.close()
if r["ist_ziel"]:
inhalt = ("""
<div class="adv-card" style="text-align:center">
<div class="win-emoji">🎉🧱🎉</div>
<div class="adv-floor">Mission erfüllt</div>
<h1>%s</h1>
<p class="adv-desc">%s</p>
<p class="adv-frage" style="color:#f59e0b">Everything is awesome! 🎵</p>
<a href="/app/" class="exit-link">Nochmal spielen 🔁</a>
<a href="/app/pinnwand" class="ghost-link">Sieg in den Kommentaren feiern 💬</a>
</div>""" % (esc(r["name"]), esc(r["beschreibung"])))
return adventure_seite(inhalt)
exits_html = ""
for a in ausgaenge:
exits_html += ('<a class="exit-link" href="/app/raum/%d">%s</a>'
% (a["nach_raum"], esc(a["richtung"])))
if not ausgaenge:
exits_html = '<a class="exit-link" href="/app/">Zurück zum Start</a>'
geschafft = ""
if fragen: # Etage hatte Fragen und alle wurden gelöst
geschafft = ('<p class="adv-progress">✅ Alle Sicherheitsfragen geknackt '
'der Weg ist frei!</p>')
inhalt = ("""
<div class="adv-card">
<div class="adv-floor">🏢 %s</div>
<h1>%s</h1>
<p class="adv-desc">%s</p>
%s
<div>%s</div>
</div>""" % (esc(r["name"]), esc(r["name"]), esc(r["beschreibung"]),
geschafft, exits_html))
return adventure_seite(inhalt)
# ─────────────────────────────────────────────────────────────────────────────

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

144
init_db.py Normal file
View file

@ -0,0 +1,144 @@
"""
U18 Klick-Adventure "Einbruch im Octan-Tower" (The-Lego-Movie-Style)
=====================================================================
Legt die SQLite-Datenbank `adventure.db` an und befüllt sie mit:
- raeume : die Etagen des Octan-Towers
- ausgaenge : welcher Ausgang führt in welche Etage
- fragen : Trivia-Fragen pro Etage (Multiple-Choice)
Leitprinzip (wie in U17):
LOKAL schreiben & pushen auf dem SERVER einmalig ausführen:
python3 init_db.py
Danach entsteht adventure.db direkt neben diesem Skript.
"""
import os
import sqlite3
# adventure.db immer neben dieser Datei ablegen (unabhängig vom Arbeitsverzeichnis)
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
DB_PATH = os.path.join(BASE_DIR, "adventure.db")
db = sqlite3.connect(DB_PATH)
# Tabellen bei jedem Lauf sauber neu aufbauen (DROP statt DELETE, damit auch
# geänderte Spalten greifen).
db.executescript("""
DROP TABLE IF EXISTS raeume;
DROP TABLE IF EXISTS ausgaenge;
DROP TABLE IF EXISTS fragen;
CREATE TABLE raeume (
id INTEGER PRIMARY KEY,
name TEXT,
beschreibung TEXT,
ist_ziel INTEGER DEFAULT 0
);
CREATE TABLE ausgaenge (
von_raum INTEGER,
richtung TEXT,
nach_raum INTEGER
);
CREATE TABLE fragen (
id INTEGER PRIMARY KEY,
raum_id INTEGER,
frage TEXT,
antwort TEXT,
optionen TEXT -- Antwortmöglichkeiten, per | getrennt
);
""")
# ─── Räume: der Aufstieg durch den Octan-Tower ───────────────────────────────
raeume = [
(1, "Lobby des Octan-Towers",
"Die Türen fallen hinter dir zu. Präsident Business' Wolkenkratzer ragt "
"über dir auf. Ein gläserner Aufzug führt nach oben ganz oben wartet das "
"Kragle. Everything is ... noch nicht awesome.", 0),
(2, "1. Etage Die Hai-Etage 🦈",
"Ein tiefer Wassergraben voller hungriger Haie trennt dich vom nächsten "
"Aufzug. Der Sicherheits-Wächter lässt dich nur weiter, wenn du seine "
"Fragen richtig beantwortest.", 0),
(3, "2. Etage Die Laser-Etage 🔴",
"Rote Laserstrahlen zerschneiden den Raum zu einem glühenden Gitter. Ein "
"Terminal blinkt: Beantworte die Fragen korrekt, sonst bleibt das "
"Laser-Gitter für immer aktiv.", 0),
(4, "3. Etage Die Laserhai-Etage 🦈⚡",
"Und dann ... Haie. Mit Laserstrahlen. Auf dem Kopf. Präsident Business "
"meint es wirklich ernst. Nur wer clever genug ist, kommt am "
"Laserhai-Becken vorbei.", 0),
(5, "Das Dach Der Kragle-Tresor 🏆",
"Du stehst vor dem Kragle ein einziger Klecks Kleber, der die ganze "
"Welt für immer einfrieren könnte. Doch du hast es rechtzeitig geschafft "
"und Präsident Business gestoppt!", 1),
]
db.executemany(
"INSERT INTO raeume (id, name, beschreibung, ist_ziel) VALUES (?,?,?,?)",
raeume,
)
# ─── Ausgänge: linearer Aufstieg (jede Etage → nächste Etage) ────────────────
ausgaenge = [
(1, "Aufzug nach oben 🛗", 2),
(2, "Treppe hoch 🪜", 3),
(3, "Treppe hoch 🪜", 4),
(4, "Aufs Dach 🏢", 5),
]
db.executemany(
"INSERT INTO ausgaenge (von_raum, richtung, nach_raum) VALUES (?,?,?)",
ausgaenge,
)
# ─── Trivia-Fragen pro Etage ─────────────────────────────────────────────────
# Pro Etage 4 Fragen: 12 thematisch passend, danach Popkultur.
# "antwort" ist die korrekte Option, "optionen" alle Auswahlmöglichkeiten (|-getrennt).
fragen = [
# ── Etage 1: Haie ─────────────────────────────────────────────
(2, "🦈 Welcher Hai ist der größte Fisch der Welt?",
"Walhai", "Weißer Hai|Walhai|Hammerhai"),
(2, "🦈 Wie heißt Steven Spielbergs berühmter Hai-Film von 1975?",
"Der weiße Hai", "Der weiße Hai|Free Willy|Findet Nemo"),
(2, "🎮 Wie heißt Marios grün gekleideter Bruder?",
"Luigi", "Luigi|Wario|Yoshi"),
(2, "🎬 In welcher Weltraum-Saga kämpfen Jedi gegen Sith?",
"Star Wars", "Star Trek|Star Wars|Herr der Ringe"),
# ── Etage 2: Laser ────────────────────────────────────────────
(3, "🔴 Wofür steht das Akronym LASER?",
"Light Amplification by Stimulated Emission of Radiation",
"Light Amplification by Stimulated Emission of Radiation|"
"Large Automatic System for Energy Reflection|"
"Leuchtende Aktive Strahlen Emissions-Röhre"),
(3, "⚔️ Welche leuchtende Laser-Waffe führt ein Jedi in Star Wars?",
"Lichtschwert", "Lichtschwert|Laserpistole|Zauberstab"),
(3, "🦸 Welcher Superheld trägt ein blaues Kostüm mit rotem Umhang und einem 'S'?",
"Superman", "Batman|Superman|Spider-Man"),
(3, "🎮 In welchem Spiel fängt man Kreaturen in kleinen Bällen?",
"Pokémon", "Minecraft|Tetris|Pokémon"),
# ── Etage 3: Laserhaie ────────────────────────────────────────
(4, "🦈⚡ In welchem Film wünscht sich Dr. Evil „Haie mit Laserstrahlen auf dem Kopf“?",
"Austin Powers", "James Bond|Austin Powers|Men in Black"),
(4, "🧱 Wie heißt der Bösewicht, in dessen Turm du gerade einbrichst?",
"Lord Business", "Lord Business|Lord Vader|Lord Farquaad"),
(4, "🎵 Wie lautet der Ohrwurm-Titelsong aus „The Lego Movie“?",
"Everything Is Awesome", "Everything Is Awesome|Let It Go|Happy"),
(4, "🧙 Welches Zauberer-Internat besucht Harry Potter?",
"Hogwarts", "Narnia|Hogwarts|Mordor"),
]
db.executemany(
"INSERT INTO fragen (raum_id, frage, antwort, optionen) VALUES (?,?,?,?)",
fragen,
)
db.commit()
db.close()
print("Datenbank adventure.db angelegt:", DB_PATH)
print(" Räume:", len(raeume),
"| Ausgänge:", len(ausgaenge),
"| Fragen:", len(fragen))

View file

@ -43,6 +43,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html" class="active">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html" class="active">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>

View file

@ -45,6 +45,7 @@
<a href="api.html">API Demo</a>
<a href="kanban.html">Kanban</a>
<a href="quiz.html">Quiz</a>
<a href="/app/">Adventure</a>
<a href="/app/pinnwand">Kommentare</a>
</nav>
</div>