feat: add GUI test client with tkinter for mobile-like interface
This commit is contained in:
281
gui_client.py
Executable file
281
gui_client.py
Executable file
@@ -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()
|
||||
Reference in New Issue
Block a user