From a715cd7bff808208ab1d0a308acc1a6249d396c3 Mon Sep 17 00:00:00 2001 From: Daniel Nagel Date: Thu, 12 Mar 2026 14:23:09 +0000 Subject: [PATCH] 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