| | 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. |
| |
|
| | |
| |
|
| | ```python |
| | import gradio as gr |
| | import dataclasses |
| | import json |
| | import os |
| | import time |
| | import random |
| | from typing import List, Tuple, Dict, Any, Union |
| |
|
| | |
| |
|
| | @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 |
| |
|
| | |
| |
|
| | 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"}, |
| | } |
| |
|
| | |
| | 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" |
| |
|
| |
|
| | |
| |
|
| | 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) |
| |
|
| | |
| |
|
| | 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) |
| |
|
| | |
| |
|
| | 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 |
| | |
| | |
| | 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} |
| |
|
| | |
| | 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 |
| | 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") |
| |
|
| | |
| | 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 |
| | |
| | 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 |
| | |
| | elif action == "S": |
| | log.append("You check your surroundings.") |
| | log.append(get_enemy_status(enemy)) |
| | log.append(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") |
| |
|
| | else: |
| | log.append("Invalid action. Use A (Attack), D (Defend), I (Item), or S (Status).") |
| | audio_path = FAILURE_AUDIO |
| | player_attacked = False |
| |
|
| | |
| | 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") |
| |
|
| | |
| | if player_attacked: |
| | |
| | |
| | enemy_damage = max(1, enemy.attack - player.defense) |
| | |
| | if action == "D": |
| | |
| | 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.") |
| |
|
| | |
| | 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 |
| | |
| | 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") |
| | |
| | |
| | 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") |
| |
|
| | |
| |
|
| | 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 |
| | |
| | |
| | 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 |
| |
|
| | |
| |
|
| | 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 |
| |
|
| | |
| |
|
| | 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: |
| | |
| | |
| | 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>") |
| |
|
| | |
| | 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_output = gr.Audio(label="Audio Feedback", visible=False, autoplay=True, show_label=False) |
| |
|
| | with gr.Tabs(): |
| | |
| | |
| | |
| | |
| | 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. |
| | """ |
| | ) |
| | |
| | |
| | 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 |
| | ) |
| | |
| | |
| | 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] |
| | ) |
| |
|
| | |
| | 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] |
| | ) |
| |
|
| | |
| | |
| | |
| | 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 |
| | ) |
| | |
| | |
| | |
| | demo.load( |
| | fn=display_shop, |
| | inputs=[player_state], |
| | outputs=[shop_status_display, shop_options, shop_player_status] |
| | ) |
| | |
| | |
| | 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] |
| | ) |
| | |
| | |
| | |
| | |
| | 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") |
| | |
| | |
| | load_leaderboard_event = demo.load( |
| | fn=get_leaderboard_data, |
| | outputs=leaderboard_output |
| | ) |
| | leaderboard_refresh_btn.click( |