From 0f914f3117f750a43033cf08d6d5be1cdd3ad12c Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 5 Mar 2026 15:19:40 +0000 Subject: [PATCH] Erstellen der mmr_rules Matrix. Berechnung der Matches, erster Teil. Aufsetzten der MMR Berechnung. --- ...-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json | 2 +- data/data_api.py | 78 +++++++++++- data/setup_database.py | 75 ++++++++++++ gui/match_gui.py | 5 +- mmr calculations/calc_match.py | 1 - mmr_calculations/calc_match.py | 62 ++++++++++ mmr_calculations/calc_mmr_change.py | 29 +++++ mmr_calculations/mmr_rules_spearhead.json | 112 ++++++++++++++++++ mmr_calculations/mmr_rules_warhammer_40k.json | 112 ++++++++++++++++++ .../mmr_rules_warhammer_age_of_sigmar.json | 112 ++++++++++++++++++ 10 files changed, 581 insertions(+), 7 deletions(-) delete mode 100644 mmr calculations/calc_match.py create mode 100644 mmr_calculations/calc_match.py create mode 100644 mmr_calculations/calc_mmr_change.py create mode 100644 mmr_calculations/mmr_rules_spearhead.json create mode 100644 mmr_calculations/mmr_rules_warhammer_40k.json create mode 100644 mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json diff --git a/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json b/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json index 9963eea..93161d0 100644 --- a/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json +++ b/.nicegui/storage-user-83ffc178-0f94-4ada-8ca6-1c51b99b4b9c.json @@ -1 +1 @@ -{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Daniel Nagel","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"} \ No newline at end of file +{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verzweifelter Regelvergesser","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 95423e6..be3524f 100644 --- a/data/data_api.py +++ b/data/data_api.py @@ -99,9 +99,49 @@ 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) + cursor = connection.cursor() -import sqlite3 -from data.setup_database import DB_PATH + # Wir suchen exakt nach dem Namen + cursor.execute("SELECT id FROM gamesystems WHERE name = ?", (system_name,)) + row = cursor.fetchone() + + connection.close() + + # Wenn wir einen Treffer haben, geben wir die erste Spalte (die ID) zurück + if row: + return row[0] + + # Wenn das System nicht existiert + 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 @@ -224,7 +264,7 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2): # Sicherheitscheck (sollte eigentlich nie passieren) if not sys_row: connection.close() - return False + return None sys_id = sys_row[0] @@ -235,11 +275,12 @@ 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 connection.commit() connection.close() - return True + return new_match_id @@ -305,4 +346,33 @@ def get_last_20_match_scores(player_id, system_name): 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 + + \ No newline at end of file diff --git a/data/setup_database.py b/data/setup_database.py index 8d65ffd..0ef0b29 100644 --- a/data/setup_database.py +++ b/data/setup_database.py @@ -1,5 +1,7 @@ import sqlite3 import os +import json + DB_PATH = "data/warhammer_league.db" @@ -117,6 +119,7 @@ def seed_gamesystems(): print("Spielsysteme angelegt!") #Nächster Schritt: Standard Achievments eintragen. + generate_default_mmr_rules() seed_achievements() def seed_achievements(): @@ -179,3 +182,75 @@ def seed_dummy_player(): connection.close() 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": 5, + "rank_matrix": { + "10": {"win": 10, "draw": 30}, + "9": {"win": 10, "draw": 30}, + "8": {"win": 10, "draw": 20}, + "7": {"win": 10, "draw": 20}, + "6": {"win": 20, "draw": 10}, + "5": {"win": 20, "draw": 10}, + "4": {"win": 20, "draw": 0}, + "3": {"win": 20, "draw": 0}, + "2": {"win": 30, "draw": 0}, + "1": {"win": 30, "draw": 0}, + "0": {"win": 30, "draw": 0}, + "-1": {"win": 30, "draw": 0}, + "-2": {"win": 40, "draw": 0}, + "-3": {"win": 40, "draw": -10}, + "-4": {"win": 50, "draw": -20}, + "-5": {"win": 60, "draw": -20}, + "-6": {"win": 70, "draw": -20}, + "-7": {"win": 80, "draw": -50}, + "-8": {"win": 100, "draw": -50}, + "-9": {"win": 120, "draw": -60}, + "-10": {"win": 150, "draw": -60} + }, + "score_bonus": [ + {"min_diff": 95, "bonus": 40}, + {"min_diff": 85, "bonus": 30}, + {"min_diff": 65, "bonus": 20}, + {"min_diff": 35, "bonus": 10}, + {"min_diff": 0, "bonus": 0} + ] + } + + # 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/match_gui.py b/gui/match_gui.py index e8c81ab..5880d2a 100644 --- a/gui/match_gui.py +++ b/gui/match_gui.py @@ -1,6 +1,7 @@ from nicegui import ui, app from gui import gui_style from data import data_api +from mmr_calculations import calc_match def setup_routes(): @@ -54,6 +55,7 @@ def setup_routes(): ui.space() + # Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern. def input_match_to_database(): # 1. Prüfen, ob ein Gegner ausgewählt wurde p2_id = opponent_select.value @@ -66,10 +68,11 @@ def setup_routes(): score_p2 = p2_points.value # 3. Daten an die API schicken (my_id haben wir oben schon von app.storage.user geholt) - data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2) + match_id = data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2) # 4. Erfolgsmeldung und zurück zur Übersicht ui.notify("Match erfolgreich eingetragen!", color="green") + calc_match.calculate_inserted_match(systemname, match_id) ui.navigate.to('/') with ui.row().classes("w-full items-center justify-between"): diff --git a/mmr calculations/calc_match.py b/mmr calculations/calc_match.py deleted file mode 100644 index c791851..0000000 --- a/mmr calculations/calc_match.py +++ /dev/null @@ -1 +0,0 @@ -from data import database \ No newline at end of file diff --git a/mmr_calculations/calc_match.py b/mmr_calculations/calc_match.py new file mode 100644 index 0000000..660b035 --- /dev/null +++ b/mmr_calculations/calc_match.py @@ -0,0 +1,62 @@ +from data import data_api +from mmr_calculations import * + +# 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_inserted_match (gamesystem, match_id): + match_data = data_api.get_match_by_id(match_id) + if not match_data: + print("Fehler: Match nicht gefunden!") + return + # Laden und umsetzen 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'] + + rules = load_mmr_rule_matrix(systemname) + draw_diff = rules["draw_point_difference"] + + calculated = False + winner_id = None + looser_id = None + match_is_draw = False + winner_score = 0 + looser_score = 0 + + # Abgrenzen ob das Match schon berechnet wurde. Weil ein Draw kan 4 Punkte unterschied haben + # 43-41 ist ein Draw aber rein Mathematisch würde es auch ein anderes if triggern + while not calculated: + # Match is a Draw + if -draw_diff <= (p1_score-p2_score) <= draw_diff: + match_is_draw = True + winner_id = p1_id + looser_id = p2_id + winner_score = p1_score + looser_score = p2_score + + calculated = True + break + + # p1 ist der Sieger. + if score_p1 > score_p2: + winner_id = p1_id + looser_id = p2_id + winner_score = p1_score + looser_score = p2_score + + calculated = True + break + + # p2 ist der Sieger. + if score_p1 < score_p2: + winner_id = p2_id + looser_id = p1_id + winner_score = p2_score + looser_score = p1_score + + calculated = True + break + + calc_mmr_change.calc_mmr_change(sys_name, winner_id, looser_id, winner_score, looser_score, match_is_draw) \ No newline at end of file diff --git a/mmr_calculations/calc_mmr_change.py b/mmr_calculations/calc_mmr_change.py new file mode 100644 index 0000000..0a3bf80 --- /dev/null +++ b/mmr_calculations/calc_mmr_change.py @@ -0,0 +1,29 @@ +from data import data_api +import json +import os + +def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw): + 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) + + rules = load_mmr_rule_matrix(systemname) + + if match_is_draw: + mmr_change = rules["rank_matrix"][str(winner_rank-looser_rank)]["draw"] + + else: + mmr_change = rules["rank_matrix"][str(winner_rank-looser_rank)]["win"] + + + + +def load_mmr_rule_matrix(systemname): + safe_name = systemname.replace(" ", "_").lower() + + file_path = f"mmr_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/mmr_calculations/mmr_rules_spearhead.json b/mmr_calculations/mmr_rules_spearhead.json new file mode 100644 index 0000000..f0efcba --- /dev/null +++ b/mmr_calculations/mmr_rules_spearhead.json @@ -0,0 +1,112 @@ +{ + "system_info": "Balancing für Spearhead", + "draw_point_difference": 5, + "rank_matrix": { + "10": { + "win": 10, + "draw": 30 + }, + "9": { + "win": 10, + "draw": 30 + }, + "8": { + "win": 10, + "draw": 20 + }, + "7": { + "win": 10, + "draw": 20 + }, + "6": { + "win": 20, + "draw": 10 + }, + "5": { + "win": 20, + "draw": 10 + }, + "4": { + "win": 20, + "draw": 0 + }, + "3": { + "win": 20, + "draw": 0 + }, + "2": { + "win": 30, + "draw": 0 + }, + "1": { + "win": 30, + "draw": 0 + }, + "0": { + "win": 30, + "draw": 0 + }, + "-1": { + "win": 30, + "draw": 0 + }, + "-2": { + "win": 40, + "draw": 0 + }, + "-3": { + "win": 40, + "draw": -10 + }, + "-4": { + "win": 50, + "draw": -20 + }, + "-5": { + "win": 60, + "draw": -20 + }, + "-6": { + "win": 70, + "draw": -20 + }, + "-7": { + "win": 80, + "draw": -50 + }, + "-8": { + "win": 100, + "draw": -50 + }, + "-9": { + "win": 120, + "draw": -60 + }, + "-10": { + "win": 150, + "draw": -60 + } + }, + "score_bonus": [ + { + "min_diff": 95, + "bonus": 40 + }, + { + "min_diff": 85, + "bonus": 30 + }, + { + "min_diff": 65, + "bonus": 20 + }, + { + "min_diff": 35, + "bonus": 10 + }, + { + "min_diff": 0, + "bonus": 0 + } + ] +} \ No newline at end of file diff --git a/mmr_calculations/mmr_rules_warhammer_40k.json b/mmr_calculations/mmr_rules_warhammer_40k.json new file mode 100644 index 0000000..892fe7d --- /dev/null +++ b/mmr_calculations/mmr_rules_warhammer_40k.json @@ -0,0 +1,112 @@ +{ + "system_info": "Balancing für Warhammer 40k", + "draw_point_difference": 5, + "rank_matrix": { + "10": { + "win": 10, + "draw": 30 + }, + "9": { + "win": 10, + "draw": 30 + }, + "8": { + "win": 10, + "draw": 20 + }, + "7": { + "win": 10, + "draw": 20 + }, + "6": { + "win": 20, + "draw": 10 + }, + "5": { + "win": 20, + "draw": 10 + }, + "4": { + "win": 20, + "draw": 0 + }, + "3": { + "win": 20, + "draw": 0 + }, + "2": { + "win": 30, + "draw": 0 + }, + "1": { + "win": 30, + "draw": 0 + }, + "0": { + "win": 30, + "draw": 0 + }, + "-1": { + "win": 30, + "draw": 0 + }, + "-2": { + "win": 40, + "draw": 0 + }, + "-3": { + "win": 40, + "draw": -10 + }, + "-4": { + "win": 50, + "draw": -20 + }, + "-5": { + "win": 60, + "draw": -20 + }, + "-6": { + "win": 70, + "draw": -20 + }, + "-7": { + "win": 80, + "draw": -50 + }, + "-8": { + "win": 100, + "draw": -50 + }, + "-9": { + "win": 120, + "draw": -60 + }, + "-10": { + "win": 150, + "draw": -60 + } + }, + "score_bonus": [ + { + "min_diff": 95, + "bonus": 40 + }, + { + "min_diff": 85, + "bonus": 30 + }, + { + "min_diff": 65, + "bonus": 20 + }, + { + "min_diff": 35, + "bonus": 10 + }, + { + "min_diff": 0, + "bonus": 0 + } + ] +} \ 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 new file mode 100644 index 0000000..679c737 --- /dev/null +++ b/mmr_calculations/mmr_rules_warhammer_age_of_sigmar.json @@ -0,0 +1,112 @@ +{ + "system_info": "Balancing für Warhammer Age of Sigmar", + "draw_point_difference": 5, + "rank_matrix": { + "10": { + "win": 10, + "draw": 30 + }, + "9": { + "win": 10, + "draw": 30 + }, + "8": { + "win": 10, + "draw": 20 + }, + "7": { + "win": 10, + "draw": 20 + }, + "6": { + "win": 20, + "draw": 10 + }, + "5": { + "win": 20, + "draw": 10 + }, + "4": { + "win": 20, + "draw": 0 + }, + "3": { + "win": 20, + "draw": 0 + }, + "2": { + "win": 30, + "draw": 0 + }, + "1": { + "win": 30, + "draw": 0 + }, + "0": { + "win": 30, + "draw": 0 + }, + "-1": { + "win": 30, + "draw": 0 + }, + "-2": { + "win": 40, + "draw": 0 + }, + "-3": { + "win": 40, + "draw": -10 + }, + "-4": { + "win": 50, + "draw": -20 + }, + "-5": { + "win": 60, + "draw": -20 + }, + "-6": { + "win": 70, + "draw": -20 + }, + "-7": { + "win": 80, + "draw": -50 + }, + "-8": { + "win": 100, + "draw": -50 + }, + "-9": { + "win": 120, + "draw": -60 + }, + "-10": { + "win": 150, + "draw": -60 + } + }, + "score_bonus": [ + { + "min_diff": 95, + "bonus": 40 + }, + { + "min_diff": 85, + "bonus": 30 + }, + { + "min_diff": 65, + "bonus": 20 + }, + { + "min_diff": 35, + "bonus": 10 + }, + { + "min_diff": 0, + "bonus": 0 + } + ] +} \ No newline at end of file