Compare commits

..

3 Commits

Author SHA1 Message Date
Daniel Nagel
1b1031a5e7 data api function angepoasst. Besserer Namen.
Backend Seasons System funktioniert.
2026-06-09 13:23:00 +00:00
Daniel Nagel
e281160ec7 some print() lines deleted. 2026-06-09 07:41:03 +00:00
85e0b57833 Merge pull request 'master' (#48) from master into dev_league_seasons
Reviewed-on: #48
2026-05-31 12:03:25 +02:00
9 changed files with 159 additions and 131 deletions

View File

@ -1,3 +0,0 @@
Projekt und Repo werden nicht mehr geupdatet.
Werde das in ein Liga System für mehrere Clubs umwandeln. Das aktuelle System ist nur auf einen Club ausgelegt. Wer es als Grundlage verwenden will für was eigenes darf sich gerne bedienen.

View File

@ -182,41 +182,75 @@ def get_player_statistics(player_id):
return rows
def get_player_statistic(player_id, system_id):
def get_player_statistic(player_id, system_id, season_id=None):
"""
- Wenn season_id angegeben: Daten für diese Season.
- Wenn season_id = None: Aggregierte Gesamtstatistik.
"""
connection = db_connection()
cursor = connection.cursor(dictionary=True)
query = """
SELECT
sys.id AS gamesystem_id,
sys.name AS gamesystem_name,
sys.description,
stat.mmr,
stat.games_in_system,
stat.points,
stat.avv_points,
stat.last_played,
stat.win_rate,
stat.trend,
stat.tyrann_id,
stat.pushover_id,
stat.nemesis_id
FROM gamesystems sys
LEFT JOIN player_game_statistic stat
ON sys.id = stat.gamesystem_id AND stat.player_id = %s
WHERE sys.id = %s
"""
if season_id:
query = """
SELECT
sys.name AS gamesystem_name,
stat.mmr, stat.games_in_system, stat.points,
stat.avv_points, stat.last_played, stat.win_rate,
stat.trend, stat.tyrann_id, stat.pushover_id, stat.nemesis_id
FROM gamesystems sys
LEFT JOIN player_game_statistic stat
ON sys.id = stat.gamesystem_id
AND stat.player_id = %s
AND stat.season_id = %s
WHERE sys.id = %s
"""
cursor.execute(query, (player_id, season_id, system_id))
else:
query = """
SELECT
sys.name AS gamesystem_name,
SUM(stat.mmr) / COUNT(stat.season_id) AS mmr, -- Durchschnitt MMR über Seasons
SUM(stat.games_in_system) AS games_in_system,
SUM(stat.points) AS points,
SUM(stat.points) / SUM(stat.games_in_system) AS avv_points,
MAX(stat.last_played) AS last_played
FROM gamesystems sys
LEFT JOIN player_game_statistic stat
ON sys.id = stat.gamesystem_id
AND stat.player_id = %s
WHERE sys.id = %s
GROUP BY sys.id
"""
cursor.execute(query, (player_id, system_id))
cursor.execute(query, (player_id, system_id))
row = cursor.fetchone()
cursor.close()
connection.close()
return row
def get_player_rank(player_id, system_id):
"""Holt den Rang eines Spielers in einem System basierend auf MMR (absteigend)."""
def get_seasons_by_system(gamesystem_id):
"""Holt alle Seasons eines Spielsystems, sortiert nach Nummer absteigend."""
connection = db_connection()
cursor = connection.cursor(dictionary=True)
query = """
SELECT id, name, season_number
FROM seasons
WHERE gamesystem_id = %s
ORDER BY season_number DESC
"""
cursor.execute(query, (gamesystem_id,))
seasons = cursor.fetchall()
cursor.close()
connection.close()
return seasons
def get_player_rank(player_id, system_id, season_id):
"""Holt den Rang eines Spielers in einem System für eine spezifische Season."""
connection = db_connection()
cursor = connection.cursor(dictionary=True)
@ -224,23 +258,24 @@ def get_player_rank(player_id, system_id):
SELECT COUNT(*) + 1 AS rank
FROM player_game_statistic stat
WHERE stat.gamesystem_id = %s
AND stat.season_id = %s
AND stat.games_in_system > 0
AND stat.mmr > (
SELECT stat2.mmr
FROM player_game_statistic stat2
WHERE stat2.player_id = %s
AND stat2.gamesystem_id = %s
AND stat2.season_id = %s
)
"""
cursor.execute(query, (system_id, player_id, system_id))
# Parameter: gamesystem, season, player, gamesystem, season
cursor.execute(query, (system_id, season_id, player_id, system_id, season_id))
row = cursor.fetchone()
cursor.close()
connection.close()
if row and row['rank'] is not None:
return row['rank']
return None
return row['rank'] if row and row['rank'] is not None else 1
@ -548,7 +583,7 @@ def get_player_name(player_id):
return "Gelöschter Spieler"
def get_system_name(sys_id):
def get_system_name_by_id(sys_id):
if sys_id is None:
return "Unbekanntes System"

View File

@ -20,9 +20,8 @@ def db_connection():
password=os.getenv("DB_PASSWORD"),
database=os.getenv(f"{prefix}NAME")
)
return connection
if is_prod:
print("Verbindung mit Test DB")
except mariadb.Error as e:
raise RuntimeError(f"Fehler beim Verbinden mit {'Prod' if is_prod else 'Test'}-DB: {e}")

View File

@ -3,6 +3,7 @@ from data import database, data_api
from gui import gui_style
from wood import logger
from gui import main_gui
from utils.season_scheduler import check_new_seasons
def setup_routes():
@ui.page('/admin', dark=True)
@ -15,7 +16,13 @@ def setup_routes():
if app.storage.user.get('authenticated', False):
with ui.row().classes("w-full"):
ui.button(text= "test", on_click=lambda: data_api.create_random_dummy_match(2))
ui.button(icon="refresh", on_click= lambda: ui.navigate.to("/admin") )
ui.button(icon="refresh", on_click= lambda: ui.navigate.to("/admin") )
def handle_manual_check():
ui.notify('Starte Season-Check...', type='info')
check_new_seasons()
ui.notify('Season-Check abgeschlossen!', type='positive')
ui.button('Admin: Season-Check erzwingen', on_click=handle_manual_check)
with ui.card().classes("w-full"):
ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4')

View File

@ -7,7 +7,6 @@ from gui.templates.leaderboard import Leaderboard
def setup_routes():
@ui.page('/statistic/{system_name}', dark=True)
def gamesystem_statistic_page(system_name: str):
if not app.storage.user.get('authenticated', False):
ui.navigate.to('/')
return
@ -16,76 +15,5 @@ def setup_routes():
player_id = app.storage.user.get('db_id')
system_id = data_api.get_gamesystem_id_by_name(system_name)
print(player_id, system_id)
player_stats = data_api.get_player_statistic(player_id, system_id)
# Header
with ui.header().classes('items-center justify-between bg-zinc-900 p-4 shadow-lg'):
ui.button(icon="arrow_back", on_click=lambda: ui.navigate.to('/')).props("round")
ui.button("Spiel eintragen", on_click=lambda: ui.navigate.to(f'/add-match/{system_name}'))
with ui.column().classes('w-full items-center justify-center mt-10'):
ui.label(f'Deine Statistik in {system_name}').classes('text-3xl justify-center font-bold text-normaltext')
# --- BLOCK 1 (MMR & Rang | Rangliste) ---
with ui.element('div').classes("w-full grid grid-cols-1 lg:grid-cols-3 gap-4 mt-4"):
with ui.column().classes("w-full gap-4"):
with ui.card().classes("w-full items-center justify-center text-center"):
with ui.row().classes("w-full items-center text-center"):
ui.label("MMR Punkte: ").classes('justify-center text-2xl font-bold text-normaltext')
ui.space()
info_system.create_info_button("mmr_info")
ui.label(str(player_stats["mmr"])).classes('text-4xl font-bold text-accent')
with ui.card().classes("w-full items-center justify-center text-center"):
with ui.row().classes("w-full items-center text-center"):
ui.label("Rang: ").classes('justify-center text-2xl font-bold text-normaltext')
ui.space()
info_system.create_info_button("rang_info")
ui.label(str(data_api.get_player_rank(player_id,system_id))).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("w-full lg:col-span-2 max-h-[50vh] overflow-y-auto"):
ui.label("Liga Rangliste").classes("text-xl font-bold text-white mb-2 top-0 bg-zinc-900 z-10")
Leaderboard(system_name)
# --- BLOCK 2 (5 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-3 gap-4"):
with ui.card().classes("items-center justify-center text-center"):
ui.label("Spiele: ").classes('text-2xl font-bold')
ui.label(str(player_stats["games_in_system"])).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Ø Punkte pro Spiel: ").classes('text-2xl font-bold')
ui.label(str(player_stats["avv_points"])).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Win-Rate: ").classes('text-2xl font-bold')
ui.label("-").classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Letztes Spiel am: ").classes('text-2xl font-bold')
ui.label(str(player_stats["last_played"])).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Win-Streak: ").classes('text-2xl font-bold')
ui.label("-").classes('text-4xl font-bold text-blue-100')
# --- BLOCK 3 (3 Karten) ---
with ui.card().classes("w-full"):
with ui.element('div').classes("w-full grid grid-cols-1 md:grid-cols-3 lg:grid-cols-3 gap-4"):
with ui.card().classes("items-center justify-center text-center"):
ui.label("Dein Nemesis: ").classes('text-2xl font-bold')
ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Meisten Spiele mit: ").classes('text-2xl font-bold')
ui.label(str(player_stats["nemesis_id"])).classes('text-4xl font-bold text-blue-100')
with ui.card().classes("items-center justify-center text-center"):
ui.label("Dein 'Prügelknabe': ").classes('text-2xl font-bold')
ui.label(str(player_stats["pushover_id"])).classes('text-4xl font-bold text-blue-100')

View File

@ -72,7 +72,6 @@ def setup_routes(admin_discord_id):
new_name = name_input.value
# Nur speichern, wenn ein Name drinsteht und er anders ist als vorher
if new_name and new_name != display_name:
print("save")
data_api.change_display_name(player_id, new_name) # Deine DB Funktion
app.storage.user['display_name'] = new_name
ui.notify('Name gespeichert!', color='positive')

View File

@ -6,7 +6,7 @@ from wood import logger
from match_calculations.achievments import achievments
point_inflation = 1 # => entspricht % 1 == 100% 0.5 == 50%
point_inflation = 1 # => 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.
@ -79,23 +79,23 @@ def calculate_match (match_id):
"looser_id" : looser_id, # <-- String-Key für save_calculated_match
winner_id: { # <-- Variable als Key (z.B. 42: {...})
"base" : w_base,
"khorne" : w_khorne,
"slaanesh" : slaanesh,
"tzeentch" : tzeentch,
"total" : int(w_base * rust_factor + w_khorne + slaanesh + tzeentch)
"base" : w_base,
"khorne" : w_khorne,
"slaanesh" : slaanesh,
"tzeentch" : tzeentch,
"total" : int((w_base + w_khorne + slaanesh + tzeentch) * rust_factor),
},
looser_id: { # <-- Variable als Key (z.B. 7: {...})
"base" : -l_base,
"khorne" : l_khorne,
"slaanesh" : -slaanesh,
"tzeentch" : -tzeentch,
"total" : int(-l_base * rust_factor + l_khorne - slaanesh - tzeentch)
"base" : -l_base,
"khorne" : l_khorne,
"slaanesh" : -slaanesh,
"tzeentch" : -tzeentch,
"total" : int((-l_base + l_khorne - slaanesh - tzeentch) * rust_factor),
}
}
logger.log(f"...Winner: Base({w_base} × {rust_factor}) + Khorne({w_khorne}) + Slaanesh({slaanesh}) + Tzeentch({tzeentch}) = {total}")
logger.log(f"...Loser: Base(-{l_base} × {rust_factor}) + Khorne({l_khorne}) - Slaanesh({slaanesh}) - Tzeentch({tzeentch}) = {total}")
logger.log(f"Match{match_id}: 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"Match{match_id}: 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)
achievments.check_player_achievments(winner_id, system_id)
achievments.check_player_achievments(looser_id, system_id)

View File

@ -37,17 +37,18 @@ def calc_rust_factor(winner_id, looser_id, gamesystem_id):
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ößere der beiden Werte wird verwendet.
# Der größeren der beiden Werte wird verwendet.
days_ago = max(w_days, l_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.4 # Maximal eingerostet (40% der Punkteänderung)
return 0.1 # Maximal eingerostet (nur 10% der Punkteänderung)
else:
# Lineare Rampe von 0.9 (bei Tag 31) runter auf 0.4 (bei Tag 90)
factor = 0.9 - 0.4 * ((days_ago - 30) / 60)
# 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)

View File

@ -1,26 +1,88 @@
import schedule
import time
from datetime import date
from data import database
from data import database, data_api
from wood import logger
def check_new_seasons():
print("Season mach brrrrr")
today = date.today()
conn = database.db_connection()
cursor = conn.cursor(dictionary=True)
# 1. Prüfen, ob für heute ein Start geplant ist
cursor.execute(
"SELECT id, gamesystem_id FROM season_schedule "
"WHERE start_month = %s AND start_day = %s",
(today.month, today.day)
)
triggers = cursor.fetchall()
print(triggers)
print(f"[Season Check] {today} – {len(triggers)} Treffer gefunden")
for trigger in triggers:
gamesystem_id = trigger['gamesystem_id']
# 2. Prüfen, ob heute schon eine Season für das System erstellt wurde
cursor.execute(
"SELECT id FROM seasons WHERE gamesystem_id = %s AND start_date = %s",
(gamesystem_id, today)
)
if cursor.fetchone():
continue
# 3. Alte Season beenden (end_date = gestern)
cursor.execute(
"UPDATE seasons SET end_date = DATE_SUB(CURDATE(), INTERVAL 1 DAY) "
"WHERE gamesystem_id = %s AND end_date IS NULL",
(gamesystem_id,)
)
# 4. Neue Season anlegen
cursor.execute(
"SELECT MAX(season_number) as last_num FROM seasons WHERE gamesystem_id = %s",
(gamesystem_id,)
)
result = cursor.fetchone()
last_num = result['last_num'] if result['last_num'] else 0
new_season_number = last_num + 1
cursor.execute(
"INSERT INTO seasons (gamesystem_id, name, start_date, season_number) "
"VALUES (%s, %s, %s, %s)",
(gamesystem_id, f"{data_api.get_system_name_by_id(gamesystem_id)} - Season{new_season_number}", today, new_season_number)
)
new_season_id = cursor.lastrowid
logger.log(f"new Season for {data_api.get_system_name_by_id(gamesystem_id)}, Season ID:{new_season_id}")
# 5. AUTO-JOIN: Spieler der alten Season in die neue Season kopieren
cursor.execute(
"SELECT id FROM seasons WHERE gamesystem_id = %s AND id != %s ORDER BY id DESC LIMIT 1",
(gamesystem_id, new_season_id)
)
old_season = cursor.fetchone()
if old_season:
old_season_id = old_season['id']
# Der Auto-Join Befehl
# Wir nehmen player_id und gamesystem_id aus der alten Season
# und setzen die neue season_id ein.
cursor.execute("""
INSERT INTO player_game_statistic (player_id, gamesystem_id, season_id, wins, loss, draws, games_in_system)
SELECT player_id, gamesystem_id, %s, 0, 0, 0, 0
FROM player_game_statistic
WHERE season_id = %s
""", (new_season_id, old_season_id))
logger.log(f"Player copied from SeasonID {old_season_id} to SeasonID {new_season_id}")
conn.commit()
cursor.close()
conn.close()
def run_scheduler():
"""Läuft in einem Hintergrund-Thread."""