umbau auf bessere und einfache MMR Berechnung. Bessere zuordnung der Winner-Looser Daten.

This commit is contained in:
Daniel Nagel 2026-03-15 20:46:45 +01:00
parent c5dcd56410
commit 1dcfb0694b
6 changed files with 228 additions and 180 deletions

View File

@ -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"}
{}

View File

@ -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,14 +444,35 @@ 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 = """
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 + ?,
@ -460,15 +482,46 @@ 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))
logger.log("MATCH CALC", f"Bei {get_player_name(player_id)} haben sich die MMR Punkte um {mmr_change} geändert.")
# 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()

View File

@ -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),

View File

@ -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)
@ -14,7 +18,7 @@ def calculate_match (match_id):
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):

View File

@ -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")

View File

@ -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")