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."

← Zurück zum Lehrpfad