File size: 11,086 Bytes
d518bf8
3be7282
681b063
 
 
 
3be7282
681b063
3be7282
 
 
4efefa6
 
3be7282
 
681b063
 
4efefa6
3be7282
5ab9912
 
681b063
3be7282
 
681b063
3be7282
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4efefa6
681b063
4efefa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3be7282
681b063
3be7282
 
681b063
3be7282
 
 
 
 
4efefa6
681b063
3be7282
 
 
681b063
3be7282
 
 
 
 
681b063
3be7282
 
 
 
 
 
 
 
 
 
681b063
3be7282
 
4efefa6
681b063
 
3be7282
 
4efefa6
3be7282
681b063
3be7282
 
 
 
681b063
 
 
 
 
 
 
 
 
 
4efefa6
681b063
 
 
 
 
 
 
 
 
 
 
 
 
d518bf8
 
3be7282
 
 
681b063
3be7282
681b063
3be7282
 
 
 
 
681b063
d518bf8
3be7282
 
681b063
3be7282
4efefa6
3be7282
 
 
4efefa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681b063
3be7282
 
 
 
681b063
 
 
 
3be7282
 
 
 
 
 
 
 
 
4efefa6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3be7282
 
 
4efefa6
 
 
3be7282
681b063
3be7282
 
4efefa6
3be7282
 
d518bf8
3be7282
 
 
 
 
d518bf8
681b063
 
 
 
 
 
3be7282
c026d41
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
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)