From dba39b4f0493904d186bf835bfdc529ecb11a38a Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Tue, 10 Mar 2026 14:34:25 +0000 Subject: [PATCH 01/30] =?UTF-8?q?Logger=20eingef=C3=BCgt.=20Loggt=20ein=20?= =?UTF-8?q?paar=20Funktionen.=20Admin=20Panel=20zeigt=20Logs=20an.=20Bilde?= =?UTF-8?q?r=20in=20die=20Gamesystem=20Cards=20eingef=C3=BCgt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 1 + data/data_api.py | 64 +++++++++++++--- data/database.py | 2 +- gui/admin_gui.py | 37 ++++++++- gui/main_gui.py | 40 ++++------ main.py | 3 + wood/logger.py | 75 +++++++++++++++++++ 7 files changed, 181 insertions(+), 41 deletions(-) create mode 100644 .nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json create mode 100644 wood/logger.py diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json new file mode 100644 index 0000000..e026266 --- /dev/null +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 2d9e783..20f0e78 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -1,5 +1,6 @@ import sqlite3 import random +from wood import logger from data.setup_database import DB_PATH @@ -50,13 +51,11 @@ def get_or_create_player(discord_id, discord_name, avatar_url): 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("NEW PLAYER", str("Ein neuer Spieler wurde angelegt - " + discord_name), player_id) connection.commit() - cursor.execute("SELECT id, discord_name, display_name, discord_avatar_url FROM players WHERE discord_id = ?", (discord_id,)) player = cursor.fetchone() else: @@ -225,7 +224,7 @@ def join_league(player_id, gamesystem_id): INSERT INTO player_game_statistic (player_id, gamesystem_id) VALUES (?, ?) """ - + logger.log("INTO", f"{get_player_name(player_id)} ist Liga {gamesystem_id} beigetreten", player_id) cursor.execute(query, (player_id, gamesystem_id)) connection.commit() @@ -233,7 +232,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 +296,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("MATCH", f"Match eingetragen. {get_system_name(sys_id)} - {get_player_name(player1_id)} ({score_p1}) vs. {get_player_name(player2_id)} ({score_p2})", player1_id) + connection.commit() connection.close() @@ -394,8 +394,6 @@ def get_match_by_id(match_id): return dict(row) return None # Falls die ID nicht existiert - - from datetime import datetime def get_days_since_last_game(player_id): @@ -465,7 +463,7 @@ def apply_match_to_player_statistic (player_id, gamesystem_id, mmr_change, score # 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)) - + logger.log("MATCH CALC", f"Bei {get_player_name(player_id)} haben sich die MMR Punkte um {mmr_change} geändert.") connection.commit() connection.close() @@ -515,6 +513,48 @@ def update_match_mmr_change(match_id, p1_change, p2_change): +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" + + + + + + # ----------------------------------------------------- # Matches Bestätigen, Löschen, Berechnen, ... # ----------------------------------------------------- @@ -544,14 +584,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 ("MATCH", f"Match mit ID:{match_id} wurde gelöscht von {get_player_name(player_id)}") connection.commit() connection.close() @@ -562,7 +602,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 ("MATCH CALC", f"Match mit ID{match_id} wurde als 'Confirmed' gekennzeichnet") connection.commit() connection.close() @@ -574,7 +614,7 @@ 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,)) - + logger.log ("MATCH CALC", f"Match mit ID{match_id} wurde als 'Counted' gekennzeichnet") connection.commit() connection.close() diff --git a/data/database.py b/data/database.py index 7d1e160..9bd6420 100644 --- a/data/database.py +++ b/data/database.py @@ -10,7 +10,7 @@ if __name__ == "__main__": 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): diff --git a/gui/admin_gui.py b/gui/admin_gui.py index 7a30fa8..d299557 100644 --- a/gui/admin_gui.py +++ b/gui/admin_gui.py @@ -1,10 +1,41 @@ from nicegui import ui, app from data import database, data_api from gui import gui_style +from wood import logger def setup_routes(): @ui.page('/admin', dark=True) - def home_page(): + def admin_page(): gui_style.apply_design() - if app.storage.user.get('authenticated', False): - ui.card().classes("w-full") \ No newline at end of file + + 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): + with ui.card().classes("w-full"): + with ui.row().classes("w-full"): + ui.button("test") + ui.button(icon="refresh", on_click=lambda: ui.navigate.reload) + + 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': 'Zeitstempel', 'field': 'timestamp', 'align': 'left', 'sortable': True}, + {'name': 'user', 'label': 'Auslöser', 'field': 'player_name', 'align': 'left'}, + {'name': 'action', 'label': 'Aktion', 'field': 'action', 'align': 'left', 'sortable': True}, + {'name': 'details', 'label': 'Details', 'field': 'details', 'align': 'left'} + ] + + if len(log_data) > 0: + # Wir schneiden bei den Millisekunden vom Zeitstempel wieder etwas ab [:19] + for row in log_data: + row['timestamp'] = str(row['timestamp'])[:19] + + # Tabelle zeichnen + 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') \ No newline at end of file diff --git a/gui/main_gui.py b/gui/main_gui.py index 197eedb..43f0d57 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -105,10 +105,10 @@ def setup_routes(admin_discord_id): 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) @@ -119,7 +119,7 @@ def setup_routes(admin_discord_id): 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) ui.notify("Spiel abgelehnt und gelöscht!", color="warning") @@ -129,10 +129,7 @@ def setup_routes(admin_discord_id): ui.notify("Spiel akzeptiert. Wird Berechnet.") ui.navigate.reload() # Lädt die Seite neu, um die Karte zu aktualisieren 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." @@ -146,11 +143,11 @@ def setup_routes(admin_discord_id): # 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)) + # --------------------------- # --- 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'): @@ -160,7 +157,7 @@ def setup_routes(admin_discord_id): # 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() @@ -178,7 +175,6 @@ def setup_routes(admin_discord_id): # --------------------------- # --- 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") @@ -186,8 +182,6 @@ def setup_routes(admin_discord_id): placements = data_api.get_player_statistics(player_id) systems = data_api.get_gamesystem_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') + 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') # 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: diff --git a/main.py b/main.py index 006e3de..9c30b26 100644 --- a/main.py +++ b/main.py @@ -4,6 +4,7 @@ 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 wood import logger # 1. Lade die geheimen Variablen aus der .env Datei in den Speicher load_dotenv() @@ -22,6 +23,8 @@ 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) diff --git a/wood/logger.py b/wood/logger.py new file mode 100644 index 0000000..c50652d --- /dev/null +++ b/wood/logger.py @@ -0,0 +1,75 @@ +import sqlite3 +import os + +from data.setup_database import DB_PATH + +DB_FILE = "wood/log.db" + +def log(action, details, player_id = None): + """Schreibt ein Ereignis (Audit Trail) in das System-Log.""" + connection = sqlite3.connect(DB_FILE) + cursor = connection.cursor() + + cursor.execute( + "INSERT INTO system_log (player_id, action, details) VALUES (?, ?, ?)", + (player_id, action, details) + ) + + connection.commit() + connection.close() + + +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, + player_id INTEGER, + action 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() + + # 2. Spielernamen aus der Haupt-DB holen + main_conn = sqlite3.connect(DB_PATH) + main_cursor = main_conn.cursor() + main_cursor.execute("SELECT id, display_name, discord_name FROM players") + players_dict = {row[0]: f"{row[1]} ({row[2]})" for row in main_cursor.fetchall()} + main_conn.close() + + # 3. Die IDs in den Logs durch Namen ersetzen + for log_entry in logs: + p_id = log_entry['player_id'] + if p_id is not None and p_id in players_dict: + log_entry['player_name'] = players_dict[p_id] + else: + log_entry['player_name'] = "System / Unbekannt" + + return logs \ No newline at end of file From b6fea8b84d3333c2c153a47302a3f8a517e5336d Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 11 Mar 2026 14:00:01 +0000 Subject: [PATCH 02/30] =?UTF-8?q?Bug=20in=20Logger=20gel=C3=B6st.=20Neue?= =?UTF-8?q?=20spieler=20konnten=20sich=20nicht=20beim=20System=20anmelden.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- data/data_api.py | 2 +- mmr_calculations/mmr_rules_spearhead.json | 2 +- mmr_calculations/mmr_rules_warhammer_40k.json | 2 +- mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index e026266..0792cd3 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 20f0e78..10aca36 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -54,7 +54,7 @@ def get_or_create_player(discord_id, discord_name, avatar_url): # 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("NEW PLAYER", str("Ein neuer Spieler wurde angelegt - " + discord_name), player_id) + logger.log("NEW PLAYER", str("Ein neuer Spieler wurde angelegt - " + 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() diff --git a/mmr_calculations/mmr_rules_spearhead.json b/mmr_calculations/mmr_rules_spearhead.json index f0efcba..346554f 100644 --- a/mmr_calculations/mmr_rules_spearhead.json +++ b/mmr_calculations/mmr_rules_spearhead.json @@ -1,6 +1,6 @@ { "system_info": "Balancing für Spearhead", - "draw_point_difference": 5, + "draw_point_difference": 3, "rank_matrix": { "10": { "win": 10, diff --git a/mmr_calculations/mmr_rules_warhammer_40k.json b/mmr_calculations/mmr_rules_warhammer_40k.json index 892fe7d..a7163db 100644 --- a/mmr_calculations/mmr_rules_warhammer_40k.json +++ b/mmr_calculations/mmr_rules_warhammer_40k.json @@ -1,6 +1,6 @@ { "system_info": "Balancing für Warhammer 40k", - "draw_point_difference": 5, + "draw_point_difference": 3, "rank_matrix": { "10": { "win": 10, diff --git a/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json b/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json index 679c737..ce24867 100644 --- a/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json +++ b/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json @@ -1,6 +1,6 @@ { "system_info": "Balancing für Warhammer Age of Sigmar", - "draw_point_difference": 5, + "draw_point_difference": 3, "rank_matrix": { "10": { "win": 10, From 4dfdf2165c63b2d0858890bd5b7cee1916f9d9c0 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 11 Mar 2026 18:36:11 +0000 Subject: [PATCH 03/30] Umbau auf MMR Dynamische Berechnung von Matches. --- data/data_api.py | 64 ++++++++++ data/setup_database.py | 71 ----------- gui/admin_gui.py | 3 +- gui/main_gui.py | 2 +- match_calculations/calc_match.py | 10 -- match_calculations/calc_mmr_change.py | 91 +++++++++----- match_calculations/mmr_rules_spearhead.json | 112 ------------------ .../mmr_rules_warhammer_40k.json | 112 ------------------ .../mmr_rules_warhammer_age_of_sigmar.json | 112 ------------------ mmr_calculations/mmr_rules_spearhead.json | 112 ------------------ mmr_calculations/mmr_rules_warhammer_40k.json | 112 ------------------ .../mmr_rules_warhammer_age_of_sigmar.json | 112 ------------------ 12 files changed, 127 insertions(+), 786 deletions(-) delete mode 100644 match_calculations/mmr_rules_spearhead.json delete mode 100644 match_calculations/mmr_rules_warhammer_40k.json delete mode 100644 match_calculations/mmr_rules_warhammer_age_of_sigmar.json delete mode 100644 mmr_calculations/mmr_rules_spearhead.json delete mode 100644 mmr_calculations/mmr_rules_warhammer_40k.json delete mode 100644 mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json diff --git a/data/data_api.py b/data/data_api.py index 10aca36..9e68fe0 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -535,6 +535,7 @@ def get_player_name(player_id): else: return "Gelöschter Spieler" + def get_system_name(sys_id): if sys_id is None: return "Unbekanntes System" @@ -552,6 +553,27 @@ def get_system_name(sys_id): +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 + + @@ -668,3 +690,45 @@ 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("TEST_MATCH", f"Zufallsspiel generiert. [{sys_name}]: {p1_name} ({score_p1}) vs. {dummy_name} ({score_p2})", player_id) + + return True + diff --git a/data/setup_database.py b/data/setup_database.py index 7dc1fab..3ca6ceb 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -197,74 +197,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.") - diff --git a/gui/admin_gui.py b/gui/admin_gui.py index d299557..a9cdd25 100644 --- a/gui/admin_gui.py +++ b/gui/admin_gui.py @@ -2,6 +2,7 @@ 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) @@ -14,7 +15,7 @@ def setup_routes(): if app.storage.user.get('authenticated', False): with ui.card().classes("w-full"): with ui.row().classes("w-full"): - ui.button("test") + ui.button(text= "test", on_click=lambda: data_api.create_random_dummy_match(2)) ui.button(icon="refresh", on_click=lambda: ui.navigate.reload) ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4') diff --git a/gui/main_gui.py b/gui/main_gui.py index 43f0d57..84dd00e 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -285,7 +285,7 @@ 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") diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index cd48ecd..ceeccf1 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -80,13 +80,3 @@ def calculate_match (match_id): # Das Match als Berechnet markieren data_api.set_match_counted(match_id) - - -def load_mmr_rule_matrix(systemname): - safe_name = systemname.replace(" ", "_").lower() - - file_path = f"match_calculations/mmr_rules_{safe_name}.json" - with open(file_path, "r", encoding="utf-8") as file: - rules = json.load(file) - - return rules diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py index 99bbf50..1995bb0 100644 --- a/match_calculations/calc_mmr_change.py +++ b/match_calculations/calc_mmr_change.py @@ -4,29 +4,69 @@ from data import data_api # 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"] +def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw): - # Slaanesh Points berechnen und dem Change hinzufügen. - mmr_change += (sla_points := slaanesh_delight(winner_points, looser_points, rules)) + point_inflation = 0.7 # Verlierer verliert nur 70% der Punkte + K_FACTOR = 35 # Die "Border" (Maximalpunkte) die ein Sieg gibt. - # 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)) + def get_rust_dampener(days_ago): + """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) - 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 + def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw, rules): + gamesystem_id = data_api.get_gamesystem_id_by_name(systemname) + + # 1. Die aktuellen MMR-Punkte holen + w_stat = data_api.get_player_statistics_for_system(winner_id, gamesystem_id) + l_stat = data_api.get_player_statistics_for_system(looser_id, gamesystem_id) + + w_mmr = w_stat['mmr'] + l_mmr = l_stat['mmr'] + + + # 2. Die fließende Elo-Mathematik (Ersetzt die JSON Datei) + # Berechnet die Siegwahrscheinlichkeit des Gewinners (Wert zwischen 0.0 und 1.0) + expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr) / 400)) + + 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 - expected_win) + else: + # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele! + base_change = K_FACTOR * (1.0 - expected_win) + + # 3. Den "Rostigkeits-Dämpfer" anwenden + 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) + + + + # 4. Deine Sonderregeln (Slaanesh & Khorne) addieren + sla_points = slaanesh_delight(winner_points, looser_points, rules) + w_mmr_change += sla_points + + # Auch der Verlierer bekommt Slaanesh Punkte addiert, wenn es die Regel so sagt + l_mmr_change += sla_points + + winner_final = int(w_mmr_change + wrath_of_khorne(winner_id)) + + # 5. Verlierer-Punkte abziehen und Inflation (0.7) anwenden + if match_is_draw: + looser_final = int(-w_mmr_change) # Bei Draw spiegeln wir es einfach (ohne Inflation) + else: + looser_base = int(l_mmr_change + wrath_of_khorne(looser_id)) + looser_final = -int(looser_base * point_inflation) + + return winner_final, looser_final @@ -34,8 +74,6 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin # ----------------- 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"] @@ -46,16 +84,7 @@ def wrath_of_khorne(player_id): 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 + print() def tzeentch_scemes(): diff --git a/match_calculations/mmr_rules_spearhead.json b/match_calculations/mmr_rules_spearhead.json deleted file mode 100644 index bd619ba..0000000 --- a/match_calculations/mmr_rules_spearhead.json +++ /dev/null @@ -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 - } - ] -} \ No newline at end of file diff --git a/match_calculations/mmr_rules_warhammer_40k.json b/match_calculations/mmr_rules_warhammer_40k.json deleted file mode 100644 index a7163db..0000000 --- a/match_calculations/mmr_rules_warhammer_40k.json +++ /dev/null @@ -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 - } - ] -} \ No newline at end of file diff --git a/match_calculations/mmr_rules_warhammer_age_of_sigmar.json b/match_calculations/mmr_rules_warhammer_age_of_sigmar.json deleted file mode 100644 index a4bfbbf..0000000 --- a/match_calculations/mmr_rules_warhammer_age_of_sigmar.json +++ /dev/null @@ -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 - } - ] -} \ No newline at end of file diff --git a/mmr_calculations/mmr_rules_spearhead.json b/mmr_calculations/mmr_rules_spearhead.json deleted file mode 100644 index 346554f..0000000 --- a/mmr_calculations/mmr_rules_spearhead.json +++ /dev/null @@ -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": 95, - "bonus": 40 - }, - { - "min_diff": 85, - "bonus": 30 - }, - { - "min_diff": 65, - "bonus": 20 - }, - { - "min_diff": 35, - "bonus": 10 - }, - { - "min_diff": 0, - "bonus": 0 - } - ] -} \ No newline at end of file diff --git a/mmr_calculations/mmr_rules_warhammer_40k.json b/mmr_calculations/mmr_rules_warhammer_40k.json deleted file mode 100644 index a7163db..0000000 --- a/mmr_calculations/mmr_rules_warhammer_40k.json +++ /dev/null @@ -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 - } - ] -} \ No newline at end of file diff --git a/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json b/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json deleted file mode 100644 index ce24867..0000000 --- a/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json +++ /dev/null @@ -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": 95, - "bonus": 40 - }, - { - "min_diff": 85, - "bonus": 30 - }, - { - "min_diff": 65, - "bonus": 20 - }, - { - "min_diff": 35, - "bonus": 10 - }, - { - "min_diff": 0, - "bonus": 0 - } - ] -} \ No newline at end of file From 9b399af32310ebcb7f29320077c73d34f6cafd36 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 11 Mar 2026 20:43:30 +0100 Subject: [PATCH 04/30] =?UTF-8?q?Info=20Pop=20Ups=20f=C3=BCr=20Tutorial=20?= =?UTF-8?q?und=20Erkl=C3=A4rungen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ge-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json | 1 + data/setup_database.py | 1 - gui/league_statistic.py | 12 ++++++++++-- gui/main_gui.py | 12 ++++++++++-- gui/match_gui.py | 10 +++++++++- 5 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 .nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json diff --git a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json new file mode 100644 index 0000000..ed7d579 --- /dev/null +++ b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":1,"display_name":"Schwitzender Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/setup_database.py b/data/setup_database.py index 3ca6ceb..263c6ff 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -131,7 +131,6 @@ def seed_gamesystems(): print("Spielsysteme angelegt!") #Nächster Schritt: Standard Achievments eintragen. - generate_default_mmr_rules() seed_achievements() def seed_achievements(): diff --git a/gui/league_statistic.py b/gui/league_statistic.py index 4823823..7da6de1 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -62,10 +62,18 @@ def setup_routes(): # LINKE SEITE (Belegt 1 Spalte) # flex-col setzt die beiden Karten exakt übereinander with ui.column().classes("w-full gap-4"): + with ui.dialog().classes("w-full items-center") as mmr_info, ui.card(): + ui.label('MMR Punkte sind die Liga Punkte um die gespielt wird. Verliert man ein Spiel, verliert man Punkte. Und umgekehrt.').classes("font-bold text-white text-l") + ui.button(icon="close", on_click=mmr_info.close).classes("w-10 h-8 rounded-full") 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("") + ui.space() + ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold') + ui.space() + ui.button(icon="help", color="info" ,on_click=mmr_info.open).classes("w-9 h-8 rounded-full") + ui.label(str(stats["mmr"])).classes('text-4xl font-bold text-blue-400') with ui.card().classes("w-full items-center justify-center text-center"): ui.label("Rang: ").classes('text-2xl font-bold') diff --git a/gui/main_gui.py b/gui/main_gui.py index 84dd00e..8391fbb 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -32,7 +32,7 @@ def setup_routes(admin_discord_id): # --- 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.image("gui/pictures/wsdg.png").classes('w-20 h-20 rounded-full') ui.label('Diceghost Liga').classes('text-2xl font-bold text-white') # --- MITTE --- @@ -177,7 +177,15 @@ def setup_routes(admin_discord_id): # --------------------------- 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.dialog().classes("w-full items-center") as league_info, ui.card(): + ui.label('Um einer Liga beizutreten einfach auf "BEITRETEN" drücken und bestätigen.').classes("font-bold text-white text-l") + ui.label("Um deine Statistik in einer Liga zu sehen, klick auf eine Liga.").classes("font-bold text-white text-l") + ui.label("Du kannst, wenn du willst, gerne allen Ligen beitreten.").classes("font-bold text-white text-l") + ui.button(icon="close", on_click=league_info.close) + + with ui.row().classes("w-full items-center"): + ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl") + ui.button(icon="help", color="information" ,on_click=league_info.open).classes("w-10 h-8 rounded-full") placements = data_api.get_player_statistics(player_id) systems = data_api.get_gamesystem_data() diff --git a/gui/match_gui.py b/gui/match_gui.py index e1b5451..072a361 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -71,7 +71,15 @@ def setup_routes(): ui.notify("Match erfolgreich eingetragen!", color="green") ui.navigate.to(f'/statistic/{systemname}') + + with ui.dialog().classes("w-full items-center") as form_info, ui.card(): + ui.label('Um ein Spiel einzutragen einfach deine erspielten Punkte, deinen Gegner und die Punkte von deinem Gegner eintragen').classes("font-bold text-white text-l") + ui.label('ACHTUNG: Damit ein Spieler als Gegner ausgewählt werden kann, muss er der Liga beigetreten sein!').classes("font-bold text-white text-l") + ui.label('Nach dem Absenden muss dein Gegner in seiner Liga App das Spiel noch kurz bestätigen. Solltest du einen Fehler gemacht haben, kannst du das Spiel (bevor es bestätigt wurde) auf der Hauptseite selber löschen.').classes("font-bold text-white text-l") + ui.button(icon="close", on_click=form_info.close).classes("w-10 h-8 rounded-full") + # 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/{systemname}')).classes("w-10 h-8 rounded-full") + ui.button(icon="help", color="information" ,on_click=form_info.open).classes("w-10 h-8 rounded-full") ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database()) From c363e35e53440f13ec38d8d25ec817f018110c63 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 12 Mar 2026 08:34:22 +0000 Subject: [PATCH 05/30] =?UTF-8?q?Info=20Button=20System=20mit=20text.json?= =?UTF-8?q?=20f=C3=BCr=20die=20Info=20Texte.=20GUI=20ein=20wenig=20neu=20d?= =?UTF-8?q?esignt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gui/gui_style.py | 26 ++++++++++++------------ gui/info_text/info_system.py | 37 +++++++++++++++++++++++++++++++++++ gui/info_text/info_texts.json | 16 +++++++++++++++ gui/league_statistic.py | 13 ++++-------- gui/main_gui.py | 25 ++++++++++------------- main.py | 1 + 6 files changed, 80 insertions(+), 38 deletions(-) create mode 100644 gui/info_text/info_system.py create mode 100644 gui/info_text/info_texts.json diff --git a/gui/gui_style.py b/gui/gui_style.py index caccc84..efffbea 100644 --- a/gui/gui_style.py +++ b/gui/gui_style.py @@ -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;') diff --git a/gui/info_text/info_system.py b/gui/info_text/info_system.py new file mode 100644 index 0000000..1a80ac1 --- /dev/null +++ b/gui/info_text/info_system.py @@ -0,0 +1,37 @@ +import json +import os +from nicegui import ui + +# 1. Pfad zur JSON Datei berechnen +BASE_DIR = os.path.dirname(os.path.abspath(__file__)) +JSON_PATH = os.path.join(BASE_DIR, "info_texts.json") + +# 2. 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() +print(TEXT_DICTIONARY.get("league_info")) +print(JSON_PATH) +print(BASE_DIR) + +# 3. Unser neuer Baustein für die Webseite +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("font-bold text-white text-lg text-center") + + # --- DER BUTTON (Sichtbar auf der Seite) --- + ui.button(icon="info_outline", color= "", on_click=info_dialog.open).props('round size') diff --git a/gui/info_text/info_texts.json b/gui/info_text/info_texts.json new file mode 100644 index 0000000..b53be0a --- /dev/null +++ b/gui/info_text/info_texts.json @@ -0,0 +1,16 @@ +{ + "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!" + ] +} diff --git a/gui/league_statistic.py b/gui/league_statistic.py index 7da6de1..d78531a 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -1,6 +1,7 @@ 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 @@ -19,7 +20,7 @@ def setup_routes(): 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'): @@ -59,21 +60,15 @@ def setup_routes(): # 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.dialog().classes("w-full items-center") as mmr_info, ui.card(): - ui.label('MMR Punkte sind die Liga Punkte um die gespielt wird. Verliert man ein Spiel, verliert man Punkte. Und umgekehrt.').classes("font-bold text-white text-l") - ui.button(icon="close", on_click=mmr_info.close).classes("w-10 h-8 rounded-full") - with ui.card().classes("w-full items-center justify-center text-center"): with ui.row().classes("w-full items-center text-center"): ui.label("") ui.space() ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold') ui.space() - ui.button(icon="help", color="info" ,on_click=mmr_info.open).classes("w-9 h-8 rounded-full") - ui.label(str(stats["mmr"])).classes('text-4xl font-bold text-blue-400') + info_system.create_info_button("mmr_info") + ui.label(str(stats["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') diff --git a/gui/main_gui.py b/gui/main_gui.py index 8391fbb..0021a4c 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -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) @@ -32,8 +33,8 @@ def setup_routes(admin_discord_id): # --- 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') + ui.image("gui/pictures/wsdg.png").classes('w-20 h-20 rounded-full') + ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext') # --- MITTE --- discord_id = app.storage.user.get("discord_id") @@ -55,12 +56,12 @@ def setup_routes(admin_discord_id): # --- 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') + ui.label(display_name).classes('text-xl font-bold text-normaltext') + ui.label("'aka'").classes('text-sm text-infotext') + ui.label(discord_name).classes('text-lg text-infotext') # Ein runder Button (.props('round')) - ui.button(icon='edit', color='primary', on_click=toggle_edit_mode).props('round dense') + 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: @@ -87,7 +88,7 @@ def setup_routes(admin_discord_id): 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') @@ -98,7 +99,7 @@ def setup_routes(admin_discord_id): 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') else: auth_url = discord_login.get_auth_url() @@ -177,15 +178,9 @@ def setup_routes(admin_discord_id): # --------------------------- if app.storage.user.get('authenticated', False): with ui.card().classes("w-full"): - with ui.dialog().classes("w-full items-center") as league_info, ui.card(): - ui.label('Um einer Liga beizutreten einfach auf "BEITRETEN" drücken und bestätigen.').classes("font-bold text-white text-l") - ui.label("Um deine Statistik in einer Liga zu sehen, klick auf eine Liga.").classes("font-bold text-white text-l") - ui.label("Du kannst, wenn du willst, gerne allen Ligen beitreten.").classes("font-bold text-white text-l") - ui.button(icon="close", on_click=league_info.close) - with ui.row().classes("w-full items-center"): ui.label(text="Meine Ligaplätze").classes("font-bold text-white text-xl") - ui.button(icon="help", color="information" ,on_click=league_info.open).classes("w-10 h-8 rounded-full") + info_system.create_info_button("league_info") placements = data_api.get_player_statistics(player_id) systems = data_api.get_gamesystem_data() diff --git a/main.py b/main.py index 9c30b26..2a7621a 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,7 @@ 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 wood import logger +from gui.info_text import info_system # 1. Lade die geheimen Variablen aus der .env Datei in den Speicher load_dotenv() From a715cd7bff808208ab1d0a308acc1a6249d396c3 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 12 Mar 2026 14:23:09 +0000 Subject: [PATCH 06/30] MMR Berechnung weiter gemacht. Dynamische Elo berechnung. --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- ...-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json | 1 + data/database.py | 1 - data/setup_database.py | 2 +- gui/info_text/info_system.py | 5 +- gui/league_statistic.py | 10 +-- gui/main_gui.py | 62 +++++++-------- match_calculations/calc_match.py | 17 +++- match_calculations/calc_mmr_change.py | 79 +++++++++++-------- 9 files changed, 96 insertions(+), 83 deletions(-) create mode 100644 .nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index 0792cd3..e85e364 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Schwitzender Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json b/.nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json new file mode 100644 index 0000000..359add1 --- /dev/null +++ b/.nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel N","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/database.py b/data/database.py index 9bd6420..e67a275 100644 --- a/data/database.py +++ b/data/database.py @@ -9,7 +9,6 @@ if __name__ == "__main__": setup_database.init_db() def check_db(): - # --- DATENBANK CHECK --- # Prüfen, ob die Datei existiert db_file = DB_PATH diff --git a/data/setup_database.py b/data/setup_database.py index 263c6ff..3f07502 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -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(): diff --git a/gui/info_text/info_system.py b/gui/info_text/info_system.py index 1a80ac1..df52357 100644 --- a/gui/info_text/info_system.py +++ b/gui/info_text/info_system.py @@ -18,9 +18,6 @@ def load_info_texts(): # Wir laden das Wörterbuch genau 1x beim Serverstart TEXT_DICTIONARY = load_info_texts() -print(TEXT_DICTIONARY.get("league_info")) -print(JSON_PATH) -print(BASE_DIR) # 3. Unser neuer Baustein für die Webseite def create_info_button(topic_key): @@ -34,4 +31,4 @@ def create_info_button(topic_key): ui.markdown(sentence).classes("font-bold text-white text-lg text-center") # --- DER BUTTON (Sichtbar auf der Seite) --- - ui.button(icon="info_outline", color= "", on_click=info_dialog.open).props('round size') + ui.button(icon="info_outline", color= "", on_click=info_dialog.open).props('round dense') diff --git a/gui/league_statistic.py b/gui/league_statistic.py index d78531a..07137b7 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -23,10 +23,9 @@ def setup_routes(): 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') + 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 (2 Karten) --- # 1. Daten für die Rangliste holen leaderboard_data = data_api.get_leaderboard(systemname) @@ -59,13 +58,10 @@ def setup_routes(): # --- 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"): - with ui.column().classes("w-full gap-4"): with ui.card().classes("w-full items-center justify-center text-center"): with ui.row().classes("w-full items-center text-center"): - ui.label("") - ui.space() - ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold') + 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(stats["mmr"])).classes('text-4xl font-bold text-accent') diff --git a/gui/main_gui.py b/gui/main_gui.py index 0021a4c..77de795 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -28,18 +28,19 @@ def setup_routes(admin_discord_id): # --- NAVIGATIONSLEISTE (HEADER) --- # --------------------------- - with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'): + with ui.header().classes('items-center justify-between bg-zinc-900 shadow-lg').props("reveal reveal-offset=1"): # --- 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') + 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): @@ -53,22 +54,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-normaltext') - ui.label("'aka'").classes('text-sm text-infotext') - ui.label(discord_name).classes('text-lg text-infotext') - - # Ein runder Button (.props('round')) - ui.button(icon='edit', color='accent', 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: @@ -85,7 +85,6 @@ 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', color="accent", on_click=generate_random_silly_name).props('round dense') @@ -93,13 +92,13 @@ def setup_routes(admin_discord_id): 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(icon="logout", on_click=logout).props('round dense') + ui.button(icon="logout", on_click=logout).props('round dense size=lg') else: auth_url = discord_login.get_auth_url() @@ -116,7 +115,7 @@ def setup_routes(admin_discord_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: @@ -139,10 +138,10 @@ def setup_routes(admin_discord_id): # 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)) # --------------------------- @@ -155,7 +154,6 @@ def setup_routes(admin_discord_id): 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') 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, player_id) @@ -177,9 +175,9 @@ def setup_routes(admin_discord_id): # --- Spielsysteme --- # --------------------------- if app.storage.user.get('authenticated', False): - with ui.card().classes("w-full"): - with ui.row().classes("w-full items-center"): - 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) @@ -206,12 +204,12 @@ def setup_routes(admin_discord_id): sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors") with sys_card: - ui.label(text=sys_name).classes('text-xl font-bold text-center') + 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") if sys_description: - ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2') + 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: @@ -235,16 +233,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] @@ -290,5 +288,5 @@ def setup_routes(admin_discord_id): # 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('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") diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index ceeccf1..c562bda 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -21,9 +21,6 @@ def calculate_match (match_id): sys_name = match_data['gamesystem_name'] sys_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 @@ -31,6 +28,8 @@ def calculate_match (match_id): winner_score = 0 looser_score = 0 + draw_diff = determine_draw_diff(sys_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 @@ -60,7 +59,7 @@ def calculate_match (match_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) + 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) 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) @@ -80,3 +79,13 @@ def calculate_match (match_id): # Das Match als Berechnet markieren data_api.set_match_counted(match_id) + +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 \ No newline at end of file diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py index 1995bb0..307858f 100644 --- a/match_calculations/calc_mmr_change.py +++ b/match_calculations/calc_mmr_change.py @@ -21,52 +21,65 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin factor = 0.8 - 0.7 * ((days_ago - 30) / 60) return round(factor, 2) + gamesystem_id = data_api.get_gamesystem_id_by_name(systemname) - def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw, rules): - gamesystem_id = data_api.get_gamesystem_id_by_name(systemname) + # 1. Die aktuellen MMR-Punkte holen + w_stat = data_api.get_player_system_stats(winner_id, gamesystem_id) + l_stat = data_api.get_player_system_stats(looser_id, gamesystem_id) - # 1. Die aktuellen MMR-Punkte holen - w_stat = data_api.get_player_statistics_for_system(winner_id, gamesystem_id) - l_stat = data_api.get_player_statistics_for_system(looser_id, gamesystem_id) - - w_mmr = w_stat['mmr'] - l_mmr = l_stat['mmr'] + 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 - # 2. Die fließende Elo-Mathematik (Ersetzt die JSON Datei) - # Berechnet die Siegwahrscheinlichkeit des Gewinners (Wert zwischen 0.0 und 1.0) - expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr) / 400)) + # 2. Die fließende Elo-Mathematik (Ersetzt die JSON Datei) + # Berechnet die Siegwahrscheinlichkeit des Gewinners (Wert zwischen 0.0 und 1.0) + expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr) / 400)) - 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 - expected_win) + 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 - expected_win) + else: + # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele! + base_change = K_FACTOR * (1.0 - expected_win) + + # 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) + + def get_rust_dampener(days_ago): + if days_ago <= 30: + return 1.0 # Volle Punkte + elif days_ago > 90: + return 0.1 # Maximal eingerostet (nur 10% der Punkteänderung) else: - # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele! - base_change = K_FACTOR * (1.0 - expected_win) + # 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) - # 3. Den "Rostigkeits-Dämpfer" anwenden - 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) + rust_factor = get_rust_dampener(w_days) + w_mmr_change = base_change * rust_factor + l_mmr_change = base_change * rust_factor + + # 4. Deine Sonderregeln (Slaanesh & Khorne) addieren + sla_points = 0 #slaanesh_delight(winner_points, looser_points) + w_mmr_change += sla_points - # 4. Deine Sonderregeln (Slaanesh & Khorne) addieren - sla_points = slaanesh_delight(winner_points, looser_points, rules) - w_mmr_change += sla_points + # Auch der Verlierer bekommt Slaanesh Punkte addiert, wenn es die Regel so sagt + l_mmr_change += sla_points - # Auch der Verlierer bekommt Slaanesh Punkte addiert, wenn es die Regel so sagt - l_mmr_change += sla_points + winner_final = int(w_mmr_change + wrath_of_khorne(winner_id)) - winner_final = int(w_mmr_change + wrath_of_khorne(winner_id)) + # 5. Verlierer-Punkte abziehen und Inflation (0.7) anwenden + if match_is_draw: + looser_final = int(-w_mmr_change) # Bei Draw spiegeln wir es einfach (ohne Inflation) + else: + looser_base = int(l_mmr_change + wrath_of_khorne(looser_id)) + looser_final = -int(looser_base * point_inflation) - # 5. Verlierer-Punkte abziehen und Inflation (0.7) anwenden - if match_is_draw: - looser_final = int(-w_mmr_change) # Bei Draw spiegeln wir es einfach (ohne Inflation) - else: - looser_base = int(l_mmr_change + wrath_of_khorne(looser_id)) - looser_final = -int(looser_base * point_inflation) - - return winner_final, looser_final + return winner_final, looser_final From b10a1508f226bb474c0f7687b3c95929658b80fe Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 12 Mar 2026 15:11:54 +0000 Subject: [PATCH 07/30] MMR Berechnung. Aber irgendwas stimmt nicht. --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- data/data_api.py | 9 ---- match_calculations/calc_match.py | 3 +- match_calculations/calc_mmr_change.py | 46 ++++++------------- 4 files changed, 15 insertions(+), 45 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index e85e364..719b094 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Schwitzender Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Guardmen","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 9e68fe0..a66d8b1 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -459,7 +459,6 @@ def apply_match_to_player_statistic (player_id, gamesystem_id, mmr_change, score 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)) @@ -512,7 +511,6 @@ def update_match_mmr_change(match_id, p1_change, p2_change): connection.close() - def get_player_name(player_id): """Gibt den Namen eines Spielers im Format 'Anzeigename (Discordname)' zurück.""" @@ -552,7 +550,6 @@ def get_system_name(sys_id): 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) @@ -576,7 +573,6 @@ def get_days_since_last_system_game(player_id, gamesystem_id): - # ----------------------------------------------------- # Matches Bestätigen, Löschen, Berechnen, ... # ----------------------------------------------------- @@ -613,7 +609,6 @@ def delete_match(match_id, player_id): # DELETE FROM löscht die gesamte Zeile, bei der die ID übereinstimmt. cursor.execute("DELETE FROM matches WHERE id = ?", (match_id,)) - logger.log ("MATCH", f"Match mit ID:{match_id} wurde gelöscht von {get_player_name(player_id)}") connection.commit() connection.close() @@ -624,7 +619,6 @@ 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 ("MATCH CALC", f"Match mit ID{match_id} wurde als 'Confirmed' gekennzeichnet") connection.commit() connection.close() @@ -636,7 +630,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,)) - logger.log ("MATCH CALC", f"Match mit ID{match_id} wurde als 'Counted' gekennzeichnet") connection.commit() connection.close() @@ -665,8 +658,6 @@ 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.""" connection = sqlite3.connect(DB_PATH) diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index c562bda..06fe0f8 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -2,6 +2,7 @@ from data import data_api from match_calculations import calc_mmr_change import json import os +from wood import logger # 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): @@ -30,7 +31,6 @@ def calculate_match (match_id): draw_diff = determine_draw_diff(sys_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) @@ -60,7 +60,6 @@ def calculate_match (match_id): 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) - 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) diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py index 307858f..9f66ac7 100644 --- a/match_calculations/calc_mmr_change.py +++ b/match_calculations/calc_mmr_change.py @@ -1,4 +1,5 @@ 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. @@ -7,7 +8,7 @@ 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): point_inflation = 0.7 # Verlierer verliert nur 70% der Punkte - K_FACTOR = 35 # Die "Border" (Maximalpunkte) die ein Sieg gibt. + K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt. def get_rust_dampener(days_ago): """Berechnet den Dämpfungsfaktor basierend auf den vergangen Tagen.""" @@ -27,10 +28,10 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin w_stat = data_api.get_player_system_stats(winner_id, gamesystem_id) l_stat = data_api.get_player_system_stats(looser_id, gamesystem_id) - w_mmr = w_stat['mmr'] if w_stat and w_stat['mmr'] is not None else 1000 + # Wenn ein Spieler noch keine keine Stats hat wird None zurück gegeben. Fallback für diesen Fall + 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 - # 2. Die fließende Elo-Mathematik (Ersetzt die JSON Datei) # Berechnet die Siegwahrscheinlichkeit des Gewinners (Wert zwischen 0.0 und 1.0) expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr) / 400)) @@ -46,42 +47,21 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin 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) - def get_rust_dampener(days_ago): - 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) + days = max(w_days, l_days) - rust_factor = get_rust_dampener(w_days) - - w_mmr_change = base_change * rust_factor - l_mmr_change = base_change * rust_factor - - - # 4. Deine Sonderregeln (Slaanesh & Khorne) addieren + rust_factor = get_rust_dampener(days) sla_points = 0 #slaanesh_delight(winner_points, looser_points) - w_mmr_change += sla_points - # Auch der Verlierer bekommt Slaanesh Punkte addiert, wenn es die Regel so sagt - l_mmr_change += sla_points + logger.log(f"MATCH CALC", f"Base Change {base_change}, Rost Faktor {rust_factor}, EloFaktor {expected_win}") - winner_final = int(w_mmr_change + wrath_of_khorne(winner_id)) - - # 5. Verlierer-Punkte abziehen und Inflation (0.7) anwenden + # Wenn Das Match ein Draw ist, können keine Slaanesh Punkte auftreten. if match_is_draw: - looser_final = int(-w_mmr_change) # Bei Draw spiegeln wir es einfach (ohne Inflation) + winner_final_mmr_change = int(w_mmr_change + wrath_of_khorne(winner_id)* rust_factor) + looser_final_mmr_change = int(l_mmr_change + wrath_of_khorne(looser_id)* rust_factor) else: - looser_base = int(l_mmr_change + wrath_of_khorne(looser_id)) - looser_final = -int(looser_base * point_inflation) - - return winner_final, looser_final - - + winner_final_mmr_change = int((base_change + wrath_of_khorne(winner_id) + sla_points) * rust_factor) + looser_final_mmr_change = int((base_change + wrath_of_khorne(winner_id) - sla_points) * rust_factor) + return winner_final_mmr_change, looser_final_mmr_change # ----------------- From 80071e2b5724f15a38d575f89192872a0f7e44b8 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Fri, 13 Mar 2026 10:29:01 +0000 Subject: [PATCH 08/30] Elo System funktioniert. QA Test mit Marian! --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- data/data_api.py | 1 + gui/main_gui.py | 2 +- match_calculations/calc_match.py | 2 +- match_calculations/calc_mmr_change.py | 27 +++++++++---------- 5 files changed, 17 insertions(+), 17 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index 719b094..e85e364 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Guardmen","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Schwitzender Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index a66d8b1..071bb70 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -326,6 +326,7 @@ def get_player_system_stats(player_id, system_name): # Wenn wir was finden, machen wir ein Dictionary draus, ansonsten geben wir None zurück if row: return dict(row) + logger.log("data_api/Get Player Data", "get_player_system_stats returned None. New player?") return None diff --git a/gui/main_gui.py b/gui/main_gui.py index 77de795..97f7d57 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -121,7 +121,7 @@ def setup_routes(admin_discord_id): # 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 diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 06fe0f8..bd46d48 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -61,7 +61,7 @@ def calculate_match (match_id): 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) 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) + 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: diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py index 9f66ac7..a394589 100644 --- a/match_calculations/calc_mmr_change.py +++ b/match_calculations/calc_mmr_change.py @@ -3,13 +3,11 @@ 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. -point_inflation = 0.7 # => entspricht % ! z.B. 0.7 = 70% +point_inflation = 0.7 # => entspricht % +K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt. def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw): - point_inflation = 0.7 # Verlierer verliert nur 70% der Punkte - K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt. - def get_rust_dampener(days_ago): """Berechnet den Dämpfungsfaktor basierend auf den vergangen Tagen.""" if days_ago <= 30: @@ -25,20 +23,21 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin gamesystem_id = data_api.get_gamesystem_id_by_name(systemname) # 1. Die aktuellen MMR-Punkte holen - w_stat = data_api.get_player_system_stats(winner_id, gamesystem_id) - l_stat = data_api.get_player_system_stats(looser_id, gamesystem_id) + w_stat = data_api.get_player_system_stats(winner_id, systemname) + l_stat = data_api.get_player_system_stats(looser_id, systemname) # Wenn ein Spieler noch keine keine Stats hat wird None zurück gegeben. Fallback für diesen Fall 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 - # 2. Die fließende Elo-Mathematik (Ersetzt die JSON Datei) - # Berechnet die Siegwahrscheinlichkeit des Gewinners (Wert zwischen 0.0 und 1.0) - expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr) / 400)) + expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr)/400)) + + logger.log("MATCH CALC", f"1 / (1 + 10 ** (({l_mmr} - {w_mmr})/400))") 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 - expected_win) + logger.log("MATCH CALC", "Draw") else: # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele! base_change = K_FACTOR * (1.0 - expected_win) @@ -50,17 +49,17 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin days = max(w_days, l_days) rust_factor = get_rust_dampener(days) - sla_points = 0 #slaanesh_delight(winner_points, looser_points) + sla_points = 0 #slaanesh_delight(winner_points, looser_points) - logger.log(f"MATCH CALC", f"Base Change {base_change}, Rost Faktor {rust_factor}, EloFaktor {expected_win}") + logger.log(f"MATCH CALC", f"Base Change: {base_change}, Rost Faktor: {rust_factor}, EloFaktor: {expected_win}") # Wenn Das Match ein Draw ist, können keine Slaanesh Punkte auftreten. if match_is_draw: - winner_final_mmr_change = int(w_mmr_change + wrath_of_khorne(winner_id)* rust_factor) - looser_final_mmr_change = int(l_mmr_change + wrath_of_khorne(looser_id)* rust_factor) + winner_final_mmr_change = int(base_change + wrath_of_khorne(winner_id)* rust_factor) + looser_final_mmr_change = int(base_change + wrath_of_khorne(looser_id)* rust_factor) else: winner_final_mmr_change = int((base_change + wrath_of_khorne(winner_id) + sla_points) * rust_factor) - looser_final_mmr_change = int((base_change + wrath_of_khorne(winner_id) - sla_points) * rust_factor) + looser_final_mmr_change = int(((base_change + wrath_of_khorne(winner_id) - sla_points) * rust_factor)* point_inflation) return winner_final_mmr_change, looser_final_mmr_change From c91271dc48f7b73a03af69c21151b8e0cfaaae4a Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Sat, 14 Mar 2026 19:10:56 +0000 Subject: [PATCH 09/30] Draw Base_Change halbiert bei der Match berechnung. --- .nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json | 1 + .nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json | 1 + .nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json | 1 + match_calculations/calc_mmr_change.py | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 .nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json create mode 100644 .nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json create mode 100644 .nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json diff --git a/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json b/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json new file mode 100644 index 0000000..8a8a4ed --- /dev/null +++ b/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"159672532159561728","discord_name":"buddyl33t","db_id":3,"display_name":"Marian","discord_avatar_url":"https://cdn.discordapp.com/avatars/159672532159561728/866d0dddade9d551f3c5025bb6467da0.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json b/.nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json new file mode 100644 index 0000000..26c1238 --- /dev/null +++ b/.nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"113708052485636100","discord_name":"staelwulf","db_id":4,"display_name":"Max","discord_avatar_url":"https://cdn.discordapp.com/avatars/113708052485636100/d53339dd6a6659231c5c16645ba258df.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json b/.nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json new file mode 100644 index 0000000..e026266 --- /dev/null +++ b/.nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py index a394589..4785afd 100644 --- a/match_calculations/calc_mmr_change.py +++ b/match_calculations/calc_mmr_change.py @@ -36,7 +36,7 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin 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 - expected_win) + base_change = (K_FACTOR * (0.5 - expected_win)/2) logger.log("MATCH CALC", "Draw") else: # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele! From c5dcd5641088d4ce22d7f649ed8a87d400708ad6 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Sat, 14 Mar 2026 21:12:09 +0100 Subject: [PATCH 10/30] Messy MMR+ / - in der DB. Anzeige und Berechnung stimmt was nicht. --- ...-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json | 2 +- data/setup_database.py | 14 +++++++++++++- gui/match_history_gui.py | 8 ++++++-- match_calculations/calc_match.py | 2 +- match_calculations/calc_mmr_change.py | 8 ++++++-- 5 files changed, 27 insertions(+), 7 deletions(-) diff --git a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json index ed7d579..90a427a 100644 --- a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json +++ b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":1,"display_name":"Schwitzender Grot","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"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"} \ No newline at end of file diff --git a/data/setup_database.py b/data/setup_database.py index 3f07502..87cda8d 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -54,6 +54,7 @@ def init_db(): points INTEGER DEFAULT 0, avv_points INTEGER DEFAULT 0, last_played TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + trend INTEGER DEFAULT 0, FOREIGN KEY (player_id) REFERENCES players (id), FOREIGN KEY (gamesystem_id) REFERENCES gamesystems (id) ) @@ -69,8 +70,19 @@ 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, + player2_check INTEGER DEFAULT 0, match_is_counted INTEGER DEFAULT 0, FOREIGN KEY (gamesystem_id) REFERENCES gamesystems (id), @@ -89,7 +101,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, diff --git a/gui/match_history_gui.py b/gui/match_history_gui.py index 4357885..1ccd79a 100644 --- a/gui/match_history_gui.py +++ b/gui/match_history_gui.py @@ -55,6 +55,8 @@ def setup_routes(): mmr_text = "0" elif my_mmr_change > 0: mmr_text = f"+{my_mmr_change}" + elif my_mmr_change < 0: + mmr_text = f"{my_mmr_change}" else: mmr_text = str(my_mmr_change) @@ -63,7 +65,8 @@ def setup_routes(): 'date': str(match['played_at'])[:10], 'system': match['gamesystem_name'], 'opponent': opponent, - 'score': f"{my_score} : {opp_score}", + 'score': f"{my_score}", + 'opp_score': f"{opp_score}", 'result': result, 'mmr': mmr_text }) @@ -72,8 +75,9 @@ def setup_routes(): columns = [ {'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'}, {'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'}, + {'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'left'}, {'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'}, - {'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'center'}, + {'name': 'opp_score', 'label': 'Gegner Punkte', 'field': 'score', 'align': 'center'}, {'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'}, {'name': 'mmr', 'label': 'MMR Änderung', 'field': 'mmr', 'align': 'right'} ] diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index bd46d48..06fe0f8 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -61,7 +61,7 @@ def calculate_match (match_id): 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) 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) + 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: diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py index 4785afd..eb26f8a 100644 --- a/match_calculations/calc_mmr_change.py +++ b/match_calculations/calc_mmr_change.py @@ -59,8 +59,12 @@ def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_poin looser_final_mmr_change = int(base_change + wrath_of_khorne(looser_id)* rust_factor) else: winner_final_mmr_change = int((base_change + wrath_of_khorne(winner_id) + sla_points) * rust_factor) - looser_final_mmr_change = int(((base_change + wrath_of_khorne(winner_id) - sla_points) * rust_factor)* point_inflation) - return winner_final_mmr_change, looser_final_mmr_change + looser_final_mmr_change = int(((base_change + wrath_of_khorne(winner_id) - sla_points) * rust_factor)* point_inflation) + + logger.log("MMR CALC", f"Winner: Base:{base_change} Khorne:{wrath_of_khorne(winner_id)} Sla:{sla_points} = {winner_final_mmr_change}") + logger.log("MMR CALC", f"Looser: Base:{base_change} Khorne:{wrath_of_khorne(winner_id)} Sla:{sla_points} = {looser_final_mmr_change}") + + return winner_final_mmr_change, (looser_final_mmr_change) # ----------------- From 1dcfb0694b6f53175f0335669bcaf207d6db020f Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Sun, 15 Mar 2026 20:46:45 +0100 Subject: [PATCH 11/30] umbau auf bessere und einfache MMR Berechnung. Bessere zuordnung der Winner-Looser Daten. --- ...-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json | 2 +- data/data_api.py | 117 +++++++++++------ data/setup_database.py | 4 + match_calculations/calc_match.py | 121 ++++++++++-------- match_calculations/calc_mmr_change.py | 90 ------------- match_calculations/calculation.py | 74 +++++++++++ 6 files changed, 228 insertions(+), 180 deletions(-) delete mode 100644 match_calculations/calc_mmr_change.py create mode 100644 match_calculations/calculation.py diff --git a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json index 90a427a..9e26dfe 100644 --- a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json +++ b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json @@ -1 +1 @@ -{"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"} \ No newline at end of file +{} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 071bb70..b5d5fb3 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -46,7 +46,7 @@ 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() @@ -224,7 +224,7 @@ def join_league(player_id, gamesystem_id): INSERT INTO player_game_statistic (player_id, gamesystem_id) VALUES (?, ?) """ - logger.log("INTO", f"{get_player_name(player_id)} ist Liga {gamesystem_id} beigetreten", player_id) + logger.log("DataAPI", f"{get_player_name(player_id)} joined {gamesystem_id}", player_id) cursor.execute(query, (player_id, gamesystem_id)) connection.commit() @@ -296,7 +296,7 @@ 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("MATCH", f"Match eingetragen. {get_system_name(sys_id)} - {get_player_name(player1_id)} ({score_p1}) vs. {get_player_name(player2_id)} ({score_p2})", player1_id) + logger.log("DataAPI", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {gamesystem_id}, {get_player_name(player2_id)}:({score_p2})", player_id) connection.commit() connection.close() @@ -397,6 +397,7 @@ def get_match_by_id(match_id): 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) @@ -443,32 +444,84 @@ def get_days_since_last_game(player_id): return None - - -def apply_match_to_player_statistic (player_id, gamesystem_id, mmr_change, scored_points): +# Eintragen eines berechneten Matches. +def save_calculated_match(results): + """ + Speichert alle Match-Details und aktualisiert gleichzeitig die Statistiken beider Spieler. + 'results' ist ein Dictionary (dict) mit allen berechneten Werten. + """ 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)) - logger.log("MATCH CALC", f"Bei {get_player_name(player_id)} haben sich die MMR Punkte um {mmr_change} geändert.") - connection.commit() - connection.close() + try: + # 1. Die Match-Tabelle updaten (Alle Detailwerte eintragen und als berechnet markieren) + 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, ( + results.get('p1_base', 0), results.get('p1_khorne', 0), results.get('p1_slaanesh', 0), results.get('p1_tzeentch', 0), results.get('p1_total', 0), + results.get('p2_base', 0), results.get('p2_khorne', 0), results.get('p2_slaanesh', 0), results.get('p2_tzeentch', 0), results.get('p2_total', 0), + results.get('rust_factor', 1.0), results.get('elo_factor', 0.5), results.get('point_inflation', 0.7), + results['match_id'] + )) + + # 2. Vorlage für das Update der Spieler-Statistiken + 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 = ? + """ + + # 3. Statistik für Spieler 1 überschreiben + cursor.execute(stat_query, ( + results.get('p1_total', 0), results.get('p1_score', 0), results.get('p1_score', 0), + results['p1_id'], results['gamesystem_id'] + )) + + # 4. Statistik für Spieler 2 überschreiben + cursor.execute(stat_query, ( + results.get('p2_total', 0), results.get('p2_score', 0), results.get('p2_score', 0), + results['p2_id'], results['gamesystem_id'] + )) + + # 5. WICHTIG: Erst jetzt (wenn alle 3 Befehle fehlerfrei durchliefen) speichern wir fest! + connection.commit() + + # 6. Ein sauberer Log-Eintrag + # (Achtung: Stelle sicher, dass du deinen 'logger' hier richtig importiert hast) + logger.log("MATCH_CALC", f"Match ID {results['match_id']} wurde komplett berechnet und verbucht.") + return True + + except Exception as e: + # FAIL-SAFE: Wenn irgendwas schiefgeht, machen wir einen ROLLBACK! + # Die Datenbank wird auf den Stand vor dieser Funktion zurückgesetzt. Nichts wird gespeichert. + connection.rollback() + print(f"KRITISCHER FEHLER beim Speichern des Matches: {e}") + return False + + finally: + # Die Verbindung wird am Ende immer geschlossen + connection.close() + + +# ----------------------------------------------------- +# Get Data Funktionen +# ----------------------------------------------------- + def get_leaderboard(system_name): """Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste.""" connection = sqlite3.connect(DB_PATH) @@ -497,21 +550,6 @@ 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.""" - 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)) - - connection.commit() - connection.close() - - def get_player_name(player_id): """Gibt den Namen eines Spielers im Format 'Anzeigename (Discordname)' zurück.""" @@ -610,6 +648,7 @@ def delete_match(match_id, player_id): # DELETE FROM löscht die gesamte Zeile, bei der die ID übereinstimmt. cursor.execute("DELETE FROM matches WHERE id = ?", (match_id,)) + logger.log("data_api.delete_match", f"Match mit ID{match_id} von User gelöscht.", f"{player_id}") connection.commit() connection.close() @@ -620,6 +659,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("data_api.confirm_match", f"Match mit ID{match_id} von Player2 bestätigt.") connection.commit() connection.close() @@ -631,6 +671,7 @@ 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,)) + logger.log("data_api.set_match_counted", f"Match mit ID: {match_id} berechnet.") connection.commit() connection.close() diff --git a/data/setup_database.py b/data/setup_database.py index 87cda8d..75b6848 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -83,6 +83,10 @@ def init_db(): 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), diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 06fe0f8..06773e8 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -1,9 +1,13 @@ 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 = 0.7 # => 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): match_data = data_api.get_match_by_id(match_id) @@ -13,8 +17,8 @@ def calculate_match (match_id): 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'] @@ -22,61 +26,76 @@ def calculate_match (match_id): sys_name = match_data['gamesystem_name'] sys_id = match_data['gamesystem_id'] - calculated = False - winner_id = None - looser_id = None - match_is_draw = False - winner_score = 0 - looser_score = 0 - - draw_diff = determine_draw_diff(sys_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 (Bleibt gleich) +# ========================================== if -draw_diff <= (p1_score - p2_score) <= draw_diff: - 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? + winner_id, looser_id = p1_id, p2_id 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 else: - match_is_draw = False - winner_id = p2_id - looser_id = p1_id - winner_score = p2_score - looser_score = p1_score + winner_id, looser_id = p2_id, p1_id - 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) - 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 +# ========================================== +# 2. DIE BERECHNUNG & ROUTING-TABELLE +# ========================================== +# Wir speichern die Ergebnisse direkt in einem temporären Dictionary, +# und nutzen die SPIELER-ID als Schlüsselwort! + +calc_results = { + "match_id" : match_id, + "sys_id" : sys_id, + "elo_factor" : calculation.calc_elo_factor(winner_id, looser_id, system_name), + "rust_factor" : calculation.calc_rust_factor(winner_id, looser_id, gamesystem_id), + + winner_id: { + "base": calculation.calc_base_change(), + "khorne": calculation.wrath_of_khorne(winner_id), + "slaanesh": calculation.slaanesh_delight(), + "tzeentch": calculation.tzeentch_scemes(), + "total": winner_total + }, + looser_id: { + "base": looser_base, + "khorne": wrath_of_khorne(looser_id), + "slaanesh": slaanesh_delight(winner_score, looser_score, rules), + "total": looser_total + } +} + + + +meine_ergebnisse = { + # --- Identifikation --- + "match_id": match_id, + "gamesystem_id": sys_id, + + # --- Match-Globale Werte --- + "rust_factor": 1.0, # Dein errechneter Wert (als Kommazahl / Float) + "elo_factor": 40.0, # K-Faktor + "point_inflation": 0.7, # Deine 70% + + # --- Spieler 1 --- + "p1_id": p1_id, + "p1_score": p1_score, + "p1_base": p1_base_change, + "p1_khorne": khorne_p1, + "p1_slaanesh": slaanesh_points, + "p1_tzeentch": 0, + "p1_total": winner_final, # Die endgültige MMR Änderung! + + # --- Spieler 2 --- + "p2_id": p2_id, + "p2_score": p2_score, + "p2_base": p2_base_change, + "p2_khorne": khorne_p2, + "p2_slaanesh": slaanesh_points, + "p2_tzeentch": 0, + "p2_total": looser_final # Die endgültige MMR Änderung! +} - # 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) def determine_draw_diff(sys_id): diff --git a/match_calculations/calc_mmr_change.py b/match_calculations/calc_mmr_change.py deleted file mode 100644 index eb26f8a..0000000 --- a/match_calculations/calc_mmr_change.py +++ /dev/null @@ -1,90 +0,0 @@ -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. -point_inflation = 0.7 # => entspricht % -K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt. - -def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw): - - def get_rust_dampener(days_ago): - """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) - - gamesystem_id = data_api.get_gamesystem_id_by_name(systemname) - - # 1. Die aktuellen MMR-Punkte holen - w_stat = data_api.get_player_system_stats(winner_id, systemname) - l_stat = data_api.get_player_system_stats(looser_id, systemname) - - # Wenn ein Spieler noch keine keine Stats hat wird None zurück gegeben. Fallback für diesen Fall - 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 - - expected_win = 1 / (1 + 10 ** ((l_mmr - w_mmr)/400)) - - logger.log("MATCH CALC", f"1 / (1 + 10 ** (({l_mmr} - {w_mmr})/400))") - - 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 - expected_win)/2) - logger.log("MATCH CALC", "Draw") - else: - # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele! - base_change = K_FACTOR * (1.0 - expected_win) - - # 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) - - days = max(w_days, l_days) - - rust_factor = get_rust_dampener(days) - sla_points = 0 #slaanesh_delight(winner_points, looser_points) - - logger.log(f"MATCH CALC", f"Base Change: {base_change}, Rost Faktor: {rust_factor}, EloFaktor: {expected_win}") - - # Wenn Das Match ein Draw ist, können keine Slaanesh Punkte auftreten. - if match_is_draw: - winner_final_mmr_change = int(base_change + wrath_of_khorne(winner_id)* rust_factor) - looser_final_mmr_change = int(base_change + wrath_of_khorne(looser_id)* rust_factor) - else: - winner_final_mmr_change = int((base_change + wrath_of_khorne(winner_id) + sla_points) * rust_factor) - looser_final_mmr_change = int(((base_change + wrath_of_khorne(winner_id) - sla_points) * rust_factor)* point_inflation) - - logger.log("MMR CALC", f"Winner: Base:{base_change} Khorne:{wrath_of_khorne(winner_id)} Sla:{sla_points} = {winner_final_mmr_change}") - logger.log("MMR CALC", f"Looser: Base:{base_change} Khorne:{wrath_of_khorne(winner_id)} Sla:{sla_points} = {looser_final_mmr_change}") - - return winner_final_mmr_change, (looser_final_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): - print() - - -def tzeentch_scemes(): - print("k") - -def nurgles_entropy(): - print("k") \ No newline at end of file diff --git a/match_calculations/calculation.py b/match_calculations/calculation.py new file mode 100644 index 0000000..9752d97 --- /dev/null +++ b/match_calculations/calculation.py @@ -0,0 +1,74 @@ +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): + 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) + + +def calc_elo_factor(winner_id, looser_id, system_name): + + # ------------------------------------------ + # 1. Die aktuellen MMR-Punkte holen + w_stat = data_api.get_player_system_stats(winner_id, systemname) + l_stat = data_api.get_player_system_stats(looser_id, systemname) + + # Wenn ein Spieler noch keine keine Stats hat wird None zurück gegeben. Fallback für diesen Fall + 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 = max(w_days, l_days) + rust_factor = get_rust_dampener(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): + # ----------------- + khorne_days = 16 + khorne_bonus = 8 + # ----------------- + + 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): + print() + +def tzeentch_scemes(): + print("k") + +def nurgles_entropy(): + print("k") \ No newline at end of file From aaaf96caed6cf06ba1d43add436ab7885bac956c Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Mon, 16 Mar 2026 07:21:02 +0100 Subject: [PATCH 12/30] Immer noch mmr berechnung... --- match_calculations/calc_match.py | 95 +++++++++++++++---------------- match_calculations/calculation.py | 14 ++--- 2 files changed, 52 insertions(+), 57 deletions(-) diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 06773e8..8195bdb 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -26,15 +26,24 @@ def calculate_match (match_id): sys_name = match_data['gamesystem_name'] sys_id = match_data['gamesystem_id'] + match_is_draw = False + # ========================================== # 1. IDENTIFIKATION (Bleibt gleich) # ========================================== + # 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 + # P1 Winner elif p1_score > p2_score: winner_id, looser_id = p1_id, p2_id + winner_score, looser_score = p1_score, p2_score + # P2 Winner else: winner_id, looser_id = p2_id, p1_id + winner_score, looser_score = p2_score, p1_score # ========================================== @@ -42,60 +51,50 @@ def calculate_match (match_id): # ========================================== # 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 = calculation.calc_base_change(elo_factor, match_is_draw) + rust_factor = calculation.calc_rust_factor(winner_id, looser_id, gamesystem_id) -calc_results = { - "match_id" : match_id, - "sys_id" : sys_id, - "elo_factor" : calculation.calc_elo_factor(winner_id, looser_id, system_name), - "rust_factor" : calculation.calc_rust_factor(winner_id, looser_id, gamesystem_id), + #winner + w_base = base_change + w_khorne = calculation.wrath_of_khorne(winner_id) - winner_id: { - "base": calculation.calc_base_change(), - "khorne": calculation.wrath_of_khorne(winner_id), - "slaanesh": calculation.slaanesh_delight(), - "tzeentch": calculation.tzeentch_scemes(), - "total": winner_total - }, - looser_id: { - "base": looser_base, - "khorne": wrath_of_khorne(looser_id), - "slaanesh": slaanesh_delight(winner_score, looser_score, rules), - "total": looser_total + #looser + l_base = int(base_change*point_inflation) + l_khorne = calculation.wrath_of_khorne(looser_id) + + slaanesh = calculation.slaanesh_delight(), + tzeentch = calculation.tzeentch_scemes(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: { + "base": w_base, + "khorne": w_khorne, + "slaanesh": slaanesh, + "tzeentch": tzeentch, + "total": int((w_base + w_khorne + slaanesh + tzeentch)*rust_factor), + }, + looser_id: { + "base": l_base, + "khorne": l_khorne, + "slaanesh": -slaanesh, + "tzeentch": calculation.tzeentch_scemes(winner_score, looser_score), + "total": int((calc_results[looser_id]["base"] + calc_results[looser_id]["khorne"] - calc_results[looser_id]["slaanesh"] - calc_results[looser_id]["tzeentch"])*calc_results["rust_factor"]), + } } -} -meine_ergebnisse = { - # --- Identifikation --- - "match_id": match_id, - "gamesystem_id": sys_id, - - # --- Match-Globale Werte --- - "rust_factor": 1.0, # Dein errechneter Wert (als Kommazahl / Float) - "elo_factor": 40.0, # K-Faktor - "point_inflation": 0.7, # Deine 70% - - # --- Spieler 1 --- - "p1_id": p1_id, - "p1_score": p1_score, - "p1_base": p1_base_change, - "p1_khorne": khorne_p1, - "p1_slaanesh": slaanesh_points, - "p1_tzeentch": 0, - "p1_total": winner_final, # Die endgültige MMR Änderung! - - # --- Spieler 2 --- - "p2_id": p2_id, - "p2_score": p2_score, - "p2_base": p2_base_change, - "p2_khorne": khorne_p2, - "p2_slaanesh": slaanesh_points, - "p2_tzeentch": 0, - "p2_total": looser_final # Die endgültige MMR Änderung! -} - - def determine_draw_diff(sys_id): diff --git a/match_calculations/calculation.py b/match_calculations/calculation.py index 9752d97..2694157 100644 --- a/match_calculations/calculation.py +++ b/match_calculations/calculation.py @@ -5,7 +5,7 @@ from wood import logger # 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): +def calc_base_change(elo_factor, match_is_draw): 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) @@ -64,11 +64,7 @@ def wrath_of_khorne(player_id): return 0 -def slaanesh_delight(winner_points, looser_points, rules): - print() - -def tzeentch_scemes(): - print("k") - -def nurgles_entropy(): - print("k") \ No newline at end of file +def slaanesh_delight(): + return 0 +def tzeentch_scemes(winner_score, looser_score): + return 0 From 931189baca764e20383e968404c6c6fe22391859 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Mon, 16 Mar 2026 15:04:08 +0000 Subject: [PATCH 13/30] =?UTF-8?q?MMR=20Berechnung=20sollte=20jeztt=20funkt?= =?UTF-8?q?ionieren.=20Muss=20noch=20testen.=20Ein=20paar=20Tooltips=20ver?= =?UTF-8?q?teilen=20w=C3=A4re=20noch=20nett...?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- data/data_api.py | 300 ++++++------------ gui/info_text/info_texts.json | 8 + gui/league_statistic.py | 105 ++---- gui/main_gui.py | 1 + match_calculations/calc_match.py | 64 ++-- match_calculations/calculation.py | 27 +- 7 files changed, 178 insertions(+), 329 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index e85e364..6a6bb3f 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Schwitzender Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verwirrter Servitor","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index b5d5fb3..6d08e53 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -70,8 +70,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() @@ -88,8 +86,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 @@ -119,6 +115,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) @@ -138,32 +135,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) @@ -305,219 +276,98 @@ 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.""" - 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 = ? - """ - - cursor.execute(query, (player_id, system_name)) - 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) - logger.log("data_api/Get Player Data", "get_player_system_stats returned None. New player?") - return None - - -def get_last_20_match_scores(player_id, system_name): - """Holt die erspielten Punkte und das Datum der letzten 20 Matches.""" - connection = sqlite3.connect(DB_PATH) - 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)) - rows = cursor.fetchall() - connection.close() - - # Wieder umdrehen für den zeitlichen Verlauf (links alt, rechts neu) - rows.reverse() - - points_list = [] - labels_list = [] - - for row in rows: - if row['player1_id'] == player_id: - points_list.append(row['score_player1']) - else: - points_list.append(row['score_player2']) - - # 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 - - -# Eintragen eines berechneten Matches. -def save_calculated_match(results): - """ - Speichert alle Match-Details und aktualisiert gleichzeitig die Statistiken beider Spieler. - 'results' ist ein Dictionary (dict) mit allen berechneten Werten. - """ +def save_calculated_match(calc_results: dict): connection = sqlite3.connect(DB_PATH) cursor = connection.cursor() try: - # 1. Die Match-Tabelle updaten (Alle Detailwerte eintragen und als berechnet markieren) + match_id = calc_results["match_id"] + winner_id = calc_results["winner_id"] + looser_id = calc_results["looser_id"] + + # 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 + 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, ( - results.get('p1_base', 0), results.get('p1_khorne', 0), results.get('p1_slaanesh', 0), results.get('p1_tzeentch', 0), results.get('p1_total', 0), - results.get('p2_base', 0), results.get('p2_khorne', 0), results.get('p2_slaanesh', 0), results.get('p2_tzeentch', 0), results.get('p2_total', 0), - results.get('rust_factor', 1.0), results.get('elo_factor', 0.5), results.get('point_inflation', 0.7), - results['match_id'] + 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. Vorlage für das Update der Spieler-Statistiken + # 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 + 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 = ? """ - # 3. Statistik für Spieler 1 überschreiben + # 4. Statistik Spieler 1 cursor.execute(stat_query, ( - results.get('p1_total', 0), results.get('p1_score', 0), results.get('p1_score', 0), - results['p1_id'], results['gamesystem_id'] + p1["total"], score_p1, score_p1, + player1_id, gamesystem_id )) - # 4. Statistik für Spieler 2 überschreiben + # 5. Statistik Spieler 2 cursor.execute(stat_query, ( - results.get('p2_total', 0), results.get('p2_score', 0), results.get('p2_score', 0), - results['p2_id'], results['gamesystem_id'] + p2["total"], score_p2, score_p2, + player2_id, gamesystem_id )) - # 5. WICHTIG: Erst jetzt (wenn alle 3 Befehle fehlerfrei durchliefen) speichern wir fest! connection.commit() - - # 6. Ein sauberer Log-Eintrag - # (Achtung: Stelle sicher, dass du deinen 'logger' hier richtig importiert hast) - logger.log("MATCH_CALC", f"Match ID {results['match_id']} wurde komplett berechnet und verbucht.") + logger.log("MATCH_CALC", f"Match ID {match_id} wurde komplett berechnet und verbucht.") return True except Exception as e: - # FAIL-SAFE: Wenn irgendwas schiefgeht, machen wir einen ROLLBACK! - # Die Datenbank wird auf den Stand vor dieser Funktion zurückgesetzt. Nichts wird gespeichert. connection.rollback() print(f"KRITISCHER FEHLER beim Speichern des Matches: {e}") return False finally: - # Die Verbindung wird am Ende immer geschlossen connection.close() + # ----------------------------------------------------- # Get Data Funktionen # ----------------------------------------------------- @@ -550,6 +400,42 @@ def get_leaderboard(system_name): return result +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() + + 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,)) + + 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: + print(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.""" diff --git a/gui/info_text/info_texts.json b/gui/info_text/info_texts.json index b53be0a..83c107b 100644 --- a/gui/info_text/info_texts.json +++ b/gui/info_text/info_texts.json @@ -12,5 +12,13 @@ "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!" + ], + + "tyrann_info":[ + + ], + + "prügelknabe_info":[ + ] } diff --git a/gui/league_statistic.py b/gui/league_statistic.py index 07137b7..a1cddfb 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -4,11 +4,9 @@ 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 @@ -16,47 +14,53 @@ 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: + # None-Werte absichern + 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(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 justify-center mt-10'): ui.label(f'Deine Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext') - # --- BLOCK 1 (2 Karten) --- - # 1. Daten für die Rangliste holen + # --- 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"): with ui.column().classes("w-full gap-4"): with ui.card().classes("w-full items-center justify-center text-center"): @@ -64,30 +68,27 @@ def setup_routes(): 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(stats["mmr"])).classes('text-4xl font-bold text-accent') + 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') @@ -95,8 +96,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"): @@ -119,52 +118,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') diff --git a/gui/main_gui.py b/gui/main_gui.py index 97f7d57..dc0c5f6 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -128,6 +128,7 @@ def setup_routes(admin_discord_id): 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) with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'): diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 8195bdb..7621740 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -6,7 +6,7 @@ from wood import logger point_inflation = 0.7 # => entspricht % -K_FACTOR = 40 # Die "Border" (Maximalpunkte) die ein Sieg gibt. +K_FACTOR = 30 # 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): @@ -16,20 +16,19 @@ def calculate_match (match_id): print("Fehler: Match nicht gefunden!") return - data_api.confirm_match(match_id) - # 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'] match_is_draw = False + draw_diff = determine_draw_diff(system_id) # ========================================== -# 1. IDENTIFIKATION (Bleibt gleich) +# 1. IDENTIFIKATION # ========================================== # Draw if -draw_diff <= (p1_score - p2_score) <= draw_diff: @@ -52,49 +51,51 @@ def calculate_match (match_id): # 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 = calculation.calc_base_change(elo_factor, match_is_draw) - rust_factor = calculation.calc_rust_factor(winner_id, looser_id, gamesystem_id) + 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) + #winner - w_base = base_change - w_khorne = calculation.wrath_of_khorne(winner_id) + w_base = int(base_change) + w_khorne = int(calculation.wrath_of_khorne(winner_id, system_id)) #looser l_base = int(base_change*point_inflation) - l_khorne = calculation.wrath_of_khorne(looser_id) - - slaanesh = calculation.slaanesh_delight(), - tzeentch = calculation.tzeentch_scemes(winner_score, looser_score), + l_khorne = int(calculation.wrath_of_khorne(looser_id, system_id)) + slaanesh = int(calculation.slaanesh_delight()) + tzeentch = int(calculation.tzeentch_scemes(winner_score, looser_score)) # ========================================== # 3. Daten Verpacken # ========================================== calc_results = { - "match_id" : match_id, - "elo_factor" : elo_factor, - "rust_factor" : rust_factor, + "match_id" : match_id, + "elo_factor" : elo_factor, + "rust_factor" : rust_factor, "point_inflation" : point_inflation, - winner_id: { - "base": w_base, - "khorne": w_khorne, - "slaanesh": slaanesh, - "tzeentch": tzeentch, - "total": int((w_base + w_khorne + slaanesh + tzeentch)*rust_factor), + "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: { - "base": l_base, - "khorne": l_khorne, - "slaanesh": -slaanesh, - "tzeentch": calculation.tzeentch_scemes(winner_score, looser_score), - "total": int((calc_results[looser_id]["base"] + calc_results[looser_id]["khorne"] - calc_results[looser_id]["slaanesh"] - calc_results[looser_id]["tzeentch"])*calc_results["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), } } - - + data_api.save_calculated_match(calc_results) def determine_draw_diff(sys_id): @@ -104,5 +105,4 @@ def determine_draw_diff(sys_id): draw_diff = 3 case 2,3: draw_diff = 1 - return draw_diff \ No newline at end of file diff --git a/match_calculations/calculation.py b/match_calculations/calculation.py index 2694157..9262e85 100644 --- a/match_calculations/calculation.py +++ b/match_calculations/calculation.py @@ -5,7 +5,7 @@ from wood import logger # 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): +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) @@ -13,19 +13,21 @@ def calc_base_change(elo_factor, match_is_draw): # 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) - # ------------------------------------------ - # 1. Die aktuellen MMR-Punkte holen - w_stat = data_api.get_player_system_stats(winner_id, systemname) - l_stat = data_api.get_player_system_stats(looser_id, systemname) + # 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) - # Wenn ein Spieler noch keine keine Stats hat wird None zurück gegeben. Fallback für diesen Fall - w_mmr = w_stat['mmr'] if w_stat and w_stat['mmr'] is not None else 1000 + 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))) + return (1 / (1 + 10 ** ((l_mmr - w_mmr) / 400))) @@ -36,8 +38,7 @@ def calc_rust_factor(winner_id, looser_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 = max(w_days, l_days) - rust_factor = get_rust_dampener(days) + days_ago = max(w_days, l_days) """Berechnet den Dämpfungsfaktor basierend auf den vergangen Tagen.""" if days_ago <= 30: @@ -51,20 +52,20 @@ def calc_rust_factor(winner_id, looser_id, gamesystem_id): return round(factor, 2) -def wrath_of_khorne(player_id): +def wrath_of_khorne(player_id, system_id): # ----------------- khorne_days = 16 khorne_bonus = 8 # ----------------- - last_played = data_api.get_days_since_last_game(player_id)["days_ago"] + 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_scemes(winner_score, looser_score): return 0 From 2d3c17bcf269ef76a814490cf18bded0ca5b9b54 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Tue, 17 Mar 2026 15:08:38 +0000 Subject: [PATCH 14/30] MMR funktioniert. Tzeentch regel muss angepasst werden. --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- data/data_api.py | 49 ++++++++++++------- gui/main_gui.py | 2 +- match_calculations/calc_match.py | 7 +-- match_calculations/calculation.py | 10 +++- 5 files changed, 46 insertions(+), 24 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index 6a6bb3f..4effb41 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verwirrter Servitor","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Servitor","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 6d08e53..5ed378e 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -136,23 +136,6 @@ def get_gamesystem_id_by_name(system_name): -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 - def get_player_statistics(player_id): @@ -371,6 +354,36 @@ def save_calculated_match(calc_results: dict): # ----------------------------------------------------- # 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() + return dict(zip([col[0] for col in cursor.description], row)) if row else None + + + + + +def get_gamesystems_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 + + def get_leaderboard(system_name): """Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste.""" @@ -475,6 +488,8 @@ def get_system_name(sys_id): 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) diff --git a/gui/main_gui.py b/gui/main_gui.py index dc0c5f6..d70b36b 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -182,7 +182,7 @@ def setup_routes(admin_discord_id): 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() my_stats = { p['gamesystem_id']: p for p in placements } diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 7621740..a6c7511 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -54,7 +54,6 @@ def calculate_match (match_id): 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) - #winner w_base = int(base_change) w_khorne = int(calculation.wrath_of_khorne(winner_id, system_id)) @@ -64,8 +63,7 @@ def calculate_match (match_id): l_khorne = int(calculation.wrath_of_khorne(looser_id, system_id)) slaanesh = int(calculation.slaanesh_delight()) - tzeentch = int(calculation.tzeentch_scemes(winner_score, looser_score)) - + tzeentch = int(calculation.tzeentch_scemes(system_id, winner_score, looser_score)) # ========================================== # 3. Daten Verpacken @@ -95,6 +93,9 @@ def calculate_match (match_id): } } + logger.log("MATCH CALC", f"Match{match_id} berechnet.") + logger.log("MATCH CALC", f"Winner {data_api.get_player_name(winner_id)}: Base {w_base} + Khorne {w_khorne} + Slaanesh {slaanesh} + Tzeentch {tzeentch}") + logger.log("MATCH CALC", f"Looser {data_api.get_player_name(looser_id)}: Base {l_base} + Khorne {l_khorne} - Slaanesh {slaanesh} - Tzeentch {tzeentch}") data_api.save_calculated_match(calc_results) diff --git a/match_calculations/calculation.py b/match_calculations/calculation.py index 9262e85..e0ed11b 100644 --- a/match_calculations/calculation.py +++ b/match_calculations/calculation.py @@ -67,5 +67,11 @@ def wrath_of_khorne(player_id, system_id): def slaanesh_delight(): return 0 -def tzeentch_scemes(winner_score, looser_score): - return 0 +def tzeentch_scemes(system_id, winner_score, looser_score): + sys_data = data_api.get_gamesystem_data(system_id) + min_score = sys_data["min_score"] + max_score = sys_data["max_score"] + + bonus = int(((max_score*(winner_score-looser_score)))/(max_score*1.1)) #Multiplikatiionsfaktor für die Berechnung. + + return bonus From 58e7b0596e89c54bb59c777d4744c53bacf202bb Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 18 Mar 2026 07:22:42 +0100 Subject: [PATCH 15/30] =?UTF-8?q?tzeenth=20Regel=20noch=20AUS.=20F=C3=BCr?= =?UTF-8?q?=20den=20Moment.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- match_calculations/calculation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/match_calculations/calculation.py b/match_calculations/calculation.py index e0ed11b..4444bb3 100644 --- a/match_calculations/calculation.py +++ b/match_calculations/calculation.py @@ -74,4 +74,4 @@ def tzeentch_scemes(system_id, winner_score, looser_score): bonus = int(((max_score*(winner_score-looser_score)))/(max_score*1.1)) #Multiplikatiionsfaktor für die Berechnung. - return bonus + return 0 From 21fa6773a3b48cfcf46b76a7f627cffd8454a1e0 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 18 Mar 2026 12:52:50 +0000 Subject: [PATCH 16/30] HOTFIX. Match Eingabe Formular hat Fehler geworfen weil logger Funktion falsch war. --- .nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- data/data_api.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index 4effb41..cf2e059 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Stolpernder Servitor","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"DN","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 5ed378e..97dc0d6 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -250,7 +250,7 @@ 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("DataAPI", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {gamesystem_id}, {get_player_name(player2_id)}:({score_p2})", player_id) + logger.log("DataAPI", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id) connection.commit() connection.close() From aaaf6d44e979f7f697090033f1f34ffde3c18d59 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 18 Mar 2026 15:14:29 +0000 Subject: [PATCH 17/30] Kleine UI Verbesserung auf der main seite. --- gui/main_gui.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/gui/main_gui.py b/gui/main_gui.py index d70b36b..adc4251 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -132,9 +132,10 @@ def setup_routes(admin_discord_id): calc_match.calculate_match(m_id) 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'): @@ -143,7 +144,7 @@ def setup_routes(admin_discord_id): ui.space() # BESTÄTIGEN und spiel berechnen lassen 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 --- @@ -152,7 +153,7 @@ def setup_routes(admin_discord_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 @@ -163,14 +164,13 @@ def setup_routes(admin_discord_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: 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 --- From 7c5f1d1e3300c45610aee4f7fbd21b5f0fb58dd0 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 18 Mar 2026 21:24:05 +0100 Subject: [PATCH 18/30] Log Messages in der data_api angepasst. --- ...-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json | 2 +- data/data_api.py | 26 +++-- gui/match_history_gui.py | 108 +++++++++++------- 3 files changed, 81 insertions(+), 55 deletions(-) diff --git a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json index 9e26dfe..cf2e059 100644 --- a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json +++ b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json @@ -1 +1 @@ -{} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"DN","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 97dc0d6..ab2a1fc 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -54,7 +54,7 @@ def get_or_create_player(discord_id, discord_name, avatar_url): # 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("NEW PLAYER", str("Ein neuer Spieler wurde angelegt - " + discord_name)) + logger.log("data_api.get_or_create_player", str("Ein neuer Spieler wurde angelegt - " + 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() @@ -178,7 +178,7 @@ def join_league(player_id, gamesystem_id): INSERT INTO player_game_statistic (player_id, gamesystem_id) VALUES (?, ?) """ - logger.log("DataAPI", f"{get_player_name(player_id)} joined {gamesystem_id}", player_id) + logger.log("data_api.join_league", f"{get_player_name(player_id)} joined {gamesystem_id}", player_id) cursor.execute(query, (player_id, gamesystem_id)) connection.commit() @@ -250,7 +250,7 @@ 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("DataAPI", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id) + logger.log("data_api.add_new_match", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id) connection.commit() connection.close() @@ -335,12 +335,12 @@ def save_calculated_match(calc_results: dict): )) connection.commit() - logger.log("MATCH_CALC", f"Match ID {match_id} wurde komplett berechnet und verbucht.") + logger.log("pi.save_calculated_match", f"Match ID:{match_id} berechnet.") return True except Exception as e: connection.rollback() - print(f"KRITISCHER FEHLER beim Speichern des Matches: {e}") + logger.log("data_api.save_calculated_match", f"KRITISCHER FEHLER beim Speichern des Matches: {e}") return False finally: @@ -363,9 +363,6 @@ def get_gamesystem_data(system_id): return dict(zip([col[0] for col in cursor.description], row)) if row else None - - - def get_gamesystems_data(): connection = sqlite3.connect(DB_PATH) @@ -572,7 +569,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,)) - logger.log("data_api.set_match_counted", f"Match mit ID: {match_id} berechnet.") connection.commit() connection.close() @@ -602,15 +598,20 @@ def get_submitted_matches(player_id): 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 @@ -629,6 +630,7 @@ def get_match_history_log(player_id): + # ----------------------------------------------------- # Testing and Prototyping # ----------------------------------------------------- diff --git a/gui/match_history_gui.py b/gui/match_history_gui.py index 1ccd79a..8244ec2 100644 --- a/gui/match_history_gui.py +++ b/gui/match_history_gui.py @@ -1,6 +1,7 @@ from nicegui import ui, app from data import data_api -from gui import gui_style +from gui import gui_style + def setup_routes(): @ui.page('/matchhistory', dark=True) @@ -8,17 +9,14 @@ 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 max-w-7xl 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") ui.button("Zurück", icon="arrow_back", on_click=lambda: ui.navigate.to('/')).classes('bg-zinc-700 text-white') @@ -26,21 +24,37 @@ def setup_routes(): 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'] + my_khorne = match['player1_khorne'] + my_tzeentch = match['player1_tzeentch'] + my_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'] + my_khorne = match['player2_khorne'] + my_tzeentch = match['player2_tzeentch'] + my_slaanesh = match['player2_slaanesh'] - # Ergebnis Text if my_score > opp_score: result = "Gewonnen" elif my_score < opp_score: @@ -48,51 +62,61 @@ 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}" - 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'], + 'score': str(my_score), 'opponent': opponent, - 'score': f"{my_score}", - 'opp_score': f"{opp_score}", + 'opp_score': str(opp_score), 'result': result, - 'mmr': mmr_text + '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(my_khorne, pending), + 'tzeentch': fmt_signed(my_tzeentch, pending), + 'slaanesh': fmt_signed(my_slaanesh, pending), + 'mmr': fmt_signed(my_mmr_change, pending), }) - # Spalten definieren columns = [ - {'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'}, - {'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'}, - {'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'left'}, - {'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'}, - {'name': 'opp_score', 'label': 'Gegner 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': 'Eigene Punkte', 'field': 'score', 'align': 'center'}, + {'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'}, + {'name': 'opp_score','label': 'Gegner Punkte', 'field': 'opp_score','align': 'center'}, + {'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'}, + {'name': 'elo', 'label': 'Elo Faktor', 'field': 'elo', '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 Änderung', '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 = ''' + + + {{ props.row[props.col.field] }} + + + ''' - # 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', ''' - - - {{ props.row.mmr }} - - - ''') + 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") From a262088e00b9c882f9e6e0d829f7c0e6145724d2 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 18 Mar 2026 21:48:06 +0100 Subject: [PATCH 19/30] Match Formular angepasst. --- gui/info_text/info_texts.json | 17 +++++------------ gui/match_gui.py | 30 +++++++++--------------------- 2 files changed, 14 insertions(+), 33 deletions(-) diff --git a/gui/info_text/info_texts.json b/gui/info_text/info_texts.json index 83c107b..034cce9 100644 --- a/gui/info_text/info_texts.json +++ b/gui/info_text/info_texts.json @@ -3,22 +3,15 @@ "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!" + "**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." ], - - "tyrann_info":[ - - ], - - "prügelknabe_info":[ - - ] -} + "tyrann_info": [], + "prügelknabe_info": [] +} \ No newline at end of file diff --git a/gui/match_gui.py b/gui/match_gui.py index 072a361..d4a91eb 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -2,6 +2,7 @@ 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(): @@ -21,37 +22,31 @@ def setup_routes(): # 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("Meine Punkte:").classes('text-xl font-bold w-full text-left') - # ÄNDERUNG: h-60 entfernt, stattdessen gap-6 (Abstand zwischen den Elementen) 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(systemname) my_id = app.storage.user.get('db_id') - # 3. Eine saubere Optionen-Liste für NiceGUI bauen 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') + with ui.row().classes("w-full items-center justify-between"): + p1_points = ui.slider(min=0, max=100, value=10).classes("w-70") + ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext") 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') + with ui.row().classes("w-full items-center justify-between"): + p2_points = ui.slider(min=0, max=100, value=10).classes("w-70") + ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext") ui.space() @@ -71,15 +66,8 @@ def setup_routes(): ui.notify("Match erfolgreich eingetragen!", color="green") ui.navigate.to(f'/statistic/{systemname}') - - with ui.dialog().classes("w-full items-center") as form_info, ui.card(): - ui.label('Um ein Spiel einzutragen einfach deine erspielten Punkte, deinen Gegner und die Punkte von deinem Gegner eintragen').classes("font-bold text-white text-l") - ui.label('ACHTUNG: Damit ein Spieler als Gegner ausgewählt werden kann, muss er der Liga beigetreten sein!').classes("font-bold text-white text-l") - ui.label('Nach dem Absenden muss dein Gegner in seiner Liga App das Spiel noch kurz bestätigen. Solltest du einen Fehler gemacht haben, kannst du das Spiel (bevor es bestätigt wurde) auf der Hauptseite selber löschen.').classes("font-bold text-white text-l") - ui.button(icon="close", on_click=form_info.close).classes("w-10 h-8 rounded-full") - # Buttons ganz unten in einer Reihe with ui.row().classes("w-full items-center justify-between mt-8"): ui.button(icon="close", on_click=lambda: ui.navigate.to(f'/statistic/{systemname}')).classes("w-10 h-8 rounded-full") - ui.button(icon="help", color="information" ,on_click=form_info.open).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()) From 6855f87d02c0b8f98a9fb781ea851446a78dcfdb Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Fri, 20 Mar 2026 10:17:29 +0000 Subject: [PATCH 20/30] =?UTF-8?q?Tzeench=20Regel=20eingebunden.=20Point=20?= =?UTF-8?q?Inflation=20auf=200.8=20ge=C3=A4ndert.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...e-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- match_calculations/calc_match.py | 13 ++++++------- match_calculations/calculation.py | 12 ++++++++---- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index cf2e059..768c8da 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"DN","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verwirrter Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index a6c7511..58db5dc 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -5,8 +5,8 @@ import os from wood import logger -point_inflation = 0.7 # => entspricht % -K_FACTOR = 30 # Die "Border" (Maximalpunkte) die ein Sieg gibt. +point_inflation = 0,8 # => 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): @@ -62,8 +62,8 @@ def calculate_match (match_id): l_base = int(base_change*point_inflation) l_khorne = int(calculation.wrath_of_khorne(looser_id, system_id)) - slaanesh = int(calculation.slaanesh_delight()) - tzeentch = int(calculation.tzeentch_scemes(system_id, winner_score, looser_score)) + slaanesh = calculation.slaanesh_delight() + tzeentch = calculation.tzeentch_schemes(system_id, winner_score, looser_score) # ========================================== # 3. Daten Verpacken @@ -93,9 +93,8 @@ def calculate_match (match_id): } } - logger.log("MATCH CALC", f"Match{match_id} berechnet.") - logger.log("MATCH CALC", f"Winner {data_api.get_player_name(winner_id)}: Base {w_base} + Khorne {w_khorne} + Slaanesh {slaanesh} + Tzeentch {tzeentch}") - logger.log("MATCH CALC", f"Looser {data_api.get_player_name(looser_id)}: Base {l_base} + Khorne {l_khorne} - Slaanesh {slaanesh} - Tzeentch {tzeentch}") + logger.log(f"calc_match ID:{match_id}", f"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"calc_match ID:{match_id}", f"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) diff --git a/match_calculations/calculation.py b/match_calculations/calculation.py index 4444bb3..f2c85b4 100644 --- a/match_calculations/calculation.py +++ b/match_calculations/calculation.py @@ -67,11 +67,15 @@ def wrath_of_khorne(player_id, system_id): def slaanesh_delight(): return 0 -def tzeentch_scemes(system_id, winner_score, looser_score): + +def tzeentch_schemes(system_id, winner_score, looser_score): sys_data = data_api.get_gamesystem_data(system_id) - min_score = sys_data["min_score"] max_score = sys_data["max_score"] - bonus = int(((max_score*(winner_score-looser_score)))/(max_score*1.1)) #Multiplikatiionsfaktor für die Berechnung. + diff_normalized = (winner_score - looser_score) / max_score # 0.0 – 1.0 + threshold = 0.1 # Bonus startet erst ab 10% Vorsprung - return 0 + bonus = int(max(0, diff_normalized - threshold) * 10) # Ergibt 0–9 + bonus = min(bonus, 9) # Sicherheits-Cap + + return bonus From 46eee731dce4bd6289e81e7ecb0cbfed0e94f275 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Fri, 20 Mar 2026 10:18:34 +0000 Subject: [PATCH 21/30] Tzeench Regel und Point Inflation angepasst. --- match_calculations/calc_match.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 58db5dc..1c92452 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -5,7 +5,7 @@ import os from wood import logger -point_inflation = 0,8 # => entspricht % +point_inflation = 0.8 # => 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. From b861ebf79d93bd7528bb907d7a432e714f87eae1 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 26 Mar 2026 08:24:33 +0000 Subject: [PATCH 22/30] Info texts. Min Max Punkte werden jetzt richitg angewandt im Match-Formular. --- ...-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 2 +- ...-253576f7-9827-4994-a103-e67a11c8053c.json | 1 + ...-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json | 2 +- gui/info_text/info_system.py | 2 +- gui/match_gui.py | 26 +++++++++++-------- match_calculations/calc_match.py | 2 +- 6 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 .nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json index 768c8da..6c3d053 100644 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verwirrter Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json new file mode 100644 index 0000000..6c3d053 --- /dev/null +++ b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json b/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json index 8a8a4ed..9e26dfe 100644 --- a/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json +++ b/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"159672532159561728","discord_name":"buddyl33t","db_id":3,"display_name":"Marian","discord_avatar_url":"https://cdn.discordapp.com/avatars/159672532159561728/866d0dddade9d551f3c5025bb6467da0.png"} \ No newline at end of file +{} \ No newline at end of file diff --git a/gui/info_text/info_system.py b/gui/info_text/info_system.py index df52357..b584714 100644 --- a/gui/info_text/info_system.py +++ b/gui/info_text/info_system.py @@ -28,7 +28,7 @@ def create_info_button(topic_key): 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("font-bold text-white text-lg text-center") + 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') diff --git a/gui/match_gui.py b/gui/match_gui.py index d4a91eb..552c84a 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -7,8 +7,8 @@ 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): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf! gui_style.apply_design() # --- SICHERHEITS-CHECK --- @@ -17,16 +17,21 @@ 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.column().classes("w-full items-center gap-6"): # 1. Daten aus der DB holen - raw_players = data_api.get_all_players_from_system(systemname) + raw_players = data_api.get_all_players_from_system(system_name) my_id = app.storage.user.get('db_id') dropdown_options = {} @@ -36,16 +41,15 @@ def setup_routes(): dropdown_options[p['player_id']] = f"{p['display_name']} 'aka' {p['discord_name']}" with ui.row().classes("w-full items-center justify-between"): - p1_points = ui.slider(min=0, max=100, value=10).classes("w-70") + p1_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-70") ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext") ui.separator().classes('w-full mt-4') # Ein schöner Trennstrich für die Optik - ui.label("Gegner:").classes('text-xl font-bold w-full text-left') opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full') with ui.row().classes("w-full items-center justify-between"): - p2_points = ui.slider(min=0, max=100, value=10).classes("w-70") + p2_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-70") ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext") ui.space() @@ -60,14 +64,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(icon="close", on_click=lambda: ui.navigate.to(f'/statistic/{systemname}')).classes("w-10 h-8 rounded-full") + 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()) diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 1c92452..1a42ab4 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -5,7 +5,7 @@ import os from wood import logger -point_inflation = 0.8 # => entspricht % +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. From 9cf54bd0511bf24061f3f51d3519618ae3cc059b Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 26 Mar 2026 08:55:11 +0000 Subject: [PATCH 23/30] =?UTF-8?q?Match=20formular=20f=C3=BCr=20mobile=20an?= =?UTF-8?q?gepasst.=20Punkte=20Eintrag=20hat=20jetzt=20+=20-=20Buttons=20f?= =?UTF-8?q?=C3=BCr=20Pr=C3=A4zise=20Einstellungen.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-b1350d67-43b3-4813-9c12-9090a9f37b57.json | 1 + gui/match_gui.py | 53 +++++++++++++------ 2 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 .nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json diff --git a/.nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json b/.nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json new file mode 100644 index 0000000..6c3d053 --- /dev/null +++ b/.nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json @@ -0,0 +1 @@ +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/gui/match_gui.py b/gui/match_gui.py index 552c84a..de18d6e 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -19,7 +19,6 @@ def setup_routes(): # --- 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"] @@ -27,32 +26,52 @@ def setup_routes(): # Text-Center hinzugefügt, falls der Systemname sehr lang ist und auf dem Handy umbricht 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.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') + 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') + def add_point(): + p1_points.value += 1 + + def sub_point(): + p1_points.value -= 1 + + 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']}" - with ui.row().classes("w-full items-center justify-between"): - p1_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-70") - ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext") - - ui.separator().classes('w-full mt-4') # Ein schöner Trennstrich für die Optik - - ui.label("Gegner:").classes('text-xl font-bold w-full text-left') opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full') - with ui.row().classes("w-full items-center justify-between"): - p2_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-70") - ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext") - ui.space() + def add_point(): + p2_points.value += 1 + + 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(): From 7021bc8629041ab2427dc036c4cfa50d891c04f8 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 26 Mar 2026 15:20:46 +0000 Subject: [PATCH 24/30] =?UTF-8?q?Match=20History=20sch=C3=B6ner=20gestalte?= =?UTF-8?q?t.=20Basis=20MMR=20haben=20in=20der=20Historie=20gefehlt.=20Wer?= =?UTF-8?q?den=20jetzt=20abgefragt=20und=20angezeigt.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...-253576f7-9827-4994-a103-e67a11c8053c.json | 2 +- data/data_api.py | 10 ++++- data/setup_database.py | 8 ++++ gui/league_statistic.py | 1 - gui/match_history_gui.py | 43 ++++++++++--------- 5 files changed, 39 insertions(+), 25 deletions(-) diff --git a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json index 6c3d053..9963eea 100644 --- a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json +++ b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel Nagel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index ab2a1fc..7f341f7 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -150,12 +150,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() diff --git a/data/setup_database.py b/data/setup_database.py index 75b6848..b68b828 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -54,8 +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) ) ''') diff --git a/gui/league_statistic.py b/gui/league_statistic.py index a1cddfb..fa679cf 100644 --- a/gui/league_statistic.py +++ b/gui/league_statistic.py @@ -23,7 +23,6 @@ def setup_routes(): ) if system_stat: - # None-Werte absichern mmr = system_stat["mmr"] or 0 games = system_stat["games_in_system"] or 0 points = system_stat["points"] or 0 diff --git a/gui/match_history_gui.py b/gui/match_history_gui.py index 8244ec2..3a55d64 100644 --- a/gui/match_history_gui.py +++ b/gui/match_history_gui.py @@ -15,7 +15,7 @@ def setup_routes(): player_id = app.storage.user.get('db_id') - with ui.column().classes('w-full max-w-7xl mx-auto p-4'): + with ui.column().classes('w-full mx-auto p-4'): with ui.row().classes('w-full items-center justify-between mb-6'): ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white") @@ -42,18 +42,20 @@ def setup_routes(): 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'] - my_khorne = match['player1_khorne'] - my_tzeentch = match['player1_tzeentch'] - my_slaanesh = match['player1_slaanesh'] + 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'] - my_khorne = match['player2_khorne'] - my_tzeentch = match['player2_tzeentch'] - my_slaanesh = match['player2_slaanesh'] + mmr_base = match["player2_base_change"] + mmr_change = match['player2_mmr_change'] + khorne = match['player2_khorne'] + tzeentch = match['player2_tzeentch'] + slaanesh = match['player2_slaanesh'] if my_score > opp_score: result = "Gewonnen" @@ -70,30 +72,29 @@ def setup_routes(): 'date': str(match['played_at'])[:10], 'system': match['gamesystem_name'], 'score': str(my_score), - 'opponent': opponent, - 'opp_score': str(opp_score), - 'result': result, + '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(my_khorne, pending), - 'tzeentch': fmt_signed(my_tzeentch, pending), - 'slaanesh': fmt_signed(my_slaanesh, pending), - 'mmr': fmt_signed(my_mmr_change, pending), + 'khorne': fmt_signed(khorne, pending), + 'tzeentch': fmt_signed(tzeentch, pending), + 'slaanesh': fmt_signed(slaanesh, pending), + 'mmr': fmt_signed(mmr_change, pending), }) columns = [ {'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'}, {'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'}, - {'name': 'score', 'label': 'Eigene Punkte', 'field': 'score', 'align': 'center'}, - {'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'}, - {'name': 'opp_score','label': 'Gegner Punkte', 'field': 'opp_score','align': 'center'}, - {'name': 'result', 'label': 'Ergebnis', 'field': 'result', '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 Änderung', 'field': 'mmr', 'align': 'right'}, + {'name': 'mmr', 'label': 'MMR GESAMT', 'field': 'mmr', 'align': 'right'}, + ] # Shared slot template for colored signed values From 986e3ca4bd6c8d9f38790b173a0b3a2f88e50e02 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Fri, 27 Mar 2026 08:38:29 +0000 Subject: [PATCH 25/30] Logger Funktion Verbessert und Angepasst. --- ...-253576f7-9827-4994-a103-e67a11c8053c.json | 2 +- data/data_api.py | 20 ++++--- gui/admin_gui.py | 19 +++---- gui/info_text/info_system.py | 5 +- gui/info_text/info_texts.json | 4 ++ match_calculations/calc_match.py | 4 +- wood/logger.py | 56 ++++++++++--------- 7 files changed, 57 insertions(+), 53 deletions(-) diff --git a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json index 9963eea..cfdc4d2 100644 --- a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json +++ b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel Nagel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel N.","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/data/data_api.py b/data/data_api.py index 7f341f7..6421c93 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -15,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! @@ -54,7 +56,7 @@ def get_or_create_player(discord_id, discord_name, avatar_url): # 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("data_api.get_or_create_player", str("Ein neuer Spieler wurde angelegt - " + discord_name)) + 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() @@ -184,7 +186,7 @@ def join_league(player_id, gamesystem_id): INSERT INTO player_game_statistic (player_id, gamesystem_id) VALUES (?, ?) """ - logger.log("data_api.join_league", f"{get_player_name(player_id)} joined {gamesystem_id}", player_id) + logger.log(f"{get_player_name(player_id)} joined {gamesystem_id}") cursor.execute(query, (player_id, gamesystem_id)) connection.commit() @@ -256,7 +258,7 @@ 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("data_api.add_new_match", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id) + logger.log(f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id) connection.commit() connection.close() @@ -341,12 +343,12 @@ def save_calculated_match(calc_results: dict): )) connection.commit() - logger.log("pi.save_calculated_match", f"Match ID:{match_id} berechnet.") + logger.log(f"Match ID:{match_id} calculated.") return True except Exception as e: connection.rollback() - logger.log("data_api.save_calculated_match", f"KRITISCHER FEHLER beim Speichern des Matches: {e}") + logger.log(f"KRITISCHER FEHLER beim Speichern des Matches: {e}") return False finally: @@ -445,7 +447,7 @@ def get_match_by_id(match_id: int) -> dict | None: return dict(zip(columns, row)) except Exception as e: - print(f"Fehler beim Laden des Matches: {e}") + logger.log(f"Fehler beim Laden des Matches: {e}") return None finally: @@ -552,7 +554,7 @@ def delete_match(match_id, player_id): # DELETE FROM löscht die gesamte Zeile, bei der die ID übereinstimmt. cursor.execute("DELETE FROM matches WHERE id = ?", (match_id,)) - logger.log("data_api.delete_match", f"Match mit ID{match_id} von User gelöscht.", f"{player_id}") + logger.log(f"Match ID{match_id} deleted from user {get_player_name(player_id)}(ID:{player_id})") connection.commit() connection.close() @@ -563,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("data_api.confirm_match", f"Match mit ID{match_id} von Player2 bestätigt.") + logger.log(f"Match mit ID{match_id} von Player2 bestätigt.") connection.commit() connection.close() @@ -670,7 +672,7 @@ def create_random_dummy_match(player_id): dummy_name = get_player_name(dummy_id) sys_name = get_system_name(1) - logger.log("TEST_MATCH", f"Zufallsspiel generiert. [{sys_name}]: {p1_name} ({score_p1}) vs. {dummy_name} ({score_p2})", player_id) + logger.log(f"Zufallsspiel generiert. [{sys_name}]: {p1_name} ({score_p1}) vs. {dummy_name} ({score_p2})") return True diff --git a/gui/admin_gui.py b/gui/admin_gui.py index a9cdd25..5b609ad 100644 --- a/gui/admin_gui.py +++ b/gui/admin_gui.py @@ -12,12 +12,11 @@ def setup_routes(): 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): - with ui.card().classes("w-full"): - with ui.row().classes("w-full"): + if app.storage.user.get('authenticated', False): + 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.reload) - + 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 @@ -25,18 +24,16 @@ def setup_routes(): # Tabelle aufbauen columns = [ - {'name': 'time', 'label': 'Zeitstempel', 'field': 'timestamp', 'align': 'left', 'sortable': True}, - {'name': 'user', 'label': 'Auslöser', 'field': 'player_name', 'align': 'left'}, - {'name': 'action', 'label': 'Aktion', 'field': 'action', 'align': 'left', 'sortable': True}, + {'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: - # Wir schneiden bei den Millisekunden vom Zeitstempel wieder etwas ab [:19] + # Millisekunden vom Zeitstempel abschneiden [:19] for row in log_data: row['timestamp'] = str(row['timestamp'])[:19] - - # Tabelle zeichnen 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') \ No newline at end of file diff --git a/gui/info_text/info_system.py b/gui/info_text/info_system.py index b584714..0187ef5 100644 --- a/gui/info_text/info_system.py +++ b/gui/info_text/info_system.py @@ -2,11 +2,11 @@ import json import os from nicegui import ui -# 1. Pfad zur JSON Datei berechnen +# Pfad zur JSON Datei berechnen BASE_DIR = os.path.dirname(os.path.abspath(__file__)) JSON_PATH = os.path.join(BASE_DIR, "info_texts.json") -# 2. JSON Datei in den Speicher laden +# 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 (ä, ö, ü)! @@ -19,7 +19,6 @@ def load_info_texts(): # Wir laden das Wörterbuch genau 1x beim Serverstart TEXT_DICTIONARY = load_info_texts() -# 3. Unser neuer Baustein für die Webseite 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!"]) diff --git a/gui/info_text/info_texts.json b/gui/info_text/info_texts.json index 034cce9..182e91f 100644 --- a/gui/info_text/info_texts.json +++ b/gui/info_text/info_texts.json @@ -12,6 +12,10 @@ "**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." ], + "khorne": ["Khorne will Blut und Schädel!"], + "tzeentch": ["tzeentch Pläne gehen auf!"], + "basis_mmr": ["Berechnet mit dem MMR Unterschied."], + "tyrann_info": [], "prügelknabe_info": [] } \ No newline at end of file diff --git a/match_calculations/calc_match.py b/match_calculations/calc_match.py index 1a42ab4..433cd04 100644 --- a/match_calculations/calc_match.py +++ b/match_calculations/calc_match.py @@ -93,8 +93,8 @@ def calculate_match (match_id): } } - logger.log(f"calc_match ID:{match_id}", f"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"calc_match ID:{match_id}", f"Looser {data_api.get_player_name(looser_id)}: -Base({l_base}) + Khorne({l_khorne}) - Slaanesh({slaanesh}) - Tzeentch({tzeentch}) = {calc_results[looser_id]["total"]}") + 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) diff --git a/wood/logger.py b/wood/logger.py index c50652d..9c51bd5 100644 --- a/wood/logger.py +++ b/wood/logger.py @@ -1,22 +1,39 @@ import sqlite3 import os +import inspect from data.setup_database import DB_PATH DB_FILE = "wood/log.db" -def log(action, details, player_id = None): - """Schreibt ein Ereignis (Audit Trail) in das System-Log.""" - connection = sqlite3.connect(DB_FILE) - cursor = connection.cursor() +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}" - cursor.execute( - "INSERT INTO system_log (player_id, action, details) VALUES (?, ?, ?)", - (player_id, action, details) - ) + # 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 - connection.commit() - connection.close() def setup_log_db(): @@ -34,8 +51,8 @@ def setup_log_db(): CREATE TABLE IF NOT EXISTS system_log ( id INTEGER PRIMARY KEY AUTOINCREMENT, timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - player_id INTEGER, - action TEXT, + file TEXT, + source TEXT, details TEXT ) ''') @@ -57,19 +74,4 @@ def get_full_log(): logs = [dict(row) for row in log_cursor.fetchall()] log_conn.close() - # 2. Spielernamen aus der Haupt-DB holen - main_conn = sqlite3.connect(DB_PATH) - main_cursor = main_conn.cursor() - main_cursor.execute("SELECT id, display_name, discord_name FROM players") - players_dict = {row[0]: f"{row[1]} ({row[2]})" for row in main_cursor.fetchall()} - main_conn.close() - - # 3. Die IDs in den Logs durch Namen ersetzen - for log_entry in logs: - p_id = log_entry['player_id'] - if p_id is not None and p_id in players_dict: - log_entry['player_name'] = players_dict[p_id] - else: - log_entry['player_name'] = "System / Unbekannt" - return logs \ No newline at end of file From f9ed925643dbc18c27964994b6e64d55766ccf87 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 27 Mar 2026 17:36:08 +0100 Subject: [PATCH 26/30] =?UTF-8?q?gitignore=20ge=C3=A4ndert=20dass=20die=20?= =?UTF-8?q?Nutzer=20Metadaten=20ignoriert=20werden.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 7783289..f460b62 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ # 2. Python Caches __pycache__/ *.py[cod] +.nicegui/storage* # 3. Virtuelle Umgebungen venv/ From 397d454ebc90c93e0fda7b8b43417fbfdd7d5793 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 27 Mar 2026 18:21:24 +0100 Subject: [PATCH 27/30] gitignore anggepasst damit die storage-user Chache Daten ignoriert werden. --- .nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json | 1 - .nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json | 1 - .nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json | 1 - .nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json | 1 - .nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json | 1 - .nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json | 1 - .nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json | 1 - .nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json | 1 - .nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json | 1 - .nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json | 1 - 10 files changed, 10 deletions(-) delete mode 100644 .nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json delete mode 100644 .nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json delete mode 100644 .nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json delete mode 100644 .nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json delete mode 100644 .nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json delete mode 100644 .nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json delete mode 100644 .nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json delete mode 100644 .nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json delete mode 100644 .nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json delete mode 100644 .nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json diff --git a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json b/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json deleted file mode 100644 index 6c3d053..0000000 --- a/.nicegui/storage-user-21d6fdd4-43d2-4625-8dc2-9282b6aa433f.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json b/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json deleted file mode 100644 index cfdc4d2..0000000 --- a/.nicegui/storage-user-253576f7-9827-4994-a103-e67a11c8053c.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel N.","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json b/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json deleted file mode 100644 index 90a427a..0000000 --- a/.nicegui/storage-user-47e8cea6-65ad-4725-abd9-23cf84587a03.json +++ /dev/null @@ -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"} \ No newline at end of file diff --git a/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json b/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json deleted file mode 100644 index 9e26dfe..0000000 --- a/.nicegui/storage-user-62e9c394-87c8-4ee0-a57f-d66ec22cd26a.json +++ /dev/null @@ -1 +0,0 @@ -{} \ No newline at end of file diff --git a/.nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json b/.nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json deleted file mode 100644 index 26c1238..0000000 --- a/.nicegui/storage-user-6b089ae9-7465-463d-af3d-d6f1ef5fa8bd.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"113708052485636100","discord_name":"staelwulf","db_id":4,"display_name":"Max","discord_avatar_url":"https://cdn.discordapp.com/avatars/113708052485636100/d53339dd6a6659231c5c16645ba258df.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json b/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json deleted file mode 100644 index 73615f2..0000000 --- a/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json +++ /dev/null @@ -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"} \ No newline at end of file diff --git a/.nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json b/.nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json deleted file mode 100644 index 6c3d053..0000000 --- a/.nicegui/storage-user-b1350d67-43b3-4813-9c12-9090a9f37b57.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Kultist","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json b/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json deleted file mode 100644 index cf2e059..0000000 --- a/.nicegui/storage-user-c446a3b8-a6ed-40c3-a878-3069e9d230cb.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"DN","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json b/.nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json deleted file mode 100644 index 359add1..0000000 --- a/.nicegui/storage-user-e8a9032e-ad8d-46ae-82a2-2344e91072b9.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel N","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file diff --git a/.nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json b/.nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json deleted file mode 100644 index e026266..0000000 --- a/.nicegui/storage-user-f6ee5d0b-f6d4-4518-965e-c2520973577a.json +++ /dev/null @@ -1 +0,0 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file From b707dd96d69ff99b85fbaa710f02a4c967f2a8d7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Fri, 27 Mar 2026 18:21:50 +0100 Subject: [PATCH 28/30] Infotext --- gui/info_text/info_texts.json | 16 ++++++++++++---- gui/match_history_gui.py | 2 ++ 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/gui/info_text/info_texts.json b/gui/info_text/info_texts.json index 182e91f..3d4ccc6 100644 --- a/gui/info_text/info_texts.json +++ b/gui/info_text/info_texts.json @@ -12,10 +12,18 @@ "**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." ], - "khorne": ["Khorne will Blut und Schädel!"], - "tzeentch": ["tzeentch Pläne gehen auf!"], - "basis_mmr": ["Berechnet mit dem MMR Unterschied."], - + "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": [] } \ No newline at end of file diff --git a/gui/match_history_gui.py b/gui/match_history_gui.py index 3a55d64..7373ac9 100644 --- a/gui/match_history_gui.py +++ b/gui/match_history_gui.py @@ -1,6 +1,7 @@ from nicegui import ui, app from data import data_api from gui import gui_style +from gui.info_text.info_system import create_info_button def setup_routes(): @@ -19,6 +20,7 @@ def setup_routes(): 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) From b7f9e7a6bb8b38ad6c955d51db3b521a15d0e382 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Tue, 7 Apr 2026 08:30:53 +0000 Subject: [PATCH 29/30] =?UTF-8?q?Impressums=20Seite.=20Datenschutzerkl?= =?UTF-8?q?=C3=A4rung.=20Servitor=20Basic=20Test=20funktion.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bot/servitor.py | 15 +++ gui/imprint.json | 124 ++++++++++++++++++ gui/imprint_gui.py | 75 +++++++++++ ...e_statistic.py => league_statistic_gui.py} | 0 gui/main_gui.py | 8 +- gui/match_gui.py | 3 +- main.py | 6 +- 7 files changed, 222 insertions(+), 9 deletions(-) create mode 100644 bot/servitor.py create mode 100644 gui/imprint.json create mode 100644 gui/imprint_gui.py rename gui/{league_statistic.py => league_statistic_gui.py} (100%) diff --git a/bot/servitor.py b/bot/servitor.py new file mode 100644 index 0000000..3832529 --- /dev/null +++ b/bot/servitor.py @@ -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) + diff --git a/gui/imprint.json b/gui/imprint.json new file mode 100644 index 0000000..190143a --- /dev/null +++ b/gui/imprint.json @@ -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." + ] + } + ] + } + ] + } + } +} diff --git a/gui/imprint_gui.py b/gui/imprint_gui.py new file mode 100644 index 0000000..484b181 --- /dev/null +++ b/gui/imprint_gui.py @@ -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") diff --git a/gui/league_statistic.py b/gui/league_statistic_gui.py similarity index 100% rename from gui/league_statistic.py rename to gui/league_statistic_gui.py diff --git a/gui/main_gui.py b/gui/main_gui.py index adc4251..b088ee9 100644 --- a/gui/main_gui.py +++ b/gui/main_gui.py @@ -16,19 +16,17 @@ 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 shadow-lg').props("reveal reveal-offset=1"): + 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) @@ -291,3 +289,5 @@ def setup_routes(admin_discord_id): else: 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')) \ No newline at end of file diff --git a/gui/match_gui.py b/gui/match_gui.py index de18d6e..0873928 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -5,10 +5,9 @@ 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/{system_name}', dark=True) - def match_form_page(system_name: str): # <--- WICHTIG: Hier fangen wir das Wort aus der URL auf! + def match_form_page(system_name: str): # <-- Hier wird der Name des Spielsystems gefiltert. gui_style.apply_design() # --- SICHERHEITS-CHECK --- diff --git a/main.py b/main.py index 2a7621a..85cbfd5 100644 --- a/main.py +++ b/main.py @@ -3,7 +3,7 @@ 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 @@ -30,11 +30,11 @@ 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") From c90546492472d1353bd7f8f1d5ad8835f483dce1 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Tue, 7 Apr 2026 08:36:32 +0000 Subject: [PATCH 30/30] Bugfix im Logger bei Match Eintragen. --- data/data_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/data/data_api.py b/data/data_api.py index 6421c93..d6318c8 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -258,7 +258,7 @@ 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})", player1_id) + 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()