Umbau auf MMR Dynamische Berechnung von Matches.

This commit is contained in:
Daniel Nagel 2026-03-11 18:36:11 +00:00
parent b6fea8b84d
commit 4dfdf2165c
12 changed files with 127 additions and 786 deletions

View File

@ -535,6 +535,7 @@ def get_player_name(player_id):
else: else:
return "Gelöschter Spieler" return "Gelöschter Spieler"
def get_system_name(sys_id): def get_system_name(sys_id):
if sys_id is None: if sys_id is None:
return "Unbekanntes System" 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() connection.close()
return [dict(row) for row in rows] 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

View File

@ -197,74 +197,3 @@ def seed_dummy_player():
print("Test-Gegner 'Dummy Mc DummDumm' ist bereit und in allen Ligen angemeldet!") 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.")

View File

@ -2,6 +2,7 @@ from nicegui import ui, app
from data import database, data_api from data import database, data_api
from gui import gui_style from gui import gui_style
from wood import logger from wood import logger
from gui import main_gui
def setup_routes(): def setup_routes():
@ui.page('/admin', dark=True) @ui.page('/admin', dark=True)
@ -14,7 +15,7 @@ def setup_routes():
if app.storage.user.get('authenticated', False): if app.storage.user.get('authenticated', False):
with ui.card().classes("w-full"): with ui.card().classes("w-full"):
with ui.row().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.button(icon="refresh", on_click=lambda: ui.navigate.reload)
ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4') ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4')

View File

@ -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') 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 # 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: else:
ui.label("Noch keine Spiele absolviert.").classes("text-gray-500 italic") ui.label("Noch keine Spiele absolviert.").classes("text-gray-500 italic")

View File

@ -80,13 +80,3 @@ def calculate_match (match_id):
# Das Match als Berechnet markieren # Das Match als Berechnet markieren
data_api.set_match_counted(match_id) 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

View File

@ -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. # 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 % ! z.B. 0.7 = 70%
def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw, rules): def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw):
#Rang der Spieler holen.
point_inflation = 0.7 # Verlierer verliert nur 70% der Punkte
K_FACTOR = 35 # 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:
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 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) 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) # 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: if match_is_draw:
mmr_change = rules["rank_matrix"][str(max(-10, min(10, winner_rank-looser_rank)))]["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: else:
mmr_change = rules["rank_matrix"][str(max(-10, min(10, winner_rank-looser_rank)))]["win"] # Sieg (1.0). Gewinnt der Favorit, gibt es wenig Punkte. Gewinnt der Underdog, gibt es viele!
base_change = K_FACTOR * (1.0 - expected_win)
# Slaanesh Points berechnen und dem Change hinzufügen. # 3. Den "Rostigkeits-Dämpfer" anwenden
mmr_change += (sla_points := slaanesh_delight(winner_points, looser_points, rules)) 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)
# 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))
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 # 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_days = 16
khorne_bonus = 8 khorne_bonus = 8
# ----------------- # -----------------
def wrath_of_khorne(player_id): def wrath_of_khorne(player_id):
last_played = data_api.get_days_since_last_game(player_id)["days_ago"] 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): def slaanesh_delight(winner_points, looser_points, rules):
print()
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
def tzeentch_scemes(): def tzeentch_scemes():

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}

View File

@ -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
}
]
}