skat-tracker / app.py
slevis's picture
Make app a progressive web app
c026d41 verified
import gradio as gr
import pandas as pd
from huggingface_hub import HfApi, HfFolder
import os
import uuid
from pathlib import Path
# --- Constants for the Calculation ---
GRUNDWERTE = {"Karo": 9, "Herz": 10, "Pik": 11, "Kreuz": 12, "Grand": 24}
NULL_SPIELE = {"Null": 23, "Null Hand": 35, "Null Ouvert": 46, "Null Hand Ouvert": 59}
SPIELER = ["Kirsten", "Robert", "Samuel"]
# Added a new game type 'Ramsch' to the list of choices
SPIELARTEN = ['Karo', 'Herz', 'Pik', 'Kreuz', 'Grand', 'Null', 'Null Hand', 'Null Ouvert', 'Null Hand Ouvert', 'Ramsch']
HISTORY_DF_HEADERS = ["Kirsten", "Robert", "Samuel", "Spielwert", "Spielart", "Alleinspieler", "Details", "Verloren"]
# --- Hugging Face Configuration ---
LOCAL_TOURNAMENT_DIR = Path("saved_tournaments/")
LOCAL_TOURNAMENT_DIR.mkdir(exist_ok=True)
token = os.getenv('dataset_access_token')
# --- Core Functions ---
def calculate_spielwert(spielart, spitzen_str, hand, schneider, schwarz, schneider_angesagt, schwarz_angesagt, offen):
"""Calculates the pure game value (Reizwert) based on the game rules."""
if spielart in NULL_SPIELE:
return NULL_SPIELE[spielart]
if spielart not in GRUNDWERTE or not spitzen_str:
return 0
spitzen_anzahl = int(spitzen_str.split(" ")[1])
gewinnstufe = 1
if hand: gewinnstufe += 1
if schneider: gewinnstufe += 1
if schneider_angesagt: gewinnstufe += 1
if schwarz: gewinnstufe += 1
if schwarz_angesagt: gewinnstufe += 1
if offen: gewinnstufe += 1
grundwert = GRUNDWERTE[spielart]
return (spitzen_anzahl + gewinnstufe) * grundwert
def spiel_hinzufuegen(history_df, spieler, spielart, spitzen, hand, schneider, schwarz, schneider_angesagt, schwarz_angesagt, offen, verloren, ramsch_verlierer=None, ramsch_punkte=None, ramsch_geschoben=None):
"""Adds a new game to the history DataFrame and dynamically calculates the total scores."""
# --- New logic for 'Ramsch' game type ---
if spielart == 'Ramsch':
if not ramsch_verlierer or not isinstance(ramsch_punkte, (int, float)) or ramsch_punkte <= 0:
# Return current state if Ramsch inputs are invalid or no points were made.
total_scores = history_df[SPIELER].sum() if not history_df.empty else {s: 0 for s in SPIELER}
totals_df = pd.DataFrame([total_scores])
return totals_df, history_df
# Per user request: a negative score for the loser based on their points and how often the skat was passed ("geschoben").
# A standard rule is that each "schieben" doubles the game's value.
# Final score = loser_points * (2 ** number_of_passes). This is robust and handles the 0-pass case correctly.
spielwert = ramsch_punkte * (2 ** ramsch_geschoben)
neue_punkte = {name: 0 for name in SPIELER}
# Only the loser's score is affected, as specified.
neue_punkte[ramsch_verlierer] = -spielwert
details = f"Augen: {int(ramsch_punkte)}, Geschoben: {ramsch_geschoben} mal"
neues_spiel_eintrag = {
"Kirsten": neue_punkte["Kirsten"],
"Robert": neue_punkte["Robert"],
"Samuel": neue_punkte["Samuel"],
"Spielwert": spielwert,
"Spielart": "Ramsch",
"Alleinspieler": "N/A", # No soloist in Ramsch
"Details": details,
"Verloren": "Ja" # A Ramsch game always has a loser
}
new_row_df = pd.DataFrame([neues_spiel_eintrag])
updated_history_df = pd.concat([history_df, new_row_df], ignore_index=True)
total_scores = updated_history_df[SPIELER].sum().astype(int)
totals_df = pd.DataFrame([total_scores])
return totals_df, updated_history_df
# --- Original logic for standard games ---
spielwert = calculate_spielwert(spielart, spitzen, hand, schneider, schwarz, schneider_angesagt, schwarz_angesagt, offen)
neue_punkte = {name: 0 for name in SPIELER}
if spieler:
if not verloren:
neue_punkte[spieler] = 50 + spielwert
for p in SPIELER:
if p != spieler:
neue_punkte[p] = -40
else: # Game lost
neue_punkte[spieler] = -50 - (2 * spielwert)
for p in SPIELER:
if p != spieler:
neue_punkte[p] = 40
details = f"Spitzen: {spitzen or 'N/A'}"
if hand: details += ", Hand"
if schneider_angesagt: details += ", Schneider angesagt"
if schwarz_angesagt: details += ", Schwarz angesagt"
if offen: details += ", Offen"
neues_spiel_eintrag = {
"Kirsten": neue_punkte["Kirsten"],
"Robert": neue_punkte["Robert"],
"Samuel": neue_punkte["Samuel"],
"Spielwert": spielwert,
"Spielart": spielart,
"Alleinspieler": spieler or "N/A",
"Details": details,
"Verloren": "Ja" if verloren else "Nein"
}
new_row_df = pd.DataFrame([neues_spiel_eintrag])
updated_history_df = pd.concat([history_df, new_row_df], ignore_index=True)
total_scores = updated_history_df[SPIELER].sum().astype(int)
totals_df = pd.DataFrame([total_scores])
return totals_df, updated_history_df
def reset_turnier():
"""Resets the entire score and history."""
empty_totals = pd.DataFrame([{"Kirsten": 0, "Robert": 0, "Samuel": 0}])
empty_history = pd.DataFrame(columns=HISTORY_DF_HEADERS)
return empty_totals, empty_history
def save_turnier_to_hf(history_df):
"""Saves the current tournament history to a local JSON file and immediately uploads it to the Hugging Face Hub."""
if HfFolder.get_token() is None:
return "Hugging Face Token nicht gefunden. Bitte via `huggingface-cli login` anmelden."
if history_df.empty:
return "Keine Spieldaten zum Speichern vorhanden."
tournament_filename = f"skat_tournament_{uuid.uuid4()}.jsonl"
local_file_path = LOCAL_TOURNAMENT_DIR / tournament_filename
history_df.to_json(local_file_path, orient="records", lines=True, force_ascii=False)
api = HfApi()
repo_id = "slevis/skat-scores"
try:
api.upload_file(
path_or_fileobj=local_file_path,
path_in_repo=f"data/{tournament_filename}",
repo_id=repo_id,
repo_type="dataset",
commit_message=f"Add new skat tournament data: {tournament_filename}"
)
return f"Turnier erfolgreich nach '{repo_id}' auf Hugging Face hochgeladen."
except Exception as e:
return f"Fehler beim Upload: {e}"
# --- UI Definition with Gradio ---
with gr.Blocks() as demo:
gr.Markdown("# Skat-Punkte-Tracker")
gr.Markdown("### Gesamtpunktestand")
initial_totals_df, initial_history_df = reset_turnier()
totals_output = gr.Dataframe(
value=initial_totals_df,
interactive=False,
show_row_numbers=False
)
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### Neues Spiel eingeben")
spielart_input = gr.Dropdown(
choices=SPIELARTEN,
label="Spielart"
)
# --- Container for Standard Game Inputs (visible by default) ---
with gr.Group(visible=True) as standard_spiel_group:
gr.Markdown("#### Standard-Spiel")
spieler_input = gr.Dropdown(SPIELER, label="Alleinspieler")
spitzen_input = gr.Dropdown(
choices=[f"{m} {i}" for m in ["Mit", "Ohne"] for i in range(1, 5)],
label="Spitzen"
)
with gr.Row():
schneider_input = gr.Checkbox(label="Schneider")
schwarz_input = gr.Checkbox(label="Schwarz")
verloren_input = gr.Checkbox(label="Verloren?")
with gr.Accordion("Optionen für Handspiele", open=False):
hand_input = gr.Checkbox(label="Handspiel")
schneider_angesagt_input = gr.Checkbox(label="Schneider angesagt")
schwarz_angesagt_input = gr.Checkbox(label="Schwarz angesagt")
offen_input = gr.Checkbox(label="Offen / Ouvert")
# --- Container for Ramsch Game Inputs (hidden by default) ---
with gr.Group(visible=False) as ramsch_spiel_group:
gr.Markdown("#### Ramsch-Spiel")
ramsch_verlierer_input = gr.Dropdown(SPIELER, label="Verlierer")
ramsch_punkte_input = gr.Number(label="Augen des Verlierers", value=0, precision=0)
ramsch_geschoben_input = gr.Dropdown(
choices=[("Keinmal (Standard)", 0), ("1 mal", 1), ("2 mal", 2), ("3 mal", 3)],
value=0,
label="Wie oft wurde der Skat geschoben?"
)
with gr.Row():
speichere_spiel = gr.Button("Speichere Spiel", variant="primary", scale=2)
reset_button = gr.Button("Reset", variant="stop", scale=1)
save_to_hf_button = gr.Button("Turnier auf Hugging Face speichern", variant="secondary")
hf_status_output = gr.Label(value="", label="Upload Status")
with gr.Column(scale=2):
gr.Markdown("### Spielverlauf")
turnier_output = gr.Dataframe(
value=initial_history_df,
headers=HISTORY_DF_HEADERS,
interactive=False,
wrap=True
)
# --- Function to dynamically show/hide input sections ---
def toggle_spielart_inputs(spielart):
if spielart == "Ramsch":
return {
standard_spiel_group: gr.update(visible=False),
ramsch_spiel_group: gr.update(visible=True)
}
else:
return {
standard_spiel_group: gr.update(visible=True),
ramsch_spiel_group: gr.update(visible=False)
}
# --- Event Handlers ---
# Link the dropdown change to the visibility function
spielart_input.change(
fn=toggle_spielart_inputs,
inputs=spielart_input,
outputs=[standard_spiel_group, ramsch_spiel_group]
)
# Consolidate all possible inputs for the click handler
all_inputs = [
turnier_output,
# Standard game inputs
spieler_input, spielart_input, spitzen_input,
hand_input, schneider_input, schwarz_input,
schneider_angesagt_input, schwarz_angesagt_input, offen_input,
verloren_input,
# Ramsch game inputs
ramsch_verlierer_input, ramsch_punkte_input, ramsch_geschoben_input
]
speichere_spiel.click(
fn=spiel_hinzufuegen,
inputs=all_inputs,
outputs=[totals_output, turnier_output]
)
reset_button.click(
fn=reset_turnier,
inputs=[],
outputs=[totals_output, turnier_output]
)
save_to_hf_button.click(
fn=save_turnier_to_hf,
inputs=[turnier_output],
outputs=[hf_status_output]
)
if __name__ == "__main__":
demo.launch(pwa=True)