Falkencode · 09.09.2026
Falkencode: db.py — Konten, Spielstand, Post
db.py ist das Gedächtnis von Falkenmoor: SQLite für alles Persistente. Passwörter liegen ausschließlich als bcrypt-Hashes, Spielstände als JSON pro Spieler — Position, Beutel, Truhe, Bankguthaben, Rasse, Gilde und Sprachwahl inklusive.
Dazu zwei Community-Systeme: das Postfach (ungelesene Zähler für den Login-Hinweis, Gottes-Autoantwort inklusive) und das Anschlagbrett — löschen dürfen nur Verfasser und Gott. Alle Zugriffe laufen über einen Thread-Lock, damit sich die gleichzeitigen WebSocket-Spieler nicht in die Quere kommen.
#!/usr/bin/env python3
"""Spielerkonten & Persistenz (SQLite + bcrypt)."""
import os, sqlite3, threading, json
import bcrypt
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "players.db")
_lock = threading.Lock()
_SCHEMA = """
CREATE TABLE IF NOT EXISTS spieler (
name TEXT PRIMARY KEY,
pw_hash TEXT NOT NULL,
erstellt TEXT DEFAULT CURRENT_TIMESTAMP,
daten TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS post (
id INTEGER PRIMARY KEY AUTOINCREMENT,
absender TEXT NOT NULL,
empfaenger TEXT NOT NULL,
text TEXT NOT NULL,
gelesen INTEGER DEFAULT 0,
zeit TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS brett (
id INTEGER PRIMARY KEY AUTOINCREMENT,
autor TEXT NOT NULL,
text TEXT NOT NULL,
zeit TEXT DEFAULT CURRENT_TIMESTAMP
);
"""
def _verbinden():
con = sqlite3.connect(DB_PATH)
con.row_factory = sqlite3.Row
return con
def init():
with _lock:
con = _verbinden()
con.executescript(_SCHEMA)
con.commit()
con.close()
def registrieren(name, passwort):
name = name.strip()[:24]
if not name or not passwort or len(passwort) < 4:
return None, "Name und Passwort nötig (Passwort min. 4 Zeichen)."
with _lock:
con = _verbinden()
if con.execute("SELECT 1 FROM spieler WHERE name = ?", (name,)).fetchone():
con.close()
return None, "Dieser Name ist bereits vergeben."
pw_hash = bcrypt.hashpw(passwort.encode(), bcrypt.gensalt()).decode()
con.execute("INSERT INTO spieler (name, pw_hash, daten) VALUES (?, ?, ?)",
(name, pw_hash, json.dumps({})))
con.commit()
con.close()
return name, None
def einloggen(name, passwort):
name = name.strip()[:24]
with _lock:
con = _verbinden()
reihen = con.execute("SELECT name, pw_hash FROM spieler").fetchall()
con.close()
# Exakter Treffer gewinnt; sonst Groß-/Kleinschreibungstoleranz
# (Touchscreen-Tastaturen wandeln gern den ersten Buchstaben).
treffer = [r for r in reihen if r["name"] == name]
if not treffer:
treffer = [r for r in reihen if r["name"].lower() == name.lower()]
if len(treffer) > 1:
return None, "Dieser Name ist nicht eindeutig – bitte exakt schreiben."
if not treffer:
return None, "Unbekannter Held. Registriere dich zuerst."
if not bcrypt.checkpw(passwort.encode(), treffer[0]["pw_hash"].encode()):
return None, "Falsches Passwort."
return treffer[0]["name"], None
def speichern(name, daten):
with _lock:
con = _verbinden()
con.execute("UPDATE spieler SET daten = ? WHERE name = ?",
(json.dumps(daten, ensure_ascii=False), name))
con.commit()
con.close()
def laden(name):
with _lock:
con = _verbinden()
row = con.execute("SELECT daten FROM spieler WHERE name = ?", (name,)).fetchone()
con.close()
if not row or not row["daten"]:
return {}
return json.loads(row["daten"])
# ---------- Postsystem ----------
def alle_spieler():
with _lock:
con = _verbinden()
namen = [r["name"] for r in con.execute("SELECT name FROM spieler ORDER BY name")]
con.close()
return namen
def mail_senden(absender, empfaenger, text):
with _lock:
con = _verbinden()
con.execute("INSERT INTO post (absender, empfaenger, text) VALUES (?, ?, ?)",
(absender, empfaenger, text))
con.commit()
con.close()
def mail_postfach(name):
"""Alle Briefe für den Spieler, älteste zuerst; markiert sie als gelesen."""
with _lock:
con = _verbinden()
briefe = [dict(r) for r in con.execute(
"SELECT id, absender, text, zeit FROM post WHERE empfaenger = ? ORDER BY id", (name,))]
con.execute("UPDATE post SET gelesen = 1 WHERE empfaenger = ?", (name,))
con.commit()
con.close()
return briefe
def mail_ungezaehlt(name):
with _lock:
con = _verbinden()
n = con.execute("SELECT COUNT(*) FROM post WHERE empfaenger = ? AND gelesen = 0",
(name,)).fetchone()[0]
con.close()
return n
def mail_an_gott():
with _lock:
con = _verbinden()
briefe = [dict(r) for r in con.execute(
"SELECT absender, text, zeit FROM post WHERE empfaenger = 'Gott' ORDER BY id DESC LIMIT 30")]
con.close()
return briefe
# ---------- Anschlagbrett ----------
def brett_lesen():
with _lock:
con = _verbinden()
eintraege = [dict(r) for r in con.execute(
"SELECT id, autor, text, zeit FROM brett ORDER BY id DESC LIMIT 15")]
con.close()
return eintraege
def brett_schreiben(autor, text):
with _lock:
con = _verbinden()
con.execute("INSERT INTO brett (autor, text) VALUES (?, ?)", (autor, text))
con.commit()
con.close()
def brett_loeschen(nr, spieler, ist_gott=False):
"""Löscht Eintrag nr, wenn er vom Spieler stammt (oder Gott es will)."""
with _lock:
con = _verbinden()
row = con.execute("SELECT autor FROM brett WHERE id = ?", (nr,)).fetchone()
if not row:
con.close()
return "Kein Eintrag mit dieser Nummer."
if row["autor"] != spieler and not ist_gott:
con.close()
return "Nur eigene Einträge (oder Gott) dürfen entfernt werden."
con.execute("DELETE FROM brett WHERE id = ?", (nr,))
con.commit()
con.close()
return f"Eintrag #{nr} vom Brett entfernt."
Mehr aus der Kategorie „Falkencode“
- Falkencode: app.py — Webserver & WebSocket 09.09.2026
- Falkencode: engine.py — Die Spiellogik 09.09.2026
- Falkencode: uebersetz.py — Der DE/EN-Katalog 09.09.2026
- Falkencode: gen_world.py — Der Weltgenerator 09.09.2026