Compare commits

..

No commits in common. "master" and "dev_achievments" have entirely different histories.

33 changed files with 199 additions and 374 deletions

2
.gitignore vendored
View File

@ -18,5 +18,3 @@ env/
# 5. IDE / Editor Einstellungen
.vscode/
.idea/
data_trans.py

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

@ -190,7 +190,7 @@ def get_player_statistic(player_id, system_id):
SELECT
sys.id AS gamesystem_id,
sys.name AS gamesystem_name,
sys.description,
sys.*,
stat.mmr,
stat.games_in_system,
stat.points,
@ -206,7 +206,6 @@ def get_player_statistic(player_id, system_id):
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()
@ -215,87 +214,21 @@ def get_player_statistic(player_id, system_id):
return row
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)
query = """
SELECT COUNT(*) + 1 AS rank
FROM player_game_statistic stat
WHERE stat.gamesystem_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
)
"""
cursor.execute(query, (system_id, player_id, system_id))
row = cursor.fetchone()
cursor.close()
connection.close()
if row and row['rank'] is not None:
return row['rank']
return None
def join_league(player_id, gamesystem_id):
try:
# 1. Die richtige Season für dieses System holen
season_id = get_current_season_id(gamesystem_id)
connection = db_connection()
cursor = connection.cursor()
query = """
INSERT INTO player_game_statistic
(player_id, gamesystem_id, season_id)
VALUES (%s, %s, %s)
"""
cursor.execute(query, (player_id, gamesystem_id, season_id))
connection.commit()
logger.log(f"Erfolg: Spieler {player_id} joined System {gamesystem_id} in Season {season_id}")
except Exception as e:
logger.log(f"FEHLER beim Beitreten: {e}")
raise
finally:
# Hier ist ein kleiner Check, ob die connection überhaupt existiert
if 'connection' in locals() and connection:
cursor.close()
connection.close()
def get_current_season_id(gamesystem_id):
connection = db_connection()
cursor = connection.cursor(dictionary=True)
cursor = connection.cursor()
query = """
SELECT id FROM seasons
WHERE gamesystem_id = %s
ORDER BY start_date DESC
LIMIT 1
INSERT INTO player_game_statistic (player_id, gamesystem_id)
VALUES (%s, %s)
"""
cursor.execute(query, (gamesystem_id,))
result = cursor.fetchone()
logger.log(f"{get_player_name(player_id)} joined {gamesystem_id}")
cursor.execute(query, (player_id, gamesystem_id))
connection.commit()
cursor.close()
connection.close()
if result:
return result['id']
else:
# Hier wissen wir jetzt genau, welches Spielsystem keine Saison hat
raise RuntimeError(f"Keine Saison für {gamesystem_id} gefunden!")
# -----------------------------------------------------
# Matches: Lesen
@ -474,21 +407,19 @@ def get_gamesystems_data():
return rows
def get_leaderboard(system_name):
"""Holt alle Spieler eines Systems sortiert nach MMR für die Rangliste."""
connection = db_connection()
cursor = connection.cursor(dictionary=True)
query = query = """
SELECT p.id, p.display_name, p.discord_name, p.discord_id, p.discord_avatar_url,
stat.mmr, stat.games_in_system, stat.wins, stat.loss, stat.avv_points
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 = %s AND stat.games_in_system > 0
ORDER BY stat.mmr DESC
"""
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 = %s AND stat.games_in_system > 0
ORDER BY stat.mmr DESC
"""
cursor.execute(query, (system_name,))
rows = cursor.fetchall()

View File

@ -1,3 +1,4 @@
import mariadb
import os
from dotenv import load_dotenv
@ -5,24 +6,25 @@ from dotenv import load_dotenv
load_dotenv()
def db_connection():
# Wir wählen die Konfiguration basierend auf der URL
is_prod = os.getenv("APP_URL") == "https://liga.au-fab.eu"
# Prefix für die Umgebungsvariablen wählen
prefix = "DB_" if is_prod else "DB_TEST_"
try:
connection = mariadb.connect(
host=os.getenv("DB_HOST", "localhost"),
port=int(os.getenv("DB_PORT", 3306)),
user=os.getenv(f"{prefix}USER"),
user=os.getenv("DB_USER"),
password=os.getenv("DB_PASSWORD"),
database=os.getenv(f"{prefix}NAME")
database=os.getenv("DB_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}")
raise RuntimeError(f"Fehler beim Verbinden mit DB: {e}")

View File

@ -2,11 +2,10 @@ from nicegui import ui, app
from gui import gui_style
from data import data_api
from gui.info_text import info_system
from gui.templates.leaderboard import Leaderboard
def setup_routes():
@ui.page('/statistic/{system_name}', dark=True)
def gamesystem_statistic_page(system_name: str):
@ui.page('/statistic/{systemname}', dark=True)
def gamesystem_statistic_page(systemname: str):
if not app.storage.user.get('authenticated', False):
ui.navigate.to('/')
@ -15,77 +14,112 @@ def setup_routes():
gui_style.apply_design()
player_id = app.storage.user.get('db_id')
system_id = data_api.get_gamesystem_id_by_name(system_name)
print(player_id, system_id)
all_stats = data_api.get_player_statistics(player_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}'))
# Passendes System anhand des Namens (case-insensitive) herausfiltern
system_stat = next(
(s for s in all_stats if s["gamesystem_name"].lower() == systemname.lower()),
None
)
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')
if system_stat:
mmr = system_stat["mmr"] or 0
games = system_stat["games_in_system"] or 0
points = system_stat["points"] or 0
avv_points = system_stat.get("avv_points") or "-"
last_played_raw = system_stat.get("last_played")
last_played = str(last_played_raw)[:10] if last_played_raw else "-"
# --- 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.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/{systemname}'))
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.column().classes('w-full items-center justify-center mt-10'):
ui.label(f'Deine Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext')
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 1 (MMR & Rang | Rangliste) ---
leaderboard_data = data_api.get_leaderboard(systemname)
table_rows = []
my_rank = "-"
# --- 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"):
for index, player in enumerate(leaderboard_data):
current_rank = index + 1
if player['id'] == player_id:
my_rank = current_rank
table_rows.append({
'rank': current_rank,
'trend': '',
'name': f"{player['display_name']} 'aka' {player['discord_name']}",
'mmr': player['mmr']
})
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')
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'},
]
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.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(mmr)).classes('text-4xl font-bold text-accent')
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("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(my_rank)).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("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')
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 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"):
# --- 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("Spiele: ").classes('text-2xl font-bold')
ui.label(str(games)).classes('text-4xl font-bold text-blue-100')
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("Ø Punkte pro Spiel: ").classes('text-2xl font-bold')
ui.label(str(avv_points)).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("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("Dein 'Prügelknabe': ").classes('text-2xl font-bold')
ui.label(str(player_stats["pushover_id"])).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(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("-").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("-").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("-").classes('text-4xl font-bold text-blue-100')
else:
with ui.column().classes('w-full items-center justify-center mt-10'):
ui.label(f'Keine Statistik für "{systemname}" gefunden.').classes('text-red-500 text-xl mt-4')

View File

@ -1,24 +0,0 @@
from nicegui import ui, app
from gui import gui_style
from data import data_api
from gui.info_text import info_system
from gui.templates.leaderboard import Leaderboard
def setup_routes():
@ui.page('/statistic/visitor/{systemname}', dark=True)
def gamesystem_statistic_visitor_page(systemname: str):
if not app.storage.user.get('authenticated', False):
ui.navigate.to('/')
return
gui_style.apply_design()
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")
with ui.column().classes('w-full items-center justify-center mt-10'):
ui.label(f'Die aktuelle Statistik in {systemname}').classes('text-3xl justify-center font-bold text-normaltext')
Leaderboard(systemname)

View File

@ -21,9 +21,7 @@ def setup_routes(admin_discord_id):
ui.navigate.reload()
return
# -----------------------------------------
# ---------------------------
# --- NAVIGATIONSLEISTE (HEADER)
# ---------------------------
@ -33,19 +31,19 @@ def setup_routes(admin_discord_id):
# --- LINKE SEITE ---
# Vereinslogo und den Titel in einer eigenen Reihe (Reihe 1)
with ui.row().classes('items-center'):
ui.image("gui/pictures/wsdg.png").classes('w-12 h-12 md:w-25 md:h-25 rounded-full')
ui.label('Westside Diceghost Liga').classes('text-lg md:text-3xl font-bold text-normaltext')
ui.image("gui/pictures/wsdg.png").classes('w-15 h-15 rounded-full')
ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext')
# --- MITTE ---
if app.storage.user.get('authenticated', False):
discord_id = app.storage.user.get("discord_id")
if discord_id == admin_discord_id:
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).classes('w-2 h-2')
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).props("round")
# --- RECHTE SEITE ---
if app.storage.user.get('authenticated', False):
with ui.row().classes('items-center gap-4'):
ui.image(app.storage.user.get('discord_avatar_url')).classes('w-10 h-10 md:w-25 md:h-25 rounded-full border-1 border-red-900')
ui.image(app.storage.user.get('discord_avatar_url')).classes('w-15 h-15 rounded-full')
discord_name = app.storage.user.get('discord_name')
display_name = app.storage.user.get('display_name')
player_id = app.storage.user.get('db_id')
@ -59,10 +57,10 @@ def setup_routes(admin_discord_id):
# --- ANSICHT 1: Der normale Text mit Edit-Button ---
with ui.column().classes('items-center gap-0') as display_row:
with ui.column():
ui.label(display_name).classes('md:text-3xl font-bold text-normaltext')
ui.label(display_name).classes('text-xl font-bold text-normaltext')
with ui.row().classes("items-center justify-between"):
ui.label("'aka'").classes('md:text-lg text-xs text-italic text-infotext')
ui.label(discord_name).classes('text-sm md:text-xl text-bold text-infotext')
ui.label("'aka'").classes('text-sm text-italic text-infotext')
ui.label(discord_name).classes('text-m text-bold text-infotext')
edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense')
# --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) ---
@ -203,25 +201,19 @@ def setup_routes(admin_discord_id):
sys_description = sys['description']
sys_card = ui.card().classes("h-60 w-full items-center justify-center transition-colors")
with sys_card:
# Wir erstellen einen Container für den "klickbaren" Teil
clickable_content = ui.column().classes("w-full h-full items-center")
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
if sys_logo:
ui.image(f"/pictures/{sys_logo}").classes("w-60")
with clickable_content:
ui.label(text=sys_name).classes('text-xl font-bold text-center text-normaltext')
if sys_logo:
ui.image(f"/pictures/{sys_logo}").classes("w-60")
if sys_description:
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
if sys_description:
ui.label(text=sys['description']).classes('text-xs text-gray-400 text-center mt-2 text-infotext')
# Prüfen: Ist diese sys_id in den Stats des Spielers? UND hat er ein MMR?
if sys_id not in my_stats or my_stats[sys_id]['mmr'] is None:
ui.label(text="Du bist noch nicht in dieser Liga.").classes("text-red-500 font-bold")
clickable_content.classes("cursor-pointer hover:bg-zinc-800")
clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/visitor/{name}'))
join_row = ui.row().classes('items-center gap-2')
confirm_row = ui.row().classes('items-center gap-2')
confirm_row.visible = False
@ -233,17 +225,18 @@ def setup_routes(admin_discord_id):
ui.button("Liga Beitreten", color="green", on_click=lambda e, p=player_id, s=sys_id: click_join_league(p, s))
ui.button(icon='cancel', color='red', on_click=lambda e, r1=join_row, r2=confirm_row: toggle_visibility(r1, r2)).props('round dense')
else:
# Spieler IST in der Liga
stat = my_stats[sys_id]
# Spieler IST in der Liga!
stat = my_stats[sys_id] # Wir holen uns seine Stats aus dem Wörterbuch
# JETZT machen wir nur den Container klickbar, nicht die ganze Karte!
clickable_content.classes("cursor-pointer hover:bg-zinc-800")
clickable_content.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
sys_card.classes("cursor-pointer hover:bg-zinc-800")
sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
with ui.row().classes('items-center gap-4'):
ui.label(text=f"MMR: {stat['mmr']}").classes("text-lg font-bold text-accenttext")
ui.label(text=f"Spiele: {stat['games_in_system']}").classes("text-lg font-bold text-accenttext")
# ---------------------------
# Match Historie
# ---------------------------

View File

@ -33,12 +33,19 @@ def setup_routes():
raw_players = data_api.get_all_players_from_system(system_name)
my_id = app.storage.user.get('db_id')
with ui.column().classes("items-center justify-between"):
p1_points = ui.number(placeholder="Punkte eingeben",
min=system_data["min_score"],
max=system_data["max_score"],
precision=0
).classes("w-35").props("clearable")
def add_point():
p1_points.value += 1
def sub_point():
p1_points.value -= 1
with ui.row().classes("w-full items-center justify-between"):
p1_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35")
with ui.column().classes("items-center justify-between"):
# Punkte Up- Down- Buttons. und Textanzeige
ui.button(icon="expand_less", on_click=add_point)
ui.label().bind_text_from(p1_points, 'value').classes("text-lg text-normaltext")
ui.button(icon="expand_more", on_click=sub_point)
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
dropdown_options = {}
@ -49,39 +56,36 @@ def setup_routes():
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full')
with ui.column().classes("items-center justify-between"):
p2_points = ui.number(placeholder="Punkte eingeben",
min=system_data["min_score"],
max=system_data["max_score"],
precision=0
).classes("w-35").props("clearable")
def add_point():
p2_points.value += 1
def sub_point():
p2_points.value -= 1
with ui.row().classes("w-full items-center justify-between"):
p2_points = ui.slider(min=min_score, max=max_score, value=10).classes("w-35")
with ui.column().classes("items-center justify-between"):
# Punkte Up- Down- Buttons. und Textanzeige
ui.button(icon="expand_less", on_click=add_point)
ui.label().bind_text_from(p2_points, 'value').classes("text-lg text-normaltext")
ui.button(icon="expand_more", on_click=sub_point)
# Das Match in die Datenbank eintragen lassen.
# Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern.
def input_match_to_database():
p2_id = opponent_select.value
score_p1 = p1_points.value
score_p2 = p2_points.value
# Check der Eingegebenen Daten.
# Fehler wenn kein Spieler oder keine Punkte eingegeben werden. Das Punkte Eingabefeld fängt mit min_score und max_score ab,
# ob für das Spielsystem legale Punkte eingetragen wurden.
if p2_id is None:
ui.notify("Bitte wähle zuerst einen Gegner aus!", color="red", position="top")
return
if score_p1 is None:
ui.notify("Ungültige Punkteeingabe Spieler 1!", color="red", position="top")
return
score_p1 = p1_points.value
score_p2 = p2_points.value
if score_p2 is None:
ui.notify("Ungültige Punkteeingabe Spieler 2!", color="red", position="top")
return
match_id = data_api.add_new_match(system_name, my_id, p2_id, score_p1, score_p2)
# 4. Erfolgsmeldung und Berechnung
ui.notify("Match erfolgreich eingetragen!", color="green")
ui.navigate.to(f'/statistic/{system_name}')
# Buttons ganz unten in einer Reihe

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 46 KiB

View File

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 36 KiB

View File

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 35 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 28 KiB

View File

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

Before

Width:  |  Height:  |  Size: 50 KiB

After

Width:  |  Height:  |  Size: 50 KiB

View File

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

View File

@ -1,24 +0,0 @@
from nicegui import ui
from data import data_api
from gui.templates.player_card import PlayerCard
class Leaderboard:
def __init__(self, system_name: str):
# Daten holen
leaderboard_data = data_api.get_leaderboard(system_name)
# Container
with ui.column().classes('w-full gap-2') as self.container:
for index, player in enumerate(leaderboard_data):
PlayerCard(
rank=index + 1,
display_name=player['display_name'],
discord_name=player['discord_name'],
mmr=player['mmr'],
discord_avatar_url=player.get('discord_avatar_url'),
discord_id=player.get('discord_id'),
games_in_system=player.get('games_in_system', 0),
wins=player.get('wins', 0),
loss=player.get('loss', 0),
avv_points=player.get('avv_points', 0)
)

View File

@ -1,41 +0,0 @@
from nicegui import ui
class PlayerCard:
def __init__(self, rank: int, display_name: str, discord_name: str, mmr: int,
discord_avatar_url: str = None, discord_id: str = None,
games_in_system: int = 0, wins: int = 0, loss: int = 0, avv_points: int = 0):
# Rang links, Card rechts
with ui.row().classes('w-full items-center gap-1 flex-nowrap'):
ui.label(f'#{rank}').classes('text-2xl font-bold w-10 text-center shrink-0')
# Player Card
with ui.card().classes("flex-1 no-shadow bg-zinc-900 min-w-0 overflow-hidden"):
# Discord Block - klickbar
if discord_id:
with ui.link(target=f'https://discord.com/users/{discord_id}', new_tab=True).classes('no-underline w-full'):
self._discord_content(discord_avatar_url, display_name, discord_name)
else:
self._discord_content(discord_avatar_url, display_name, discord_name)
# Stats und Achievments
with ui.row().classes('w-full items-center flex-nowrap'):
with ui.row().classes('flex-2 items-center gap-2 md:gap-3 md:justify-center shrink min-w-0'):
ui.label(f'{mmr} MMR').classes('font-bold md:text-lg')
with ui.row().classes('flex-1 justify-end items-center gap-1 md:gap-3 shrink-0'):
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
ui.image('gui/pictures/achievments/AchievementIcon_4_1Sieg.png').classes('w-5 md:w-15')
def _discord_content(self, avatar_url, display_name, discord_name):
short_name = display_name[:15] + '...' if len(display_name) > 15 else display_name
short_discord = discord_name[:15] + '...' if len(discord_name) > 15 else discord_name
with ui.card().classes("w-full md:w-90 bg-indigo-950 no-shadow"):
with ui.row().classes('items-center gap-3 min-w-0'):
if avatar_url:
ui.image(avatar_url).classes('w-10 h-10 md:w-20 md:h-20 rounded-full border-red-900 border-2 shrink-0')
with ui.column().classes('gap-0 min-w-0'):
ui.label(short_name).classes('text-sm md:text-2xl font-bold whitespace-nowrap truncate')
ui.label(short_discord).classes('text-xs md:text-sm opacity-60 whitespace-nowrap truncate')

18
main.py
View File

@ -1,13 +1,11 @@
import os
import schedule
from dotenv import load_dotenv
from nicegui import ui, app
from data import database
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui, league_statistic_visitor_gui
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui
from wood import logger
from gui.info_text import info_system
from utils import season_scheduler
# 1. Lade die geheimen Variablen aus der .env Datei in den Speicher
load_dotenv()
@ -35,18 +33,6 @@ match_gui.setup_routes()
admin_gui.setup_routes()
match_history_gui.setup_routes()
imprint_gui.setup_routes()
league_statistic_visitor_gui.setup_routes()
# Timer der schedule regelmäßig prüft
@app.on_startup
def setup_scheduler():
import threading
from utils.season_scheduler import run_scheduler
thread = threading.Thread(target=run_scheduler, daemon=True)
thread.start()
# 4. Starten der App Runtime auf dem Server.
# 4. Wir starten die NiceGUI App
ui.run(title="Westside Diceghost Liga", port=9000, storage_secret="EIN_super-geheimes_Pa$$wort#!", favicon="gui/pictures/wsdg.png")

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

@ -31,7 +31,7 @@ sudo apt install -y \
python3-venv \
python3-full \
libmariadb3 \
libmariadb-dev
libmariadb-dev
echo "✅ Updates installiert. Überprüfe Softwarestand von Git ..."
@ -60,8 +60,7 @@ pip install \
nicegui \
requests \
python-dotenv \
pymysql \
schedule
mariadb
echo "✅ Pakete installiert. Starte Liga Service ..."

View File

@ -1,31 +0,0 @@
import schedule
import time
from datetime import date
from data import database
def check_new_seasons():
today = date.today()
conn = database.db_connection()
cursor = conn.cursor(dictionary=True)
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(f"[Season Check] {today} {len(triggers)} Treffer gefunden")
cursor.close()
conn.close()
def run_scheduler():
"""Läuft in einem Hintergrund-Thread."""
schedule.every().day.at("00:01").do(check_new_seasons)
while True:
schedule.run_pending()
time.sleep(60)