348 lines
No EOL
9.7 KiB
Python
348 lines
No EOL
9.7 KiB
Python
from flask import Flask, request, session, redirect
|
|
import sqlite3
|
|
from datetime import datetime
|
|
|
|
app = Flask(__name__)
|
|
app.secret_key = "sneaky-snakes-snacken-snacks"
|
|
|
|
|
|
def get_db():
|
|
db = sqlite3.connect("adventure.db")
|
|
db.row_factory = sqlite3.Row
|
|
return db
|
|
|
|
|
|
def layout(content):
|
|
return f"""
|
|
<style>
|
|
:root {{
|
|
--bg1: #050608;
|
|
--bg2: #0b1220;
|
|
--card: rgba(15, 23, 42, 0.92);
|
|
--border: rgba(148, 163, 184, 0.18);
|
|
--text: #e5e7eb;
|
|
--muted: #94a3b8;
|
|
--accent: #7f1d1d;
|
|
--accent2: #991b1b;
|
|
--shadow: rgba(0,0,0,0.55);
|
|
}}
|
|
|
|
* {{
|
|
box-sizing: border-box;
|
|
}}
|
|
|
|
body {{
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background:
|
|
radial-gradient(circle at top, rgba(127, 29, 29, 0.18), transparent 30%),
|
|
radial-gradient(circle at bottom right, rgba(148, 163, 184, 0.06), transparent 24%),
|
|
linear-gradient(180deg, var(--bg2), var(--bg1));
|
|
color: var(--text);
|
|
font-family: Inter, "Segoe UI", system-ui, sans-serif;
|
|
display: grid;
|
|
place-items: center;
|
|
padding: 2rem 1rem;
|
|
line-height: 1.6;
|
|
}}
|
|
|
|
.card {{
|
|
width: min(840px, 100%);
|
|
background: var(--card);
|
|
border: 1px solid var(--border);
|
|
border-radius: 22px;
|
|
padding: 2rem;
|
|
box-shadow: 0 20px 60px var(--shadow);
|
|
backdrop-filter: blur(10px);
|
|
}}
|
|
|
|
h1 {{
|
|
margin-top: 0;
|
|
margin-bottom: 0.75rem;
|
|
font-size: clamp(2rem, 4vw, 3rem);
|
|
letter-spacing: 0.06em;
|
|
text-transform: uppercase;
|
|
}}
|
|
|
|
p, small, label {{
|
|
color: var(--text);
|
|
}}
|
|
|
|
.muted {{
|
|
color: var(--muted);
|
|
}}
|
|
|
|
form {{
|
|
margin-top: 1rem;
|
|
}}
|
|
|
|
input {{
|
|
width: 100%;
|
|
max-width: 360px;
|
|
margin: 0.35rem 0 0.8rem;
|
|
padding: 0.8rem 1rem;
|
|
border-radius: 12px;
|
|
border: 1px solid var(--border);
|
|
background: #0b1220;
|
|
color: var(--text);
|
|
font: inherit;
|
|
outline: none;
|
|
}}
|
|
|
|
input:focus {{
|
|
border-color: rgba(127, 29, 29, 0.9);
|
|
box-shadow: 0 0 0 3px rgba(127, 29, 29, 0.2);
|
|
}}
|
|
|
|
a, button {{
|
|
display: inline-block;
|
|
margin: .35rem .35rem 0 0;
|
|
padding: .7rem 1rem;
|
|
border-radius: 12px;
|
|
border: 1px solid rgba(127, 29, 29, 0.95);
|
|
background: linear-gradient(180deg, var(--accent2), var(--accent));
|
|
color: #fff;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
font: inherit;
|
|
transition: transform .15s ease, filter .15s ease, background .15s ease;
|
|
}}
|
|
|
|
a:hover, button:hover {{
|
|
transform: translateY(-1px);
|
|
filter: brightness(1.08);
|
|
}}
|
|
|
|
.secondary {{
|
|
background: #111827;
|
|
border-color: rgba(148, 163, 184, 0.18);
|
|
}}
|
|
|
|
.exits {{
|
|
margin-top: 1rem;
|
|
}}
|
|
|
|
.highscore {{
|
|
margin-top: 1.5rem;
|
|
padding-top: 1rem;
|
|
border-top: 1px solid rgba(148, 163, 184, 0.15);
|
|
}}
|
|
|
|
ol {{
|
|
margin: 0.5rem 0 0;
|
|
padding-left: 1.3rem;
|
|
}}
|
|
|
|
li {{
|
|
margin: 0.35rem 0;
|
|
}}
|
|
</style>
|
|
<div class="card">
|
|
{content}
|
|
</div>
|
|
"""
|
|
|
|
|
|
def ensure_columns():
|
|
db = get_db()
|
|
db.execute("""
|
|
CREATE TABLE IF NOT EXISTS highscore (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
spieler TEXT NOT NULL,
|
|
sekunden REAL NOT NULL,
|
|
gewonnen_am TEXT NOT NULL
|
|
)
|
|
""")
|
|
try:
|
|
db.execute("ALTER TABLE spielstand ADD COLUMN startzeit TEXT")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
try:
|
|
db.execute("ALTER TABLE spielstand ADD COLUMN fertig INTEGER DEFAULT 0")
|
|
except sqlite3.OperationalError:
|
|
pass
|
|
db.commit()
|
|
db.close()
|
|
|
|
|
|
ensure_columns()
|
|
|
|
|
|
@app.route("/", methods=["GET", "POST"])
|
|
def start():
|
|
if request.method == "POST":
|
|
name = request.form["name"].strip()
|
|
if name != "":
|
|
session["spieler"] = name
|
|
session["startzeit"] = datetime.utcnow().isoformat()
|
|
|
|
db = get_db()
|
|
row = db.execute(
|
|
"SELECT spieler FROM spielstand WHERE spieler=?",
|
|
(name,)
|
|
).fetchone()
|
|
|
|
if row:
|
|
db.execute(
|
|
"UPDATE spielstand SET raum_id=1, startzeit=?, fertig=0 WHERE spieler=?",
|
|
(session["startzeit"], name)
|
|
)
|
|
else:
|
|
db.execute(
|
|
"INSERT INTO spielstand (spieler, raum_id, startzeit, fertig) VALUES (?, 1, ?, 0)",
|
|
(name, session["startzeit"])
|
|
)
|
|
|
|
db.commit()
|
|
db.close()
|
|
return redirect("/app/raum/1")
|
|
|
|
db = get_db()
|
|
anzahl = db.execute("SELECT COUNT(*) AS n FROM spielstand").fetchone()["n"]
|
|
highscores = db.execute(
|
|
"SELECT spieler, sekunden, gewonnen_am FROM highscore ORDER BY sekunden ASC, gewonnen_am ASC LIMIT 10"
|
|
).fetchall()
|
|
db.close()
|
|
|
|
html = "<h1>Verlies</h1>"
|
|
html += f"<p class='muted'>{anzahl} Abenteurer waren schon hier.</p>"
|
|
|
|
if "spieler" in session:
|
|
db = get_db()
|
|
s = db.execute(
|
|
"SELECT raum_id FROM spielstand WHERE spieler=?",
|
|
(session["spieler"],)
|
|
).fetchone()
|
|
db.close()
|
|
if s:
|
|
html += f"<p>Willkommen zurück, <strong>{session['spieler']}</strong>!</p>"
|
|
html += f'<a href="/app/raum/{s["raum_id"]}">Weiterspielen</a>'
|
|
html += '<a class="secondary" href="/app/reset">Neues Abenteuer</a>'
|
|
|
|
html += """
|
|
<form method="post">
|
|
<label for="name">Dein Name:</label><br>
|
|
<input id="name" name="name" placeholder="Zum Beispiel Mara">
|
|
<br>
|
|
<button type="submit">Abenteuer starten</button>
|
|
</form>
|
|
"""
|
|
|
|
html += "<div class='highscore'><h2>Highscore</h2>"
|
|
if highscores:
|
|
html += "<ol>"
|
|
for hs in highscores:
|
|
minuten = int(hs["sekunden"] // 60)
|
|
sekunden = int(hs["sekunden"] % 60)
|
|
zeit = f"{minuten}m {sekunden}s" if minuten else f"{sekunden}s"
|
|
html += f"<li><strong>{hs['spieler']}</strong> — {zeit}</li>"
|
|
html += "</ol>"
|
|
else:
|
|
html += "<p class='muted'>Noch kein Schatz gefunden.</p>"
|
|
html += "</div>"
|
|
|
|
return layout(html)
|
|
|
|
|
|
@app.route("/raum/<int:id>", methods=["GET", "POST"])
|
|
def raum(id):
|
|
db = get_db()
|
|
|
|
if "spieler" in session:
|
|
db.execute(
|
|
"UPDATE spielstand SET raum_id=? WHERE spieler=?",
|
|
(id, session["spieler"])
|
|
)
|
|
db.commit()
|
|
|
|
r = db.execute("SELECT * FROM raeume WHERE id=?", (id,)).fetchone()
|
|
|
|
if r["raetsel_frage"]:
|
|
if request.method == "POST":
|
|
if request.form["antwort"].strip() == r["raetsel_antwort"]:
|
|
now = datetime.utcnow()
|
|
startzeit_str = session.get("startzeit")
|
|
if not startzeit_str:
|
|
row = db.execute(
|
|
"SELECT startzeit FROM spielstand WHERE spieler=?",
|
|
(session["spieler"],)
|
|
).fetchone()
|
|
startzeit_str = row["startzeit"] if row else None
|
|
|
|
if startzeit_str and "spieler" in session:
|
|
startzeit = datetime.fromisoformat(startzeit_str)
|
|
sekunden = (now - startzeit).total_seconds()
|
|
|
|
bereits = db.execute(
|
|
"SELECT 1 FROM highscore WHERE spieler=? AND gewonnen_am=?",
|
|
(session["spieler"], now.date().isoformat())
|
|
).fetchone()
|
|
|
|
db.execute(
|
|
"INSERT INTO highscore (spieler, sekunden, gewonnen_am) VALUES (?, ?, ?)",
|
|
(session["spieler"], sekunden, now.date().isoformat())
|
|
)
|
|
db.execute(
|
|
"UPDATE spielstand SET fertig=1 WHERE spieler=?",
|
|
(session["spieler"],)
|
|
)
|
|
db.commit()
|
|
|
|
db.close()
|
|
return layout("""
|
|
<h1>Du findest einen Schatz</h1>
|
|
<p>Herzlichen Glückwunsch. Du hast das Verlies überlebt.</p>
|
|
<a href="/app/">Zurück zum Anfang</a>
|
|
""")
|
|
|
|
db.close()
|
|
return layout(f"""
|
|
<h1>Leider falsch</h1>
|
|
<p>Die Antwort war nicht richtig.</p>
|
|
<a href="/app/raum/{id}">Nochmal</a>
|
|
""")
|
|
|
|
db.close()
|
|
return layout(f"""
|
|
<h1>{r["name"]}</h1>
|
|
<p>{r["beschreibung"]}</p>
|
|
<form method="post">
|
|
<label for="antwort">{r["raetsel_frage"]}</label><br>
|
|
<input id="antwort" name="antwort" placeholder="Antwort eingeben">
|
|
<br>
|
|
<button type="submit">OK</button>
|
|
</form>
|
|
""")
|
|
|
|
ausgaenge = db.execute(
|
|
"SELECT richtung, nach_raum FROM ausgaenge WHERE von_raum=?",
|
|
(id,)
|
|
).fetchall()
|
|
db.close()
|
|
|
|
exits_html = ""
|
|
for a in ausgaenge:
|
|
exits_html += f'<a href="/app/raum/{a["nach_raum"]}">{a["richtung"]}</a>'
|
|
|
|
spieler_html = ""
|
|
if "spieler" in session:
|
|
spieler_html = f'<p class="muted">Unterwegs als: {session["spieler"]}</p>'
|
|
|
|
html = f"""
|
|
<h1>{r["name"]}</h1>
|
|
<p>{r["beschreibung"]}</p>
|
|
{spieler_html}
|
|
<div class="exits">{exits_html}</div>
|
|
"""
|
|
return layout(html)
|
|
|
|
|
|
@app.route("/reset")
|
|
def reset():
|
|
session.pop("spieler", None)
|
|
session.pop("startzeit", None)
|
|
return redirect("/app/")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
app.run(host="0.0.0.0", port=9008) |