Initial Commit
This commit is contained in:
651
game.py
Normal file
651
game.py
Normal file
@@ -0,0 +1,651 @@
|
|||||||
|
import serial
|
||||||
|
import tkinter as tk
|
||||||
|
from tkinter import simpledialog, messagebox
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
import time
|
||||||
|
import serial.tools.list_ports # Import hinzugefügt
|
||||||
|
|
||||||
|
class TimeGuessGame:
|
||||||
|
def __init__(self, root):
|
||||||
|
self.root = root
|
||||||
|
self.root.title("Zeit-Schätz-Spiel")
|
||||||
|
self.root.geometry("800x600")
|
||||||
|
self.root.resizable(True, True) # Resizable aktiviert
|
||||||
|
|
||||||
|
# Serielle Verbindung
|
||||||
|
self.serial_port = None
|
||||||
|
self.connected = False
|
||||||
|
self.serial_thread = None
|
||||||
|
|
||||||
|
# Spielstatus
|
||||||
|
self.game_active = False
|
||||||
|
self.current_game = None
|
||||||
|
|
||||||
|
# Rangliste
|
||||||
|
self.leaderboard = []
|
||||||
|
self.load_leaderboard()
|
||||||
|
|
||||||
|
# UI-Elemente
|
||||||
|
self.canvas_frame_id = None # Hinzugefügt: ID für das Canvas-Fenster
|
||||||
|
self.create_ui()
|
||||||
|
|
||||||
|
# Verbindung nicht automatisch beim Start herstellen
|
||||||
|
# Serielle Kommunikation wird in setup_connection gestartet
|
||||||
|
|
||||||
|
# Protokoll für das Schließen des Fensters
|
||||||
|
self.root.protocol("WM_DELETE_WINDOW", self.on_closing)
|
||||||
|
|
||||||
|
|
||||||
|
def create_ui(self):
|
||||||
|
# ... (Restlicher Code von create_ui bleibt gleich bis zum Ranglisten-Teil) ...
|
||||||
|
|
||||||
|
# Frame für Spielstatus
|
||||||
|
status_frame = tk.Frame(self.root, padx=10, pady=10)
|
||||||
|
status_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.status_label = tk.Label(status_frame, text="Spiel bereit", font=("Arial", 16))
|
||||||
|
self.status_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.connection_label = tk.Label(status_frame, text="Nicht verbunden", fg="red", font=("Arial", 12))
|
||||||
|
self.connection_label.pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
# Frame für Spielsteuerung
|
||||||
|
control_frame = tk.Frame(self.root, padx=10, pady=5)
|
||||||
|
control_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.start_button = tk.Button(control_frame, text="Neues Spiel starten",
|
||||||
|
command=self.start_new_game, font=("Arial", 14),
|
||||||
|
bg="#4CAF50", fg="white", padx=10, pady=5,
|
||||||
|
state=tk.DISABLED) # Standardmäßig deaktiviert bis Verbindung steht
|
||||||
|
self.start_button.pack(pady=10)
|
||||||
|
|
||||||
|
# Frame für Spielinformationen
|
||||||
|
game_frame = tk.Frame(self.root, padx=10, pady=10, bg="#f0f0f0")
|
||||||
|
game_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.target_time_label = tk.Label(game_frame, text="Zielzeit: -",
|
||||||
|
font=("Arial", 24), bg="#f0f0f0")
|
||||||
|
self.target_time_label.pack(pady=5)
|
||||||
|
|
||||||
|
self.instruction_label = tk.Label(game_frame,
|
||||||
|
text="Verbinde dich zuerst mit dem Arduino über das Menü.",
|
||||||
|
font=("Arial", 12), bg="#f0f0f0")
|
||||||
|
self.instruction_label.pack(pady=5)
|
||||||
|
|
||||||
|
self.elapsed_time_label = tk.Label(game_frame, text="Deine Zeit: -",
|
||||||
|
font=("Arial", 22), bg="#f0f0f0")
|
||||||
|
self.elapsed_time_label.pack(pady=5)
|
||||||
|
|
||||||
|
self.deviation_label = tk.Label(game_frame, text="Abweichung: -",
|
||||||
|
font=("Arial", 22), bg="#f0f0f0")
|
||||||
|
self.deviation_label.pack(pady=5)
|
||||||
|
|
||||||
|
# Frame für Spielername
|
||||||
|
name_frame = tk.Frame(self.root, padx=10, pady=10)
|
||||||
|
name_frame.pack(fill=tk.X)
|
||||||
|
|
||||||
|
self.name_label = tk.Label(name_frame, text="Spielername:", font=("Arial", 12))
|
||||||
|
self.name_label.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.name_entry = tk.Entry(name_frame, font=("Arial", 12), width=20)
|
||||||
|
self.name_entry.pack(side=tk.LEFT, padx=5, fill=tk.X, expand=True) # Nimmt verfügbaren Platz ein
|
||||||
|
|
||||||
|
button_frame = tk.Frame(name_frame)
|
||||||
|
button_frame.pack(side=tk.LEFT)
|
||||||
|
|
||||||
|
self.submit_button = tk.Button(button_frame, text="Zur Rangliste hinzufügen",
|
||||||
|
command=self.submit_score, state=tk.DISABLED,
|
||||||
|
bg="#2196F3", fg="white")
|
||||||
|
self.submit_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
self.skip_button = tk.Button(button_frame, text="Überspringen",
|
||||||
|
command=self.skip_score, state=tk.DISABLED,
|
||||||
|
bg="#9E9E9E", fg="white")
|
||||||
|
self.skip_button.pack(side=tk.LEFT, padx=5)
|
||||||
|
|
||||||
|
|
||||||
|
# --- Frame für Rangliste (Container) ---
|
||||||
|
leaderboard_outer_frame = tk.Frame(self.root, padx=10, pady=10)
|
||||||
|
leaderboard_outer_frame.pack(fill=tk.BOTH, expand=True) # Füllt restlichen Platz
|
||||||
|
|
||||||
|
tk.Label(leaderboard_outer_frame, text="Rangliste", font=("Arial", 16, "bold")).pack()
|
||||||
|
|
||||||
|
# --- Canvas und Scrollbar für die Rangliste ---
|
||||||
|
self.leaderboard_canvas = tk.Canvas(leaderboard_outer_frame)
|
||||||
|
self.leaderboard_scrollbar = tk.Scrollbar(leaderboard_outer_frame, orient=tk.VERTICAL, command=self.leaderboard_canvas.yview)
|
||||||
|
self.scrollable_leaderboard_frame = tk.Frame(self.leaderboard_canvas) # Frame *innerhalb* des Canvas
|
||||||
|
|
||||||
|
# --- Änderung 1: ID speichern und Bindung an Canvas ---
|
||||||
|
# Das Frame in den Canvas einbetten und die ID speichern
|
||||||
|
self.canvas_frame_id = self.leaderboard_canvas.create_window(
|
||||||
|
(0, 0),
|
||||||
|
window=self.scrollable_leaderboard_frame,
|
||||||
|
anchor="nw" # Wichtig: Anker oben links
|
||||||
|
)
|
||||||
|
|
||||||
|
# Konfigurations-Event an den *Canvas* binden, um die Breite des Frames anzupassen
|
||||||
|
self.leaderboard_canvas.bind("<Configure>", self._on_canvas_configure)
|
||||||
|
# --- Ende Änderung 1 ---
|
||||||
|
|
||||||
|
self.leaderboard_canvas.configure(yscrollcommand=self.leaderboard_scrollbar.set)
|
||||||
|
|
||||||
|
self.leaderboard_canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||||
|
self.leaderboard_scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||||
|
# --- Ende Canvas/Scrollbar ---
|
||||||
|
|
||||||
|
# Initiales Anzeigen der (leeren) Rangliste
|
||||||
|
self.update_leaderboard_display()
|
||||||
|
|
||||||
|
# Menü
|
||||||
|
menubar = tk.Menu(self.root)
|
||||||
|
self.root.config(menu=menubar)
|
||||||
|
|
||||||
|
file_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Datei", menu=file_menu)
|
||||||
|
file_menu.add_command(label="Rangliste zurücksetzen", command=self.reset_leaderboard)
|
||||||
|
file_menu.add_command(label="Beenden", command=self.on_closing) # Geändert zu on_closing
|
||||||
|
|
||||||
|
connection_menu = tk.Menu(menubar, tearoff=0)
|
||||||
|
menubar.add_cascade(label="Verbindung", menu=connection_menu)
|
||||||
|
connection_menu.add_command(label="Verbinden", command=self.setup_connection)
|
||||||
|
connection_menu.add_command(label="Trennen", command=self.disconnect_serial) # Trennen Option
|
||||||
|
|
||||||
|
# --- Änderung 2: Neue Methode zum Anpassen der Frame-Breite ---
|
||||||
|
def _on_canvas_configure(self, event):
|
||||||
|
"""Passt die Breite des Frames im Canvas an die Canvas-Breite an."""
|
||||||
|
canvas_width = event.width
|
||||||
|
# Setzt die Breite des *Fenster-Elements* im Canvas, das den Frame enthält
|
||||||
|
self.leaderboard_canvas.itemconfig(self.canvas_frame_id, width=canvas_width)
|
||||||
|
# Aktualisiere die Scrollregion, nachdem die Breite angepasst wurde
|
||||||
|
# Notwendig, damit die Scrollbar weiß, wie groß der Inhalt ist
|
||||||
|
self.leaderboard_canvas.configure(scrollregion=self.leaderboard_canvas.bbox("all"))
|
||||||
|
# --- Ende Änderung 2 ---
|
||||||
|
|
||||||
|
|
||||||
|
def update_leaderboard_display(self):
|
||||||
|
# Bestehende Einträge im scrollbaren Frame löschen
|
||||||
|
for widget in self.scrollable_leaderboard_frame.winfo_children():
|
||||||
|
widget.destroy()
|
||||||
|
|
||||||
|
# --- Spaltenkonfiguration mit Gewichten für dynamische Breite ---
|
||||||
|
# Definieren Sie die relativen Breiten (Gewichte) der Spalten
|
||||||
|
column_weights = [1, 5, 3, 3, 3] # Name etwas breiter gemacht
|
||||||
|
headers = ["Rang", "Name", "Zielzeit (s)", "Gemessene Zeit (s)", "Abweichung (s)"]
|
||||||
|
|
||||||
|
# Konfiguriere die Spalten im scrollable_leaderboard_frame, damit sie sich anpassen
|
||||||
|
# Wichtig: Dies wirkt sich nur aus, wenn scrollable_leaderboard_frame
|
||||||
|
# die Breite hat, um die Gewichte anzuwenden (daher _on_canvas_configure)
|
||||||
|
for i, weight in enumerate(column_weights):
|
||||||
|
self.scrollable_leaderboard_frame.columnconfigure(i, weight=weight, uniform='leaderboard') # 'uniform' kann helfen
|
||||||
|
|
||||||
|
# --- Kopfzeile erstellen mit grid ---
|
||||||
|
for i, header in enumerate(headers):
|
||||||
|
lbl = tk.Label(self.scrollable_leaderboard_frame, text=header,
|
||||||
|
font=("Arial", 11, "bold"), relief=tk.RIDGE, padx=5, pady=2, bd=1) # Etwas kleiner, mit Rand
|
||||||
|
lbl.grid(row=0, column=i, sticky='nsew') # nsew füllt Zelle komplett
|
||||||
|
|
||||||
|
# --- Einträge in die Rangliste einfügen mit grid ---
|
||||||
|
for i, entry in enumerate(self.leaderboard):
|
||||||
|
row_num = i + 1
|
||||||
|
|
||||||
|
bg_color = "#f0f0f0" if i % 2 == 0 else "#ffffff"
|
||||||
|
|
||||||
|
# Zellen erstellen und platzieren
|
||||||
|
data_font = ("Arial", 10) # Kleinere Schrift für Daten
|
||||||
|
|
||||||
|
# Rang
|
||||||
|
lbl_rank = tk.Label(self.scrollable_leaderboard_frame, text=str(i + 1), bg=bg_color, padx=5, pady=2, font=data_font)
|
||||||
|
lbl_rank.grid(row=row_num, column=0, sticky='nsew')
|
||||||
|
|
||||||
|
# Name
|
||||||
|
lbl_name = tk.Label(self.scrollable_leaderboard_frame, text=entry['name'], bg=bg_color, anchor='w', padx=5, pady=2, font=data_font)
|
||||||
|
lbl_name.grid(row=row_num, column=1, sticky='nsew')
|
||||||
|
|
||||||
|
# Zielzeit
|
||||||
|
lbl_target = tk.Label(self.scrollable_leaderboard_frame, text=f"{entry['target_time'] / 1000:.3f}", bg=bg_color, padx=5, pady=2, font=data_font)
|
||||||
|
lbl_target.grid(row=row_num, column=2, sticky='nsew')
|
||||||
|
|
||||||
|
# Gemessene Zeit
|
||||||
|
lbl_elapsed = tk.Label(self.scrollable_leaderboard_frame, text=f"{entry['elapsed_time'] / 1000:.3f}", bg=bg_color, padx=5, pady=2, font=data_font)
|
||||||
|
lbl_elapsed.grid(row=row_num, column=3, sticky='nsew')
|
||||||
|
|
||||||
|
# Abweichung
|
||||||
|
deviation = abs(entry['deviation'] / 1000)
|
||||||
|
color = "green" if deviation < 0.5 else "orange" if deviation < 1.0 else "red"
|
||||||
|
lbl_dev = tk.Label(self.scrollable_leaderboard_frame, text=f"{deviation:.3f}", fg=color, bg=bg_color, padx=5, pady=2, font=data_font)
|
||||||
|
lbl_dev.grid(row=row_num, column=4, sticky='nsew')
|
||||||
|
|
||||||
|
# Wichtig: Nach dem Hinzufügen von Widgets und Konfigurieren der Spalten
|
||||||
|
# muss der Canvas möglicherweise seine Scrollregion neu berechnen.
|
||||||
|
# Dies geschieht jetzt in _on_canvas_configure. Ein initiales
|
||||||
|
# Update hier schadet aber nicht.
|
||||||
|
self.scrollable_leaderboard_frame.update_idletasks()
|
||||||
|
self.leaderboard_canvas.configure(scrollregion=self.leaderboard_canvas.bbox("all"))
|
||||||
|
|
||||||
|
|
||||||
|
# ... (Rest der Methoden: setup_connection, check_initial_connection, etc. bleiben unverändert) ...
|
||||||
|
def setup_connection(self):
|
||||||
|
if self.connected:
|
||||||
|
messagebox.showinfo("Info", "Bereits verbunden.")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
ports = list(serial.tools.list_ports.comports())
|
||||||
|
if not ports:
|
||||||
|
messagebox.showerror("Fehler", "Keine seriellen Ports gefunden!")
|
||||||
|
return
|
||||||
|
|
||||||
|
port_names = [p.device for p in ports]
|
||||||
|
port_info = [f"{p.device} - {p.description}" for p in ports] # Mehr Infos anzeigen
|
||||||
|
|
||||||
|
if len(ports) == 1:
|
||||||
|
selected_port_device = ports[0].device
|
||||||
|
# Keine Nachfrage bei nur einem Port
|
||||||
|
else:
|
||||||
|
# Dialog zur Auswahl des Ports
|
||||||
|
selected_port_str = simpledialog.askstring("Port auswählen",
|
||||||
|
f"Verfügbare Ports:\n" + "\n".join(port_info) + "\n\nBitte Port eingeben (z.B. COM3 oder /dev/ttyACM0):",
|
||||||
|
parent=self.root)
|
||||||
|
if not selected_port_str:
|
||||||
|
return # Benutzer hat abgebrochen
|
||||||
|
|
||||||
|
# Prüfen, ob der eingegebene Port gültig ist
|
||||||
|
if selected_port_str not in port_names:
|
||||||
|
messagebox.showerror("Fehler", f"Ungültiger Port: {selected_port_str}")
|
||||||
|
return
|
||||||
|
selected_port_device = selected_port_str
|
||||||
|
|
||||||
|
# Verbindung herstellen
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
self.serial_port.close()
|
||||||
|
|
||||||
|
self.serial_port = serial.Serial(selected_port_device, 9600, timeout=0.1)
|
||||||
|
self.root.after(2000, self.check_initial_connection) # Warte 2s und prüfe Verbindung
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
messagebox.showerror("Verbindungsfehler", f"Konnte Port {selected_port_device} nicht öffnen:\n{e}")
|
||||||
|
self.connected = False
|
||||||
|
self.connection_label.config(text="Nicht verbunden", fg="red")
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Verbindungsfehler", str(e))
|
||||||
|
self.connected = False
|
||||||
|
self.connection_label.config(text="Nicht verbunden", fg="red")
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def check_initial_connection(self):
|
||||||
|
# Nach kurzer Wartezeit prüfen, ob der Arduino bereit ist
|
||||||
|
# (Optional: Sende einen PING oder erwarte eine READY Nachricht)
|
||||||
|
try:
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
# Optional: Warte auf eine "READY" Nachricht vom Arduino
|
||||||
|
# self.serial_port.write(b"CHECK_READY\n") # Beispielbefehl
|
||||||
|
# Oder gehe einfach davon aus, dass es geklappt hat
|
||||||
|
self.connected = True
|
||||||
|
self.connection_label.config(text=f"Verbunden mit {self.serial_port.port}", fg="green")
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
self.instruction_label.config(text="Drücke 'Neues Spiel starten' oder den Taster") # Aktualisiert
|
||||||
|
|
||||||
|
# Starte den Lese-Thread erst *nach* erfolgreicher Verbindung
|
||||||
|
if self.serial_thread is None or not self.serial_thread.is_alive():
|
||||||
|
self.serial_thread = threading.Thread(target=self.read_serial, daemon=True)
|
||||||
|
self.serial_thread.start()
|
||||||
|
|
||||||
|
messagebox.showinfo("Verbindung", f"Erfolgreich mit {self.serial_port.port} verbunden!")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Dieser Fall sollte eigentlich nicht eintreten, wenn Serial() erfolgreich war
|
||||||
|
raise serial.SerialException("Port nicht mehr offen nach Wartezeit.")
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
port_name = self.serial_port.port if self.serial_port else "Unbekannt"
|
||||||
|
messagebox.showerror("Verbindungsfehler", f"Keine Antwort vom Arduino auf {port_name}:\n{e}")
|
||||||
|
self.disconnect_serial() # Verbindung sauber trennen
|
||||||
|
|
||||||
|
|
||||||
|
def disconnect_serial(self):
|
||||||
|
"""Trennt die serielle Verbindung sauber."""
|
||||||
|
self.game_active = False # Spiel stoppen, falls aktiv
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
try:
|
||||||
|
self.serial_port.close()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Schließen des Ports: {e}")
|
||||||
|
self.serial_port = None
|
||||||
|
self.connected = False # Wichtig: Setze connected auf False
|
||||||
|
# UI Updates im Hauptthread sicherstellen
|
||||||
|
self.root.after(0, self._update_ui_disconnected)
|
||||||
|
print("Serielle Verbindung getrennt.")
|
||||||
|
# Der Lese-Thread beendet sich selbst, da self.connected False wird
|
||||||
|
|
||||||
|
def _update_ui_disconnected(self):
|
||||||
|
"""Aktualisiert die UI-Elemente nach dem Trennen der Verbindung."""
|
||||||
|
self.connection_label.config(text="Nicht verbunden", fg="red")
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
self.status_label.config(text="Verbindung getrennt")
|
||||||
|
self.instruction_label.config(text="Verbinde dich zuerst mit dem Arduino über das Menü.")
|
||||||
|
# Reset game info labels
|
||||||
|
self.target_time_label.config(text="Zielzeit: -")
|
||||||
|
self.elapsed_time_label.config(text="Deine Zeit: -")
|
||||||
|
self.deviation_label.config(text="Abweichung: -", fg='black')
|
||||||
|
self.submit_button.config(state=tk.DISABLED)
|
||||||
|
self.skip_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def start_new_game(self):
|
||||||
|
if not self.connected or not self.serial_port or not self.serial_port.is_open:
|
||||||
|
messagebox.showerror("Fehler", "Keine Verbindung zum Arduino!", parent=self.root)
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Starte ein neues Spiel
|
||||||
|
print("Sende START_GAME an Arduino")
|
||||||
|
self.serial_port.write(b"START_GAME\n")
|
||||||
|
self.serial_port.flush() # Sicherstellen, dass Daten gesendet werden
|
||||||
|
self.game_active = True
|
||||||
|
|
||||||
|
# UI zurücksetzen
|
||||||
|
self.target_time_label.config(text="Warte auf Zielzeit...")
|
||||||
|
self.elapsed_time_label.config(text="Deine Zeit: -")
|
||||||
|
self.deviation_label.config(text="Abweichung: -")
|
||||||
|
self.status_label.config(text="Spiel wird vorbereitet...")
|
||||||
|
self.instruction_label.config(text="Warte auf Spielstart...")
|
||||||
|
self.deviation_label.config(fg="black") # Farbe zurücksetzen
|
||||||
|
|
||||||
|
# Deaktiviere Start-Button während des Spiels
|
||||||
|
self.start_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
# Deaktiviere Eingabefelder für Score
|
||||||
|
self.submit_button.config(state=tk.DISABLED)
|
||||||
|
self.skip_button.config(state=tk.DISABLED)
|
||||||
|
self.name_entry.delete(0, tk.END)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
messagebox.showerror("Kommunikationsfehler", f"Fehler beim Senden an Arduino:\n{e}\nVerbindung wird getrennt.", parent=self.root)
|
||||||
|
self.disconnect_serial() # Bei Sendefehler Verbindung trennen
|
||||||
|
except Exception as e:
|
||||||
|
messagebox.showerror("Fehler", f"Fehler beim Starten des Spiels: {e}", parent=self.root)
|
||||||
|
self.game_active = False
|
||||||
|
# Nur wenn noch verbunden, Start-Button wieder aktivieren
|
||||||
|
if self.connected:
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
|
||||||
|
|
||||||
|
def read_serial(self):
|
||||||
|
print("Serial Read-Thread gestartet.")
|
||||||
|
buffer = ""
|
||||||
|
while self.connected: # Schleife läuft nur solange connected=True
|
||||||
|
if self.serial_port and self.serial_port.is_open:
|
||||||
|
try:
|
||||||
|
# Daten lesen, wenn verfügbar
|
||||||
|
if self.serial_port.in_waiting > 0:
|
||||||
|
# Lese Bytes und füge sie zum Puffer hinzu
|
||||||
|
data = self.serial_port.read(self.serial_port.in_waiting).decode('utf-8', errors='ignore')
|
||||||
|
buffer += data
|
||||||
|
|
||||||
|
# Verarbeite vollständige Zeilen im Puffer
|
||||||
|
while '\n' in buffer:
|
||||||
|
line, buffer = buffer.split('\n', 1)
|
||||||
|
line = line.strip() # Entferne Leerzeichen und ggf. \r
|
||||||
|
if line:
|
||||||
|
print(f"Arduino: {line}") # Debug-Ausgabe
|
||||||
|
# Stelle sicher, dass UI-Updates im Hauptthread laufen
|
||||||
|
self.root.after(0, self.process_arduino_message, line)
|
||||||
|
|
||||||
|
except serial.SerialException as e:
|
||||||
|
print(f"Serieller Fehler im Lesethread: {e}")
|
||||||
|
# Bei Lesefehler Verbindung als verloren markieren
|
||||||
|
# Stelle sicher, dass dies im Hauptthread passiert
|
||||||
|
if self.connected: # Nur wenn wir dachten, wir wären verbunden
|
||||||
|
self.root.after(0, self.handle_connection_loss)
|
||||||
|
break # Thread beenden
|
||||||
|
except Exception as e:
|
||||||
|
# Verhindert Absturz bei unerwarteten Dekodierungsfehlern etc.
|
||||||
|
print(f"Allgemeiner Fehler im Lesethread: {e}")
|
||||||
|
# Optional: Hier auch Verbindung trennen?
|
||||||
|
time.sleep(0.1) # Kurze Pause bei unerwartetem Fehler
|
||||||
|
else:
|
||||||
|
# Wenn Port nicht mehr offen ist oder None, Thread beenden
|
||||||
|
print("Port nicht mehr offen oder nicht vorhanden. Read-Thread wird beendet.")
|
||||||
|
break # Beendet die while self.connected Schleife
|
||||||
|
|
||||||
|
time.sleep(0.05) # Kurze Pause, um CPU zu schonen
|
||||||
|
|
||||||
|
print("Serial Read-Thread beendet.")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_connection_loss(self):
|
||||||
|
"""Wird aufgerufen, wenn ein serieller Fehler auftritt (im Hauptthread)."""
|
||||||
|
if self.connected: # Nur handeln, wenn wir dachten, wir wären verbunden
|
||||||
|
# Verhindert mehrere Fehlermeldungen bei schnellen Fehlern
|
||||||
|
self.connected = False # Sofort auf False setzen
|
||||||
|
messagebox.showerror("Verbindungsfehler", "Verbindung zum Arduino verloren!", parent=self.root)
|
||||||
|
self.disconnect_serial() # Ruft UI-Updates etc. auf
|
||||||
|
|
||||||
|
|
||||||
|
def process_arduino_message(self, message):
|
||||||
|
# Diese Funktion wird jetzt über self.root.after() im Hauptthread aufgerufen
|
||||||
|
if not self.connected and not message.startswith("PC_READY"):
|
||||||
|
# Ignoriere Nachrichten nach Verbindungsverlust, außer PC_READY
|
||||||
|
# PC_READY könnte von einem Neustart des Arduinos kommen
|
||||||
|
print(f"Ignoriere Nachricht nach Verbindungsverlust: {message}")
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
if message.startswith("TARGET_TIME:"):
|
||||||
|
target_time = int(message.split(":")[1].strip())
|
||||||
|
self.current_game = {
|
||||||
|
'target_time': target_time,
|
||||||
|
'elapsed_time': 0,
|
||||||
|
'deviation': 0
|
||||||
|
}
|
||||||
|
self.target_time_label.config(text=f"Zielzeit: {target_time/1000:.1f} Sekunden")
|
||||||
|
self.status_label.config(text="Spiel läuft") # Status hier anpassen
|
||||||
|
self.instruction_label.config(text="Drücke Taster zum Starten...") # Erste Anweisung nach Zielzeit
|
||||||
|
|
||||||
|
elif message == "WAITING_FOR_BUTTON":
|
||||||
|
self.instruction_label.config(
|
||||||
|
text="Drücke den Taster zum Starten der Zeitmessung")
|
||||||
|
self.status_label.config(text="Bereit zum Start") # Klarerer Status
|
||||||
|
|
||||||
|
elif message == "TIME_STARTED":
|
||||||
|
self.status_label.config(text="Zeitmessung läuft...")
|
||||||
|
self.instruction_label.config(
|
||||||
|
text="Drücke den Taster erneut zum Stoppen") # Verkürzt
|
||||||
|
|
||||||
|
elif message.startswith("ELAPSED_TIME:"):
|
||||||
|
# Nur verarbeiten, wenn ein Spiel aktiv ist
|
||||||
|
if self.current_game:
|
||||||
|
elapsed_time = int(message.split(":")[1].strip())
|
||||||
|
self.current_game['elapsed_time'] = elapsed_time
|
||||||
|
self.elapsed_time_label.config(
|
||||||
|
text=f"Deine Zeit: {elapsed_time/1000:.3f} Sekunden")
|
||||||
|
|
||||||
|
elif message.startswith("DEVIATION:"):
|
||||||
|
# Nur verarbeiten, wenn ein Spiel aktiv ist
|
||||||
|
if self.current_game:
|
||||||
|
deviation = int(message.split(":")[1].strip())
|
||||||
|
self.current_game['deviation'] = deviation
|
||||||
|
deviation_abs_sec = abs(deviation) / 1000
|
||||||
|
self.deviation_label.config(
|
||||||
|
text=f"Abweichung: {deviation_abs_sec:.3f} Sekunden")
|
||||||
|
|
||||||
|
# Farbe der Abweichung je nach Genauigkeit anpassen
|
||||||
|
color = "green" if deviation_abs_sec < 0.5 else "orange" if deviation_abs_sec < 1.0 else "red"
|
||||||
|
self.deviation_label.config(fg=color)
|
||||||
|
|
||||||
|
elif message == "GAME_ENDED":
|
||||||
|
self.game_active = False # Wichtig: Spielstatus aktualisieren
|
||||||
|
self.status_label.config(text="Spiel beendet")
|
||||||
|
self.instruction_label.config(
|
||||||
|
text="Trage Namen ein oder starte neu") # Verkürzt
|
||||||
|
if self.connected: # Nur aktivieren, wenn noch verbunden
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
self.submit_button.config(state=tk.NORMAL) # Buttons aktivieren
|
||||||
|
self.skip_button.config(state=tk.NORMAL)
|
||||||
|
else:
|
||||||
|
# Sicherstellen, dass Buttons deaktiviert bleiben, wenn keine Verbindung
|
||||||
|
self.submit_button.config(state=tk.DISABLED)
|
||||||
|
self.skip_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
|
||||||
|
elif message == "PC_READY": # Arduino meldet Bereitschaft nach Neustart/Init
|
||||||
|
print("Arduino meldet PC_READY")
|
||||||
|
# Wenn wir verbunden sind oder versuchen zu verbinden, UI aktualisieren
|
||||||
|
if self.connected or (self.serial_port and self.serial_port.is_open):
|
||||||
|
self.status_label.config(text="Arduino bereit")
|
||||||
|
self.start_button.config(state=tk.NORMAL)
|
||||||
|
self.instruction_label.config(text="Drücke 'Neues Spiel starten'")
|
||||||
|
else:
|
||||||
|
# Wenn keine Verbindung (mehr) besteht, nur loggen
|
||||||
|
print("PC_READY empfangen, aber nicht verbunden.")
|
||||||
|
|
||||||
|
|
||||||
|
# Handle andere unerwartete Nachrichten (optional)
|
||||||
|
# else:
|
||||||
|
# print(f"Unbekannte Nachricht vom Arduino: {message}")
|
||||||
|
|
||||||
|
except ValueError as e:
|
||||||
|
print(f"Fehler beim Konvertieren der Nachricht '{message}': {e}")
|
||||||
|
except IndexError as e:
|
||||||
|
print(f"Fehler beim Splitten der Nachricht '{message}': {e}")
|
||||||
|
except Exception as e:
|
||||||
|
# Catch-all für unerwartete Fehler während der Nachrichtenverarbeitung
|
||||||
|
import traceback
|
||||||
|
print(f"Unerwarteter Fehler beim Verarbeiten der Nachricht '{message}': {e}")
|
||||||
|
traceback.print_exc() # Gibt detaillierten Traceback aus
|
||||||
|
|
||||||
|
def submit_score(self):
|
||||||
|
name = self.name_entry.get().strip()
|
||||||
|
if not name:
|
||||||
|
messagebox.showwarning("Warnung", "Bitte gib einen Namen ein!", parent=self.root)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Stelle sicher, dass Spieldaten vorhanden und gültig sind
|
||||||
|
if self.current_game and 'target_time' in self.current_game and 'elapsed_time' in self.current_game and 'deviation' in self.current_game:
|
||||||
|
entry = {
|
||||||
|
'name': name,
|
||||||
|
'target_time': self.current_game['target_time'],
|
||||||
|
'elapsed_time': self.current_game['elapsed_time'],
|
||||||
|
'deviation': self.current_game['deviation']
|
||||||
|
}
|
||||||
|
|
||||||
|
self.leaderboard.append(entry)
|
||||||
|
self.leaderboard.sort(key=lambda x: abs(x['deviation'])) # Sortieren nach absoluter Abweichung
|
||||||
|
|
||||||
|
# Nur Top N behalten (optional)
|
||||||
|
max_leaderboard_size = 20
|
||||||
|
if len(self.leaderboard) > max_leaderboard_size:
|
||||||
|
self.leaderboard = self.leaderboard[:max_leaderboard_size]
|
||||||
|
|
||||||
|
self.save_leaderboard()
|
||||||
|
self.update_leaderboard_display() # Rangliste aktualisieren
|
||||||
|
|
||||||
|
# UI zurücksetzen für nächste Runde/Eingabe
|
||||||
|
self.name_entry.delete(0, tk.END)
|
||||||
|
self.submit_button.config(state=tk.DISABLED) # Deaktivieren bis nächstes Spielende
|
||||||
|
self.skip_button.config(state=tk.DISABLED)
|
||||||
|
self.status_label.config(text="Bereit für ein neues Spiel") # Status aktualisieren
|
||||||
|
# Alte Spielergebnisse bleiben sichtbar bis zum nächsten Spielstart
|
||||||
|
messagebox.showinfo("Rangliste", f"'{name}' zur Rangliste hinzugefügt!", parent=self.root)
|
||||||
|
else:
|
||||||
|
messagebox.showerror("Fehler", "Keine gültigen Spieldaten zum Speichern vorhanden.\nBitte zuerst ein Spiel beenden.", parent=self.root)
|
||||||
|
self.submit_button.config(state=tk.DISABLED) # Sicherstellen, dass Button deaktiviert ist
|
||||||
|
self.skip_button.config(state=tk.DISABLED)
|
||||||
|
|
||||||
|
def skip_score(self):
|
||||||
|
# UI zurücksetzen ohne Score zu speichern
|
||||||
|
self.name_entry.delete(0, tk.END)
|
||||||
|
self.submit_button.config(state=tk.DISABLED)
|
||||||
|
self.skip_button.config(state=tk.DISABLED)
|
||||||
|
self.status_label.config(text="Bereit für ein neues Spiel")
|
||||||
|
# Die alten Spielwerte bleiben sichtbar, bis ein neues Spiel gestartet wird
|
||||||
|
|
||||||
|
|
||||||
|
def load_leaderboard(self):
|
||||||
|
leaderboard_file = "leaderboard.json"
|
||||||
|
try:
|
||||||
|
if os.path.exists(leaderboard_file):
|
||||||
|
with open(leaderboard_file, "r", encoding='utf-8') as file:
|
||||||
|
try:
|
||||||
|
loaded_data = json.load(file)
|
||||||
|
# Validierung: Ist es eine Liste und enthalten die Elemente die nötigen Keys?
|
||||||
|
if isinstance(loaded_data, list):
|
||||||
|
self.leaderboard = [
|
||||||
|
entry for entry in loaded_data
|
||||||
|
if isinstance(entry, dict) and all(k in entry for k in ['name', 'target_time', 'elapsed_time', 'deviation'])
|
||||||
|
]
|
||||||
|
# Sortieren nach dem Laden
|
||||||
|
self.leaderboard.sort(key=lambda x: abs(x.get('deviation', float('inf')))) # .get() für Robustheit
|
||||||
|
else:
|
||||||
|
print(f"Warnung: Inhalt von {leaderboard_file} ist keine Liste. Rangliste wird zurückgesetzt.")
|
||||||
|
self.leaderboard = []
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
print(f"Fehler: {leaderboard_file} ist korrupt oder leer. Rangliste wird zurückgesetzt.")
|
||||||
|
self.leaderboard = []
|
||||||
|
# Optional: Backup erstellen oder Datei löschen/neu erstellen
|
||||||
|
# try:
|
||||||
|
# os.rename(leaderboard_file, leaderboard_file + ".corrupt")
|
||||||
|
# except OSError:
|
||||||
|
# pass
|
||||||
|
else:
|
||||||
|
self.leaderboard = [] # Keine Datei vorhanden, starte mit leerer Liste
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Allgemeiner Fehler beim Laden der Rangliste: {e}")
|
||||||
|
self.leaderboard = []
|
||||||
|
|
||||||
|
|
||||||
|
def save_leaderboard(self):
|
||||||
|
leaderboard_file = "leaderboard.json"
|
||||||
|
try:
|
||||||
|
# Stelle sicher, dass wir eine Liste von Dictionaries speichern
|
||||||
|
if isinstance(self.leaderboard, list):
|
||||||
|
with open(leaderboard_file, "w", encoding='utf-8') as file:
|
||||||
|
json.dump(self.leaderboard, file, indent=4) # indent für bessere Lesbarkeit
|
||||||
|
else:
|
||||||
|
print("Fehler: Ranglistendaten sind keine Liste. Speichern abgebrochen.")
|
||||||
|
messagebox.showerror("Speicherfehler", "Interner Fehler: Ranglistendaten sind korrupt.", parent=self.root)
|
||||||
|
|
||||||
|
except TypeError as e:
|
||||||
|
print(f"Fehler beim Serialisieren der Rangliste: {e}")
|
||||||
|
messagebox.showerror("Speicherfehler", f"Fehler beim Konvertieren der Ranglistendaten:\n{e}", parent=self.root)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Allgemeiner Fehler beim Speichern der Rangliste: {e}")
|
||||||
|
messagebox.showerror("Speicherfehler", f"Die Rangliste konnte nicht gespeichert werden:\n{e}", parent=self.root)
|
||||||
|
|
||||||
|
|
||||||
|
def reset_leaderboard(self):
|
||||||
|
if messagebox.askyesno("Rangliste zurücksetzen", "Möchtest du wirklich die gesamte Rangliste löschen?\nDiese Aktion kann nicht rückgängig gemacht werden.", parent=self.root, icon='warning'):
|
||||||
|
self.leaderboard = []
|
||||||
|
self.save_leaderboard() # Leere Liste speichern
|
||||||
|
self.update_leaderboard_display() # Anzeige aktualisieren
|
||||||
|
messagebox.showinfo("Rangliste", "Die Rangliste wurde zurückgesetzt!", parent=self.root)
|
||||||
|
|
||||||
|
|
||||||
|
def on_closing(self):
|
||||||
|
"""Wird aufgerufen, wenn das Fenster geschlossen wird."""
|
||||||
|
print("Schließe Anwendung...")
|
||||||
|
# Trenne die Verbindung sauber, wenn sie besteht
|
||||||
|
if self.connected:
|
||||||
|
self.disconnect_serial()
|
||||||
|
|
||||||
|
# Warten bis der Thread wirklich beendet ist (optional, aber sicherer)
|
||||||
|
if self.serial_thread and self.serial_thread.is_alive():
|
||||||
|
print("Warte auf Beendigung des Lese-Threads...")
|
||||||
|
try:
|
||||||
|
# Gib dem Thread etwas Zeit zum Beenden nach dem Setzen von self.connected = False
|
||||||
|
self.serial_thread.join(timeout=0.5) # Warte max 0.5 Sekunden
|
||||||
|
if self.serial_thread.is_alive():
|
||||||
|
print("Warnung: Lese-Thread konnte nicht sauber beendet werden.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Fehler beim Warten auf Thread: {e}")
|
||||||
|
|
||||||
|
print("Tkinter-Fenster wird zerstört.")
|
||||||
|
self.root.destroy() # Tkinter-Fenster zerstören
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
root = tk.Tk()
|
||||||
|
app = TimeGuessGame(root)
|
||||||
|
root.mainloop()
|
||||||
Reference in New Issue
Block a user