Hab wieder irgendwas falsch gemacht mit git ...

Merge branch 'dev'
This commit is contained in:
Daniel Nagel 2026-04-07 08:39:15 +00:00
commit 7b119a085f
28 changed files with 1079 additions and 1345 deletions

1
.gitignore vendored
View File

@ -5,6 +5,7 @@
# 2. Python Caches
__pycache__/
*.py[cod]
.nicegui/storage*
# 3. Virtuelle Umgebungen
venv/

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Meta-Chaser","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +0,0 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Blinder Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

15
bot/servitor.py Normal file
View File

@ -0,0 +1,15 @@
import requests
webhook_url = "https://liga-n8n.au-fab.eu/webhook/21066d30-2757-4d64-9c72-e439ecd70f94"
discord_tokken = "MTQ3NTg1ODU5OTQwNTQyNDcwMQ.GRo63W.1erAk0janBqS8NlHB6FHEbXM1bolIOPH2Uc4vs"
def send_message(event, text):
data = {
event: "Neuer Spieler",
text: "mr_teels"
}
response = requests.post(webhook_url, json=data)

View File

@ -1,5 +1,6 @@
import sqlite3
import random
from wood import logger
from data.setup_database import DB_PATH
@ -14,10 +15,12 @@ def validate_user_session(db_id, discord_id):
# 1. Fall: Die ID gibt es gar nicht mehr in der Datenbank
if result is None:
logger.log(f"Player not found in database. Discord:{discord_id}")
return False
# 2. Fall: Die ID gehört jetzt einem anderen Discord-Account (Datenbank wurde resettet)
if str(result[0]) != str(discord_id):
logger.log(f"Player with false coockies logged in! {discord_id} doesnt belong to {db_id}")
return False
# 3. Fall: Alles ist korrekt!
@ -45,18 +48,16 @@ def get_or_create_player(discord_id, discord_name, avatar_url):
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# REPARIERT: Wir fragen 4 Dinge ab (id, discord_name, display_name, discord_avatar_url)
# fragen 4 Dinge ab (id, discord_name, display_name, discord_avatar_url)
cursor.execute("SELECT id, discord_name, display_name, discord_avatar_url FROM players WHERE discord_id = ?", (discord_id,))
player = cursor.fetchone()
if player is None:
# Random Silly Name Generator für neue Spieler. Damit sie angeregt werden ihren richtigen Namen einzutragen.
silly_name = generate_silly_name()
cursor.execute("INSERT INTO players (discord_id, discord_name, display_name, discord_avatar_url) VALUES (?, ?, ?, ?)", (discord_id, discord_name, silly_name, avatar_url))
logger.log(f"new player added. Discord:{discord_name}")
connection.commit()
cursor.execute("SELECT id, discord_name, display_name, discord_avatar_url FROM players WHERE discord_id = ?", (discord_id,))
player = cursor.fetchone()
else:
@ -71,8 +72,6 @@ def get_or_create_player(discord_id, discord_name, avatar_url):
return player
def get_all_players():
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
@ -89,8 +88,6 @@ def get_all_players():
return result
def get_all_players_from_system(system_name):
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
@ -120,6 +117,7 @@ def get_all_players_from_system(system_name):
return result
def get_gamesystem_id_by_name(system_name):
"""Holt die interne ID eines Spielsystems anhand seines Namens (z.B. 'Warhammer 40k' -> 1)."""
connection = sqlite3.connect(DB_PATH)
@ -139,49 +137,6 @@ def get_gamesystem_id_by_name(system_name):
return None
def get_player_rank(player_id, gamesystem_id):
"""Sortiert die Liga nach MMR und gibt den aktuellen Platz (Rang) des Spielers zurück."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
# Alle Spieler dieser Liga, sortiert nach MMR (höchstes zuerst)
query = """
SELECT player_id
FROM player_game_statistic
WHERE gamesystem_id = ?
ORDER BY mmr DESC
"""
cursor.execute(query, (gamesystem_id,))
rows = cursor.fetchall()
connection.close()
# Wir zählen die Liste durch (enumerate fängt bei 0 an, also machen wir +1 für den Rang)
for index, row in enumerate(rows):
if row['player_id'] == player_id:
return index + 1
# Falls der Spieler nicht in der Liga ist (sollte nicht passieren)
return 999
def get_gamesystem_data():
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
cursor.execute("SELECT * FROM gamesystems")
rows = cursor.fetchall()
connection.close()
# SQLite-Rows in normale Python-Dictionaries um
result = []
for row in rows:
result.append(dict(row))
return result
@ -197,12 +152,18 @@ def get_player_statistics(player_id):
sys.*,
stat.mmr,
stat.games_in_system,
stat.points
stat.points,
stat.avv_points,
stat.last_played,
stat.win_rate,
stat.trend,
stat.tyrann_id,
stat.pushover_id,
stat.nemesis_id
FROM gamesystems sys
LEFT JOIN player_game_statistic stat
ON sys.id = stat.gamesystem_id AND stat.player_id = ?
"""
cursor.execute(query, (player_id,))
rows = cursor.fetchall()
connection.close()
@ -225,7 +186,7 @@ def join_league(player_id, gamesystem_id):
INSERT INTO player_game_statistic (player_id, gamesystem_id)
VALUES (?, ?)
"""
logger.log(f"{get_player_name(player_id)} joined {gamesystem_id}")
cursor.execute(query, (player_id, gamesystem_id))
connection.commit()
@ -233,7 +194,6 @@ def join_league(player_id, gamesystem_id):
def get_recent_matches_for_player(player_id):
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
@ -298,6 +258,8 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
cursor.execute(query, (sys_id, player1_id, player2_id, score_p1, score_p2))
new_match_id = cursor.lastrowid
logger.log(f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})")
connection.commit()
connection.close()
@ -305,169 +267,126 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
def get_player_system_stats(player_id, system_name):
"""Holt die gespeicherten Statistiken eines Spielers für ein spezielles System direkt aus der Tabelle."""
def save_calculated_match(calc_results: dict):
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
# stat.* holt einfach ALLE Spalten, die in der Tabelle player_game_statistic stehen
query = """
SELECT stat.*
FROM player_game_statistic stat
JOIN gamesystems sys ON stat.gamesystem_id = sys.id
WHERE stat.player_id = ? AND sys.name = ?
"""
try:
match_id = calc_results["match_id"]
winner_id = calc_results["winner_id"]
looser_id = calc_results["looser_id"]
cursor.execute(query, (player_id, system_name))
# Match aus DB lesen
cursor.execute(
"SELECT player1_id, player2_id, gamesystem_id FROM matches WHERE id = ?",
(match_id,)
)
row = cursor.fetchone()
if not row:
raise ValueError(f"Match ID {match_id} nicht in der Datenbank gefunden.")
player1_id, player2_id, gamesystem_id = row
# Daten der Spieler aus calc_results holen (Key = player_id als int/str)
p1 = calc_results[player1_id]
p2 = calc_results[player2_id]
# 1. Match-Tabelle updaten
match_query = """
UPDATE matches
SET
player1_base_change = ?, player1_khorne = ?, player1_slaanesh = ?,
player1_tzeentch = ?, player1_mmr_change = ?,
player2_base_change = ?, player2_khorne = ?, player2_slaanesh = ?,
player2_tzeentch = ?, player2_mmr_change = ?,
rust_factor = ?, elo_factor = ?, point_inflation = ?,
match_is_counted = 1
WHERE id = ?
"""
cursor.execute(match_query, (
p1["base"], p1["khorne"], p1["slaanesh"], p1["tzeentch"], p1["total"],
p2["base"], p2["khorne"], p2["slaanesh"], p2["tzeentch"], p2["total"],
calc_results["rust_factor"], calc_results["elo_factor"], calc_results["point_inflation"],
match_id
))
# 2. Scores holen
cursor.execute(
"SELECT score_player1, score_player2 FROM matches WHERE id = ?",
(match_id,)
)
score_row = cursor.fetchone()
score_p1, score_p2 = score_row if score_row else (0, 0)
# 3. Statistik-Query
stat_query = """
UPDATE player_game_statistic
SET
mmr = mmr + ?,
games_in_system = games_in_system + 1,
points = points + ?,
avv_points = (points + ?) / (games_in_system + 1),
last_played = CURRENT_TIMESTAMP
WHERE player_id = ? AND gamesystem_id = ?
"""
# 4. Statistik Spieler 1
cursor.execute(stat_query, (
p1["total"], score_p1, score_p1,
player1_id, gamesystem_id
))
# 5. Statistik Spieler 2
cursor.execute(stat_query, (
p2["total"], score_p2, score_p2,
player2_id, gamesystem_id
))
connection.commit()
logger.log(f"Match ID:{match_id} calculated.")
return True
except Exception as e:
connection.rollback()
logger.log(f"KRITISCHER FEHLER beim Speichern des Matches: {e}")
return False
finally:
connection.close()
# -----------------------------------------------------
# Get Data Funktionen
# -----------------------------------------------------
def get_gamesystem_data(system_id):
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
cursor.execute("SELECT * FROM gamesystems WHERE id = ?", (system_id,))
row = cursor.fetchone()
connection.close()
# Wenn wir was finden, machen wir ein Dictionary draus, ansonsten geben wir None zurück
if row:
return dict(row)
return None
return dict(zip([col[0] for col in cursor.description], row)) if row else None
def get_last_20_match_scores(player_id, system_name):
"""Holt die erspielten Punkte und das Datum der letzten 20 Matches."""
def get_gamesystems_data():
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
# NEU: Wir haben 'm.played_at' im SELECT hinzugefügt!
query = """
SELECT m.player1_id, m.score_player1, m.score_player2, m.played_at
FROM matches m
JOIN gamesystems sys ON m.gamesystem_id = sys.id
WHERE sys.name = ? AND (m.player1_id = ? OR m.player2_id = ?)
ORDER BY m.played_at DESC
LIMIT 20
"""
cursor.execute(query, (system_name, player_id, player_id))
cursor.execute("SELECT * FROM gamesystems")
rows = cursor.fetchall()
connection.close()
# Wieder umdrehen für den zeitlichen Verlauf (links alt, rechts neu)
rows.reverse()
points_list = []
labels_list = []
# SQLite-Rows in normale Python-Dictionaries um
result = []
for row in rows:
if row['player1_id'] == player_id:
points_list.append(row['score_player1'])
else:
points_list.append(row['score_player2'])
result.append(dict(row))
# NEU: Wir schneiden das Datum ab (z.B. 2024-03-04) und nutzen es als Label!
date_clean = str(row['played_at'])[:10]
labels_list.append(date_clean)
return {"points": points_list, "labels": labels_list}
def get_match_by_id(match_id):
"""Holt alle Daten eines spezifischen Matches anhand der Match-ID."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row # Damit wir wieder ein schönes Dictionary bekommen
cursor = connection.cursor()
# m.* holt alle Spalten aus dem Match (Punkte, IDs, Datum)
# sys.name AS gamesystem_name holt uns direkt den passenden Text für deine JSON-Ladefunktion
query = """
SELECT m.*, sys.name AS gamesystem_name
FROM matches m
JOIN gamesystems sys ON m.gamesystem_id = sys.id
WHERE m.id = ?
"""
cursor.execute(query, (match_id,))
row = cursor.fetchone()
connection.close()
# Wenn ein Match gefunden wurde, geben wir es als Dictionary zurück
if row:
return dict(row)
return None # Falls die ID nicht existiert
from datetime import datetime
def get_days_since_last_game(player_id):
"""
Sucht das absolut letzte Spiel eines Spielers (über alle Systeme)
und berechnet, wie viele Tage das her ist.
"""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# MAX(last_played) sucht den absolut neuesten Zeitstempel aus allen Einträgen dieses Spielers
query = """
SELECT MAX(last_played)
FROM player_game_statistic
WHERE player_id = ?
"""
cursor.execute(query, (player_id,))
row = cursor.fetchone()
connection.close()
# row[0] enthält jetzt unseren Zeitstempel (z.B. '2026-03-05 14:30:00')
last_played_str = row[0] if row else None
# Sicherheitscheck: Hat der Spieler überhaupt schon Einträge?
if not last_played_str:
return None
try:
# [:19] schneidet Millisekunden ab, damit das Format exakt passt.
last_played_date = datetime.strptime(last_played_str[:19], '%Y-%m-%d %H:%M:%S')
# 2. Die Differenz zu "Jetzt genau in diesem Moment" berechnen
time_difference = datetime.now() - last_played_date
# 3. .days holt aus der Zeitdifferenz nur die reinen, vollen Tage heraus
days_ago = time_difference.days
return {
"date_string": last_played_str[:10], # Nur das Datum (YYYY-MM-DD) für die GUI
"days_ago": days_ago # Die nackte Zahl zum Rechnen (z.B. 14)
}
except ValueError:
# Falls in der Datenbank mal ein kaputter String steht
return None
def apply_match_to_player_statistic (player_id, gamesystem_id, mmr_change, scored_points):
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# CURRENT_TIMESTAMP setzt die Uhrzeit für das "Letzte Spiel" automatisch auf genau JETZT.
query = """
UPDATE player_game_statistic
SET
mmr = mmr + ?,
games_in_system = games_in_system + 1,
points = points + ?,
avv_points = (points + ?) / (games_in_system + 1),
last_played = CURRENT_TIMESTAMP
WHERE player_id = ? AND gamesystem_id = ?
"""
# ACHTUNG: Wir müssen scored_points ZWEIMAL übergeben!
# Einmal für die 'points' und einmal für die Berechnung der 'avv_points'.
cursor.execute(query, (mmr_change, scored_points, scored_points, player_id, gamesystem_id))
connection.commit()
connection.close()
return result
@ -499,20 +418,104 @@ def get_leaderboard(system_name):
return result
def update_match_mmr_change(match_id, p1_change, p2_change):
"""Speichert die tatsächliche MMR-Änderung für das Log fest im Match ab."""
def get_match_by_id(match_id: int) -> dict | None:
"""Gibt alle Match-Daten inkl. Gamesystem-Name als Dict zurück."""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
cursor.execute("""
UPDATE matches
SET player1_mmr_change = ?, player2_mmr_change = ?
WHERE id = ?
""", (p1_change, p2_change, match_id))
try:
cursor.execute("""
SELECT
m.id,
m.gamesystem_id,
g.name AS gamesystem_name,
m.player1_id,
m.score_player1,
m.player2_id,
m.score_player2,
m.played_at
FROM matches m
JOIN gamesystems g ON m.gamesystem_id = g.id
WHERE m.id = ?
""", (match_id,))
connection.commit()
row = cursor.fetchone()
if not row:
return None
columns = [desc[0] for desc in cursor.description]
return dict(zip(columns, row))
except Exception as e:
logger.log(f"Fehler beim Laden des Matches: {e}")
return None
finally:
connection.close()
def get_player_name(player_id):
"""Gibt den Namen eines Spielers im Format 'Anzeigename (Discordname)' zurück."""
# Sicherheits-Check: Falls aus Versehen gar keine ID übergeben wird
if player_id is None:
return "Unbekannter Spieler"
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
cursor.execute("SELECT display_name, discord_name FROM players WHERE id = ?", (player_id,))
row = cursor.fetchone()
connection.close()
# Wenn die Datenbank den Spieler gefunden hat:
if row:
display_name = row[0]
discord_name = row[1]
return f"{display_name} ({discord_name})"
else:
return "Gelöschter Spieler"
def get_system_name(sys_id):
if sys_id is None:
return "Unbekanntes System"
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
cursor.execute("SELECT name FROM gamesystems WHERE id = ?", (sys_id,))
row = cursor.fetchone()
connection.close()
if row:
return row[0]
else:
return "Gelöschtes System"
def get_days_since_last_system_game(player_id, gamesystem_id):
"""Gibt zurück, wie viele Tage das letzte Spiel in einem bestimmten System her ist."""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# Julianday() berechnet in SQLite die Differenz in Tagen zwischen zwei Daten
query = """
SELECT CAST(julianday('now') - julianday(last_played) AS INTEGER)
FROM player_game_statistic
WHERE player_id = ? AND gamesystem_id = ?
"""
cursor.execute(query, (player_id, gamesystem_id))
result = cursor.fetchone()
connection.close()
# Wenn er gefunden wurde und ein Datum hat, gib die Tage zurück. Sonst (Erstes Spiel) 0.
if result and result[0] is not None:
return result[0]
return 0
# -----------------------------------------------------
@ -544,14 +547,14 @@ def get_unconfirmed_matches(player_id):
return [dict(row) for row in rows]
def delete_match(match_id):
def delete_match(match_id, player_id):
"""Löscht ein Match anhand seiner ID komplett aus der Datenbank."""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# DELETE FROM löscht die gesamte Zeile, bei der die ID übereinstimmt.
cursor.execute("DELETE FROM matches WHERE id = ?", (match_id,))
logger.log(f"Match ID{match_id} deleted from user {get_player_name(player_id)}(ID:{player_id})")
connection.commit()
connection.close()
@ -562,7 +565,7 @@ def confirm_match(match_id):
# Ändert nur die Spalte player2_check auf 1 (True)
cursor.execute("UPDATE matches SET player2_check = 1 WHERE id = ?", (match_id,))
logger.log(f"Match mit ID{match_id} von Player2 bestätigt.")
connection.commit()
connection.close()
@ -574,7 +577,6 @@ def set_match_counted(match_id):
# Ändert nur die Spalte match_is_counted auf 1 (True)
cursor.execute("UPDATE matches SET match_is_counted = 1 WHERE id = ?", (match_id,))
connection.commit()
connection.close()
@ -603,18 +605,21 @@ def get_submitted_matches(player_id):
return [dict(row) for row in rows]
def get_match_history_log(player_id):
"""Holt ALLE Matches eines Spielers inklusive der MMR-Änderungen für das Log."""
"""Holt ALLE Matches eines Spielers inklusive der MMR-Änderungen und Faktoren für das Log."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
query = """
SELECT m.played_at, sys.name AS gamesystem_name,
m.player1_id, p1.display_name AS p1_display, p1.discord_name AS p1_discord, m.score_player1, m.player1_mmr_change,
m.player2_id, p2.display_name AS p2_display, p2.discord_name AS p2_discord, m.score_player2, m.player2_mmr_change,
m.player1_id, p1.display_name AS p1_display, p1.discord_name AS p1_discord,
m.score_player1, m.player1_mmr_change,
m.player1_khorne, m.player1_tzeentch, m.player1_slaanesh, m.player1_base_change,
m.player2_id, p2.display_name AS p2_display, p2.discord_name AS p2_discord,
m.score_player2, m.player2_mmr_change,
m.player2_khorne, m.player2_tzeentch, m.player2_slaanesh, m.player2_base_change,
m.elo_factor, m.rust_factor,
m.player2_check, m.match_is_counted
FROM matches m
JOIN gamesystems sys ON m.gamesystem_id = sys.id
@ -628,3 +633,46 @@ def get_match_history_log(player_id):
connection.close()
return [dict(row) for row in rows]
# -----------------------------------------------------
# Testing and Prototyping
# -----------------------------------------------------
def create_random_dummy_match(player_id):
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# 1. Die ID des Dummys suchen (Wir suchen einfach nach dem Namen 'Dummy')
cursor.execute("SELECT id FROM players WHERE display_name LIKE '%Dummy%' OR discord_name LIKE '%Dummy%' LIMIT 1")
dummy_row = cursor.fetchone()
if not dummy_row:
connection.close()
print("Fehler: Kein Dummy in der Datenbank gefunden!")
return False
dummy_id = dummy_row[0]
# 2. Zufällige Punkte generieren (z.B. zwischen 0 und 100)
score_p1 = random.randint(0, 100)
score_p2 = random.randint(0, 100)
# 3. Das Match hart in System 1 (gamesystem_id = 1) eintragen
query = """
INSERT INTO matches (gamesystem_id, player2_id, player1_id, score_player1, score_player2, player2_check)
VALUES (?, ?, ?, ?, ?, 0)
"""
cursor.execute(query, (1, player_id, dummy_id, score_p1, score_p2))
connection.commit()
connection.close()
p1_name = get_player_name(player_id)
dummy_name = get_player_name(dummy_id)
sys_name = get_system_name(1)
logger.log(f"Zufallsspiel generiert. [{sys_name}]: {p1_name} ({score_p1}) vs. {dummy_name} ({score_p2})")
return True

View File

@ -9,8 +9,7 @@ if __name__ == "__main__":
setup_database.init_db()
def check_db():
# --- DATENBANK CHECK ---
# Prüfen, ob die Datei im aktuellen Ordner existiert
# Prüfen, ob die Datei existiert
db_file = DB_PATH
if not os.path.exists(db_file):

View File

@ -8,8 +8,8 @@ load_dotenv()
DB_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), "league_database.db")
dummy_is_in = True
dummy_is_in = True
def init_db():
@ -54,7 +54,16 @@ def init_db():
points INTEGER DEFAULT 0,
avv_points INTEGER DEFAULT 0,
last_played TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
win_rate REAL DEFAULT 0,
win_streak INTEGER DEFAULT 0,
trend INTEGER DEFAULT 0,
tyrann_id INTEGER,
pushover_id INTEGER,
nemesis_id INTEGER,
FOREIGN KEY (player_id) REFERENCES players (id),
FOREIGN KEY (tyrann_id) REFERENCES players (id),
FOREIGN KEY (pushover_id) REFERENCES players (id),
FOREIGN KEY (nemesis_id) REFERENCES players (id),
FOREIGN KEY (gamesystem_id) REFERENCES gamesystems (id)
)
''')
@ -69,8 +78,23 @@ def init_db():
player2_id INTEGER,
score_player2 INTEGER,
played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
player1_base_change INTEGER,
player1_khorne INTEGER,
player1_slaanesh INTEGER,
player1_tzeentch INTEGER,
player1_mmr_change INTEGER,
player2_base_change INTEGER,
player2_khorne INTEGER,
player2_slaanesh INTEGER,
player2_tzeentch INTEGER,
player2_mmr_change INTEGER,
rust_factor REAL,
elo_factor REAL,
point_inflation REAL,
player2_check INTEGER DEFAULT 0,
match_is_counted INTEGER DEFAULT 0,
FOREIGN KEY (gamesystem_id) REFERENCES gamesystems (id),
@ -89,7 +113,7 @@ def init_db():
)
''')
# 6. Tabelle: player_achievements (Wer hat welchen Erfolg wann bekommen? - Bewegungsdaten)
# 6. Tabelle: player_achievements. Wer hat welchen Erfolg wann bekommen
cursor.execute('''
CREATE TABLE IF NOT EXISTS player_achievements (
id INTEGER PRIMARY KEY AUTOINCREMENT,
@ -131,7 +155,6 @@ def seed_gamesystems():
print("Spielsysteme angelegt!")
#Nächster Schritt: Standard Achievments eintragen.
generate_default_mmr_rules()
seed_achievements()
def seed_achievements():
@ -197,74 +220,3 @@ def seed_dummy_player():
print("Test-Gegner 'Dummy Mc DummDumm' ist bereit und in allen Ligen angemeldet!")
def generate_default_mmr_rules():
"""Erstellt für jedes Spielsystem eine Standard-JSON-Datei, falls sie noch nicht existiert."""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
cursor.execute("SELECT name FROM gamesystems")
systems = cursor.fetchall()
connection.close()
# 1. Sicherstellen, dass der Ordner existiert (Best Practice!)
folder_path = "mmr_calculations"
os.makedirs(folder_path, exist_ok=True)
# 2. Wir gehen jedes gefundene Spielsystem durch
for sys in systems:
sys_name = sys[0]
# Wir machen den Namen "dateisicher" (z.B. "Warhammer 40k" -> "warhammer_40k")
safe_name = sys_name.replace(" ", "_").lower()
file_path = os.path.join(folder_path, f"mmr_rules_{safe_name}.json")
# 3. SICHERHEITS-CHECK: Existiert die Datei schon?
# Wenn ja, ignorieren wir sie, damit wir dein händisches Balancing nicht überschreiben!
if not os.path.exists(file_path):
# Das ist unsere Standard-Vorlage (Faktor 10)
default_rules = {
"system_info": f"Balancing für {sys_name}",
"draw_point_difference": 3,
"rank_matrix": {
"10": {"win": 10, "draw": 30},
"9": {"win": 10, "draw": 30},
"8": {"win": 10, "draw": 20},
"7": {"win": 10, "draw": 20},
"6": {"win": 20, "draw": 10},
"5": {"win": 20, "draw": 10},
"4": {"win": 20, "draw": 0},
"3": {"win": 20, "draw": 0},
"2": {"win": 30, "draw": 0},
"1": {"win": 30, "draw": 0},
"0": {"win": 30, "draw": 0},
"-1": {"win": 30, "draw": 0},
"-2": {"win": 40, "draw": 0},
"-3": {"win": 40, "draw": -10},
"-4": {"win": 50, "draw": -20},
"-5": {"win": 60, "draw": -20},
"-6": {"win": 70, "draw": -20},
"-7": {"win": 80, "draw": -50},
"-8": {"win": 100, "draw": -50},
"-9": {"win": 120, "draw": -60},
"-10": {"win": 150, "draw": -60}
},
"score_bonus": [
{"min_diff": 95, "bonus": 40},
{"min_diff": 85, "bonus": 30},
{"min_diff": 65, "bonus": 20},
{"min_diff": 35, "bonus": 10},
{"min_diff": 0, "bonus": 0}
]
}
# 4. JSON-Datei schreiben
# 'w' steht für write (schreiben). indent=4 macht es für Menschen schön lesbar formatiert.
with open(file_path, 'w', encoding='utf-8') as f:
json.dump(default_rules, f, indent=4, ensure_ascii=False)
print(f"Neu: Balancing-Datei für '{sys_name}' wurde erstellt -> {file_path}")
else:
print(f"OK: Balancing-Datei für '{sys_name}' existiert bereits.")

View File

@ -1,10 +1,39 @@
from nicegui import ui, app
from data import database, data_api
from gui import gui_style
from wood import logger
from gui import main_gui
def setup_routes():
@ui.page('/admin', dark=True)
def home_page():
def admin_page():
gui_style.apply_design()
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
ui.button("Zurück", on_click= lambda: ui.navigate.to("/") )
if app.storage.user.get('authenticated', False):
ui.card().classes("w-full")
with ui.row().classes("w-full"):
ui.button(text= "test", on_click=lambda: data_api.create_random_dummy_match(2))
ui.button(icon="refresh", on_click= lambda: ui.navigate.to("/admin") )
with ui.card().classes("w-full"):
ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4')
# Daten abrufen
log_data = logger.get_full_log()
# Tabelle aufbauen
columns = [
{'name': 'time', 'label': 'Time', 'field': 'timestamp', 'align': 'left', 'sortable': True},
{'name': 'file', 'label': 'Datei', 'field': 'file', 'align': 'left'},
{'name': 'source', 'label': 'Quelle', 'field': 'source', 'align': 'left'},
{'name': 'details', 'label': 'Details', 'field': 'details', 'align': 'left'}
]
if len(log_data) > 0:
# Millisekunden vom Zeitstempel abschneiden [:19]
for row in log_data:
row['timestamp'] = str(row['timestamp'])[:19]
ui.table(columns=columns, rows=log_data, row_key='id').classes('w-full bg-zinc-900 text-gray-300')
else:
ui.label("Das Logbuch ist leer.").classes('text-gray-500 italic')

View File

@ -4,22 +4,20 @@ from nicegui import ui
# muss man es nur hier ändern!
def apply_design():
ui.add_css('body { background-color: #18181b; }')
ui.add_css('body { background-color: #171721; }')
# 1. Dark Mode aktivieren
ui.dark_mode(True)
# 2. Hier definierst du deine Farben fest im Code!
# Du kannst normale englische Farbnamen (wie 'red') oder HEX-Codes (wie '#ff0000') nutzen.
ui.colors(
primary='#b91c1c', # Hauptfarbe (z.B. für Standard-Buttons) - Hier ein dunkles Rot
secondary='#1f2937', # Zweitfarbe
accent='#3b82f6', # Akzentfarbe (z.B. Blau)
positive='#22c55e', # Farbe für Erfolg (Grün)
negative='#ef4444', # Farbe für Fehler/Abbruch (Rot)
info='#3b82f6', # Info-Farbe
warning='#f59e0b' # Warn-Farbe (Orange)
primary='#B80B0B', # Hauptfarbe (z.B. für Standard-Buttons) Wenn keine Farbe angegeben wird, ist es diese Farbe
secondary='#FF3333', # Zweitfarbe
accent='#2078D4', # Akzentfarbe
positive='#188C42', # Farbe für Erfolg (Grün)
negative='#E31919', # Farbe für Fehler/Abbruch (Rot)
info='#939393', # Info-Farbe
warning='#f59e0b', # Warn-Farbe (Orange)
# Farben für Texte
normaltext="#F7F7F7",
accenttext="#2078D4",
infotext="#A8A8A8"
)
# (Optional) Wenn du die Hintergrundfarbe der ganzen Seite ändern willst:
# ui.query('body').style('background-color: #121212;')

124
gui/imprint.json Normal file
View File

@ -0,0 +1,124 @@
{
"version": 1,
"default_locale": "de",
"locales": {
"de": {
"page": {
"title": "Info"
},
"sections": [
{
"id": "rules",
"order": 10,
"title": "Allgemeine Regeln",
"blocks": [
{
"type": "markdown",
"content": [
"Mit der Anmeldung in der Liga stimmst du folgenden Richtlinien und Regeln zu:\n",
"* Freundliches und Respektvolles Verhalten gegenüber anderen Mitspielern.",
"* Kein Cheating / absichtliches Falschmelden in die Liga! Fehler können passieren.",
"* Es giltet ein Gentleman's Agreement: 'Wir schreiben keine absichtlichen Konterlisten. Schlecht geschriebene Regeln werden nicht zu unserem Vorteil ausgenutzt!'.",
"* Wir spielen mit **Intent**. Sprich: Es werden keine Regeln der eigenen Armee dem Gegner verheimlicht um Überraschungen zu produzieren."
]
}
]
},
{
"id": "imprint",
"order": 20,
"title": "Impressum",
"blocks": [
{
"type": "markdown",
"content": "**Verantwortlich:** Daniel Nagel (Privatperson) \n**Adresse:** PRIVAT \n**Kontakt:** admin@danielnagel.at"
}
]
},
{
"id": "privacy",
"order": 30,
"title": "Datenschutz",
"blocks": [
{
"type": "markdown",
"content": [
"Die Daten die für den Betrieb der Liga nötig sind werden auf einem privaten Server in Österreich gespeichert. Das bezieht ein: \n",
"* Spieler Namen (Automatisch generiert oder Selbst eingetragen)",
"* Spieler ID in form der Discord Nutzer ID.",
"* Discord Nutzernamen",
"* Discord url Pfad zum Profilfoto",
"\nDiese Daten werden **keinem** anderen Dienst, Service, Anbieter oder Drittem zur Verfügung gestellt!",
"\nEine Löschung der eigenen Daten kann jederzeit beantragt werden wenn man nicht mehr an der Liga teilnehmen will. \n",
"Bestimmte Ereignisse oder Eingaben in die App werden in eine Log Funktion geschrieben (mit Zeitstempel) für die Qualitätssicherung und störungsfreie Funktion der App. Unter anderem: \n",
"* Alle Eingaben vom Match Formular. Inkl. Spieler der es eingegeben hat.",
"* Wenn ein Match abgelehnt oder gelöscht wird. Inkl. Spieler der es gelöscht/abgelehnt hat.",
"* Genaue Aufschlüsselung der MMR Punkte Berechnung pro eingetragenem Match.",
"* Neu angelegte Spieler und ihren Discord Namen."
]
}
]
}
]
},
"en": {
"page": {
"title": "Info"
},
"sections": [
{
"id": "rules",
"order": 10,
"title": "General Rules",
"blocks": [
{
"type": "markdown",
"content": [
"By registering for the league you agree to the following guidelines and rules:\n",
"* Friendly and respectful behaviour towards all other players.",
"* No cheating or intentional misreporting of match results! Honest mistakes can happen.",
"* A Gentleman's Agreement applies: 'We do not write intentional counter-lists. Poorly written rules are not exploited for our own advantage!'.",
"* We play with **Intent**. This means: no rules of your own army are hidden from your opponent in order to create surprises."
]
}
]
},
{
"id": "imprint",
"order": 20,
"title": "Imprint",
"blocks": [
{
"type": "markdown",
"content": "**Responsible:** Daniel Nagel (Private individual) \n**Address:** PRIVATE \n**Contact:** admin@danielnagel.at"
}
]
},
{
"id": "privacy",
"order": 30,
"title": "Privacy Policy",
"blocks": [
{
"type": "markdown",
"content": [
"Data required to operate the league is stored on a private server located in Austria. This includes: \n",
"* Player names (automatically generated or manually entered).",
"* Player ID in the form of the Discord user ID.",
"* Discord usernames.",
"* Discord URL path to the profile picture.",
"\nThis data is **not** shared with any other service, provider, or third party.",
"\nDeletion of your own data can be requested at any time if you no longer wish to participate in the league. \n",
"Certain events or inputs within the app are written to a log function (with timestamp) to ensure quality and stable operation of the app. This includes: \n",
"* All inputs from the match form, including the player who submitted it.",
"* When a match is rejected or deleted, including the player who rejected/deleted it.",
"* Detailed breakdown of the MMR point calculation per submitted match.",
"* Newly created players and their Discord names."
]
}
]
}
]
}
}
}

75
gui/imprint_gui.py Normal file
View File

@ -0,0 +1,75 @@
import json
from pathlib import Path
from nicegui import ui
from fastapi import Request
from gui import gui_style
# JSON laden - liegt laut Screenshot direkt in gui/
_JSON_PATH = Path(__file__).resolve().parent / 'imprint.json'
with open(_JSON_PATH, encoding='utf-8') as f:
imprint_data: dict = json.load(f)
# -------------------------------------------------------
SECTION_TITLES_FALLBACK = {
"howto": "Kurzanleitung",
"rules": "Allgemeine Regeln",
"imprint": "Impressum",
"privacy": "Datenschutz",
}
def _get_locale(request: Request) -> str:
default = imprint_data.get('default_locale', 'de')
lang = request.query_params.get('lang', default)
if lang not in imprint_data.get('locales', {}):
lang = default
return lang
def _get_locale_options() -> list[str]:
return list(imprint_data.get('locales', {}).keys())
def setup_routes():
@ui.page('/info', dark=True)
def info_page(request: Request):
gui_style.apply_design()
lang = _get_locale(request)
locale_data = imprint_data['locales'][lang]
page_title = locale_data.get('page', {}).get('title', 'Info')
sections = sorted(locale_data.get('sections', []), key=lambda s: s.get('order', 0))
with ui.row().classes("w-full items-center justify-between"):
ui.button(text="Zurück", on_click=lambda: ui.navigate.to('/'))
ui.label(page_title).classes("text-xl font-bold")
ui.select(
options=_get_locale_options(),
value=lang,
label='Sprache',
on_change=lambda e: ui.navigate.to(f'/info?lang={e.value}'),
).classes("min-w-[8rem]")
ui.separator().classes("my-4")
with ui.column().classes("w-full gap-4"):
for section in sections:
title = section.get('title', SECTION_TITLES_FALLBACK.get(section.get('id', ''), ''))
blocks = section.get('blocks', [])
with ui.card().classes("w-full"):
if title:
ui.label(title).classes("text-lg font-semibold mb-2")
for block in blocks:
btype = block.get('type', 'markdown')
if btype == 'markdown':
raw = block.get('content', '')
# Unterstützt sowohl String als auch Array
if isinstance(raw, list):
content = '\n'.join(raw)
else:
content = raw
ui.markdown(content).classes("prose max-w-none")
elif btype == 'divider':
ui.separator().classes("my-2")

View File

@ -0,0 +1,33 @@
import json
import os
from nicegui import ui
# Pfad zur JSON Datei berechnen
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
JSON_PATH = os.path.join(BASE_DIR, "info_texts.json")
# JSON Datei in den Speicher laden
def load_info_texts():
if os.path.exists(JSON_PATH):
# encoding="utf-8" ist wichtig für deutsche Umlaute (ä, ö, ü)!
with open(JSON_PATH, "r", encoding="utf-8") as file:
return json.load(file)
else:
print("FEHLER! Keine info_texts.json gefunden!")
return {}
# Wir laden das Wörterbuch genau 1x beim Serverstart
TEXT_DICTIONARY = load_info_texts()
def create_info_button(topic_key):
# Den Text aus dem Wörterbuch holen. Falls nicht vorhanden wird eine Fehlermeldung geworfen.
texts = TEXT_DICTIONARY.get(topic_key, ["Hilfetext nicht gefunden!"])
# --- DER DIALOG (Unsichtbar im Hintergrund) ---
with ui.dialog().classes("w-full items-center") as info_dialog, ui.card().classes("items-center text-textnormal"):
# Loopen durch die Sätze in der JSON
for sentence in texts:
ui.markdown(sentence).classes("text-normaltext text-lg text-center")
# --- DER BUTTON (Sichtbar auf der Seite) ---
ui.button(icon="info_outline", color= "", on_click=info_dialog.open).props('round dense')

View File

@ -0,0 +1,29 @@
{
"league_info": [
"Um einer Liga beizutreten einfach auf **BEITRETEN** drücken und bestätigen.",
"Um deine Statistik in einer Liga zu sehen, klick auf eine Liga."
],
"mmr_info": [
"**MMR Punkte** sind die Liga Punkte um die gespielt wird.",
"Verliert man ein Spiel, verliert man Punkte. Und umgekehrt."
],
"match_form_info": [
"Um ein Spiel einzutragen gibt einfach deine Punkte ein. Wähle deinen Gegner aus. Und gibt seine Punkte ein.",
"**ACHTUNG:** Ein Spieler ist nur als Gegner auswählbar wenn er sich in der Liga angemeldet hat!",
"Solltest du einen Fehler machen kannst du das 'falsche' Match auf der Hauptseite noch löschen bevor es bestätigt wurde."
],
"mmr_calc": [
"Die Berechnung und das System dahinter sind ein wenig komplex. Ich versuche trotzdem es so einfach wie möchlich zu erklären.",
"**Khorne**: Khorne interresiert es nicht woher die Schädel kommen! hat man in den letzten 16Tagen (~2 Wochen) in dieser Liga schon gespielt, bekommt man 8 Punkte",
"**Tzeentch**: Der Herr der Intriegen mag es wenn Pläne aufgehen. Ausgehend von den Maximalen Siegpunkten gewährt er einen Bonus wenn der Gegner mit vielen Punkten besiegt wird. 0-9 Punkte",
"**Slaanesh**: Noch in Arbeit. Irgendwas mit dem ***Tyrann***, ***Prügelknabe***, ***Nemesis***. Das System steht noch nicht.",
"**Nurgle**: Der Meister der Entrophie wird den Spielern ein wenig die MMR Punkte ***verotten***. Das System steht noch nicht!",
"**Elo Faktor**: Elo beschreibt das verhältnis zwischen den Sieger MMR und Verlierer MMR. Damit wird festgestellt wer stärker und wer schwächer ist. Ein Stärkerer Spieler der einen Schwächeren besiegt, kriegt weniger MMR Punkte als umgekehrt. Die Berechnung ist etwas komplex. Führ mehr googelt bitte 'elo schach'. Der Elo Faktor bestimmt die ***Basis MMR***.",
"**Rost Faktor**: Der Rostfaktor ist ein Punktefaktor der zum Einsatz kommt wenn ein Spieler eine Weile nicht mehr gespielt hat. Ab 30 Tagen ist er 0.8 . Von da an wird er graduell weniger bis 90 Tage (0.1). Verhindert das ***eingerostete*** Spieler fertigemacht werden oder gelegentliche gute Spieler zu weit hoch schießen.",
"**Gesamt Berechnung**:",
"Sieger (w_base + w_khorne + slaanesh + tzeentch) * rust_factor).",
"Verlierer: (-l_base + l_khorne - slaanesh - tzeentch) * rust_factor)"
],
"tyrann_info": [],
"prügelknabe_info": []
}

View File

@ -1,13 +1,12 @@
from nicegui import ui, app
from gui import gui_style
from data import data_api
from gui.info_text import info_system
def setup_routes():
# 1. Die {}-Klammern definieren eine dynamische Variable in der URL
@ui.page('/statistic/{systemname}', dark=True)
def gamesystem_statistic_page(systemname: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf!
def gamesystem_statistic_page(systemname: str):
# Sicherheitscheck: Ist der User eingeloggt?
if not app.storage.user.get('authenticated', False):
ui.navigate.to('/')
return
@ -15,80 +14,80 @@ def setup_routes():
gui_style.apply_design()
player_id = app.storage.user.get('db_id')
stats = data_api.get_player_system_stats(player_id, systemname)
all_stats = data_api.get_player_statistics(player_id)
# Passendes System anhand des Namens (case-insensitive) herausfiltern
system_stat = next(
(s for s in all_stats if s["gamesystem_name"].lower() == systemname.lower()),
None
)
if system_stat:
mmr = system_stat["mmr"] or 0
games = system_stat["games_in_system"] or 0
points = system_stat["points"] or 0
avv_points = system_stat.get("avv_points") or "-"
last_played_raw = system_stat.get("last_played")
last_played = str(last_played_raw)[:10] if last_played_raw else "-"
if stats:
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
ui.button('Zurück zur Übersicht', on_click=lambda: ui.navigate.to('/'))
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{systemname}'))
with ui.column().classes('w-full items-center mt-10'):
ui.label(f'Deine Statistik in {systemname}').classes('text-5xl font-bold text-blue-400')
# --- BLOCK 1 (2 Karten) ---
# 1. Daten für die Rangliste holen
with ui.column().classes('w-full items-center justify-center mt-10'):
ui.label(f'Deine Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext')
# --- BLOCK 1 (MMR & Rang | Rangliste) ---
leaderboard_data = data_api.get_leaderboard(systemname)
# 2. Tabelle vorbereiten und den EIGENEN Rang herausfinden
table_rows = []
my_rank = "-"
for index, player in enumerate(leaderboard_data):
current_rank = index + 1
# Wenn wir in der Liste über uns selbst stolpern, merken wir uns den Rang für unsere Karte!
if player['id'] == player_id:
my_rank = current_rank
table_rows.append({
'rank': current_rank,
'trend': '', # Platzhalter für später
'trend': '',
'name': f"{player['display_name']} 'aka' {player['discord_name']}",
'mmr': player['mmr']
})
table_columns = [
{'name': 'rank', 'label': '#', 'field': 'rank', 'align': 'left'},
{'name': 'trend', 'label': 'Trend', 'field': 'trend', 'align': 'center'},
{'name': 'name', 'label': 'Spieler', 'field': 'name', 'align': 'left'},
{'name': 'mmr', 'label': 'MMR', 'field': 'mmr', 'align': 'left'}
{'name': 'rank', 'label': '#', 'field': 'rank', 'align': 'left'},
{'name': 'trend', 'label': 'Trend', 'field': 'trend', 'align': 'center'},
{'name': 'name', 'label': 'Spieler', 'field': 'name', 'align': 'left'},
{'name': 'mmr', 'label': 'MMR', 'field': 'mmr', 'align': 'left'},
]
# --- BLOCK 1 (Links: MMR & Rang | Rechts: Rangliste) ---
# lg:grid-cols-3 teilt den Bildschirm auf großen Monitoren in 3 gleich große unsichtbare Spalten
with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"):
# LINKE SEITE (Belegt 1 Spalte)
# flex-col setzt die beiden Karten exakt übereinander
with ui.column().classes("w-full gap-4"):
with ui.card().classes("w-full items-center justify-center text-center"):
ui.label("MMR Punkte: ").classes('text-2xl font-bold')
ui.label(str(stats["mmr"])).classes('text-4xl font-bold text-blue-100')
with ui.row().classes("w-full items-center text-center"):
ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext')
ui.space()
info_system.create_info_button("mmr_info")
ui.label(str(mmr)).classes('text-4xl font-bold text-accent')
with ui.card().classes("w-full items-center justify-center text-center"):
ui.label("Rang: ").classes('text-2xl font-bold')
# Hier tragen wir jetzt unsere gefundene Platzierung ein!
ui.label(str(my_rank)).classes('text-4xl font-bold text-blue-100')
# RECHTE SEITE (Belegt 2 Spalten -> lg:col-span-2)
with ui.card().classes("w-full lg:col-span-2"):
ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2")
ui.table(columns=table_columns, rows=table_rows, row_key='rank').classes('w-full bg-zinc-900 text-white')
# --- BLOCK 2 (5 Karten) ---
with ui.card().classes("w-full"):
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4"):
with ui.card().classes("items-center justify-center text-center"):
ui.label("Spiele: ").classes('text-2xl font-bold')
ui.label(str(stats["games_in_system"])).classes('text-4xl font-bold text-blue-100')
ui.label(str(games)).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold')
ui.label(str(stats["avv_points"])).classes('text-4xl font-bold text-blue-100')
ui.label(str(avv_points)).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Win-Rate: ").classes('text-2xl font-bold')
@ -96,8 +95,6 @@ def setup_routes():
with ui.card().classes("items-center justify-center text-center"):
ui.label("Letztes Spiel am: ").classes('text-2xl font-bold')
# Schneidet die Zeit vom Datum ab, falls eins existiert
last_played = str(stats["last_played"])[:10] if stats["last_played"] else "-"
ui.label(last_played).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
@ -120,52 +117,6 @@ def setup_routes():
ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold')
ui.label("-").classes('text-4xl font-bold text-blue-100')
# --- BLOCK 4 (Diagramm der letzten 20 Spiele) ---
with ui.card().classes("w-full items-center"):
ui.label("Punkte in den letzten 20 Spielen").classes('text-2xl font-bold mb-4')
# 1. Daten aus der API holen
chart_data = data_api.get_last_20_match_scores(player_id, systemname)
# 2. Nur zeichnen, wenn es überhaupt schon Spiele gibt!
if len(chart_data['points']) > 0:
ui.echart({
'xAxis': {
'type': 'category',
'data': chart_data['labels'],
'axisLabel': {'color': '#e5e7eb'}
},
'yAxis': {
'type': 'value',
'axisLabel': {'color': '#e5e7eb'},
'splitLine': {'lineStyle': {'color': '#3f3f46'}}
},
'series': [
{
'name': 'Punkte', # Zeigt im Tooltip "Punkte: 76" statt einer unbenannten Zahl
'type': 'line',
'data': chart_data['points'],
'smooth': True,
'color': '#60a5fa'
}
],
'tooltip': {
'trigger': 'axis',
'backgroundColor': '#27272a',
'textStyle': {'color': '#ffffff'},
'borderColor': '#3f3f46' # Ein feiner Rand für den Kasten
}
}).classes('w-full h-64')
else:
ui.label("Keine Statistiken für dieses System gefunden.").classes('text-red-500 mt-4')
with ui.column().classes('w-full items-center justify-center mt-10'):
ui.label(f'Keine Statistik für "{systemname}" gefunden.').classes('text-red-500 text-xl mt-4')

View File

@ -2,6 +2,7 @@ from nicegui import ui, app
from data import database, data_api
from gui import discord_login, gui_style
from match_calculations import calc_match
from gui.info_text import info_system
def setup_routes(admin_discord_id):
@ui.page('/', dark=True)
@ -15,30 +16,29 @@ def setup_routes(admin_discord_id):
# Fehlt die Discord-ID (altes Cookie) ODER sagt die Datenbank, dass da was nicht stimmt?
if not discord_id or not data_api.validate_user_session(db_id, discord_id):
# Ausweis ungültig! Wir vernichten das Cookie sofort.
app.storage.user.clear()
ui.notify("Deine Sitzung ist ungültig oder abgelaufen. Bitte neu einloggen!", color="negative")
ui.navigate.reload()
return
# -----------------------------------------
# ---------------------------
# --- NAVIGATIONSLEISTE (HEADER) ---
# --- NAVIGATIONSLEISTE (HEADER)
# ---------------------------
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
with ui.header(fixed=False).classes('items-center justify-between bg-zinc-900 shadow-lg'):
# --- LINKE SEITE ---
# Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1)
with ui.row().classes('items-center gap-4'):
ui.image("gui/pictures/wsdg.png").classes('w-20 h-20 rounded-full')
ui.label('Diceghost Liga').classes('text-2xl font-bold text-white')
with ui.row().classes('items-center'):
ui.image("gui/pictures/wsdg.png").classes('w-15 h-15 rounded-full')
ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext')
# --- MITTE ---
discord_id = app.storage.user.get("discord_id")
if discord_id == admin_discord_id:
ui.button('Admin Panel', on_click=lambda: ui.navigate.to('/admin'))
if app.storage.user.get('authenticated', False):
discord_id = app.storage.user.get("discord_id")
if discord_id == admin_discord_id:
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).props("round")
# --- RECHTE SEITE ---
if app.storage.user.get('authenticated', False):
@ -52,22 +52,21 @@ def setup_routes(admin_discord_id):
def toggle_edit_mode():
display_row.visible = not display_row.visible
edit_row.visible = not edit_row.visible
edit_button.visible = not edit_button.visible
# --- ANSICHT 1: Der normale Text mit Edit-Button ---
with ui.row().classes('items-center gap-2') as display_row:
ui.label(display_name).classes('text-xl font-bold text-white')
ui.label("'aka'").classes('text-sm text-gray-500')
ui.label(discord_name).classes('text-lg text-gray-400')
# Ein runder Button (.props('round'))
ui.button(icon='edit', color='primary', on_click=toggle_edit_mode).props('round dense')
with ui.column().classes('items-center gap-0') as display_row:
with ui.column():
ui.label(display_name).classes('text-xl font-bold text-normaltext')
with ui.row().classes("items-center justify-between"):
ui.label("'aka'").classes('text-sm text-italic text-infotext')
ui.label(discord_name).classes('text-m text-bold text-infotext')
edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense')
# --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) ---
with ui.row().classes('items-center gap-5') as edit_row:
edit_row.visible = False # Am Anfang verstecken
def save_new_name():
new_name = name_input.value
# Nur speichern, wenn ein Name drinsteht und er anders ist als vorher
if new_name and new_name != display_name:
@ -84,110 +83,105 @@ def setup_routes(admin_discord_id):
silly_name = data_api.generate_silly_name()
name_input.value=silly_name
name_input = ui.input('Neuer Name', value=display_name).on('keydown.enter', save_new_name)
ui.button(icon='save', color='positive', on_click=save_new_name).props('round dense')
ui.button(icon='casino', on_click=generate_random_silly_name).props('round dense')
ui.button(icon='casino', color="accent", on_click=generate_random_silly_name).props('round dense')
ui.button(icon='close', color='negative', on_click=toggle_edit_mode).props('round dense')
avatar = app.storage.user.get('avatar_url')
if avatar:
ui.image(avatar).classes('w-12 h-12 rounded-full border-2 border-red-500')
ui.image(avatar).classes('w-5 h-5 rounded-full border-2 border-red-500')
def logout():
app.storage.user.clear()
ui.navigate.to('/')
ui.button('Logout', on_click=logout).classes('bg-red-500 text-white')
ui.button(icon="logout", on_click=logout).props('round dense size=lg')
else:
auth_url = discord_login.get_auth_url()
ui.button('Login with Discord', on_click=lambda: ui.navigate.to(auth_url))
# ---------------------------
# --- Match Bestätigung ---
# ---------------------------
# --- Bestätigungs-Bereich für offene Spiele --- Der "Marian Balken !!!1!11!"
# ---------------------------
# --- Match Bestätigung ---
# ---------------------------
# Bestätigungs für offene Spiele --- Der "Marian Balken !!!1!11!"
if app.storage.user.get('authenticated', False):
unconfirmed_matches = data_api.get_unconfirmed_matches(player_id)
if len(unconfirmed_matches) > 0:
# Eine auffällige, rote Karte über die volle Breite
with ui.card().classes('w-full bg-red-900/80 border-2 border-red-500 mb-6'):
ui.label(f"Aktion erforderlich: Du hast {len(unconfirmed_matches)} unbestätigte(s) Spiel(e)!").classes('text-2xl font-bold text-white mb-2')
ui.label(f"Aktion erforderlich: Du hast {len(unconfirmed_matches)} offen(e) Spiel(e)!").classes('text-2xl font-bold text-normaltext mb-2')
for match in unconfirmed_matches:
# --- NEU: Die Funktion, die beim Klick ausgeführt wird ---
# Button Funktionen. Akzeptieren oder Rejecten. Der Reject Button löscht das Match aus der DB.
def reject_match(m_id):
data_api.delete_match(m_id)
data_api.delete_match(m_id, player_id)
ui.notify("Spiel abgelehnt und gelöscht!", color="warning")
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
def acccept_match(m_id):
ui.notify("Spiel akzeptiert. Wird Berechnet.")
ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren
data_api.confirm_match(m_id)
calc_match.calculate_match(m_id)
# Für jedes Match machen wir eine kleine Reihe
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
# Info-Text: Was wurde eingetragen?
info_text = f"[{match['system_name']}] {match['p1_name']} behauptet: Er hat {match['score_player1']} : {match['score_player2']} gegen dich gespielt."
ui.label(info_text).classes('text-lg text-gray-200')
info_text = f"{match['system_name']} - {match["played_at"]} "
detail_text = f"{match['p1_name']} behauptet: {match['p1_name']} ({match['score_player1']}) vs. Du ({match['score_player2']})"
ui.label(info_text).classes('text-bold text-lg text-normaltext')
ui.label(detail_text).classes('text-bold text-normaltext')
# Die Buttons (Funktion machen wir im nächsten Schritt!)
with ui.row().classes('gap-2'):
# ABLEHNEN und Spiel löschen
ui.button("Ablehnen", color="negative", icon="close", on_click=lambda e, m_id=match['match_id']: reject_match(m_id))
ui.button(color="negative", icon="close", on_click=lambda e, m_id=match['match_id']: reject_match(m_id))
ui.space()
# BESTÄTIGEN und spiel berechnen lassen
ui.button("Bestätigen", color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id))
ui.button(color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id))
ui.label("Bestätigen wenn die Angaben stimmen, ablehnen wenn sich ein Fehler eingeschlichen hat.")
# ---------------------------
# --- Selbst eingetragene, offene Spiele ---
# ---------------------------
submitted_matches = data_api.get_submitted_matches(player_id)
if len(submitted_matches) > 0:
# Eine etwas dezentere Karte (grau)
with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-6'):
ui.label(f"Warten auf Gegner: Du hast {len(submitted_matches)} offene(s) Spiel(e) eingetragen.").classes('text-xl font-bold text-gray-300 mb-2')
ui.label(f"Warten auf Gegner: Du hast {len(submitted_matches)} offene(s) Spiel(e).").classes('text-xl font-bold text-gray-300 mb-2')
for match in submitted_matches:
# Die Lösch-Funktion, die beim Klick ausgeführt wird
def retract_match(m_id):
data_api.delete_match(m_id)
data_api.delete_match(m_id, player_id)
ui.notify("Eingetragenes Spiel zurückgezogen!", color="warning")
ui.navigate.reload()
# Für jedes Match machen wir eine kleine Reihe
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
# Info-Text: Auf wen warten wir?
info_text = f"[{match['system_name']}] Warten auf Bestätigung von {match['p2_name']} ({match['score_player1']} : {match['score_player2']})"
ui.label(info_text).classes('text-lg text-gray-400')
# Der Zurückziehen-Button (wieder mit unserem lambda m_id=... Trick!)
ui.button("Zurückziehen", color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id))
info_text = f"{match['system_name']} - {match["played_at"]} "
detail_text = f"Du ({match["score_player1"]}) vs. {match["p2_name"]}({match["score_player2"]})"
ui.label(info_text).classes('text-bold text-lg text-normaltext')
ui.label(detail_text).classes('text-bold text-normaltext')
ui.button(color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id))
ui.label("Dein Gegner muss das Match noch bestätigen. Wenn du einen Fehler gemacht hast, kannst du es löschen.")
# ---------------------------
# --- Spielsysteme ---
# ---------------------------
if app.storage.user.get('authenticated', False):
with ui.card().classes("w-full"):
ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl")
with ui.card().classes("w-full items-center"):
with ui.row():
ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl text-normaltext")
info_system.create_info_button("league_info")
placements = data_api.get_player_statistics(player_id)
systems = data_api.get_gamesystem_data()
systems = data_api.get_gamesystems_data()
# Python-Trick: Wir wandeln die Spieler-Stats in ein "Wörterbuch" (Dictionary) um.
# So können wir blitzschnell über die System-ID nachschauen, ob er Stats hat.
my_stats = { p['gamesystem_id']: p for p in placements }
def click_join_league(p_id, sys_id):
@ -203,22 +197,18 @@ def setup_routes(admin_discord_id):
for sys in systems:
sys_id = sys['id']
sys_name = sys['name']
sys_logo = sys["picture"]
sys_description = sys['description']
sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors")
with sys_card:
# Kopfzeile
ui.label(text=sys_name).classes('text-xl font-bold')
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
if sys_logo:
ui.image(f"/pictures/{sys_logo}").classes("w-60")
# --- BILD EINFÜGEN ---
# prüfen, ob die Spalte 'pictures' existiert und nicht leer ist
if 'pictures' in sys and sys['pictures']:
ui.image(f"/pictures/{sys['pictures']}").classes('w-24 h-24 object-contain m-2')
if 'description' in sys and sys['description']:
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center')
ui.space()
if sys_description:
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
# Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR?
if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None:
@ -242,16 +232,16 @@ def setup_routes(admin_discord_id):
sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
with ui.row().classes('items-center gap-4'):
ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-blue-400")
ui.label(text=f"Spiele: {stat['games_in_system']}").classes("text-lg")
ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-accenttext")
ui.label(text=f"Spiele: {stat['games_in_system']}").classes("text-lg font-bold text-accenttext")
# ---------------------------
# Match Historie
# ---------------------------
with ui.card().classes("w-full"):
ui.label(text= "Meine letzten Spiele").classes("font-bold text-white text-xl")
with ui.card().classes("w-full items-center"):
ui.label(text= "Meine letzten Spiele").classes("font-bold text-normaltext text-xl")
# 1. Daten aus der DB holen ABER per [:5] hart auf die neuesten 5 Listen-Einträge abschneiden!
raw_matches = data_api.get_recent_matches_for_player(player_id)[:5]
@ -295,7 +285,9 @@ def setup_routes(admin_discord_id):
ui.table(columns=table_columns, rows=table_rows, row_key='date').classes('w-full bg-zinc-900 text-white')
# NEU: Der Button, der zur großen Log-Seite führt
ui.button("Komplette Historie & Log anzeigen", icon="history", on_click=lambda: ui.navigate.to('/matchhistory')).classes('w-full mt-4 bg-zinc-700 text-white hover:bg-zinc-600')
ui.button("Komplette Historie & Log anzeigen", icon="history", on_click=lambda: ui.navigate.to('/matchhistory')).classes('mt-4 bg-zinc-700 text-white hover:bg-zinc-600')
else:
ui.label("Noch keine Spiele absolviert.").classes("text-gray-500 italic")
ui.label("Noch keine Spiele absolviert.").classes("text-infotext italic")
with ui.footer(fixed=False).classes('items-center justify-between bg-zinc-900 shadow-lg'):
ui.button(icon="description", on_click=lambda: ui.navigate.to('/info'))

View File

@ -2,12 +2,12 @@ from nicegui import ui, app
from gui import gui_style
from data import data_api
from match_calculations import calc_match
from gui.info_text import info_system
def setup_routes():
# 1. Die {}-Klammern definieren eine dynamische Variable in der URL
@ui.page('/add-match/{systemname}', dark=True)
def match_form_page(systemname: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf!
@ui.page('/add-match/{system_name}', dark=True)
def match_form_page(system_name: str): # <-- Hier wird der Name des Spielsystems gefiltert.
gui_style.apply_design()
# --- SICHERHEITS-CHECK ---
@ -16,44 +16,61 @@ def setup_routes():
ui.button('Back to Home', on_click=lambda: ui.navigate.to('/'))
return
# ÄNDERUNG: w-full (für Handy) + max-w-md (für PC) + mx-auto (Zentrieren) + p-6 (Innenabstand)
# --- Eingabeformular ---
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6 shadow-xl'):
system_id = data_api.get_gamesystem_id_by_name(system_name)
system_data = data_api.get_gamesystem_data(system_id)
min_score = system_data["min_score"]
max_score = system_data["max_score"]
# Text-Center hinzugefügt, falls der Systemname sehr lang ist und auf dem Handy umbricht
ui.label(f'Neues Spiel für {systemname} eintragen').classes('text-2xl font-bold text-center mb-6')
ui.label(f'Neues Spiel für {system_name} eintragen').classes('text-2xl font-bold text-center mb-6')
ui.label("Meine Punkte:").classes('text-xl font-bold w-full text-left')
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
ui.label("Meine Punkte:").classes('text-xl font-bold w-full text-left')
with ui.column().classes("w-full items-center gap-6"):
# 1. Daten aus der DB holen
raw_players = data_api.get_all_players_from_system(system_name)
my_id = app.storage.user.get('db_id')
# ÄNDERUNG: h-60 entfernt, stattdessen gap-6 (Abstand zwischen den Elementen)
with ui.column().classes("w-full items-center gap-6"):
def add_point():
p1_points.value += 1
# 1. Daten aus der DB holen
raw_players = data_api.get_all_players_from_system(systemname)
my_id = app.storage.user.get('db_id')
def sub_point():
p1_points.value -= 1
# 3. Eine saubere Optionen-Liste für NiceGUI bauen
with ui.row().classes("w-full items-center justify-between"):
p1_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35")
with ui.column().classes("items-center justify-between"):
# Punkte Up- Down- Buttons. und Textanzeige
ui.button(icon="expand_less", on_click=add_point)
ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext")
ui.button(icon="expand_more", on_click=sub_point)
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
dropdown_options = {}
for p in raw_players:
if p['player_id'] == my_id:
continue
dropdown_options[p['player_id']] = f"{p['display_name']} 'aka' {p['discord_name']}"
# ÄNDERUNG: .classes('w-full') hinzugefügt, damit der Slider sich anpasst
p1_points = ui.slider(min=0, max=100, value=10).props("label-always").classes('w-full')
ui.separator().classes('w-full mt-4') # Ein schöner Trennstrich für die Optik
# 5. Dropdown und Gegner Punkte
ui.label("Gegner:").classes('text-xl font-bold w-full text-left')
# ÄNDERUNG: w-64 durch w-full ersetzt
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full')
# ÄNDERUNG: .classes('w-full') hinzugefügt
p2_points = ui.slider(min=0, max=100, value=10).props("label-always").classes('w-full')
def add_point():
p2_points.value += 1
ui.space()
def sub_point():
p2_points.value -= 1
with ui.row().classes("w-full items-center justify-between"):
p2_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35")
with ui.column().classes("items-center justify-between"):
# Punkte Up- Down- Buttons. und Textanzeige
ui.button(icon="expand_less", on_click=add_point)
ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext")
ui.button(icon="expand_more", on_click=sub_point)
# Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern.
def input_match_to_database():
@ -65,13 +82,14 @@ def setup_routes():
score_p1 = p1_points.value
score_p2 = p2_points.value
match_id = data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2)
match_id = data_api.add_new_match(system_name, my_id, p2_id, score_p1, score_p2)
# 4. Erfolgsmeldung und Berechnung
ui.notify("Match erfolgreich eingetragen!", color="green")
ui.navigate.to(f'/statistic/{systemname}')
ui.navigate.to(f'/statistic/{system_name}')
# Buttons ganz unten in einer Reihe
with ui.row().classes("w-full items-center justify-between mt-8"):
ui.button('Cancel', on_click=lambda: ui.navigate.to(f'/statistic/{systemname}')).classes('bg-gray-500 text-white')
ui.button(icon="close", on_click=lambda: ui.navigate.to(f'/statistic/{system_name}')).classes("w-10 h-8 rounded-full")
info_system.create_info_button("match_form_info")
ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database())

View File

@ -1,6 +1,8 @@
from nicegui import ui, app
from data import data_api
from gui import gui_style
from gui import gui_style
from gui.info_text.info_system import create_info_button
def setup_routes():
@ui.page('/matchhistory', dark=True)
@ -8,39 +10,55 @@ def setup_routes():
gui_style.apply_design()
# Sicherheits-Check: Ist der Nutzer eingeloggt?
if not app.storage.user.get('authenticated', False):
ui.label('Bitte logge dich ein.').classes('text-red-500 text-2xl m-4')
return
player_id = app.storage.user.get('db_id')
# Das Haupt-Layout der Seite
with ui.column().classes('w-full max-w-5xl mx-auto p-4'):
with ui.column().classes('w-full mx-auto p-4'):
# Kopfbereich mit Zurück-Button
with ui.row().classes('w-full items-center justify-between mb-6'):
ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white")
create_info_button("mmr_calc")
ui.button("Zurück", icon="arrow_back", on_click=lambda: ui.navigate.to('/')).classes('bg-zinc-700 text-white')
raw_matches = data_api.get_match_history_log(player_id)
table_rows = []
# Daten für die Tabelle aufbereiten
def fmt_signed(val, pending=False):
"""Formatiert einen Integer-Wert mit Vorzeichen oder gibt Sondertexte zurück."""
if pending:
return "Ausstehend"
if val is None:
return "0"
if val > 0:
return f"+{val}"
return str(val)
for i, match in enumerate(raw_matches):
# Bin ich P1 oder P2?
if match['player1_id'] == player_id:
is_player1 = match['player1_id'] == player_id
pending = match['match_is_counted'] == 0
if is_player1:
opponent = f"{match['p2_display']} aka {match['p2_discord']}"
my_score = match['score_player1']
opp_score = match['score_player2']
my_mmr_change = match['player1_mmr_change']
mmr_base = match["player1_base_change"]
mmr_change = match['player1_mmr_change']
khorne = match['player1_khorne']
tzeentch = match['player1_tzeentch']
slaanesh = match['player1_slaanesh']
else:
opponent = f"{match['p1_display']} aka {match['p1_discord']}"
my_score = match['score_player2']
opp_score = match['score_player1']
my_mmr_change = match['player2_mmr_change']
mmr_base = match["player2_base_change"]
mmr_change = match['player2_mmr_change']
khorne = match['player2_khorne']
tzeentch = match['player2_tzeentch']
slaanesh = match['player2_slaanesh']
# Ergebnis Text
if my_score > opp_score:
result = "Gewonnen"
elif my_score < opp_score:
@ -48,47 +66,60 @@ def setup_routes():
else:
result = "Unentschieden"
# MMR Text schön formatieren
if match['match_is_counted'] == 0:
mmr_text = "Ausstehend"
elif my_mmr_change is None:
mmr_text = "0"
elif my_mmr_change > 0:
mmr_text = f"+{my_mmr_change}"
else:
mmr_text = str(my_mmr_change)
elo_factor = match['elo_factor']
rust_factor = match['rust_factor']
table_rows.append({
'id': i,
'date': str(match['played_at'])[:10],
'system': match['gamesystem_name'],
'opponent': opponent,
'score': f"{my_score} : {opp_score}",
'result': result,
'mmr': mmr_text
'score': str(my_score),
'opponent': (f"{opponent} ({opp_score})"),
"basis" : mmr_base,
'elo': fmt_signed(round(elo_factor, 2) if elo_factor is not None else None, pending),
'rust': fmt_signed(round(rust_factor, 2) if rust_factor is not None else None, pending),
'khorne': fmt_signed(khorne, pending),
'tzeentch': fmt_signed(tzeentch, pending),
'slaanesh': fmt_signed(slaanesh, pending),
'mmr': fmt_signed(mmr_change, pending),
})
# Spalten definieren
columns = [
{'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'},
{'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'},
{'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'},
{'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'center'},
{'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'},
{'name': 'mmr', 'label': 'MMR Änderung', 'field': 'mmr', 'align': 'right'}
{'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'},
{'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'},
{'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'center'},
{'name': 'opponent', 'label': 'Gegner (Pkt.)', 'field': 'opponent', 'align': 'left'},
{'name': 'elo', 'label': 'Elo Faktor', 'field': 'elo', 'align': 'right'},
{'name': 'basisMMR', 'label': 'Basis MMR', 'field': 'basis', 'align': 'right'},
{'name': 'rust', 'label': 'Rost Faktor', 'field': 'rust', 'align': 'right'},
{'name': 'khorne', 'label': 'Khorne', 'field': 'khorne', 'align': 'right'},
{'name': 'tzeentch', 'label': 'Tzeentch', 'field': 'tzeentch', 'align': 'right'},
{'name': 'slaanesh', 'label': 'Slaanesh', 'field': 'slaanesh', 'align': 'right'},
{'name': 'mmr', 'label': 'MMR GESAMT', 'field': 'mmr', 'align': 'right'},
]
# Tabelle zeichnen
if len(table_rows) > 0:
history_table = ui.table(columns=columns, rows=table_rows, row_key='id').classes('w-full bg-zinc-900 text-white')
# Shared slot template for colored signed values
colored_slot = '''
<q-td :props="props">
<span :class="{
'text-green-500 font-bold': props.row[props.col.field].startsWith('+'),
'text-red-500 font-bold': props.row[props.col.field].startsWith('-'),
'text-gray-400 italic': props.row[props.col.field] === 'Ausstehend'
}">
{{ props.row[props.col.field] }}
</span>
</q-td>
'''
# KLEINER TRICK: Wir färben die MMR-Spalte grün oder rot, je nachdem ob da ein "+" oder "-" steht!
history_table.add_slot('body-cell-mmr', '''
<q-td :props="props">
<span :class="{'text-green-500 font-bold': props.row.mmr.startsWith('+'), 'text-red-500 font-bold': props.row.mmr.startsWith('-'), 'text-gray-400 italic': props.row.mmr === 'Ausstehend'}">
{{ props.row.mmr }}
</span>
</q-td>
''')
if table_rows:
history_table = ui.table(
columns=columns,
rows=table_rows,
row_key='id'
).classes('w-full bg-zinc-900 text-white')
for col in ['elo', 'rust', 'khorne', 'tzeentch', 'slaanesh', 'mmr']:
history_table.add_slot(f'body-cell-{col}', colored_slot)
else:
ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic")

10
main.py
View File

@ -3,7 +3,9 @@ from dotenv import load_dotenv
from nicegui import ui, app
from data import database
from gui import main_gui, match_gui, discord_login, league_statistic, admin_gui, match_history_gui
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui
from wood import logger
from gui.info_text import info_system
# 1. Lade die geheimen Variablen aus der .env Datei in den Speicher
load_dotenv()
@ -22,15 +24,17 @@ admin_discord_id = os.getenv("ADMIN")
url = os.getenv("APP_URL")
database.check_db()
logger.setup_log_db()
# 3. Seitenrouten aufbauen
main_gui.setup_routes(admin_discord_id)
discord_login.setup_login_routes()
league_statistic.setup_routes()
league_statistic_gui.setup_routes()
match_gui.setup_routes()
admin_gui.setup_routes()
match_history_gui.setup_routes()
imprint_gui.setup_routes()
# 4. Wir starten die NiceGUI App
ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="EIN_super-geheimes_Pa$$wort#!", favicon="gui/pictures/wsdg.png")

View File

@ -1,7 +1,12 @@
from data import data_api
from match_calculations import calc_mmr_change
from match_calculations import calculation
import json
import os
from wood import logger
point_inflation = 1 # => entspricht %
K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt.
# Mach die DB abfrage für die Relevanten Daten. Von hier aus werden die "Aufgaben" und Daten dann an die kleineren Berechnungs Funktionen verteilt.
def calculate_match (match_id):
@ -11,82 +16,93 @@ def calculate_match (match_id):
print("Fehler: Match nicht gefunden!")
return
data_api.confirm_match(match_id)
# Laden und umsetzen der Match Daten
# Laden und aufdröseln der Match Daten
p1_id = match_data['player1_id']
p2_id = match_data['player2_id']
p1_score = match_data['score_player1']
p2_score = match_data['score_player2']
sys_name = match_data['gamesystem_name']
sys_id = match_data['gamesystem_id']
system_name = match_data['gamesystem_name']
system_id = match_data['gamesystem_id']
rules = load_mmr_rule_matrix(sys_name)
draw_diff = rules["draw_point_difference"]
calculated = False
winner_id = None
looser_id = None
match_is_draw = False
winner_score = 0
looser_score = 0
draw_diff = determine_draw_diff(system_id)
# Abgrenzen ob das Match schon berechnet wurde. Weil ein Draw kann 4 Punkte unterschied haben
# 43-41 ist ein Draw aber rein Mathematisch würde es auch ein anderes if triggern
# Abgrenzen, wer gewonnen hat (if, elif, else Kette)
# 1. Ist es ein Unentschieden (Draw)?
# ==========================================
# 1. IDENTIFIKATION
# ==========================================
# Draw
if -draw_diff <= (p1_score - p2_score) <= draw_diff:
winner_id, looser_id = p1_id, p2_id
winner_score, looser_score = p1_score, p2_score
match_is_draw = True
winner_id = p1_id # Bei Draw ist egal, wer wo steht
looser_id = p2_id
winner_score = p1_score
looser_score = p2_score
# 2. Wenn KEIN Draw: Hat Spieler 1 gewonnen?
# P1 Winner
elif p1_score > p2_score:
match_is_draw = False
winner_id = p1_id
looser_id = p2_id
winner_score = p1_score
looser_score = p2_score
# 3. Wenn weder Draw noch P1 Sieg, MUSS P2 gewonnen haben!
winner_id, looser_id = p1_id, p2_id
winner_score, looser_score = p1_score, p2_score
# P2 Winner
else:
match_is_draw = False
winner_id = p2_id
looser_id = p1_id
winner_score = p2_score
looser_score = p1_score
mmr_change_winner, mmr_change_looser = calc_mmr_change.calc_mmr_change(sys_name, winner_id, looser_id, winner_score, looser_score, match_is_draw, rules)
data_api.apply_match_to_player_statistic (winner_id, sys_id, mmr_change_winner, winner_score)
data_api.apply_match_to_player_statistic (looser_id, sys_id, mmr_change_looser, looser_score)
# Zuordnen: Welcher Change gehört zu P1 und welcher zu P2?
if winner_id == p1_id:
p1_change = mmr_change_winner
p2_change = mmr_change_looser
else:
# Wenn der Sieger nicht P1 ist, muss P1 der Verlierer sein (oder Draw, da ist es egal)
p1_change = mmr_change_looser
p2_change = mmr_change_winner
# Die Änderungen für das Log ins Match eintragen!
data_api.update_match_mmr_change(match_id, p1_change, p2_change)
# Das Match als Berechnet markieren
data_api.set_match_counted(match_id)
winner_id, looser_id = p2_id, p1_id
winner_score, looser_score = p2_score, p1_score
# ==========================================
# 2. DIE BERECHNUNG & ROUTING-TABELLE
# ==========================================
# Wir speichern die Ergebnisse direkt in einem temporären Dictionary,
# und nutzen die SPIELER-ID als Schlüsselwort!
elo_factor = calculation.calc_elo_factor(winner_id, looser_id, system_name)
base_change = int(calculation.calc_base_change(elo_factor, match_is_draw, K_FACTOR))
rust_factor = calculation.calc_rust_factor(winner_id, looser_id, system_id)
def load_mmr_rule_matrix(systemname):
safe_name = systemname.replace(" ", "_").lower()
#winner
w_base = int(base_change)
w_khorne = int(calculation.wrath_of_khorne(winner_id, system_id))
file_path = f"match_calculations/mmr_rules_{safe_name}.json"
with open(file_path, "r", encoding="utf-8") as file:
rules = json.load(file)
#looser
l_base = int(base_change*point_inflation)
l_khorne = int(calculation.wrath_of_khorne(looser_id, system_id))
return rules
slaanesh = calculation.slaanesh_delight()
tzeentch = calculation.tzeentch_schemes(system_id, winner_score, looser_score)
# ==========================================
# 3. Daten Verpacken
# ==========================================
calc_results = {
"match_id" : match_id,
"elo_factor" : elo_factor,
"rust_factor" : rust_factor,
"point_inflation" : point_inflation,
"winner_id" : winner_id, # <-- String-Key für save_calculated_match
"looser_id" : looser_id, # <-- String-Key für save_calculated_match
winner_id: { # <-- Variable als Key (z.B. 42: {...})
"base" : w_base,
"khorne" : w_khorne,
"slaanesh" : slaanesh,
"tzeentch" : tzeentch,
"total" : int((w_base + w_khorne + slaanesh + tzeentch) * rust_factor),
},
looser_id: { # <-- Variable als Key (z.B. 7: {...})
"base" : -l_base,
"khorne" : l_khorne,
"slaanesh" : -slaanesh,
"tzeentch" : -tzeentch,
"total" : int((-l_base + l_khorne - slaanesh - tzeentch) * rust_factor),
}
}
logger.log(f"Match{match_id}: Winner {data_api.get_player_name(winner_id)}: Base {w_base} + Khorne({w_khorne}) + Slaanesh({slaanesh}) + Tzeentch({tzeentch}) = {calc_results[winner_id]["total"]}")
logger.log(f"Match{match_id}: Looser {data_api.get_player_name(looser_id)}: -Base({l_base}) + Khorne({l_khorne}) - Slaanesh({slaanesh}) - Tzeentch({tzeentch}) = {calc_results[looser_id]["total"]}")
data_api.save_calculated_match(calc_results)
def determine_draw_diff(sys_id):
draw_diff = 0
match sys_id:
case 1:
draw_diff = 3
case 2,3:
draw_diff = 1
return draw_diff

View File

@ -1,65 +0,0 @@
from data import data_api
# Faktor für die Punkte Inflation. Um diesen Wert verliert der Verlierer weniger Punkte als der Sieger bekommt. Über Kurz oder Lang werden
# die meisten Spieler über 1000MMR sein. Sprich: Neueinsteiger, oder leute die weniger spielen sind eher im unteren Ende als in der Mitte.
point_inflation = 0.7 # => entspricht % ! z.B. 0.7 = 70%
def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw, rules):
#Rang der Spieler holen.
gamesystem_id = data_api.get_gamesystem_id_by_name(systemname)
winner_rank = data_api.get_player_rank(winner_id,gamesystem_id)
looser_rank = data_api.get_player_rank(looser_id, gamesystem_id)
if match_is_draw:
mmr_change = rules["rank_matrix"][str(max(-10, min(10, winner_rank-looser_rank)))]["draw"]
else:
mmr_change = rules["rank_matrix"][str(max(-10, min(10, winner_rank-looser_rank)))]["win"]
# Slaanesh Points berechnen und dem Change hinzufügen.
mmr_change += (sla_points := slaanesh_delight(winner_points, looser_points, rules))
# Variablen für den mmr_change anlegen. Sieger-Verlierer sind unterschiedlich!
winner_mmr_change = 0 + (mmr_change + wrath_of_khorne(winner_id))
looser_mmr_change = 0 + (mmr_change + wrath_of_khorne(looser_id))
if not match_is_draw:
# Verlierer verliert nur einen Teil der Punkte
looser_mmr_change = -int(winner_mmr_change * point_inflation)
return winner_mmr_change, looser_mmr_change
# -----------------
khorne_days = 16
khorne_bonus = 8
# -----------------
def wrath_of_khorne(player_id):
last_played = data_api.get_days_since_last_game(player_id)["days_ago"]
if last_played <= khorne_days:
return khorne_bonus
else:
return 0
def slaanesh_delight(winner_points, looser_points, rules):
point_diff = winner_points - looser_points
# Standardwert, falls gar nichts zutrifft
bonus = 0
for threshold in rules["score_bonus"]:
if point_diff >= threshold["min_diff"]:
bonus = threshold["bonus"]
break
return bonus
def tzeentch_scemes():
print("k")
def nurgles_entropy():
print("k")

View File

@ -0,0 +1,81 @@
from data import data_api
from wood import logger
# Faktor für die Punkte Inflation. Um diesen Wert verliert der Verlierer weniger Punkte als der Sieger bekommt. Über Kurz oder Lang werden
# die meisten Spieler über 1000MMR sein. Sprich: Neueinsteiger, oder leute die weniger spielen sind eher im unteren Ende als in der Mitte.
def calc_base_change(elo_factor, match_is_draw, K_FACTOR):
if match_is_draw:
# Bei einem Draw (0.5) gewinnt der Schwächere leicht Punkte, der Stärkere verliert leicht.
base_change = (K_FACTOR * (0.5 - elo_factor)/2)
else:
# Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele!
base_change = K_FACTOR * (1.0 - elo_factor)
return base_change
def calc_elo_factor(winner_id, looser_id, system_name):
w_stats = data_api.get_player_statistics(winner_id)
l_stats = data_api.get_player_statistics(looser_id)
# Passendes System aus der Liste herausfiltern
w_stat = next((s for s in w_stats if s['gamesystem_name'] == system_name), None)
l_stat = next((s for s in l_stats if s['gamesystem_name'] == system_name), None)
w_mmr = w_stat['mmr'] if w_stat and w_stat['mmr'] is not None else 1000
l_mmr = l_stat['mmr'] if l_stat and l_stat['mmr'] is not None else 1000
return (1 / (1 + 10 ** ((l_mmr - w_mmr) / 400)))
def calc_rust_factor(winner_id, looser_id, gamesystem_id):
# 3. Den "Rostigkeits-Dämpfer" errechnen.
w_days = data_api.get_days_since_last_system_game(winner_id, gamesystem_id)
l_days = data_api.get_days_since_last_system_game(looser_id, gamesystem_id)
# Der größeren der beiden Werte wird verwendet.
days_ago = max(w_days, l_days)
"""Berechnet den Dämpfungsfaktor basierend auf den vergangen Tagen."""
if days_ago <= 30:
return 1.0 # Volle Punkte
elif days_ago > 90:
return 0.1 # Maximal eingerostet (nur 10% der Punkteänderung)
else:
# Lineare Rampe von 0.8 (bei Tag 31) runter auf 0.1 (bei Tag 90)
# Formel: Startwert - (Differenz * Prozentualer Weg)
factor = 0.8 - 0.7 * ((days_ago - 30) / 60)
return round(factor, 2)
def wrath_of_khorne(player_id, system_id):
# -----------------
khorne_days = 16
khorne_bonus = 8
# -----------------
last_played = int(data_api.get_days_since_last_system_game(player_id, system_id))
if last_played <= khorne_days:
return khorne_bonus
else:
return 0
def slaanesh_delight():
return 0
def tzeentch_schemes(system_id, winner_score, looser_score):
sys_data = data_api.get_gamesystem_data(system_id)
max_score = sys_data["max_score"]
diff_normalized = (winner_score - looser_score) / max_score # 0.0 1.0
threshold = 0.1 # Bonus startet erst ab 10% Vorsprung
bonus = int(max(0, diff_normalized - threshold) * 10) # Ergibt 09
bonus = min(bonus, 9) # Sicherheits-Cap
return bonus

View File

@ -1,112 +0,0 @@
{
"system_info": "Balancing für Spearhead",
"draw_point_difference": 3,
"rank_matrix": {
"10": {
"win": 10,
"draw": 30
},
"9": {
"win": 10,
"draw": 30
},
"8": {
"win": 10,
"draw": 20
},
"7": {
"win": 10,
"draw": 20
},
"6": {
"win": 20,
"draw": 10
},
"5": {
"win": 20,
"draw": 10
},
"4": {
"win": 20,
"draw": 0
},
"3": {
"win": 20,
"draw": 0
},
"2": {
"win": 30,
"draw": 0
},
"1": {
"win": 30,
"draw": 0
},
"0": {
"win": 30,
"draw": 0
},
"-1": {
"win": 30,
"draw": 0
},
"-2": {
"win": 40,
"draw": 0
},
"-3": {
"win": 40,
"draw": -10
},
"-4": {
"win": 50,
"draw": -20
},
"-5": {
"win": 60,
"draw": -20
},
"-6": {
"win": 70,
"draw": -20
},
"-7": {
"win": 80,
"draw": -50
},
"-8": {
"win": 100,
"draw": -50
},
"-9": {
"win": 120,
"draw": -60
},
"-10": {
"win": 150,
"draw": -60
}
},
"score_bonus": [
{
"min_diff": 15,
"bonus": 40
},
{
"min_diff": 10,
"bonus": 30
},
{
"min_diff": 6,
"bonus": 20
},
{
"min_diff": 3,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}

View File

@ -1,112 +0,0 @@
{
"system_info": "Balancing für Warhammer 40k",
"draw_point_difference": 3,
"rank_matrix": {
"10": {
"win": 10,
"draw": 30
},
"9": {
"win": 10,
"draw": 30
},
"8": {
"win": 10,
"draw": 20
},
"7": {
"win": 10,
"draw": 20
},
"6": {
"win": 20,
"draw": 10
},
"5": {
"win": 20,
"draw": 10
},
"4": {
"win": 20,
"draw": 0
},
"3": {
"win": 20,
"draw": 0
},
"2": {
"win": 30,
"draw": 0
},
"1": {
"win": 30,
"draw": 0
},
"0": {
"win": 30,
"draw": 0
},
"-1": {
"win": 30,
"draw": 0
},
"-2": {
"win": 40,
"draw": 0
},
"-3": {
"win": 40,
"draw": -10
},
"-4": {
"win": 50,
"draw": -20
},
"-5": {
"win": 60,
"draw": -20
},
"-6": {
"win": 70,
"draw": -20
},
"-7": {
"win": 80,
"draw": -50
},
"-8": {
"win": 100,
"draw": -50
},
"-9": {
"win": 120,
"draw": -60
},
"-10": {
"win": 150,
"draw": -60
}
},
"score_bonus": [
{
"min_diff": 95,
"bonus": 40
},
{
"min_diff": 85,
"bonus": 30
},
{
"min_diff": 65,
"bonus": 20
},
{
"min_diff": 35,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}

View File

@ -1,112 +0,0 @@
{
"system_info": "Balancing für Warhammer Age of Sigmar",
"draw_point_difference": 3,
"rank_matrix": {
"10": {
"win": 10,
"draw": 30
},
"9": {
"win": 10,
"draw": 30
},
"8": {
"win": 10,
"draw": 20
},
"7": {
"win": 10,
"draw": 20
},
"6": {
"win": 20,
"draw": 10
},
"5": {
"win": 20,
"draw": 10
},
"4": {
"win": 20,
"draw": 0
},
"3": {
"win": 20,
"draw": 0
},
"2": {
"win": 30,
"draw": 0
},
"1": {
"win": 30,
"draw": 0
},
"0": {
"win": 30,
"draw": 0
},
"-1": {
"win": 30,
"draw": 0
},
"-2": {
"win": 40,
"draw": 0
},
"-3": {
"win": 40,
"draw": -10
},
"-4": {
"win": 50,
"draw": -20
},
"-5": {
"win": 60,
"draw": -20
},
"-6": {
"win": 70,
"draw": -20
},
"-7": {
"win": 80,
"draw": -50
},
"-8": {
"win": 100,
"draw": -50
},
"-9": {
"win": 120,
"draw": -60
},
"-10": {
"win": 150,
"draw": -60
}
},
"score_bonus": [
{
"min_diff": 40,
"bonus": 40
},
{
"min_diff": 30,
"bonus": 30
},
{
"min_diff": 20,
"bonus": 20
},
{
"min_diff": 10,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}

View File

@ -1,112 +0,0 @@
{
"system_info": "Balancing für Spearhead",
"draw_point_difference": 5,
"rank_matrix": {
"10": {
"win": 10,
"draw": 30
},
"9": {
"win": 10,
"draw": 30
},
"8": {
"win": 10,
"draw": 20
},
"7": {
"win": 10,
"draw": 20
},
"6": {
"win": 20,
"draw": 10
},
"5": {
"win": 20,
"draw": 10
},
"4": {
"win": 20,
"draw": 0
},
"3": {
"win": 20,
"draw": 0
},
"2": {
"win": 30,
"draw": 0
},
"1": {
"win": 30,
"draw": 0
},
"0": {
"win": 30,
"draw": 0
},
"-1": {
"win": 30,
"draw": 0
},
"-2": {
"win": 40,
"draw": 0
},
"-3": {
"win": 40,
"draw": -10
},
"-4": {
"win": 50,
"draw": -20
},
"-5": {
"win": 60,
"draw": -20
},
"-6": {
"win": 70,
"draw": -20
},
"-7": {
"win": 80,
"draw": -50
},
"-8": {
"win": 100,
"draw": -50
},
"-9": {
"win": 120,
"draw": -60
},
"-10": {
"win": 150,
"draw": -60
}
},
"score_bonus": [
{
"min_diff": 95,
"bonus": 40
},
{
"min_diff": 85,
"bonus": 30
},
{
"min_diff": 65,
"bonus": 20
},
{
"min_diff": 35,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}

View File

@ -1,112 +0,0 @@
{
"system_info": "Balancing für Warhammer 40k",
"draw_point_difference": 5,
"rank_matrix": {
"10": {
"win": 10,
"draw": 30
},
"9": {
"win": 10,
"draw": 30
},
"8": {
"win": 10,
"draw": 20
},
"7": {
"win": 10,
"draw": 20
},
"6": {
"win": 20,
"draw": 10
},
"5": {
"win": 20,
"draw": 10
},
"4": {
"win": 20,
"draw": 0
},
"3": {
"win": 20,
"draw": 0
},
"2": {
"win": 30,
"draw": 0
},
"1": {
"win": 30,
"draw": 0
},
"0": {
"win": 30,
"draw": 0
},
"-1": {
"win": 30,
"draw": 0
},
"-2": {
"win": 40,
"draw": 0
},
"-3": {
"win": 40,
"draw": -10
},
"-4": {
"win": 50,
"draw": -20
},
"-5": {
"win": 60,
"draw": -20
},
"-6": {
"win": 70,
"draw": -20
},
"-7": {
"win": 80,
"draw": -50
},
"-8": {
"win": 100,
"draw": -50
},
"-9": {
"win": 120,
"draw": -60
},
"-10": {
"win": 150,
"draw": -60
}
},
"score_bonus": [
{
"min_diff": 95,
"bonus": 40
},
{
"min_diff": 85,
"bonus": 30
},
{
"min_diff": 65,
"bonus": 20
},
{
"min_diff": 35,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}

View File

@ -1,112 +0,0 @@
{
"system_info": "Balancing für Warhammer Age of Sigmar",
"draw_point_difference": 5,
"rank_matrix": {
"10": {
"win": 10,
"draw": 30
},
"9": {
"win": 10,
"draw": 30
},
"8": {
"win": 10,
"draw": 20
},
"7": {
"win": 10,
"draw": 20
},
"6": {
"win": 20,
"draw": 10
},
"5": {
"win": 20,
"draw": 10
},
"4": {
"win": 20,
"draw": 0
},
"3": {
"win": 20,
"draw": 0
},
"2": {
"win": 30,
"draw": 0
},
"1": {
"win": 30,
"draw": 0
},
"0": {
"win": 30,
"draw": 0
},
"-1": {
"win": 30,
"draw": 0
},
"-2": {
"win": 40,
"draw": 0
},
"-3": {
"win": 40,
"draw": -10
},
"-4": {
"win": 50,
"draw": -20
},
"-5": {
"win": 60,
"draw": -20
},
"-6": {
"win": 70,
"draw": -20
},
"-7": {
"win": 80,
"draw": -50
},
"-8": {
"win": 100,
"draw": -50
},
"-9": {
"win": 120,
"draw": -60
},
"-10": {
"win": 150,
"draw": -60
}
},
"score_bonus": [
{
"min_diff": 95,
"bonus": 40
},
{
"min_diff": 85,
"bonus": 30
},
{
"min_diff": 65,
"bonus": 20
},
{
"min_diff": 35,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}

77
wood/logger.py Normal file
View File

@ -0,0 +1,77 @@
import sqlite3
import os
import inspect
from data.setup_database import DB_PATH
DB_FILE = "wood/log.db"
def log(details):
# Arbeitsdaten der aktuellen Funktion (log) holen
this_frame = inspect.currentframe()
try:
# Einen Schritt "nach oben" zum Aufrufer gehen
caller_frame = this_frame.f_back
# 3. Lesbare Infos aus dem Aufrufer-Frame extrahieren
info = inspect.getframeinfo(caller_frame)
file = f"{info.filename}"
source = f"{info.function}() L:{info.lineno}"
# 5. Datenbank-Verbindung aufbauen (DB_FILE muss vorher definiert sein)
connection = sqlite3.connect(DB_FILE)
cursor = connection.cursor()
# 6. Daten in die Datenbank schreiben
cursor.execute(
"INSERT INTO system_log (file, source, details) VALUES (?, ?, ?)",
(file, source, details)
)
connection.commit()
connection.close()
finally:
del this_frame
del caller_frame
def setup_log_db():
# --- DATENBANK CHECK ---
# ACHTUNG: Großschreibung bei DB_FILE beachten!
if not os.path.exists(DB_FILE):
print(f"WARNUNG: '{DB_FILE}' nicht gefunden!")
print("Starte Log-Datenbank-Einrichtung...")
# Wir bauen die Datei direkt HIER auf, ohne setup_database aufzurufen!
connection = sqlite3.connect(DB_FILE)
cursor = connection.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS system_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
file TEXT,
source TEXT,
details TEXT
)
''')
connection.commit()
connection.close()
print("Log-Datenbank aufgebaut!")
else:
print(f"OK: Log-Datenbank '{DB_FILE}' gefunden. Lade System...")
def get_full_log():
"""Holt das komplette Log und übersetzt die Spieler-IDs in Namen."""
# 1. Alle Logs aus der log.db holen
log_conn = sqlite3.connect(DB_FILE)
log_conn.row_factory = sqlite3.Row
log_cursor = log_conn.cursor()
log_cursor.execute("SELECT * FROM system_log ORDER BY timestamp DESC")
logs = [dict(row) for row in log_cursor.fetchall()]
log_conn.close()
return logs