From 4dfdf2165c63b2d0858890bd5b7cee1916f9d9c0 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Wed, 11 Mar 2026 18:36:11 +0000 Subject: [PATCH 01/12] 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 02/12] =?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 03/12] =?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 04/12] 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 05/12] 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 06/12] 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 07/12] 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 08/12] 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 09/12] 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 10/12] 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 11/12] =?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 12/12] 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