Merge pull request 'dev' (#23) from dev into master

Reviewed-on: #23
This commit is contained in:
Daniel Nagel 2026-03-20 11:33:39 +01:00
commit 776171a07b
9 changed files with 122 additions and 112 deletions

View File

@ -1 +1 @@
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"DN","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"Verwirrter Klebschnüffler","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -1 +1 @@
{}
{"authenticated":true,"discord_id":"277898241750859776","discord_name":"mrteels","db_id":2,"display_name":"DN","discord_avatar_url":"https://cdn.discordapp.com/avatars/277898241750859776/7c3446bb51fafd72b1b4c21124b4994f.png"}

View File

@ -54,7 +54,7 @@ def get_or_create_player(discord_id, discord_name, avatar_url):
# Random Silly Name Generator für neue Spieler. Damit sie angeregt werden ihren richtigen Namen einzutragen.
silly_name = generate_silly_name()
cursor.execute("INSERT INTO players (discord_id, discord_name, display_name, discord_avatar_url) VALUES (?, ?, ?, ?)", (discord_id, discord_name, silly_name, avatar_url))
logger.log("NEW PLAYER", str("Ein neuer Spieler wurde angelegt - " + discord_name))
logger.log("data_api.get_or_create_player", str("Ein neuer Spieler wurde angelegt - " + discord_name))
connection.commit()
cursor.execute("SELECT id, discord_name, display_name, discord_avatar_url FROM players WHERE discord_id = ?", (discord_id,))
player = cursor.fetchone()
@ -178,7 +178,7 @@ def join_league(player_id, gamesystem_id):
INSERT INTO player_game_statistic (player_id, gamesystem_id)
VALUES (?, ?)
"""
logger.log("DataAPI", f"{get_player_name(player_id)} joined {gamesystem_id}", player_id)
logger.log("data_api.join_league", f"{get_player_name(player_id)} joined {gamesystem_id}", player_id)
cursor.execute(query, (player_id, gamesystem_id))
connection.commit()
@ -250,7 +250,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("DataAPI", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id)
logger.log("data_api.add_new_match", f"{get_player_name(player1_id)}:({score_p1}) posted Match. System: {system_name}, {get_player_name(player2_id)}:({score_p2})", player1_id)
connection.commit()
connection.close()
@ -335,12 +335,12 @@ def save_calculated_match(calc_results: dict):
))
connection.commit()
logger.log("MATCH_CALC", f"Match ID {match_id} wurde komplett berechnet und verbucht.")
logger.log("pi.save_calculated_match", f"Match ID:{match_id} berechnet.")
return True
except Exception as e:
connection.rollback()
print(f"KRITISCHER FEHLER beim Speichern des Matches: {e}")
logger.log("data_api.save_calculated_match", f"KRITISCHER FEHLER beim Speichern des Matches: {e}")
return False
finally:
@ -363,9 +363,6 @@ def get_gamesystem_data(system_id):
return dict(zip([col[0] for col in cursor.description], row)) if row else None
def get_gamesystems_data():
connection = sqlite3.connect(DB_PATH)
@ -572,7 +569,6 @@ 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()
@ -602,15 +598,20 @@ def get_submitted_matches(player_id):
def get_match_history_log(player_id):
"""Holt ALLE Matches eines Spielers inklusive der MMR-Änderungen für das Log."""
"""Holt ALLE Matches eines Spielers inklusive der MMR-Änderungen und Faktoren für das Log."""
connection = sqlite3.connect(DB_PATH)
connection.row_factory = sqlite3.Row
cursor = connection.cursor()
query = """
SELECT m.played_at, sys.name AS gamesystem_name,
m.player1_id, p1.display_name AS p1_display, p1.discord_name AS p1_discord, m.score_player1, m.player1_mmr_change,
m.player2_id, p2.display_name AS p2_display, p2.discord_name AS p2_discord, m.score_player2, m.player2_mmr_change,
m.player1_id, p1.display_name AS p1_display, p1.discord_name AS p1_discord,
m.score_player1, m.player1_mmr_change,
m.player1_khorne, m.player1_tzeentch, m.player1_slaanesh, m.player1_base_change,
m.player2_id, p2.display_name AS p2_display, p2.discord_name AS p2_discord,
m.score_player2, m.player2_mmr_change,
m.player2_khorne, m.player2_tzeentch, m.player2_slaanesh, m.player2_base_change,
m.elo_factor, m.rust_factor,
m.player2_check, m.match_is_counted
FROM matches m
JOIN gamesystems sys ON m.gamesystem_id = sys.id
@ -629,6 +630,7 @@ def get_match_history_log(player_id):
# -----------------------------------------------------
# Testing and Prototyping
# -----------------------------------------------------

View File

@ -3,22 +3,15 @@
"Um einer Liga beizutreten einfach auf **BEITRETEN** drücken und bestätigen.",
"Um deine Statistik in einer Liga zu sehen, klick auf eine Liga."
],
"mmr_info": [
"**MMR Punkte** sind die Liga Punkte um die gespielt wird.",
"Verliert man ein Spiel, verliert man Punkte. Und umgekehrt."
],
"match_form_info": [
"Um ein Spiel einzutragen gibt einfach deine Punkte ein. Wähle deinen Gegner aus. Und gibt seine Punkte ein.",
"**ACHTUNG:** Ein Spieler ist nur als Gegner auswählbar wenn er sich in der Liga angemeldet hat!"
"**ACHTUNG:** Ein Spieler ist nur als Gegner auswählbar wenn er sich in der Liga angemeldet hat!",
"Solltest du einen Fehler machen kannst du das 'falsche' Match auf der Hauptseite noch löschen bevor es bestätigt wurde."
],
"tyrann_info":[
],
"prügelknabe_info":[
]
"tyrann_info": [],
"prügelknabe_info": []
}

View File

@ -132,9 +132,10 @@ def setup_routes(admin_discord_id):
calc_match.calculate_match(m_id)
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
# Info-Text: Was wurde eingetragen?
info_text = f"[{match['system_name']}] {match['p1_name']} behauptet: Er hat {match['score_player1']} : {match['score_player2']} gegen dich gespielt."
ui.label(info_text).classes('text-lg text-gray-200')
info_text = f"{match['system_name']} - {match["played_at"]} "
detail_text = f"{match['p1_name']} behauptet: {match['p1_name']} ({match['score_player1']}) vs. Du ({match['score_player2']})"
ui.label(info_text).classes('text-bold text-lg text-normaltext')
ui.label(detail_text).classes('text-bold text-normaltext')
# Die Buttons (Funktion machen wir im nächsten Schritt!)
with ui.row().classes('gap-2'):
@ -143,7 +144,7 @@ def setup_routes(admin_discord_id):
ui.space()
# BESTÄTIGEN und spiel berechnen lassen
ui.button(color="positive", icon="check", on_click=lambda e, m_id=match['match_id']: acccept_match(m_id))
ui.label("Bestätigen wenn die Angaben stimmen, ablehnen wenn sich ein Fehler eingeschlichen hat.")
# ---------------------------
# --- Selbst eingetragene, offene Spiele ---
@ -152,7 +153,7 @@ def setup_routes(admin_discord_id):
if len(submitted_matches) > 0:
# Eine etwas dezentere Karte (grau)
with ui.card().classes('w-full bg-zinc-800 border border-gray-600 mb-6'):
ui.label(f"Warten auf Gegner: Du hast {len(submitted_matches)} offene(s) Spiel(e) eingetragen.").classes('text-xl font-bold text-gray-300 mb-2')
ui.label(f"Warten auf Gegner: Du hast {len(submitted_matches)} offene(s) Spiel(e).").classes('text-xl font-bold text-gray-300 mb-2')
for match in submitted_matches:
# Die Lösch-Funktion, die beim Klick ausgeführt wird
@ -163,14 +164,13 @@ def setup_routes(admin_discord_id):
# Für jedes Match machen wir eine kleine Reihe
with ui.row().classes('w-full items-center justify-between bg-zinc-900 p-3 rounded shadow-inner'):
# Info-Text: Auf wen warten wir?
info_text = f"[{match['system_name']}] Warten auf Bestätigung von {match['p2_name']} ({match['score_player1']} : {match['score_player2']})"
ui.label(info_text).classes('text-lg text-gray-400')
# Der Zurückziehen-Button (wieder mit unserem lambda m_id=... Trick!)
ui.button("Zurückziehen", color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id))
info_text = f"{match['system_name']} - {match["played_at"]} "
detail_text = f"Du ({match["score_player1"]}) vs. {match["p2_name"]}({match["score_player2"]})"
ui.label(info_text).classes('text-bold text-lg text-normaltext')
ui.label(detail_text).classes('text-bold text-normaltext')
ui.button(color="warning", icon="delete", on_click=lambda e, m_id=match['match_id']: retract_match(m_id))
ui.label("Dein Gegner muss das Match noch bestätigen. Wenn du einen Fehler gemacht hast, kannst du es löschen.")
# ---------------------------
# --- Spielsysteme ---

View File

@ -2,6 +2,7 @@ from nicegui import ui, app
from gui import gui_style
from data import data_api
from match_calculations import calc_match
from gui.info_text import info_system
def setup_routes():
@ -21,37 +22,31 @@ def setup_routes():
# 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 w-full text-left')
# ÄNDERUNG: h-60 entfernt, stattdessen gap-6 (Abstand zwischen den Elementen)
with ui.column().classes("w-full items-center gap-6"):
# 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 für NiceGUI bauen
dropdown_options = {}
for p in raw_players:
if p['player_id'] == my_id:
continue
dropdown_options[p['player_id']] = f"{p['display_name']} 'aka' {p['discord_name']}"
# Ä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')
with ui.row().classes("w-full items-center justify-between"):
p1_points = ui.slider(min=0, max=100, value=10).classes("w-70")
ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext")
ui.separator().classes('w-full mt-4') # Ein schöner Trennstrich für die Optik
# 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')
with ui.row().classes("w-full items-center justify-between"):
p2_points = ui.slider(min=0, max=100, value=10).classes("w-70")
ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext")
ui.space()
@ -71,15 +66,8 @@ def setup_routes():
ui.notify("Match erfolgreich eingetragen!", color="green")
ui.navigate.to(f'/statistic/{systemname}')
with ui.dialog().classes("w-full items-center") as form_info, ui.card():
ui.label('Um ein Spiel einzutragen einfach deine erspielten Punkte, deinen Gegner und die Punkte von deinem Gegner eintragen').classes("font-bold text-white text-l")
ui.label('ACHTUNG: Damit ein Spieler als Gegner ausgewählt werden kann, muss er der Liga beigetreten sein!').classes("font-bold text-white text-l")
ui.label('Nach dem Absenden muss dein Gegner in seiner Liga App das Spiel noch kurz bestätigen. Solltest du einen Fehler gemacht haben, kannst du das Spiel (bevor es bestätigt wurde) auf der Hauptseite selber löschen.').classes("font-bold text-white text-l")
ui.button(icon="close", on_click=form_info.close).classes("w-10 h-8 rounded-full")
# Buttons ganz unten in einer Reihe
with ui.row().classes("w-full items-center justify-between mt-8"):
ui.button(icon="close", on_click=lambda: ui.navigate.to(f'/statistic/{systemname}')).classes("w-10 h-8 rounded-full")
ui.button(icon="help", color="information" ,on_click=form_info.open).classes("w-10 h-8 rounded-full")
info_system.create_info_button("match_form_info")
ui.button(text="Absenden", color="positive", on_click=lambda: input_match_to_database())

View File

@ -2,23 +2,21 @@ from nicegui import ui, app
from data import data_api
from gui import gui_style
def setup_routes():
@ui.page('/matchhistory', dark=True)
def match_history_page():
gui_style.apply_design()
# Sicherheits-Check: Ist der Nutzer eingeloggt?
if not app.storage.user.get('authenticated', False):
ui.label('Bitte logge dich ein.').classes('text-red-500 text-2xl m-4')
return
player_id = app.storage.user.get('db_id')
# Das Haupt-Layout der Seite
with ui.column().classes('w-full max-w-5xl mx-auto p-4'):
with ui.column().classes('w-full max-w-7xl mx-auto p-4'):
# Kopfbereich mit Zurück-Button
with ui.row().classes('w-full items-center justify-between mb-6'):
ui.label("Komplette Match Historie").classes("text-3xl font-bold text-white")
ui.button("Zurück", icon="arrow_back", on_click=lambda: ui.navigate.to('/')).classes('bg-zinc-700 text-white')
@ -26,21 +24,37 @@ def setup_routes():
raw_matches = data_api.get_match_history_log(player_id)
table_rows = []
# Daten für die Tabelle aufbereiten
def fmt_signed(val, pending=False):
"""Formatiert einen Integer-Wert mit Vorzeichen oder gibt Sondertexte zurück."""
if pending:
return "Ausstehend"
if val is None:
return "0"
if val > 0:
return f"+{val}"
return str(val)
for i, match in enumerate(raw_matches):
# Bin ich P1 oder P2?
if match['player1_id'] == player_id:
is_player1 = match['player1_id'] == player_id
pending = match['match_is_counted'] == 0
if is_player1:
opponent = f"{match['p2_display']} aka {match['p2_discord']}"
my_score = match['score_player1']
opp_score = match['score_player2']
my_mmr_change = match['player1_mmr_change']
my_khorne = match['player1_khorne']
my_tzeentch = match['player1_tzeentch']
my_slaanesh = match['player1_slaanesh']
else:
opponent = f"{match['p1_display']} aka {match['p1_discord']}"
my_score = match['score_player2']
opp_score = match['score_player1']
my_mmr_change = match['player2_mmr_change']
my_khorne = match['player2_khorne']
my_tzeentch = match['player2_tzeentch']
my_slaanesh = match['player2_slaanesh']
# Ergebnis Text
if my_score > opp_score:
result = "Gewonnen"
elif my_score < opp_score:
@ -48,51 +62,61 @@ def setup_routes():
else:
result = "Unentschieden"
# MMR Text schön formatieren
if match['match_is_counted'] == 0:
mmr_text = "Ausstehend"
elif my_mmr_change is None:
mmr_text = "0"
elif my_mmr_change > 0:
mmr_text = f"+{my_mmr_change}"
elif my_mmr_change < 0:
mmr_text = f"{my_mmr_change}"
else:
mmr_text = str(my_mmr_change)
elo_factor = match['elo_factor']
rust_factor = match['rust_factor']
table_rows.append({
'id': i,
'date': str(match['played_at'])[:10],
'system': match['gamesystem_name'],
'score': str(my_score),
'opponent': opponent,
'score': f"{my_score}",
'opp_score': f"{opp_score}",
'opp_score': str(opp_score),
'result': result,
'mmr': mmr_text
'elo': fmt_signed(round(elo_factor, 2) if elo_factor is not None else None, pending),
'rust': fmt_signed(round(rust_factor, 2) if rust_factor is not None else None, pending),
'khorne': fmt_signed(my_khorne, pending),
'tzeentch': fmt_signed(my_tzeentch, pending),
'slaanesh': fmt_signed(my_slaanesh, pending),
'mmr': fmt_signed(my_mmr_change, pending),
})
# Spalten definieren
columns = [
{'name': 'date', 'label': 'Datum', 'field': 'date', 'align': 'left'},
{'name': 'system', 'label': 'System', 'field': 'system', 'align': 'left'},
{'name': 'score', 'label': 'Punkte', 'field': 'score', 'align': 'left'},
{'name': 'score', 'label': 'Eigene Punkte', 'field': 'score', 'align': 'center'},
{'name': 'opponent', 'label': 'Gegner', 'field': 'opponent', 'align': 'left'},
{'name': 'opp_score', 'label': 'Gegner Punkte', 'field': 'score', 'align': 'center'},
{'name': 'opp_score','label': 'Gegner Punkte', 'field': 'opp_score','align': 'center'},
{'name': 'result', 'label': 'Ergebnis', 'field': 'result', 'align': 'left'},
{'name': 'mmr', 'label': 'MMR Änderung', 'field': 'mmr', 'align': 'right'}
{'name': 'elo', 'label': 'Elo Faktor', 'field': 'elo', 'align': 'right'},
{'name': 'rust', 'label': 'Rost Faktor', 'field': 'rust', 'align': 'right'},
{'name': 'khorne', 'label': 'Khorne', 'field': 'khorne', 'align': 'right'},
{'name': 'tzeentch', 'label': 'Tzeentch', 'field': 'tzeentch', 'align': 'right'},
{'name': 'slaanesh', 'label': 'Slaanesh', 'field': 'slaanesh', 'align': 'right'},
{'name': 'mmr', 'label': 'MMR Änderung', 'field': 'mmr', 'align': 'right'},
]
# Tabelle zeichnen
if len(table_rows) > 0:
history_table = ui.table(columns=columns, rows=table_rows, row_key='id').classes('w-full bg-zinc-900 text-white')
# KLEINER TRICK: Wir färben die MMR-Spalte grün oder rot, je nachdem ob da ein "+" oder "-" steht!
history_table.add_slot('body-cell-mmr', '''
# Shared slot template for colored signed values
colored_slot = '''
<q-td :props="props">
<span :class="{'text-green-500 font-bold': props.row.mmr.startsWith('+'), 'text-red-500 font-bold': props.row.mmr.startsWith('-'), 'text-gray-400 italic': props.row.mmr === 'Ausstehend'}">
{{ props.row.mmr }}
<span :class="{
'text-green-500 font-bold': props.row[props.col.field].startsWith('+'),
'text-red-500 font-bold': props.row[props.col.field].startsWith('-'),
'text-gray-400 italic': props.row[props.col.field] === 'Ausstehend'
}">
{{ props.row[props.col.field] }}
</span>
</q-td>
''')
'''
if table_rows:
history_table = ui.table(
columns=columns,
rows=table_rows,
row_key='id'
).classes('w-full bg-zinc-900 text-white')
for col in ['elo', 'rust', 'khorne', 'tzeentch', 'slaanesh', 'mmr']:
history_table.add_slot(f'body-cell-{col}', colored_slot)
else:
ui.label("Keine Spiele gefunden.").classes("text-gray-400 italic")

View File

@ -5,8 +5,8 @@ import os
from wood import logger
point_inflation = 0.7 # => entspricht %
K_FACTOR = 30 # Die "Border" (Maximalpunkte) die ein Sieg gibt.
point_inflation = 0.8 # => 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):
@ -62,8 +62,8 @@ def calculate_match (match_id):
l_base = int(base_change*point_inflation)
l_khorne = int(calculation.wrath_of_khorne(looser_id, system_id))
slaanesh = int(calculation.slaanesh_delight())
tzeentch = int(calculation.tzeentch_scemes(system_id, winner_score, looser_score))
slaanesh = calculation.slaanesh_delight()
tzeentch = calculation.tzeentch_schemes(system_id, winner_score, looser_score)
# ==========================================
# 3. Daten Verpacken
@ -93,9 +93,8 @@ def calculate_match (match_id):
}
}
logger.log("MATCH CALC", f"Match{match_id} berechnet.")
logger.log("MATCH CALC", f"Winner {data_api.get_player_name(winner_id)}: Base {w_base} + Khorne {w_khorne} + Slaanesh {slaanesh} + Tzeentch {tzeentch}")
logger.log("MATCH CALC", f"Looser {data_api.get_player_name(looser_id)}: Base {l_base} + Khorne {l_khorne} - Slaanesh {slaanesh} - Tzeentch {tzeentch}")
logger.log(f"calc_match ID:{match_id}", f"Winner {data_api.get_player_name(winner_id)}: Base {w_base} + Khorne({w_khorne}) + Slaanesh({slaanesh}) + Tzeentch({tzeentch}) = {calc_results[winner_id]["total"]}")
logger.log(f"calc_match ID:{match_id}", f"Looser {data_api.get_player_name(looser_id)}: -Base({l_base}) + Khorne({l_khorne}) - Slaanesh({slaanesh}) - Tzeentch({tzeentch}) = {calc_results[looser_id]["total"]}")
data_api.save_calculated_match(calc_results)

View File

@ -67,11 +67,15 @@ def wrath_of_khorne(player_id, system_id):
def slaanesh_delight():
return 0
def tzeentch_scemes(system_id, winner_score, looser_score):
def tzeentch_schemes(system_id, winner_score, looser_score):
sys_data = data_api.get_gamesystem_data(system_id)
min_score = sys_data["min_score"]
max_score = sys_data["max_score"]
bonus = int(((max_score*(winner_score-looser_score)))/(max_score*1.1)) #Multiplikatiionsfaktor für die Berechnung.
diff_normalized = (winner_score - looser_score) / max_score # 0.0 1.0
threshold = 0.1 # Bonus startet erst ab 10% Vorsprung
return 0
bonus = int(max(0, diff_normalized - threshold) * 10) # Ergibt 09
bonus = min(bonus, 9) # Sicherheits-Cap
return bonus