From f60ae06210c72639260b72405be8d16c3b9c33dc Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 9 Feb 2026 15:34:36 +0100 Subject: [PATCH] feat: add GUI test client with tkinter for mobile-like interface --- gui_client.py | 281 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100755 gui_client.py diff --git a/gui_client.py b/gui_client.py new file mode 100755 index 0000000..3ae6853 --- /dev/null +++ b/gui_client.py @@ -0,0 +1,281 @@ +#!/usr/bin/env python3 +""" +Card Game GUI Client - Simple graphical testing tool simulating mobile interface. +""" + +import tkinter as tk +from tkinter import ttk, messagebox, scrolledtext +import requests +import json +import uuid +import threading + +BASE_URL = "http://localhost:8000" + +class CardGameGUI: + def __init__(self, root): + self.root = root + self.root.title("Card Game - Test Client") + self.root.geometry("400x700") + self.root.configure(bg="#1a1a2e") + + self.token = None + self.session = requests.Session() + + # Style + self.style = ttk.Style() + self.style.theme_use('clam') + self.style.configure("TButton", padding=10, font=('Helvetica', 10)) + self.style.configure("TLabel", background="#1a1a2e", foreground="white", font=('Helvetica', 10)) + self.style.configure("TEntry", padding=5) + self.style.configure("Header.TLabel", font=('Helvetica', 16, 'bold'), foreground="#e94560") + + self._create_widgets() + + def _create_widgets(self): + # Header + header = ttk.Label(self.root, text="⚔️ Card Game", style="Header.TLabel") + header.pack(pady=20) + + # Notebook (Tabs) + self.notebook = ttk.Notebook(self.root) + self.notebook.pack(fill='both', expand=True, padx=10, pady=10) + + # Auth Tab + self.auth_frame = ttk.Frame(self.notebook) + self.notebook.add(self.auth_frame, text="🔐 Auth") + self._create_auth_tab() + + # Profile Tab + self.profile_frame = ttk.Frame(self.notebook) + self.notebook.add(self.profile_frame, text="👤 Profile") + self._create_profile_tab() + + # Chests Tab + self.chests_frame = ttk.Frame(self.notebook) + self.notebook.add(self.chests_frame, text="📦 Chests") + self._create_chests_tab() + + # Collection Tab + self.collection_frame = ttk.Frame(self.notebook) + self.notebook.add(self.collection_frame, text="🃏 Collection") + self._create_collection_tab() + + # Status bar + self.status_var = tk.StringVar(value="Not logged in") + status = ttk.Label(self.root, textvariable=self.status_var) + status.pack(pady=5) + + def _create_auth_tab(self): + frame = self.auth_frame + + # Login Section + ttk.Label(frame, text="Email:").pack(pady=(20, 5)) + self.email_entry = ttk.Entry(frame, width=30) + self.email_entry.pack() + self.email_entry.insert(0, "test@example.com") + + ttk.Label(frame, text="Password:").pack(pady=(10, 5)) + self.password_entry = ttk.Entry(frame, width=30, show="*") + self.password_entry.pack() + self.password_entry.insert(0, "password123") + + ttk.Label(frame, text="Nickname (for register):").pack(pady=(10, 5)) + self.nickname_entry = ttk.Entry(frame, width=30) + self.nickname_entry.pack() + self.nickname_entry.insert(0, "player1") + + btn_frame = ttk.Frame(frame) + btn_frame.pack(pady=20) + + ttk.Button(btn_frame, text="Login", command=self._login).pack(side='left', padx=5) + ttk.Button(btn_frame, text="Register", command=self._register).pack(side='left', padx=5) + + def _create_profile_tab(self): + frame = self.profile_frame + + ttk.Button(frame, text="🔄 Refresh Profile", command=self._refresh_profile).pack(pady=20) + + self.profile_text = scrolledtext.ScrolledText(frame, width=40, height=20) + self.profile_text.pack(padx=10, pady=10) + + def _create_chests_tab(self): + frame = self.chests_frame + + ttk.Button(frame, text="🔄 Load Chests", command=self._load_chests).pack(pady=10) + + self.chests_listbox = tk.Listbox(frame, width=40, height=8) + self.chests_listbox.pack(padx=10, pady=10) + + ttk.Button(frame, text="📦 Open Selected Chest", command=self._open_chest).pack(pady=10) + + ttk.Label(frame, text="Result:").pack() + self.chest_result = scrolledtext.ScrolledText(frame, width=40, height=10) + self.chest_result.pack(padx=10, pady=10) + + def _create_collection_tab(self): + frame = self.collection_frame + + ttk.Button(frame, text="🔄 Refresh Collection", command=self._refresh_collection).pack(pady=10) + + self.collection_text = scrolledtext.ScrolledText(frame, width=40, height=25) + self.collection_text.pack(padx=10, pady=10) + + def _headers(self): + if self.token: + return {"Authorization": f"Bearer {self.token}"} + return {} + + def _run_async(self, func): + """Run a function in a background thread.""" + thread = threading.Thread(target=func) + thread.daemon = True + thread.start() + + def _login(self): + def do_login(): + try: + resp = self.session.post(f"{BASE_URL}/auth/login", json={ + "email": self.email_entry.get(), + "password": self.password_entry.get() + }) + if resp.status_code == 200: + self.token = resp.json()["access_token"] + self.status_var.set(f"✓ Logged in as {self.email_entry.get()}") + self._refresh_profile() + else: + messagebox.showerror("Login Failed", resp.json().get("detail", "Unknown error")) + except Exception as e: + messagebox.showerror("Error", str(e)) + + self._run_async(do_login) + + def _register(self): + def do_register(): + try: + resp = self.session.post(f"{BASE_URL}/auth/register", json={ + "email": self.email_entry.get(), + "password": self.password_entry.get(), + "nickname": self.nickname_entry.get() + }) + if resp.status_code == 200: + self.token = resp.json().get("access_token") + self.status_var.set(f"✓ Registered & logged in") + self._refresh_profile() + else: + messagebox.showerror("Register Failed", resp.json().get("detail", "Unknown error")) + except Exception as e: + messagebox.showerror("Error", str(e)) + + self._run_async(do_register) + + def _refresh_profile(self): + def do_refresh(): + try: + profile_resp = self.session.get(f"{BASE_URL}/me/profile", headers=self._headers()) + wallet_resp = self.session.get(f"{BASE_URL}/me/wallet", headers=self._headers()) + + text = "=== PROFILE ===\n" + if profile_resp.status_code == 200: + text += json.dumps(profile_resp.json(), indent=2) + "\n\n" + else: + text += f"Error: {profile_resp.status_code}\n\n" + + text += "=== WALLET ===\n" + if wallet_resp.status_code == 200: + wallet = wallet_resp.json() + text += f"💰 Gold: {wallet.get('balance', 'N/A')}\n" + else: + text += f"Error: {wallet_resp.status_code}\n" + + self.profile_text.delete('1.0', tk.END) + self.profile_text.insert(tk.END, text) + except Exception as e: + self.profile_text.delete('1.0', tk.END) + self.profile_text.insert(tk.END, f"Error: {e}") + + self._run_async(do_refresh) + + def _load_chests(self): + def do_load(): + try: + resp = self.session.get(f"{BASE_URL}/catalog/chests") + self.chests_listbox.delete(0, tk.END) + if resp.status_code == 200: + self.chests_data = resp.json() + for chest in self.chests_data: + self.chests_listbox.insert(tk.END, f"{chest['name']} - {chest['cost_gold']}g") + else: + self.chests_listbox.insert(tk.END, "Failed to load chests") + except Exception as e: + self.chests_listbox.insert(tk.END, f"Error: {e}") + + self._run_async(do_load) + + def _open_chest(self): + selection = self.chests_listbox.curselection() + if not selection: + messagebox.showwarning("Select Chest", "Please select a chest first") + return + + idx = selection[0] + chest = self.chests_data[idx] + + def do_open(): + try: + headers = self._headers() + headers["Idempotency-Key"] = str(uuid.uuid4()) + + resp = self.session.post(f"{BASE_URL}/chests/{chest['id']}/open", headers=headers) + + self.chest_result.delete('1.0', tk.END) + if resp.status_code == 200: + result = resp.json() + text = f"✨ Chest Opened!\n" + text += f"💰 Spent: {result['spent_gold']}g\n\n" + text += "Cards:\n" + for item in result['items']: + emoji = "🆕" if item['outcome'] == "NEW" else "🔄" + text += f"{emoji} {item['card_name']} ({item['rarity']})" + if item['converted_gold']: + text += f" → +{item['converted_gold']}g" + text += "\n" + self.chest_result.insert(tk.END, text) + self._refresh_profile() # Update wallet + else: + self.chest_result.insert(tk.END, f"Error: {resp.json().get('detail', 'Unknown')}") + except Exception as e: + self.chest_result.insert(tk.END, f"Error: {e}") + + self._run_async(do_open) + + def _refresh_collection(self): + def do_refresh(): + try: + resp = self.session.get(f"{BASE_URL}/me/collection", headers=self._headers()) + + self.collection_text.delete('1.0', tk.END) + if resp.status_code == 200: + cards = resp.json() + if not cards: + self.collection_text.insert(tk.END, "No cards yet! Open some chests.") + else: + text = f"=== Your Collection ({len(cards)} cards) ===\n\n" + for card in cards: + rarity_emoji = {"COMMON": "⚪", "RARE": "🔵", "LEGENDARY": "🟡"}.get(card['rarity'], "") + text += f"{rarity_emoji} {card['card_name']} ({card['rarity']})\n" + self.collection_text.insert(tk.END, text) + else: + self.collection_text.insert(tk.END, f"Error: {resp.status_code}") + except Exception as e: + self.collection_text.insert(tk.END, f"Error: {e}") + + self._run_async(do_refresh) + +def main(): + root = tk.Tk() + app = CardGameGUI(root) + root.mainloop() + +if __name__ == "__main__": + main()