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