File size: 21,147 Bytes
7673dc8
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
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).

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.

### `app.py`

```python
import gradio as gr
import dataclasses
import json
import os
import time
import random
from typing import List, Tuple, Dict, Any, Union

# --- Data Structures ---

@dataclasses.dataclass
class Player:
    name: str = "Player"
    level: int = 1
    xp: int = 0
    gold: int = 10
    hp: int = 100
    max_hp: int = 100
    attack: int = 15
    defense: int = 5
    inventory: Dict[str, int] = dataclasses.field(default_factory=lambda: {"Potion": 1})
    wins: int = 0
    losses: int = 0
    in_combat: bool = False

@dataclasses.dataclass
class Enemy:
    name: str
    hp: int
    attack: int
    defense: int

# --- Constants & Configuration ---

LEADERBOARD_FILE = "leaderboard.json"
SHOP_ITEMS = {
    "Potion": {"cost": 5, "effect": "Heals 50 HP", "value": 50, "type": "consumable"},
    "Strength_Tonic": {"cost": 15, "effect": "Increases Attack by 5 permanently", "value": 5, "type": "permanent"},
    "Armor_Polish": {"cost": 15, "effect": "Increases Defense by 3 permanently", "value": 3, "type": "permanent"},
}

# Dummy Audio Paths (These must be real accessible URLs for the Audio component to function)
SUCCESS_AUDIO = "https://huggingface.co/datasets/gradio/test-files/resolve/main/audio.wav"
FAILURE_AUDIO = "https://huggingface.co/datasets/Xenova/be-my-guide-voice/resolve/main/audio.mp3"


# --- Utility Functions for State Serialization ---

def player_to_dict(player: Player) -> Dict[str, Any]:
    """Converts a Player object to a dictionary for JSON serialization."""
    return dataclasses.asdict(player)

def player_from_dict(d: Dict[str, Any]) -> Player:
    """Converts a dictionary back to a Player object."""
    return Player(**d)

# --- Leaderboard Persistence ---

def load_leaderboard() -> List[Dict]:
    """Loads leaderboard data from JSON file."""
    if os.path.exists(LEADERBOARD_FILE):
        with open(LEADERBOARD_FILE, 'r') as f:
            try:
                data = json.load(f)
                return sorted(data, key=lambda x: x['level'] * 1000 + x['xp'], reverse=True)
            except json.JSONDecodeError:
                return []
    return []

def save_leaderboard(player_data: Dict):
    """Saves/updates player data in the leaderboard."""
    leaderboard = load_leaderboard()
    
    player_found = False
    for i, entry in enumerate(leaderboard):
        if entry['name'] == player_data['name']:
            leaderboard[i] = player_data
            player_found = True
            break
    
    if not player_found:
        leaderboard.append(player_data)
    
    leaderboard = sorted(leaderboard, key=lambda x: x['level'] * 1000 + x['xp'], reverse=True)

    with open(LEADERBOARD_FILE, 'w') as f:
        json.dump(leaderboard, f, indent=4)

# --- Game Logic Core ---

def create_enemy(player_level: int) -> Enemy:
    """Creates an enemy scaled to the player's level."""
    scaling = 1 + (player_level * 0.5)
    name = random.choice(["Ogre", "Goblin Chief", "Shadow Walker", "Vampire Bat"])
    return Enemy(
        name=name,
        hp=int(50 * scaling),
        attack=int(10 * scaling),
        defense=int(3 * scaling)
    )

def xp_to_next_level(level: int) -> int:
    return level * 100

def check_level_up(player: Player) -> Tuple[Player, str]:
    """Checks if the player leveled up and updates stats."""
    message = ""
    while player.xp >= xp_to_next_level(player.level):
        player.xp -= xp_to_next_level(player.level)
        player.level += 1
        
        # Stat increases
        player.max_hp += 15
        player.attack += 3
        player.defense += 1
        player.hp = player.max_hp
        message += f"\nLEVEL UP! You are now Level {player.level}! Stats increased and HP restored. You feel stronger."
    return player, message

def get_player_status(player: Player) -> str:
    """Generates a detailed player status string for screen readers."""
    status = f"--- Player Status ---\n"
    status += f"Name: {player.name} | Level: {player.level}\n"
    status += f"HP: {player.hp}/{player.max_hp}\n"
    status += f"Attack: {player.attack} | Defense: {player.defense}\n"
    status += f"XP: {player.xp}/{xp_to_next_level(player.level)} | Gold: {player.gold}\n"
    status += f"Inventory: {', '.join([f'{k} ({v})' for k, v in player.inventory.items() if v > 0])}\n"
    return status

def get_enemy_status(enemy: Enemy) -> str:
    """Generates a detailed enemy status string."""
    return f"--- Enemy: {enemy.name} ---\nHP: {enemy.hp}\nAttack: {enemy.attack} | Defense: {enemy.defense}\n"

def start_adventure(name: str, player_state_json: str) -> Tuple[str, str, str, gr.update, gr.update, str, str, gr.update, Dict]:
    """Initializes a new combat session."""
    
    if not name:
        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}

    # Try to load existing player or create new
    leaderboard_data = load_leaderboard()
    existing_player_data = next((p for p in leaderboard_data if p['name'] == name), None)

    if existing_player_data:
        player = player_from_dict(existing_player_data)
        player.hp = player.max_hp # Restore HP before combat
    else:
        player = Player(name=name)

    enemy = create_enemy(player.level)
    player.in_combat = True

    welcome_message = f"Welcome, {name}! Your level is {player.level}. A fearsome {enemy.name} stands in your way!"
    
    player_status = get_player_status(player)
    enemy_status = get_enemy_status(enemy)
    
    game_log = f"{welcome_message}\n\n{enemy_status}{player_status}"
    action_prompt = "What do you do? (A: Attack, D: Defend, I: Use Item, S: Status)"
    
    new_player_state_json = json.dumps(player_to_dict(player))
    new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))

    return (
        game_log, 
        action_prompt, 
        new_player_state_json,
        gr.update(visible=False), 
        gr.update(visible=True, placeholder="A/D/I/S"), 
        new_enemy_state_json,
        SUCCESS_AUDIO,
        gr.update(placeholder="A/D/I/S"),
        {"visible": True, "value": get_player_status(player)}
    )

def process_action(action_text: str, player_state_json: str, enemy_state_json: str) -> Tuple[str, str, str, str, str, str, gr.update]:
    """Handles player input and performs one turn of combat."""
    
    player = player_from_dict(json.loads(player_state_json))
    enemy = Enemy(**json.loads(enemy_state_json))
    
    action = action_text.strip().upper()
    log = []
    audio_path = SUCCESS_AUDIO
    action_prompt = "What do you do next? (A: Attack, D: Defend, I: Use Item, S: Status)"
    
    if not player.in_combat:
        log.append("You are not currently in combat. Please start an adventure first.")
        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")

    # 1. Player Action Phase
    player_attacked = False
    
    if action == "A": 
        damage = max(1, player.attack - enemy.defense)
        enemy.hp -= damage
        log.append(f"You attack the {enemy.name} for {damage} damage!")
        player_attacked = True
    
    elif action == "D": 
        log.append(f"You take a defensive stance, ready to mitigate damage.")
        player_attacked = True # Counts as performing an action
    
    elif action == "I":
        if player.inventory.get("Potion", 0) > 0:
            player.inventory["Potion"] -= 1
            heal = 50
            player.hp = min(player.max_hp, player.hp + heal)
            log.append(f"You use a Potion and heal for 50 HP. Current HP: {player.hp}")
            player_attacked = True
        else:
            log.append("You have no potions! Choose another action.")
            audio_path = FAILURE_AUDIO
            player_attacked = False # Invalid action, skip enemy turn
            
    elif action == "S": 
        log.append("You check your surroundings.")
        log.append(get_enemy_status(enemy))
        log.append(get_player_status(player))
        # Skip enemy turn and keep action prompt the same
        
        new_player_state_json = json.dumps(player_to_dict(player))
        new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))
        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")

    else:
        log.append("Invalid action. Use A (Attack), D (Defend), I (Item), or S (Status).")
        audio_path = FAILURE_AUDIO
        player_attacked = False

    # 2. Check for Player Win
    if enemy.hp <= 0 and player_attacked:
        player.wins += 1
        xp_gain = enemy.attack * 5
        gold_gain = enemy.defense * 2
        
        player.xp += xp_gain
        player.gold += gold_gain
        
        log.append(f"\nVICTORY! The {enemy.name} is defeated!")
        log.append(f"You gained {xp_gain} XP and {gold_gain} Gold.")

        player, level_message = check_level_up(player)
        log.append(level_message)
        
        save_leaderboard(player_to_dict(player))
        
        player.in_combat = False
        action_prompt = "Combat ended. Go to the Adventure tab to start a new fight."
        audio_path = SUCCESS_AUDIO
        
        new_player_state_json = json.dumps(player_to_dict(player))
        new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))

        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")

    # 3. Enemy Turn (only if player performed a valid action)
    if player_attacked:
        
        # Calculate enemy damage
        enemy_damage = max(1, enemy.attack - player.defense)
        
        if action == "D":
            # Apply defense action modification
            enemy_damage = max(1, enemy_damage // 2)
            log.append(f"(Defense successful!)")

        player.hp -= enemy_damage
        log.append(f"The {enemy.name} strikes back, dealing {enemy_damage} damage.")

    # 4. Check for Player Loss
    if player.hp <= 0:
        player.losses += 1
        log.append("\nDEFEAT! You were overwhelmed and knocked out.")
        log.append("You lost 5 gold. Your HP is restored for next time.")
        player.gold = max(0, player.gold - 5)
        player.hp = player.max_hp # Reset HP
        
        save_leaderboard(player_to_dict(player))
        
        player.in_combat = False
        action_prompt = "Combat ended. Go to the Adventure tab to start a new fight."
        audio_path = FAILURE_AUDIO
        
        new_player_state_json = json.dumps(player_to_dict(player))
        new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))

        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")
        
    # 5. Continue Combat
    log.append(f"\n{get_enemy_status(enemy)}{get_player_status(player)}")
    
    new_player_state_json = json.dumps(player_to_dict(player))
    new_enemy_state_json = json.dumps(dataclasses.asdict(enemy))

    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")

# --- Shop Functions ---

def display_shop(player_state_json: str) -> Tuple[str, str, str]:
    """Generates the shop interface content."""
    player = player_from_dict(json.loads(player_state_json))
    
    shop_display = "--- Merchant's Wares ---\n"
    shop_display += f"Your Gold: {player.gold}\n\n"
    
    options = []
    
    for i, (item_name, details) in enumerate(SHOP_ITEMS.items()):
        shop_display += f"[{i+1}] {item_name}: Cost {details['cost']} Gold. Effect: {details['effect']}\n"
        options.append(str(i+1))

    shop_display += "\nEnter the number of the item you wish to buy."
    
    return shop_display, json.dumps(options), get_player_status(player)

def purchase_item(selection_num: str, player_state_json: str, shop_options_json: str) -> Tuple[str, str, str, str]:
    """Handles item purchase logic."""
    player = player_from_dict(json.loads(player_state_json))
    shop_options = json.loads(shop_options_json)
    
    try:
        selection_index = int(selection_num) - 1
        if selection_index < 0 or selection_index >= len(SHOP_ITEMS):
            raise ValueError
    except ValueError:
        return display_shop(player_state_json)[0], "Invalid selection. Please enter a valid item number.", player_state_json, FAILURE_AUDIO
    
    item_name = list(SHOP_ITEMS.keys())[selection_index]
    item_details = SHOP_ITEMS[item_name]
    cost = item_details["cost"]
    
    if player.gold < cost:
        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
    
    # Process purchase
    player.gold -= cost
    
    item_type = item_details['type']
    feedback = ""
    
    if item_type == "consumable":
        player.inventory[item_name] = player.inventory.get(item_name, 0) + 1
        feedback = f"You successfully bought one {item_name} for {cost} gold. It is added to your inventory."
    elif item_type == "permanent":
        value = item_details['value']
        if item_name == "Strength_Tonic":
            player.attack += value
            feedback = f"You feel stronger! Attack increased by {value} permanently."
        elif item_name == "Armor_Polish":
            player.defense += value
            feedback = f"Your armor shines! Defense increased by {value} permanently."
    
    save_leaderboard(player_to_dict(player))
    new_player_state_json = json.dumps(player_to_dict(player))
    
    shop_display, _, _ = display_shop(new_player_state_json)
    
    return shop_display, feedback, new_player_state_json, SUCCESS_AUDIO

# --- Leaderboard Functions ---

def get_leaderboard_data() -> str:
    """Fetches and formats leaderboard data as Markdown."""
    leaderboard = load_leaderboard()
    
    if not leaderboard:
        return "The leaderboard is empty. Start an adventure to gain XP!"
    
    table_data = []
    
    for i, entry in enumerate(leaderboard[:10]):
        table_data.append([
            i + 1,
            entry.get('name', 'N/A'),
            entry.get('level', 1),
            entry.get('xp', 0),
            entry.get('wins', 0),
            entry.get('losses', 0)
        ])

    markdown_table = "### Top 10 Adventurers\n\n"
    
    header = ["Rank", "Name", "Level", "XP", "Wins", "Losses"]
    markdown_table += "| " + " | ".join(header) + " |\n"
    markdown_table += "| " + " | ".join(["---"] * len(header)) + " |\n"
    
    for row in table_data:
        markdown_table += "| " + " | ".join(map(str, row)) + " |\n"
        
    return markdown_table

# --- Gradio UI Setup ---

initial_player = Player()
INITIAL_PLAYER_STATE = json.dumps(player_to_dict(initial_player))

with gr.Blocks(theme=gr.themes.Soft(), title="Accessible Text RPG") as demo:
    
    # CRITICAL REQUIREMENT: Header Link
    gr.HTML("<h1 style='text-align: center; color: #4B4B4B;'>Accessible English Fighting Game (Text RPG)</h1>")
    gr.HTML("<p style='text-align: center;'>Built with <a href='https://huggingface.co/spaces/akhaliq/anycoder' target='_blank'>anycoder</a></p>")

    # Hidden States (persist game data)
    player_state = gr.State(value=INITIAL_PLAYER_STATE)
    enemy_state = gr.State(value=json.dumps(dataclasses.asdict(create_enemy(1))))
    shop_options = gr.State(value=json.dumps([]))
    
    # Audio feedback component
    audio_output = gr.Audio(label="Audio Feedback", visible=False, autoplay=True, show_label=False)

    with gr.Tabs():
        
        # ----------------------
        # TAB 1: Game
        # ----------------------
        with gr.TabItem("Adventure"):
            
            gr.Markdown(
                """
                ## The Arena of Shadows
                This game is designed for blind accessibility. Interactions are based on simple text inputs and verbose descriptions.
                Actions: **A** (Attack), **D** (Defend), **I** (Use Item, Potion only for now), **S** (Status Check).
                Enter your name below and click 'Start Adventure' to begin combat.
                """
            )
            
            # Area for game output (log and status)
            game_log = gr.Textbox(
                label="Game Log and Status", 
                lines=15, 
                interactive=False, 
                autoscroll=True,
                show_copy_button=True
            )
            
            current_player_status = gr.Textbox(
                label="Player Status (Quick View)",
                value=get_player_status(initial_player),
                interactive=False,
                lines=5,
                container=True,
            )
            
            with gr.Row():
                player_name_input = gr.Textbox(
                    label="Enter Your Name (Existing player stats will be loaded)",
                    placeholder="Enter name to start adventure...",
                    scale=2
                )
                start_btn = gr.Button("Start Adventure!", variant="primary", scale=1)
            
            action_input = gr.Textbox(
                label="Your Action (A/D/I/S)",
                placeholder="A/D/I/S",
                visible=False,
                scale=3
            )
            
            # Event: Start Game
            start_btn.click(
                fn=start_adventure,
                inputs=[player_name_input, player_state],
                outputs=[game_log, action_input, player_state, start_btn, action_input, enemy_state, audio_output, player_name_input, current_player_status]
            )

            # Event: Combat Action (triggered by hitting Enter in the action_input box)
            action_input.submit(
                fn=process_action,
                inputs=[action_input, player_state, enemy_state],
                outputs=[game_log, action_input, player_state, enemy_state, current_player_status, audio_output, action_input]
            )

        # ----------------------
        # TAB 2: Shop
        # ----------------------
        with gr.TabItem("Shop"):
            gr.Markdown("## The Merchant - Spend Your Gold!")
            
            shop_status_display = gr.Textbox(
                label="Shop Inventory",
                lines=8,
                interactive=False,
                autoscroll=True
            )
            
            shop_feedback = gr.Textbox(
                label="Merchant Feedback",
                lines=1,
                interactive=False
            )
            
            with gr.Row():
                buy_input = gr.Textbox(
                    label="Item Number to Buy",
                    placeholder="Enter item number (e.g., 1)"
                )
                buy_btn = gr.Button("Buy Item", variant="secondary")

            shop_player_status = gr.Textbox(
                label="Your Current Stats",
                interactive=False,
                lines=5
            )
            
            # Load Shop UI on tab select
            # Note: Using load event here to initialize shop on first access
            demo.load(
                fn=display_shop, 
                inputs=[player_state], 
                outputs=[shop_status_display, shop_options, shop_player_status]
            )
            
            # Purchase logic
            buy_btn.click(
                fn=purchase_item,
                inputs=[buy_input, player_state, shop_options],
                outputs=[shop_status_display, shop_feedback, player_state, audio_output]
            ).then(
                fn=display_shop,
                inputs=[player_state],
                outputs=[shop_status_display, shop_options, shop_player_status]
            )
            
        # ----------------------
        # TAB 3: Leaderboard
        # ----------------------
        with gr.TabItem("Leaderboard"):
            gr.Markdown("## Hall of Heroes (Online Leaderboard)")
            
            leaderboard_output = gr.Markdown("Loading leaderboard...")
            
            leaderboard_refresh_btn = gr.Button("Refresh Leaderboard", variant="secondary")
            
            # Initial load and refresh logic
            load_leaderboard_event = demo.load(
                fn=get_leaderboard_data,
                outputs=leaderboard_output
            )
            leaderboard_refresh_btn.click(