Erstellen der mmr_rules Matrix. Berechnung der Matches, erster Teil. Aufsetzten der MMR Berechnung.

This commit is contained in:
Daniel Nagel 2026-03-05 15:19:40 +00:00
parent cd2050e6fe
commit 0f914f3117
10 changed files with 581 additions and 7 deletions

View File

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

View File

@ -99,9 +99,49 @@ def get_all_players_from_system(system_name):
return result
def get_gamesystem_id_by_name(system_name):
"""Holt die interne ID eines Spielsystems anhand seines Namens (z.B. 'Warhammer 40k' -> 1)."""
connection = sqlite3.connect(DB_PATH)
cursor = connection.cursor()
import sqlite3
from data.setup_database import DB_PATH
# Wir suchen exakt nach dem Namen
cursor.execute("SELECT id FROM gamesystems WHERE name = ?", (system_name,))
row = cursor.fetchone()
connection.close()
# Wenn wir einen Treffer haben, geben wir die erste Spalte (die ID) zurück
if row:
return row[0]
# Wenn das System nicht existiert
return None
def get_player_rank(player_id, gamesystem_id):
"""Sortiert die Liga nach MMR und gibt den aktuellen Platz (Rang) des Spielers zurück."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
# Alle Spieler dieser Liga, sortiert nach MMR (höchstes zuerst)
query = """
SELECT player_id
FROM player_game_statistic
WHERE gamesystem_id = ?
ORDER BY mmr DESC
"""
cursor.execute(query, (gamesystem_id,))
rows = cursor.fetchall()
connection.close()
# Wir zählen die Liste durch (enumerate fängt bei 0 an, also machen wir +1 für den Rang)
for index, row in enumerate(rows):
if row['player_id'] == player_id:
return index + 1
# Falls der Spieler nicht in der Liga ist (sollte nicht passieren)
return 999
@ -224,7 +264,7 @@ def add_new_match(system_name, player1_id, player2_id, score_p1, score_p2):
# Sicherheitscheck (sollte eigentlich nie passieren)
if not sys_row:
connection.close()
return False
return None
sys_id = sys_row[0]
@ -235,11 +275,12 @@ 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
connection.commit()
connection.close()
return True
return new_match_id
@ -305,4 +346,33 @@ def get_last_20_match_scores(player_id, system_name):
return {"points": points_list, "labels": labels_list}
def get_match_by_id(match_id):
"""Holt alle Daten eines spezifischen Matches anhand der Match-ID."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row # Damit wir wieder ein schönes Dictionary bekommen
cursor = connection.cursor()
# m.* holt alle Spalten aus dem Match (Punkte, IDs, Datum)
# sys.name AS gamesystem_name holt uns direkt den passenden Text für deine JSON-Ladefunktion
query = """
SELECT m.*, sys.name AS gamesystem_name
FROM matches m
JOIN gamesystems sys ON m.gamesystem_id = sys.id
WHERE m.id = ?
"""
cursor.execute(query, (match_id,))
row = cursor.fetchone()
connection.close()
# Wenn ein Match gefunden wurde, geben wir es als Dictionary zurück
if row:
return dict(row)
return None # Falls die ID nicht existiert

View File

@ -1,5 +1,7 @@
import sqlite3
import os
import json
DB_PATH = "data/warhammer_league.db"
@ -117,6 +119,7 @@ def seed_gamesystems():
print("Spielsysteme angelegt!")
#Nächster Schritt: Standard Achievments eintragen.
generate_default_mmr_rules()
seed_achievements()
def seed_achievements():
@ -179,3 +182,75 @@ def seed_dummy_player():
connection.close()
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": 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}
]
}
# 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

@ -1,6 +1,7 @@
from nicegui import ui, app
from gui import gui_style
from data import data_api
from mmr_calculations import calc_match
def setup_routes():
@ -54,6 +55,7 @@ def setup_routes():
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
@ -66,10 +68,11 @@ def setup_routes():
score_p2 = p2_points.value
# 3. Daten an die API schicken (my_id haben wir oben schon von app.storage.user geholt)
data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2)
match_id = data_api.add_new_match(systemname, my_id, p2_id, score_p1, score_p2)
# 4. Erfolgsmeldung und zurück zur Übersicht
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"):

View File

@ -1 +0,0 @@
from data import database

View File

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

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

View File

@ -0,0 +1,112 @@
{
"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": 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,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,112 @@
{
"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": 95,
"bonus": 40
},
{
"min_diff": 85,
"bonus": 30
},
{
"min_diff": 65,
"bonus": 20
},
{
"min_diff": 35,
"bonus": 10
},
{
"min_diff": 0,
"bonus": 0
}
]
}