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