Taf2023 commited on
Commit
7673dc8
·
verified ·
1 Parent(s): 807b4fc

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +547 -0
app.py ADDED
@@ -0,0 +1,547 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ The application below implements an accessible text-based English fighting game featuring state management, a leveling system, inventory, permanent stat increases via a shop, and a persistent online leaderboard (simulated via local JSON storage).
2
+
3
+ The core interaction for the blind user relies on clear text output (suitable for screen readers) and simple text inputs (A, D, I, S) for actions.
4
+
5
+ ### `app.py`
6
+
7
+ ```python
8
+ import gradio as gr
9
+ import dataclasses
10
+ import json
11
+ import os
12
+ import time
13
+ import random
14
+ from typing import List, Tuple, Dict, Any, Union
15
+
16
+ # --- Data Structures ---
17
+
18
+ @dataclasses.dataclass
19
+ class Player:
20
+ name: str = "Player"
21
+ level: int = 1
22
+ xp: int = 0
23
+ gold: int = 10
24
+ hp: int = 100
25
+ max_hp: int = 100
26
+ attack: int = 15
27
+ defense: int = 5
28
+ inventory: Dict[str, int] = dataclasses.field(default_factory=lambda: {"Potion": 1})
29
+ wins: int = 0
30
+ losses: int = 0
31
+ in_combat: bool = False
32
+
33
+ @dataclasses.dataclass
34
+ class Enemy:
35
+ name: str
36
+ hp: int
37
+ attack: int
38
+ defense: int
39
+
40
+ # --- Constants & Configuration ---
41
+
42
+ LEADERBOARD_FILE = "leaderboard.json"
43
+ SHOP_ITEMS = {
44
+ "Potion": {"cost": 5, "effect": "Heals 50 HP", "value": 50, "type": "consumable"},
45
+ "Strength_Tonic": {"cost": 15, "effect": "Increases Attack by 5 permanently", "value": 5, "type": "permanent"},
46
+ "Armor_Polish": {"cost": 15, "effect": "Increases Defense by 3 permanently", "value": 3, "type": "permanent"},
47
+ }
48
+
49
+ # Dummy Audio Paths (These must be real accessible URLs for the Audio component to function)
50
+ SUCCESS_AUDIO = "https://huggingface.co/datasets/gradio/test-files/resolve/main/audio.wav"
51
+ FAILURE_AUDIO = "https://huggingface.co/datasets/Xenova/be-my-guide-voice/resolve/main/audio.mp3"
52
+
53
+
54
+ # --- Utility Functions for State Serialization ---
55
+
56
+ def player_to_dict(player: Player) -> Dict[str, Any]:
57
+ """Converts a Player object to a dictionary for JSON serialization."""
58
+ return dataclasses.asdict(player)
59
+
60
+ def player_from_dict(d: Dict[str, Any]) -> Player:
61
+ """Converts a dictionary back to a Player object."""
62
+ return Player(**d)
63
+
64
+ # --- Leaderboard Persistence ---
65
+
66
+ def load_leaderboard() -> List[Dict]:
67
+ """Loads leaderboard data from JSON file."""
68
+ if os.path.exists(LEADERBOARD_FILE):
69
+ with open(LEADERBOARD_FILE, 'r') as f:
70
+ try:
71
+ data = json.load(f)
72
+ return sorted(data, key=lambda x: x['level'] * 1000 + x['xp'], reverse=True)
73
+ except json.JSONDecodeError:
74
+ return []
75
+ return []
76
+
77
+ def save_leaderboard(player_data: Dict):
78
+ """Saves/updates player data in the leaderboard."""
79
+ leaderboard = load_leaderboard()
80
+
81
+ player_found = False
82
+ for i, entry in enumerate(leaderboard):
83
+ if entry['name'] == player_data['name']:
84
+ leaderboard[i] = player_data
85
+ player_found = True
86
+ break
87
+
88
+ if not player_found:
89
+ leaderboard.append(player_data)
90
+
91
+ leaderboard = sorted(leaderboard, key=lambda x: x['level'] * 1000 + x['xp'], reverse=True)
92
+
93
+ with open(LEADERBOARD_FILE, 'w') as f:
94
+ json.dump(leaderboard, f, indent=4)
95
+
96
+ # --- Game Logic Core ---
97
+
98
+ def create_enemy(player_level: int) -> Enemy:
99
+ """Creates an enemy scaled to the player's level."""
100
+ scaling = 1 + (player_level * 0.5)
101
+ name = random.choice(["Ogre", "Goblin Chief", "Shadow Walker", "Vampire Bat"])
102
+ return Enemy(
103
+ name=name,
104
+ hp=int(50 * scaling),
105
+ attack=int(10 * scaling),
106
+ defense=int(3 * scaling)
107
+ )
108
+
109
+ def xp_to_next_level(level: int) -> int:
110
+ return level * 100
111
+
112
+ def check_level_up(player: Player) -> Tuple[Player, str]:
113
+ """Checks if the player leveled up and updates stats."""
114
+ message = ""
115
+ while player.xp >= xp_to_next_level(player.level):
116
+ player.xp -= xp_to_next_level(player.level)
117
+ player.level += 1
118
+
119
+ # Stat increases
120
+ player.max_hp += 15
121
+ player.attack += 3
122
+ player.defense += 1
123
+ player.hp = player.max_hp
124
+ message += f"\nLEVEL UP! You are now Level {player.level}! Stats increased and HP restored. You feel stronger."
125
+ return player, message
126
+
127
+ def get_player_status(player: Player) -> str:
128
+ """Generates a detailed player status string for screen readers."""
129
+ status = f"--- Player Status ---\n"
130
+ status += f"Name: {player.name} | Level: {player.level}\n"
131
+ status += f"HP: {player.hp}/{player.max_hp}\n"
132
+ status += f"Attack: {player.attack} | Defense: {player.defense}\n"
133
+ status += f"XP: {player.xp}/{xp_to_next_level(player.level)} | Gold: {player.gold}\n"
134
+ status += f"Inventory: {', '.join([f'{k} ({v})' for k, v in player.inventory.items() if v > 0])}\n"
135
+ return status
136
+
137
+ def get_enemy_status(enemy: Enemy) -> str:
138
+ """Generates a detailed enemy status string."""
139
+ return f"--- Enemy: {enemy.name} ---\nHP: {enemy.hp}\nAttack: {enemy.attack} | Defense: {enemy.defense}\n"
140
+
141
+ def start_adventure(name: str, player_state_json: str) -> Tuple[str, str, str, gr.update, gr.update, str, str, gr.update, Dict]:
142
+ """Initializes a new combat session."""
143
+
144
+ if not name:
145
+ return "Please enter a name to begin your adventure.", "", player_state_json, gr.update(visible=True), gr.update(visible=False), None, FAILURE_AUDIO, gr.update(placeholder="Enter name to start adventure..."), {"visible": True}
146
+
147
+ # Try to load existing player or create new
148
+ leaderboard_data = load_leaderboard()
149
+ existing_player_data = next((p for p in leaderboard_data if p['name'] == name), None)
150
+
151
+ if existing_player_data:
152
+ player = player_from_dict(existing_player_data)
153
+ player.hp = player.max_hp # Restore HP before combat
154
+ else:
155
+ player = Player(name=name)
156
+
157
+ enemy = create_enemy(player.level)
158
+ player.in_combat = True
159
+
160
+ welcome_message = f"Welcome, {name}! Your level is {player.level}. A fearsome {enemy.name} stands in your way!"
161
+
162
+ player_status = get_player_status(player)
163
+ enemy_status = get_enemy_status(enemy)
164
+
165
+ game_log = f"{welcome_message}\n\n{enemy_status}{player_status}"
166
+ action_prompt = "What do you do? (A: Attack, D: Defend, I: Use Item, S: Status)"
167
+
168
+ new_player_state_json = json.dumps(player_to_dict(player))
169
+ new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))
170
+
171
+ return (
172
+ game_log,
173
+ action_prompt,
174
+ new_player_state_json,
175
+ gr.update(visible=False),
176
+ gr.update(visible=True, placeholder="A/D/I/S"),
177
+ new_enemy_state_json,
178
+ SUCCESS_AUDIO,
179
+ gr.update(placeholder="A/D/I/S"),
180
+ {"visible": True, "value": get_player_status(player)}
181
+ )
182
+
183
+ def process_action(action_text: str, player_state_json: str, enemy_state_json: str) -> Tuple[str, str, str, str, str, str, gr.update]:
184
+ """Handles player input and performs one turn of combat."""
185
+
186
+ player = player_from_dict(json.loads(player_state_json))
187
+ enemy = Enemy(**json.loads(enemy_state_json))
188
+
189
+ action = action_text.strip().upper()
190
+ log = []
191
+ audio_path = SUCCESS_AUDIO
192
+ action_prompt = "What do you do next? (A: Attack, D: Defend, I: Use Item, S: Status)"
193
+
194
+ if not player.in_combat:
195
+ log.append("You are not currently in combat. Please start an adventure first.")
196
+ return "\n".join(log), "A/D/I/S", player_state_json, enemy_state_json, get_player_status(player), FAILURE_AUDIO, gr.update(placeholder="A/D/I/S")
197
+
198
+ # 1. Player Action Phase
199
+ player_attacked = False
200
+
201
+ if action == "A":
202
+ damage = max(1, player.attack - enemy.defense)
203
+ enemy.hp -= damage
204
+ log.append(f"You attack the {enemy.name} for {damage} damage!")
205
+ player_attacked = True
206
+
207
+ elif action == "D":
208
+ log.append(f"You take a defensive stance, ready to mitigate damage.")
209
+ player_attacked = True # Counts as performing an action
210
+
211
+ elif action == "I":
212
+ if player.inventory.get("Potion", 0) > 0:
213
+ player.inventory["Potion"] -= 1
214
+ heal = 50
215
+ player.hp = min(player.max_hp, player.hp + heal)
216
+ log.append(f"You use a Potion and heal for 50 HP. Current HP: {player.hp}")
217
+ player_attacked = True
218
+ else:
219
+ log.append("You have no potions! Choose another action.")
220
+ audio_path = FAILURE_AUDIO
221
+ player_attacked = False # Invalid action, skip enemy turn
222
+
223
+ elif action == "S":
224
+ log.append("You check your surroundings.")
225
+ log.append(get_enemy_status(enemy))
226
+ log.append(get_player_status(player))
227
+ # Skip enemy turn and keep action prompt the same
228
+
229
+ new_player_state_json = json.dumps(player_to_dict(player))
230
+ new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))
231
+ return "\n".join(log), action_prompt, new_player_state_json, new_enemy_state_json, get_player_status(player), audio_path, gr.update(placeholder="A/D/I/S")
232
+
233
+ else:
234
+ log.append("Invalid action. Use A (Attack), D (Defend), I (Item), or S (Status).")
235
+ audio_path = FAILURE_AUDIO
236
+ player_attacked = False
237
+
238
+ # 2. Check for Player Win
239
+ if enemy.hp <= 0 and player_attacked:
240
+ player.wins += 1
241
+ xp_gain = enemy.attack * 5
242
+ gold_gain = enemy.defense * 2
243
+
244
+ player.xp += xp_gain
245
+ player.gold += gold_gain
246
+
247
+ log.append(f"\nVICTORY! The {enemy.name} is defeated!")
248
+ log.append(f"You gained {xp_gain} XP and {gold_gain} Gold.")
249
+
250
+ player, level_message = check_level_up(player)
251
+ log.append(level_message)
252
+
253
+ save_leaderboard(player_to_dict(player))
254
+
255
+ player.in_combat = False
256
+ action_prompt = "Combat ended. Go to the Adventure tab to start a new fight."
257
+ audio_path = SUCCESS_AUDIO
258
+
259
+ new_player_state_json = json.dumps(player_to_dict(player))
260
+ new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))
261
+
262
+ return "\n".join(log), action_prompt, new_player_state_json, new_enemy_state_json, get_player_status(player), audio_path, gr.update(placeholder="Game Over")
263
+
264
+ # 3. Enemy Turn (only if player performed a valid action)
265
+ if player_attacked:
266
+
267
+ # Calculate enemy damage
268
+ enemy_damage = max(1, enemy.attack - player.defense)
269
+
270
+ if action == "D":
271
+ # Apply defense action modification
272
+ enemy_damage = max(1, enemy_damage // 2)
273
+ log.append(f"(Defense successful!)")
274
+
275
+ player.hp -= enemy_damage
276
+ log.append(f"The {enemy.name} strikes back, dealing {enemy_damage} damage.")
277
+
278
+ # 4. Check for Player Loss
279
+ if player.hp <= 0:
280
+ player.losses += 1
281
+ log.append("\nDEFEAT! You were overwhelmed and knocked out.")
282
+ log.append("You lost 5 gold. Your HP is restored for next time.")
283
+ player.gold = max(0, player.gold - 5)
284
+ player.hp = player.max_hp # Reset HP
285
+
286
+ save_leaderboard(player_to_dict(player))
287
+
288
+ player.in_combat = False
289
+ action_prompt = "Combat ended. Go to the Adventure tab to start a new fight."
290
+ audio_path = FAILURE_AUDIO
291
+
292
+ new_player_state_json = json.dumps(player_to_dict(player))
293
+ new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))
294
+
295
+ return "\n".join(log), action_prompt, new_player_state_json, new_enemy_state_json, get_player_status(player), audio_path, gr.update(placeholder="Game Over")
296
+
297
+ # 5. Continue Combat
298
+ log.append(f"\n{get_enemy_status(enemy)}{get_player_status(player)}")
299
+
300
+ new_player_state_json = json.dumps(player_to_dict(player))
301
+ new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))
302
+
303
+ return "\n".join(log), action_prompt, new_player_state_json, new_enemy_state_json, get_player_status(player), audio_path, gr.update(placeholder="A/D/I/S")
304
+
305
+ # --- Shop Functions ---
306
+
307
+ def display_shop(player_state_json: str) -> Tuple[str, str, str]:
308
+ """Generates the shop interface content."""
309
+ player = player_from_dict(json.loads(player_state_json))
310
+
311
+ shop_display = "--- Merchant's Wares ---\n"
312
+ shop_display += f"Your Gold: {player.gold}\n\n"
313
+
314
+ options = []
315
+
316
+ for i, (item_name, details) in enumerate(SHOP_ITEMS.items()):
317
+ shop_display += f"[{i+1}] {item_name}: Cost {details['cost']} Gold. Effect: {details['effect']}\n"
318
+ options.append(str(i+1))
319
+
320
+ shop_display += "\nEnter the number of the item you wish to buy."
321
+
322
+ return shop_display, json.dumps(options), get_player_status(player)
323
+
324
+ def purchase_item(selection_num: str, player_state_json: str, shop_options_json: str) -> Tuple[str, str, str, str]:
325
+ """Handles item purchase logic."""
326
+ player = player_from_dict(json.loads(player_state_json))
327
+ shop_options = json.loads(shop_options_json)
328
+
329
+ try:
330
+ selection_index = int(selection_num) - 1
331
+ if selection_index < 0 or selection_index >= len(SHOP_ITEMS):
332
+ raise ValueError
333
+ except ValueError:
334
+ return display_shop(player_state_json)[0], "Invalid selection. Please enter a valid item number.", player_state_json, FAILURE_AUDIO
335
+
336
+ item_name = list(SHOP_ITEMS.keys())[selection_index]
337
+ item_details = SHOP_ITEMS[item_name]
338
+ cost = item_details["cost"]
339
+
340
+ if player.gold < cost:
341
+ return display_shop(player_state_json)[0], f"You do not have enough gold to buy {item_name}. Needs {cost} gold.", player_state_json, FAILURE_AUDIO
342
+
343
+ # Process purchase
344
+ player.gold -= cost
345
+
346
+ item_type = item_details['type']
347
+ feedback = ""
348
+
349
+ if item_type == "consumable":
350
+ player.inventory[item_name] = player.inventory.get(item_name, 0) + 1
351
+ feedback = f"You successfully bought one {item_name} for {cost} gold. It is added to your inventory."
352
+ elif item_type == "permanent":
353
+ value = item_details['value']
354
+ if item_name == "Strength_Tonic":
355
+ player.attack += value
356
+ feedback = f"You feel stronger! Attack increased by {value} permanently."
357
+ elif item_name == "Armor_Polish":
358
+ player.defense += value
359
+ feedback = f"Your armor shines! Defense increased by {value} permanently."
360
+
361
+ save_leaderboard(player_to_dict(player))
362
+ new_player_state_json = json.dumps(player_to_dict(player))
363
+
364
+ shop_display, _, _ = display_shop(new_player_state_json)
365
+
366
+ return shop_display, feedback, new_player_state_json, SUCCESS_AUDIO
367
+
368
+ # --- Leaderboard Functions ---
369
+
370
+ def get_leaderboard_data() -> str:
371
+ """Fetches and formats leaderboard data as Markdown."""
372
+ leaderboard = load_leaderboard()
373
+
374
+ if not leaderboard:
375
+ return "The leaderboard is empty. Start an adventure to gain XP!"
376
+
377
+ table_data = []
378
+
379
+ for i, entry in enumerate(leaderboard[:10]):
380
+ table_data.append([
381
+ i + 1,
382
+ entry.get('name', 'N/A'),
383
+ entry.get('level', 1),
384
+ entry.get('xp', 0),
385
+ entry.get('wins', 0),
386
+ entry.get('losses', 0)
387
+ ])
388
+
389
+ markdown_table = "### Top 10 Adventurers\n\n"
390
+
391
+ header = ["Rank", "Name", "Level", "XP", "Wins", "Losses"]
392
+ markdown_table += "| " + " | ".join(header) + " |\n"
393
+ markdown_table += "| " + " | ".join(["---"] * len(header)) + " |\n"
394
+
395
+ for row in table_data:
396
+ markdown_table += "| " + " | ".join(map(str, row)) + " |\n"
397
+
398
+ return markdown_table
399
+
400
+ # --- Gradio UI Setup ---
401
+
402
+ initial_player = Player()
403
+ INITIAL_PLAYER_STATE = json.dumps(player_to_dict(initial_player))
404
+
405
+ with gr.Blocks(theme=gr.themes.Soft(), title="Accessible Text RPG") as demo:
406
+
407
+ # CRITICAL REQUIREMENT: Header Link
408
+ gr.HTML("<h1 style='text-align: center; color: #4B4B4B;'>Accessible English Fighting Game (Text RPG)</h1>")
409
+ gr.HTML("<p style='text-align: center;'>Built with <a href='https://huggingface.co/spaces/akhaliq/anycoder' target='_blank'>anycoder</a></p>")
410
+
411
+ # Hidden States (persist game data)
412
+ player_state = gr.State(value=INITIAL_PLAYER_STATE)
413
+ enemy_state = gr.State(value=json.dumps(dataclasses.asdict(create_enemy(1))))
414
+ shop_options = gr.State(value=json.dumps([]))
415
+
416
+ # Audio feedback component
417
+ audio_output = gr.Audio(label="Audio Feedback", visible=False, autoplay=True, show_label=False)
418
+
419
+ with gr.Tabs():
420
+
421
+ # ----------------------
422
+ # TAB 1: Game
423
+ # ----------------------
424
+ with gr.TabItem("Adventure"):
425
+
426
+ gr.Markdown(
427
+ """
428
+ ## The Arena of Shadows
429
+ This game is designed for blind accessibility. Interactions are based on simple text inputs and verbose descriptions.
430
+ Actions: **A** (Attack), **D** (Defend), **I** (Use Item, Potion only for now), **S** (Status Check).
431
+ Enter your name below and click 'Start Adventure' to begin combat.
432
+ """
433
+ )
434
+
435
+ # Area for game output (log and status)
436
+ game_log = gr.Textbox(
437
+ label="Game Log and Status",
438
+ lines=15,
439
+ interactive=False,
440
+ autoscroll=True,
441
+ show_copy_button=True
442
+ )
443
+
444
+ current_player_status = gr.Textbox(
445
+ label="Player Status (Quick View)",
446
+ value=get_player_status(initial_player),
447
+ interactive=False,
448
+ lines=5,
449
+ container=True,
450
+ )
451
+
452
+ with gr.Row():
453
+ player_name_input = gr.Textbox(
454
+ label="Enter Your Name (Existing player stats will be loaded)",
455
+ placeholder="Enter name to start adventure...",
456
+ scale=2
457
+ )
458
+ start_btn = gr.Button("Start Adventure!", variant="primary", scale=1)
459
+
460
+ action_input = gr.Textbox(
461
+ label="Your Action (A/D/I/S)",
462
+ placeholder="A/D/I/S",
463
+ visible=False,
464
+ scale=3
465
+ )
466
+
467
+ # Event: Start Game
468
+ start_btn.click(
469
+ fn=start_adventure,
470
+ inputs=[player_name_input, player_state],
471
+ outputs=[game_log, action_input, player_state, start_btn, action_input, enemy_state, audio_output, player_name_input, current_player_status]
472
+ )
473
+
474
+ # Event: Combat Action (triggered by hitting Enter in the action_input box)
475
+ action_input.submit(
476
+ fn=process_action,
477
+ inputs=[action_input, player_state, enemy_state],
478
+ outputs=[game_log, action_input, player_state, enemy_state, current_player_status, audio_output, action_input]
479
+ )
480
+
481
+ # ----------------------
482
+ # TAB 2: Shop
483
+ # ----------------------
484
+ with gr.TabItem("Shop"):
485
+ gr.Markdown("## The Merchant - Spend Your Gold!")
486
+
487
+ shop_status_display = gr.Textbox(
488
+ label="Shop Inventory",
489
+ lines=8,
490
+ interactive=False,
491
+ autoscroll=True
492
+ )
493
+
494
+ shop_feedback = gr.Textbox(
495
+ label="Merchant Feedback",
496
+ lines=1,
497
+ interactive=False
498
+ )
499
+
500
+ with gr.Row():
501
+ buy_input = gr.Textbox(
502
+ label="Item Number to Buy",
503
+ placeholder="Enter item number (e.g., 1)"
504
+ )
505
+ buy_btn = gr.Button("Buy Item", variant="secondary")
506
+
507
+ shop_player_status = gr.Textbox(
508
+ label="Your Current Stats",
509
+ interactive=False,
510
+ lines=5
511
+ )
512
+
513
+ # Load Shop UI on tab select
514
+ # Note: Using load event here to initialize shop on first access
515
+ demo.load(
516
+ fn=display_shop,
517
+ inputs=[player_state],
518
+ outputs=[shop_status_display, shop_options, shop_player_status]
519
+ )
520
+
521
+ # Purchase logic
522
+ buy_btn.click(
523
+ fn=purchase_item,
524
+ inputs=[buy_input, player_state, shop_options],
525
+ outputs=[shop_status_display, shop_feedback, player_state, audio_output]
526
+ ).then(
527
+ fn=display_shop,
528
+ inputs=[player_state],
529
+ outputs=[shop_status_display, shop_options, shop_player_status]
530
+ )
531
+
532
+ # ----------------------
533
+ # TAB 3: Leaderboard
534
+ # ----------------------
535
+ with gr.TabItem("Leaderboard"):
536
+ gr.Markdown("## Hall of Heroes (Online Leaderboard)")
537
+
538
+ leaderboard_output = gr.Markdown("Loading leaderboard...")
539
+
540
+ leaderboard_refresh_btn = gr.Button("Refresh Leaderboard", variant="secondary")
541
+
542
+ # Initial load and refresh logic
543
+ load_leaderboard_event = demo.load(
544
+ fn=get_leaderboard_data,
545
+ outputs=leaderboard_output
546
+ )
547
+ leaderboard_refresh_btn.click(