MMR Match Berechnung funktioniert. Khorne und Slaanesh Regel sind eingefügt und Funktionieren. Liga Statistik Ansicht hat eine Rangliste bekommen. Match Eingabe sollte jetzt auf Mobile richtig Skalieren.

This commit is contained in:
Daniel Nagel 2026-03-06 11:01:58 +00:00
parent 0f914f3117
commit 4da6c4c6be
12 changed files with 654 additions and 130 deletions

View File

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

View File

@ -375,4 +375,103 @@ 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)
und berechnet, wie viele Tage das her ist.
"""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
# MAX(last_played) sucht den absolut neuesten Zeitstempel aus allen Einträgen dieses Spielers
query = """
SELECT MAX(last_played)
FROM player_game_statistic
WHERE player_id = ?
"""
cursor.execute(query, (player_id,))
row = cursor.fetchone()
connection.close()
# row[0] enthält jetzt unseren Zeitstempel (z.B. '2026-03-05 14:30:00')
last_played_str = row[0] if row else None
# Sicherheitscheck: Hat der Spieler überhaupt schon Einträge?
if not last_played_str:
return None
try:
# [:19] schneidet Millisekunden ab, damit das Format exakt passt.
last_played_date = datetime.strptime(last_played_str[:19], '%Y-%m-%d %H:%M:%S')
# 2. Die Differenz zu "Jetzt genau in diesem Moment" berechnen
time_difference = datetime.now() - last_played_date
# 3. .days holt aus der Zeitdifferenz nur die reinen, vollen Tage heraus
days_ago = time_difference.days
return {
"date_string": last_played_str[:10], # Nur das Datum (YYYY-MM-DD) für die GUI
"days_ago": days_ago # Die nackte Zahl zum Rechnen (z.B. 14)
}
except ValueError:
# Falls in der Datenbank mal ein kaputter String steht
return None
def apply_match_to_player_statistic (player_id, gamesystem_id, mmr_change, scored_points):
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))
connection.commit()
connection.close()
def get_leaderboard(system_name):
"""Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
# Wir holen ID, Namen, Discord und MMR, sortiert vom höchsten MMR zum niedrigsten
query = """
SELECT p.id, p.display_name, p.discord_name, stat.mmr
FROM players p
JOIN player_game_statistic stat ON p.id = stat.player_id
JOIN gamesystems sys ON stat.gamesystem_id = sys.id
WHERE sys.name = ?
ORDER BY stat.mmr DESC
"""
cursor.execute(query, (system_name,))
rows = cursor.fetchall()
connection.close()
result = []
for row in rows:
result.append(dict(row))
return result

View File

@ -61,6 +61,8 @@ def init_db():
player2_id INTEGER,
score_player2 INTEGER,
played_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
mmr_change_winner INTEGER,
mmr_change_looser INTEGER,
FOREIGN KEY (gamesystem_id) REFERENCES gamesystems (id),
FOREIGN KEY (player1_id) REFERENCES players (id),
FOREIGN KEY (player2_id) REFERENCES players (id)
@ -138,7 +140,7 @@ def seed_achievements():
connection.commit()
connection.close()
print("Achievements angelegt.")
seed_dummy_player()
#seed_dummy_player()
def seed_dummy_player():

View File

@ -27,16 +27,56 @@ def setup_routes():
# --- BLOCK 1 (2 Karten) ---
with ui.card().classes("w-full"):
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4"):
# 1. Daten für die Rangliste holen
leaderboard_data = data_api.get_leaderboard(systemname)
with ui.card().classes("items-center justify-center text-center"):
# 2. Tabelle vorbereiten und den EIGENEN Rang herausfinden
table_rows = []
my_rank = "-"
for index, player in enumerate(leaderboard_data):
current_rank = index + 1
# Wenn wir in der Liste über uns selbst stolpern, merken wir uns den Rang für unsere Karte!
if player['id'] == player_id:
my_rank = current_rank
table_rows.append({
'rank': current_rank,
'trend': '', # Platzhalter für später
'name': f"{player['display_name']} aka {player['discord_name']}",
'mmr': player['mmr']
})
table_columns = [
{'name': 'rank', 'label': '#', 'field': 'rank', 'align': 'left'},
{'name': 'trend', 'label': 'Trend', 'field': 'trend', 'align': 'center'},
{'name': 'name', 'label': 'Spieler', 'field': 'name', 'align': 'left'},
{'name': 'mmr', 'label': 'MMR', 'field': 'mmr', 'align': 'left'}
]
# --- 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"):
# LINKE SEITE (Belegt 1 Spalte)
# flex-col setzt die beiden Karten exakt übereinander
with ui.column().classes("w-full gap-4"):
with ui.card().classes("w-full items-center justify-center text-center"):
ui.label("MMR Punkte: ").classes('text-2xl font-bold')
ui.label(str(stats["mmr"])).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
with ui.card().classes("w-full items-center justify-center text-center"):
ui.label("Rang: ").classes('text-2xl font-bold')
ui.label("-").classes('text-4xl font-bold text-blue-100')
# Hier tragen wir jetzt unsere gefundene Platzierung ein!
ui.label(str(my_rank)).classes('text-4xl font-bold text-blue-100')
# RECHTE SEITE (Belegt 2 Spalten -> lg:col-span-2)
with ui.card().classes("w-full lg:col-span-2"):
ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2")
ui.table(columns=table_columns, rows=table_rows, row_key='rank').classes('w-full bg-zinc-900 text-white')
# --- BLOCK 2 (5 Karten) ---
with ui.card().classes("w-full"):

View File

@ -1,7 +1,7 @@
from nicegui import ui, app
from gui import gui_style
from data import data_api
from mmr_calculations import calc_match
from match_calculations import calc_match
def setup_routes():
@ -11,70 +11,68 @@ def setup_routes():
gui_style.apply_design()
# --- SICHERHEITS-CHECK ---
# Prüfen, ob der User wirklich eingeloggt ist.
if not app.storage.user.get('authenticated', False):
ui.label('Access Denied. Please log in first.').classes('text-red-500')
ui.button('Back to Home', on_click=lambda: ui.navigate.to('/'))
return
with ui.card().classes('w-150 items-center mt-10 justify-center'):
ui.label('Neues Spiel für '+ systemname + " eintragen").classes('text-2xl font-bold')
# ÄNDERUNG: w-full (für Handy) + max-w-md (für PC) + mx-auto (Zentrieren) + p-6 (Innenabstand)
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6 shadow-xl'):
ui.space()
# Text-Center hinzugefügt, falls der Systemname sehr lang ist und auf dem Handy umbricht
ui.label(f'Neues Spiel für {systemname} eintragen').classes('text-2xl font-bold text-center mb-6')
ui.label("Meine Punkte:").classes('text-xl font-bold')
ui.label("Meine Punkte:").classes('text-xl font-bold w-full text-left')
ui.space()
# ÄNDERUNG: h-60 entfernt, stattdessen gap-6 (Abstand zwischen den Elementen)
with ui.column().classes("w-full items-center gap-6"):
with ui.column().classes("w-full items-center h-60"):
# 1. Daten aus der DB holen
raw_players = data_api.get_all_players_from_system(systemname)
my_id = app.storage.user.get('db_id')
# 3. Eine saubere Optionen-Liste (Dictionary) für NiceGUI bauen
# 3. Eine saubere Optionen-Liste für NiceGUI bauen
dropdown_options = {}
for p in raw_players:
# FILTERN: Wenn die ID des Spielers MEINE EIGENE ID ist, überspringen wir ihn!
if p['player_id'] == my_id:
continue # Geht sofort zum nächsten Spieler in der Schleife
# Die ID wird der Schlüssel, der kombinierte Name wird der Anzeigetext
# Ergebnis sieht z.B. so aus: { 2: "Dummy Mc DummDumm aka dummy_user" }
continue
dropdown_options[p['player_id']] = f"{p['display_name']} aka {p['discord_name']}"
# 4. Eigene Punkte eintragen
p1_points = ui.slider(min= 0, max=100, value = 10).props("label-always")
# ÄNDERUNG: .classes('w-full') hinzugefügt, damit der Slider sich anpasst
p1_points = ui.slider(min=0, max=100, value=10).props("label-always").classes('w-full')
ui.space()
ui.separator().classes('w-full mt-4') # Ein schöner Trennstrich für die Optik
# 5. Das Dropdown mit unseren sauberen Optionen füttern
ui.label("Gegner:").classes('text-xl font-bold')
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-64')
p2_points = ui.slider(min= 0, max=100, value = 10).props("label-always")
# 5. Dropdown und Gegner Punkte
ui.label("Gegner:").classes('text-xl font-bold w-full text-left')
# ÄNDERUNG: w-64 durch w-full ersetzt
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full')
# ÄNDERUNG: .classes('w-full') hinzugefügt
p2_points = ui.slider(min=0, max=100, value=10).props("label-always").classes('w-full')
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
if p2_id is None:
ui.notify("Bitte wähle zuerst einen Gegner aus!", color="red", position="top")
return # Bricht die Funktion hier ab!
return
# 2. Punkte von den Slidern holen
score_p1 = p1_points.value
score_p2 = p2_points.value
# 3. Daten an die API schicken (my_id haben wir oben schon von app.storage.user geholt)
match_id = data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2)
# 4. Erfolgsmeldung und zurück zur Übersicht
# 4. Erfolgsmeldung und Berechnung
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"):
ui.button('Cancel', on_click=lambda: ui.navigate.to('/')).classes('bg-gray-500 text-white')
ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database()).classes("")
# Buttons ganz unten in einer Reihe
with ui.row().classes("w-full items-center justify-between mt-8"):
ui.button('Cancel', on_click=lambda: ui.navigate.to('/statistic/{systemname}')).classes('bg-gray-500 text-white')
ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database())

View File

@ -0,0 +1,73 @@
from data import data_api
from match_calculations import calc_mmr_change
import json
import os
# 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(sys_name)
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 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)?
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?
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!
else:
match_is_draw = False
winner_id = p2_id
looser_id = p1_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)
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)
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

@ -0,0 +1,65 @@
from data import data_api
# 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 % ! z.B. 0.7 = 70%
def calc_mmr_change(systemname, winner_id, looser_id, winner_points, looser_points, match_is_draw, rules):
#Rang der Spieler holen.
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)
if match_is_draw:
mmr_change = rules["rank_matrix"][str(max(-10, min(10, winner_rank-looser_rank)))]["draw"]
else:
mmr_change = rules["rank_matrix"][str(max(-10, min(10, winner_rank-looser_rank)))]["win"]
# Slaanesh Points berechnen und dem Change hinzufügen.
mmr_change += (sla_points := slaanesh_delight(winner_points, looser_points, rules))
# 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
# -----------------
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):
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():
print("k")
def nurgles_entropy():
print("k")

View File

@ -0,0 +1,113 @@
{
"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": 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

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

View File

@ -0,0 +1,113 @@
{
"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": 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,62 +0,0 @@
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)

View File

@ -1,29 +0,0 @@
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