Compare commits

..

35 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
4d43d71e97 Merge pull request 'database_update' (#47) from database_update into master
Reviewed-on: #47
2026-05-31 12:02:56 +02:00
128f27587b Alembic und SQLAlchemy wieder raus gelöscht. 2026-05-31 10:48:25 +02:00
31594a0805 Merge pull request 'master' (#46) from master into database_update
Reviewed-on: #46
2026-05-29 11:26:54 +02:00
Daniel Nagel
b1cc400611 Refractor 2026-05-29 09:26:21 +00:00
Daniel Nagel
0b42c081fe Error handling für join_league und aktive season abfrage. 2026-05-29 09:23:28 +00:00
Daniel Nagel
049089cb5f WIP Fehler 2026-05-29 08:23:07 +00:00
Daniel Nagel
3d67c3b7fa Merge branch 'master' of 192.168.0.105:WestsideDiceghost/Liga-System 2026-05-29 08:20:14 +00:00
Daniel Nagel
b5b041c889 error handling join_league() 2026-05-29 08:20:09 +00:00
Daniel Nagel
77bd91ab87 WIP. Halb im Alembic Daten Müll aufräumen. 2026-05-28 14:28:54 +00:00
e20b4143d1 Merge pull request 'database_update' (#45) from database_update into master
Reviewed-on: #45
2026-05-28 14:48:37 +02:00
Daniel Nagel
5bd43a896b alembic ini anpassen müssen an die neue DB 2026-05-28 12:47:56 +00:00
Daniel Nagel
ea4a89fc76 Umbau auf 1 DB mit Test UND Prod 2026-05-28 11:16:45 +00:00
3dd74866ce Merge pull request 'master' (#44) from master into database_update
Reviewed-on: #44
2026-05-28 11:24:43 +02:00
640bc8962b Merge pull request 'dev_league_seasons' (#42) from dev_league_seasons into master
Reviewed-on: #42
2026-05-28 11:20:54 +02:00
Daniel Nagel
9bd665c54d umbau auf alembic. Damit ich die DB Struktur besser vom Test in Prod krieg. 2026-05-28 09:19:33 +00:00
c817d9cb99 Wollte eigentlich Seasons bauen. integriere jetzt SQLAlchemy. Keine Ahnung wiso ... 2026-05-27 22:33:33 +02:00
646f828105 Merge pull request 'master' (#41) from master into dev_league_seasons
Reviewed-on: #41
2026-05-27 15:40:41 +02:00
Daniel Nagel
19f45aaa59 gui bug hotfix. visitor page and system card 2026-05-26 12:38:02 +00:00
Daniel Nagel
9a327ed138 . 2026-05-26 12:05:42 +00:00
31c4a6fc2b Merge pull request 'Merge pull request 'master' (#36) from master into dev_league_seasons' (#37) from dev_league_seasons into master
Reviewed-on: #37
2026-05-22 11:34:18 +02:00
a60fd27aa2 Merge pull request 'master' (#36) from master into dev_league_seasons
Reviewed-on: #36
2026-05-22 11:30:18 +02:00
2858289bd4 Merge pull request 'kleines Update .' (#35) from dev_gui into master
Reviewed-on: #35
2026-05-22 11:28:16 +02:00
1caefd1fb4 Merge branch 'master' into dev_gui 2026-05-22 11:28:11 +02:00
Daniel Nagel
63f95573e0 kleines Update . 2026-05-22 09:26:49 +00:00
2b6ff1d4f6 Merge pull request 'dev_gui' (#34) from dev_gui into master
Reviewed-on: #34
2026-05-21 16:03:57 +02:00
83c54501ca Merge branch 'master' into dev_gui 2026-05-21 16:03:32 +02:00
Daniel Nagel
b398049c2e gui und mobile user expirience verbessert. 2026-05-21 14:00:51 +00:00
Daniel Nagel
c97e1d2eae Player Card für Custom Leaderboad class 2026-05-20 14:16:20 +00:00
6a2868c207 Merge pull request 'dev_gui' (#33) from dev_gui into master
Reviewed-on: #33
2026-05-20 07:54:17 +02:00
Daniel Nagel
29928b8f9d Match GUI Eingabe verbessert. 2026-05-20 05:50:07 +00:00
Daniel Nagel
b106603927 visitor Page und Match Eingabe verfeinert. Still WIP ! 2026-05-19 13:51:24 +00:00
Daniel Nagel
6866368199 Vorsortierung der Achievment Icons 2026-05-18 11:35:41 +00:00
33 changed files with 376 additions and 201 deletions

2
.gitignore vendored
View File

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

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

@ -190,7 +190,7 @@ def get_player_statistic(player_id, system_id):
SELECT
sys.id AS gamesystem_id,
sys.name AS gamesystem_name,
sys.*,
sys.description,
stat.mmr,
stat.games_in_system,
stat.points,
@ -206,6 +206,7 @@ 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()
@ -214,21 +215,87 @@ def get_player_statistic(player_id, system_id):
return row
def join_league(player_id, gamesystem_id):
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()
cursor = connection.cursor(dictionary=True)
query = """
INSERT INTO player_game_statistic (player_id, gamesystem_id)
VALUES (%s, %s)
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
)
"""
logger.log(f"{get_player_name(player_id)} joined {gamesystem_id}")
cursor.execute(query, (player_id, gamesystem_id))
connection.commit()
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)
query = """
SELECT id FROM seasons
WHERE gamesystem_id = %s
ORDER BY start_date DESC
LIMIT 1
"""
cursor.execute(query, (gamesystem_id,))
result = cursor.fetchone()
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
@ -407,19 +474,21 @@ 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 = """
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
"""
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
"""
cursor.execute(query, (system_name,))
rows = cursor.fetchall()

View File

@ -1,4 +1,3 @@
import mariadb
import os
from dotenv import load_dotenv
@ -6,25 +5,24 @@ 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("DB_USER"),
user=os.getenv(f"{prefix}USER"),
password=os.getenv("DB_PASSWORD"),
database=os.getenv("DB_NAME")
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 DB: {e}")
raise RuntimeError(f"Fehler beim Verbinden mit {'Prod' if is_prod else 'Test'}-DB: {e}")

View File

@ -2,10 +2,11 @@ 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/{systemname}', dark=True)
def gamesystem_statistic_page(systemname: str):
@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('/')
@ -14,112 +15,77 @@ def setup_routes():
gui_style.apply_design()
player_id = app.storage.user.get('db_id')
all_stats = data_api.get_player_statistics(player_id)
system_id = data_api.get_gamesystem_id_by_name(system_name)
print(player_id, system_id)
# 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
)
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}'))
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 "-"
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')
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}'))
# --- 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.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 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')
# --- BLOCK 1 (MMR & Rang | Rangliste) ---
leaderboard_data = data_api.get_leaderboard(systemname)
table_rows = []
my_rank = "-"
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)
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']
})
# --- 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"):
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("Spiele: ").classes('text-2xl font-bold')
ui.label(str(player_stats["games_in_system"])).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("Ø 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("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("Win-Rate: ").classes('text-2xl font-bold')
ui.label("-").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("Letztes Spiel am: ").classes('text-2xl font-bold')
ui.label(str(player_stats["last_played"])).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"):
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')
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')
# --- 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("Ø 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("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("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("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("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')
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

@ -0,0 +1,24 @@
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,7 +21,9 @@ def setup_routes(admin_discord_id):
ui.navigate.reload()
return
# -----------------------------------------
# ---------------------------
# --- NAVIGATIONSLEISTE (HEADER)
# ---------------------------
@ -31,19 +33,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-15 h-15 rounded-full')
ui.label('Diceghost Liga').classes('text-2xl font-bold text-normaltext')
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')
# --- 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')).props("round")
ui.button(icon="hardware", on_click=lambda: ui.navigate.to('/admin')).classes('w-2 h-2')
# --- 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-15 h-15 rounded-full')
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')
discord_name = app.storage.user.get('discord_name')
display_name = app.storage.user.get('display_name')
player_id = app.storage.user.get('db_id')
@ -57,10 +59,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('text-xl font-bold text-normaltext')
ui.label(display_name).classes('md:text-3xl font-bold text-normaltext')
with ui.row().classes("items-center justify-between"):
ui.label("'aka'").classes('text-sm text-italic text-infotext')
ui.label(discord_name).classes('text-m text-bold text-infotext')
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')
edit_button = ui.button(icon='edit', color='accent', on_click=toggle_edit_mode).props('round dense')
# --- ANSICHT 2: Das Eingabefeld (startet unsichtbar!) ---
@ -201,19 +203,25 @@ 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:
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')
with sys_card:
# Wir erstellen einen Container für den "klickbaren" Teil
clickable_content = ui.column().classes("w-full h-full items-center")
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')
# 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
@ -225,18 +233,17 @@ 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] # Wir holen uns seine Stats aus dem Wörterbuch
# Spieler IST in der Liga
stat = my_stats[sys_id]
sys_card.classes("cursor-pointer hover:bg-zinc-800")
sys_card.on('click', lambda e, name=sys_name: ui.navigate.to(f'/statistic/{name}'))
# 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}'))
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,19 +33,12 @@ def setup_routes():
raw_players = data_api.get_all_players_from_system(system_name)
my_id = app.storage.user.get('db_id')
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.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")
with ui.card().classes('w-full max-w-md mx-auto items-center mt-10 p-6'):
dropdown_options = {}
@ -56,36 +49,39 @@ def setup_routes():
opponent_select = ui.select(options=dropdown_options, label='Gegner auswählen').classes('w-full')
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)
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")
# Das Match in die Datenbank eintragen lassen und die MMR Berechnung triggern.
# Das Match in die Datenbank eintragen lassen.
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
score_p1 = p1_points.value
score_p2 = p2_points.value
if score_p1 is None:
ui.notify("Ungültige Punkteeingabe Spieler 1!", color="red", position="top")
return
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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 76 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

View File

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 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.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 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

View File

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

@ -0,0 +1,41 @@
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,11 +1,13 @@
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
from gui import main_gui, match_gui, discord_login, league_statistic_gui, admin_gui, match_history_gui, imprint_gui, league_statistic_visitor_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()
@ -33,6 +35,18 @@ match_gui.setup_routes()
admin_gui.setup_routes()
match_history_gui.setup_routes()
imprint_gui.setup_routes()
league_statistic_visitor_gui.setup_routes()
# 4. Wir starten die NiceGUI App
# 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.
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 %
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

@ -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,7 +60,8 @@ pip install \
nicegui \
requests \
python-dotenv \
mariadb
pymysql \
schedule
echo "✅ Pakete installiert. Starte Liga Service ..."

31
utils/season_scheduler.py Normal file
View File

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