Compare commits

..

3 Commits

Author SHA1 Message Date
9a0330eb8c README.md hinzugefügt 2026-07-22 15:22:31 +02:00
Daniel Nagel
820394a903 Merge branch 'master' of 192.168.0.105:WestsideDiceghost/Liga-System 2026-07-15 06:16:46 +00:00
Daniel Nagel
32cf74ff5e Rost und Log Angepasst. Verwirrung weil Der Rost Faktor nicht im Log war!
Rost Faktor auf min 0.4 geändert. Start bei 31 tagen bei 0.9 ab sofort.
2026-07-15 06:16:43 +00:00
9 changed files with 131 additions and 159 deletions

3
README.md Normal file
View File

@ -0,0 +1,3 @@
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,75 +182,41 @@ def get_player_statistics(player_id):
return rows
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.
"""
def get_player_statistic(player_id, system_id):
connection = db_connection()
cursor = connection.cursor(dictionary=True)
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))
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
"""
cursor.execute(query, (player_id, system_id))
row = cursor.fetchone()
cursor.close()
connection.close()
return row
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."""
def get_player_rank(player_id, system_id):
"""Holt den Rang eines Spielers in einem System basierend auf MMR (absteigend)."""
connection = db_connection()
cursor = connection.cursor(dictionary=True)
@ -258,24 +224,23 @@ def get_player_rank(player_id, system_id, season_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
)
"""
# Parameter: gamesystem, season, player, gamesystem, season
cursor.execute(query, (system_id, season_id, player_id, system_id, season_id))
cursor.execute(query, (system_id, player_id, system_id))
row = cursor.fetchone()
cursor.close()
connection.close()
return row['rank'] if row and row['rank'] is not None else 1
if row and row['rank'] is not None:
return row['rank']
return None
@ -583,7 +548,7 @@ def get_player_name(player_id):
return "Gelöschter Spieler"
def get_system_name_by_id(sys_id):
def get_system_name(sys_id):
if sys_id is None:
return "Unbekanntes System"

View File

@ -20,8 +20,9 @@ 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,7 +3,6 @@ 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)
@ -16,13 +15,7 @@ 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") )
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)
ui.button(icon="refresh", on_click= lambda: ui.navigate.to("/admin") )
with ui.card().classes("w-full"):
ui.label("System Audit Log").classes('text-2xl font-bold text-white mb-4')

View File

@ -7,6 +7,7 @@ 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
@ -15,5 +16,76 @@ 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,6 +72,7 @@ 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 %
point_inflation = 1 # => entspricht % 1 == 100% 0.5 == 50%
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 + w_khorne + slaanesh + tzeentch) * rust_factor),
"base" : w_base,
"khorne" : w_khorne,
"slaanesh" : slaanesh,
"tzeentch" : tzeentch,
"total" : int(w_base * rust_factor + w_khorne + slaanesh + tzeentch)
},
looser_id: { # <-- Variable als Key (z.B. 7: {...})
"base" : -l_base,
"khorne" : l_khorne,
"slaanesh" : -slaanesh,
"tzeentch" : -tzeentch,
"total" : int((-l_base + l_khorne - slaanesh - tzeentch) * rust_factor),
"base" : -l_base,
"khorne" : l_khorne,
"slaanesh" : -slaanesh,
"tzeentch" : -tzeentch,
"total" : int(-l_base * rust_factor + l_khorne - slaanesh - tzeentch)
}
}
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"]}")
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}")
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,18 +37,17 @@ 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ößeren der beiden Werte wird verwendet.
# Der größere 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.1 # Maximal eingerostet (nur 10% der Punkteänderung)
return 0.4 # Maximal eingerostet (40% 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)
# Lineare Rampe von 0.9 (bei Tag 31) runter auf 0.4 (bei Tag 90)
factor = 0.9 - 0.4 * ((days_ago - 30) / 60)
return round(factor, 2)

View File

@ -1,88 +1,26 @@
import schedule
import time
from datetime import date
from data import database, data_api
from wood import logger
from data import database
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)
for trigger in triggers:
gamesystem_id = trigger['gamesystem_id']
print(f"[Season Check] {today} – {len(triggers)} Treffer gefunden")
# 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."""