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)