|
|
| """
|
| COMPLETE MMORPG with MCP Server Integration - ALL FEATURES RESTORED
|
| Fixed deadlock issue while preserving all original functionality
|
| """
|
|
|
| import gradio as gr
|
| import asyncio
|
| import json
|
| import time
|
| import uuid
|
| import random
|
| import random
|
| import threading
|
| from datetime import datetime, timedelta
|
| from typing import Dict, List, Any, Optional
|
| from dataclasses import dataclass, asdict
|
| from abc import ABC, abstractmethod
|
| from mcp import ClientSession
|
| from mcp.client.sse import sse_client
|
| from contextlib import AsyncExitStack
|
|
|
|
|
|
|
|
|
|
|
| @dataclass
|
| @dataclass
|
| class Player:
|
| id: str
|
| name: str
|
| type: str
|
| x: int = 100
|
| y: int = 100
|
| level: int = 1
|
| hp: int = 100
|
| max_hp: int = 100
|
| gold: int = 50
|
| experience: int = 0
|
| last_active: float = 0
|
| session_hash: str = ""
|
| ai_agent_id: str = ""
|
|
|
| class GameWorld:
|
| def __init__(self):
|
| self.players: Dict[str, Player] = {}
|
| self.npcs: Dict[str, Dict] = {
|
| 'read2burn_mailbox': {
|
| 'id': 'read2burn_mailbox',
|
| 'name': 'Secure Mailbox',
|
| 'x': 200, 'y': 150,
|
| 'char': '📮',
|
| 'type': 'addon'
|
| },
|
| 'merchant': {
|
| 'id': 'merchant',
|
| 'name': 'Tom the Trader',
|
| 'x': 300, 'y': 200,
|
| 'char': '🏪',
|
| 'type': 'basic'
|
| },
|
| 'weather_oracle': {
|
| 'id': 'weather_oracle',
|
| 'name': 'Weather Oracle',
|
| 'x': 150, 'y': 300,
|
| 'char': '🌤️',
|
| 'type': 'mcp'
|
| },
|
| 'scholar': {
|
| 'id': 'scholar',
|
| 'name': 'Professor Wise',
|
| 'x': 100, 'y': 100,
|
| 'char': '📚',
|
| 'type': 'learning',
|
| 'personality': 'wise_teacher'
|
| },
|
| 'jester': {
|
| 'id': 'jester',
|
| 'name': 'Funny Pete',
|
| 'x': 400, 'y': 100,
|
| 'char': '🃏',
|
| 'type': 'entertainment',
|
| 'personality': 'comedian'
|
| },
|
| 'warrior': {
|
| 'id': 'warrior',
|
| 'name': 'Captain Steel',
|
| 'x': 350, 'y': 350,
|
| 'char': '⚔️',
|
| 'type': 'combat',
|
| 'personality': 'tough_trainer'
|
| },
|
| 'healer': {
|
| 'id': 'healer',
|
| 'name': 'Sister Grace',
|
| 'x': 50, 'y': 250,
|
| 'char': '💚',
|
| 'type': 'healing',
|
| 'personality': 'gentle_healer'
|
| },
|
| 'wanderer': {
|
| 'id': 'wanderer',
|
| 'name': 'Roaming Rick',
|
| 'x': 200, 'y': 200,
|
| 'char': '🚶',
|
| 'type': 'moving',
|
| 'personality': 'traveler',
|
| 'movement': {
|
| 'speed': 2,
|
| 'direction_x': 1,
|
| 'direction_y': 1,
|
| 'last_move': time.time()
|
| }
|
| },
|
| 'sage': {
|
| 'id': 'sage',
|
| 'name': 'Ancient Sage',
|
| 'x': 450, 'y': 250,
|
| 'char': '🧙',
|
| 'type': 'magic',
|
| 'personality': 'mystical_sage'
|
| }
|
| }
|
| self.chat_messages: List[Dict] = []
|
| self.world_events: List[Dict] = []
|
| self.addon_npcs: Dict[str, 'NPCAddon'] = {}
|
| self._lock = threading.RLock()
|
|
|
|
|
| self.add_chat_message("System", "🎮 Game server started!")
|
|
|
| def add_player(self, player: Player) -> bool:
|
| print(f"[GameWorld.add_player] Adding {player.name}")
|
| with self._lock:
|
| if len(self.players) >= 20:
|
| return False
|
| self.players[player.id] = player
|
| self.add_chat_message("System", f"🎮 {player.name} ({player.type}) joined the game!")
|
| print(f"[GameWorld] Player {player.name} added. Total players: {len(self.players)}")
|
| return True
|
|
|
| def remove_player(self, player_id: str) -> bool:
|
| with self._lock:
|
| if player_id in self.players:
|
| player = self.players[player_id]
|
| self.add_chat_message("System", f"👋 {player.name} left the game!")
|
| del self.players[player_id]
|
| return True
|
| return False
|
|
|
| def move_player(self, player_id: str, direction: str) -> bool:
|
| with self._lock:
|
| if player_id not in self.players:
|
| return False
|
|
|
| player = self.players[player_id]
|
| old_x, old_y = player.x, player.y
|
|
|
|
|
| if direction == "up":
|
| player.y = max(0, player.y - 25)
|
| elif direction == "down":
|
| player.y = min(375, player.y + 25)
|
| elif direction == "left":
|
| player.x = max(0, player.x - 25)
|
| elif direction == "right":
|
| player.x = min(475, player.x + 25)
|
|
|
| player.last_active = time.time()
|
|
|
|
|
| if old_x != player.x or old_y != player.y:
|
| player.experience += 1
|
| if player.experience >= player.level * 100:
|
| player.level += 1
|
| player.max_hp += 10
|
| player.hp = player.max_hp
|
| player.gold += 10
|
| self.add_chat_message("System", f"🎉 {player.name} reached level {player.level}!")
|
|
|
| self.check_npc_proximity(player_id)
|
|
|
| return old_x != player.x or old_y != player.y
|
| def add_chat_message(self, sender: str, message: str, message_type: str = "public", target: str = None, sender_id: str = None):
|
| with self._lock:
|
| chat_msg = {
|
| 'sender': sender,
|
| 'message': message,
|
| 'timestamp': time.strftime("%H:%M:%S"),
|
| 'id': len(self.chat_messages),
|
| 'type': message_type,
|
| 'target': target,
|
| 'sender_id': sender_id
|
| }
|
| self.chat_messages.append(chat_msg)
|
| if len(self.chat_messages) > 50:
|
| self.chat_messages = self.chat_messages[-50:]
|
|
|
| def check_npc_proximity(self, player_id: str):
|
| """Check if player is near any NPCs"""
|
| player = self.players.get(player_id)
|
| if not player:
|
| return []
|
|
|
| nearby_entities = []
|
|
|
|
|
| for npc_id, npc in self.npcs.items():
|
| distance = ((player.x - npc['x'])**2 + (player.y - npc['y'])**2)**0.5
|
| if distance < 50:
|
| self.add_world_event(f"{player.name} is near {npc['name']}")
|
| nearby_entities.append({
|
| 'type': 'npc',
|
| 'id': npc_id,
|
| 'name': npc['name'],
|
| 'distance': distance
|
| })
|
|
|
|
|
| for other_id, other_player in self.players.items():
|
| if other_id != player_id:
|
| distance = ((player.x - other_player.x)**2 + (player.y - other_player.y)**2)**0.5
|
| if distance < 50:
|
| nearby_entities.append({
|
| 'type': 'player',
|
| 'id': other_id,
|
| 'name': other_player.name,
|
| 'distance': distance
|
| })
|
|
|
| return nearby_entities
|
|
|
| def send_private_message(self, sender_id: str, target_id: str, message: str) -> tuple[bool, str]:
|
| """Send private message between players or to NPC"""
|
| sender = self.players.get(sender_id)
|
| if not sender:
|
| error_msg = f"Sender player {sender_id} not found"
|
| print(f"[PRIVATE_ERROR] {error_msg}")
|
| return False, error_msg
|
|
|
| print(f"[PRIVATE] Sending message from {sender.name} ({sender_id}) to {target_id}: {message}")
|
|
|
|
|
| if target_id in self.npcs:
|
| npc = self.npcs[target_id]
|
| print(f"[PRIVATE] Found NPC: {npc['name']} (ID: {target_id})")
|
|
|
| self.add_chat_message(
|
| f"🔒 {sender.name}",
|
| f"[Private to {npc['name']}]: {message}",
|
| "private_to_npc",
|
| target_id,
|
| sender_id
|
| )
|
| npc_response = self.get_npc_response(target_id, message, sender_id)
|
| self.add_chat_message(
|
| f"🤖 {npc['name']}",
|
| npc_response,
|
| "private_from_npc",
|
| sender_id,
|
| target_id
|
| )
|
| print(f"[PRIVATE] NPC {npc['name']} responded: {npc_response}")
|
| return True, f"Message sent to {npc['name']}"
|
|
|
|
|
| target = self.players.get(target_id)
|
| if target:
|
| print(f"[PRIVATE] Found player: {target.name} (ID: {target_id})")
|
| self.add_chat_message(
|
| f"🔒 {sender.name}",
|
| f"[Private to {target.name}]: {message}",
|
| "private_to_player",
|
| target_id,
|
| sender_id )
|
| return True, f"Message sent to {target.name}"
|
|
|
|
|
| available_npcs = list(self.npcs.keys())
|
| available_players = list(self.players.keys())
|
| error_msg = f"Target '{target_id}' not found. Available NPCs: {available_npcs}, Available players: {available_players}"
|
| print(f"[PRIVATE_ERROR] {error_msg}")
|
| return False, error_msg
|
|
|
| def get_npc_response(self, npc_id: str, message: str, player_id: str = None) -> str:
|
| """Generate NPC response - checks for addons first, then falls back to generic responses"""
|
| npc = self.npcs.get(npc_id)
|
| if not npc:
|
| return "I don't understand."
|
|
|
|
|
| if npc_id in self.addon_npcs and player_id:
|
| addon = self.addon_npcs[npc_id]
|
| print(f"[NPC_RESPONSE] Found addon for {npc_id}, delegating to handle_command")
|
| return addon.handle_command(player_id, message)
|
|
|
|
|
| personality = npc.get('personality', npc_id)
|
|
|
|
|
| responses = {
|
| 'read2burn_mailbox': [
|
| "Would you like to send a secure message?",
|
| "I can help you with encrypted messaging.",
|
| "Your message will burn after reading!"
|
| ],
|
| 'merchant': [
|
| "Welcome to my shop! What would you like to buy?",
|
| "I have the finest items in the realm!",
|
| "Special discount today - 10% off all potions!"
|
| ],
|
| 'weather_oracle': [
|
| "The winds whisper of changes ahead...",
|
| "I sense a storm approaching...",
|
| "The weather spirits are restless today."
|
| ], 'wise_teacher': [
|
| "Ah, a curious mind! What would you like to learn?",
|
| "Knowledge is the greatest treasure. Ask me anything!",
|
| "I have studied the ancient texts for decades.",
|
| "Wisdom comes through questioning. What puzzles you?"
|
| ],
|
| 'comedian': [
|
| "Haha! Want to hear a joke? Why don't skeletons fight? They don't have the guts!",
|
| "What do you call a sleeping bull? A bulldozer! *laughs*",
|
| "I've got jokes for days! Life's too short not to laugh!",
|
| "Why did the scarecrow win an award? He was outstanding in his field!"
|
| ],
|
| 'tough_trainer': [
|
| "Ready for combat training? Show me your stance!",
|
| "A true warrior trains every day. Are you committed?",
|
| "Strength comes from discipline and practice!",
|
| "The blade is an extension of your will. Focus!"
|
| ],
|
| 'gentle_healer': [
|
| "Blessings upon you, traveler. Do you need healing?",
|
| "The light guides my hands. I can mend your wounds.",
|
| "Health of body and spirit go hand in hand.",
|
| "May the divine light restore your vitality!"
|
| ],
|
| 'traveler': [
|
| "The road calls to me... always moving, always exploring!",
|
| "I've seen wonders beyond imagination in my travels.",
|
| "Adventure awaits around every corner!",
|
| "Sometimes the journey is more important than the destination."
|
| ],
|
| 'mystical_sage': [
|
| "Magic flows through all things, young one.",
|
| "The arcane arts require patience and understanding.",
|
| "Ancient powers stir... can you feel them?",
|
| "Wisdom and magic are closely intertwined."
|
| ]
|
| }
|
|
|
| response_key = personality if personality in responses else npc_id
|
| return random.choice(responses.get(response_key, [
|
| "Hello there, traveler!",
|
| "How can I help you?",
|
| "Nice weather we're having."
|
| ]))
|
|
|
| def get_private_messages_for_player(self, player_id: str) -> List[Dict]:
|
| """Get private messages for a specific player"""
|
| player = self.players.get(player_id)
|
| if not player:
|
| return []
|
|
|
| private_messages = []
|
| for msg in self.chat_messages[-20:]:
|
| msg_type = msg.get('type', 'public')
|
| if msg_type != 'public':
|
|
|
| if (msg.get('target') == player_id or
|
| msg.get('sender_id') == player_id or
|
| (msg_type in ['private_from_npc', 'private_to_npc'] and
|
| (msg.get('target') == player_id or msg.get('sender_id') == player_id))):
|
|
|
| print(f"[PRIVATE_FILTER] Found private message for {player_id}: {msg['sender']}: {msg['message']} (type: {msg_type})")
|
| private_messages.append(msg)
|
|
|
| print(f"[PRIVATE_DEBUG] Player {player_id} has {len(private_messages)} private messages")
|
| return private_messages
|
|
|
| def add_world_event(self, event: str):
|
| self.world_events.append({
|
| 'event': event,
|
| 'timestamp': time.time()
|
| })
|
| if len(self.world_events) > 20:
|
| self.world_events = self.world_events[-20:]
|
|
|
| def update_moving_npcs(self):
|
| """Update positions of moving NPCs like Roaming Rick"""
|
| current_time = time.time()
|
|
|
| with self._lock:
|
| for npc_id, npc in self.npcs.items():
|
|
|
| if npc.get('type') == 'moving' and 'movement' in npc:
|
| movement = npc['movement']
|
|
|
|
|
| time_since_last_move = current_time - movement.get('last_move', 0)
|
| movement_interval = 2.0 / movement.get('speed', 1)
|
|
|
| if time_since_last_move >= movement_interval:
|
|
|
| old_x, old_y = npc['x'], npc['y']
|
|
|
|
|
| move_distance = 25
|
| direction_x = movement.get('direction_x', 1)
|
| direction_y = movement.get('direction_y', 1)
|
|
|
| new_x = npc['x'] + (move_distance * direction_x)
|
| new_y = npc['y'] + (move_distance * direction_y)
|
|
|
|
|
| if new_x <= 0 or new_x >= 475:
|
| direction_x *= -1
|
| new_x = max(0, min(475, new_x))
|
|
|
| if new_y <= 0 or new_y >= 375:
|
| direction_y *= -1
|
| new_y = max(0, min(375, new_y))
|
|
|
|
|
| npc['x'] = new_x
|
| npc['y'] = new_y
|
| movement['direction_x'] = direction_x
|
| movement['direction_y'] = direction_y
|
| movement['last_move'] = current_time
|
|
|
|
|
| if old_x != new_x or old_y != new_y:
|
| self.add_world_event(f"🚶 {npc['name']} roams to ({int(new_x)}, {int(new_y)})")
|
|
|
|
|
| for player_id, player in self.players.items():
|
| distance = ((player.x - new_x)**2 + (player.y - new_y)**2)**0.5
|
| if distance < 50:
|
| self.add_world_event(f"👀 {player.name} notices {npc['name']} nearby")
|
|
|
|
|
| game_world = GameWorld()
|
|
|
|
|
|
|
|
|
|
|
| class NPCAddon(ABC):
|
| """Base class for NPC add-ons"""
|
|
|
| @property
|
| @abstractmethod
|
| def addon_id(self) -> str:
|
| pass
|
|
|
| @property
|
| @abstractmethod
|
| def addon_name(self) -> str:
|
| pass
|
|
|
| @abstractmethod
|
| def get_interface(self) -> gr.Component:
|
| """Return Gradio interface for this add-on"""
|
| pass
|
|
|
| @abstractmethod
|
| def handle_command(self, player_id: str, command: str) -> str:
|
| """Handle player commands"""
|
| pass
|
|
|
| class Read2BurnMailboxAddon(NPCAddon):
|
| """Self-destructing secure mailbox add-on - RESTORED"""
|
|
|
| def __init__(self):
|
| self.messages: Dict[str, Dict] = {}
|
| self.access_log: List[Dict] = []
|
|
|
| @property
|
| def addon_id(self) -> str:
|
| return "read2burn_mailbox"
|
|
|
| @property
|
| def addon_name(self) -> str:
|
| return "🔥 Read2Burn Secure Mailbox"
|
|
|
| def get_interface(self) -> gr.Component:
|
| with gr.Column() as interface:
|
| gr.Markdown("""
|
| ## 🔥 Read2Burn Secure Mailbox
|
|
|
| **Features:**
|
| - Messages self-destruct after reading
|
| - End-to-end encryption simulation
|
| - 24-hour expiration
|
| - Anonymous delivery
|
|
|
| **Commands:**
|
| - `create Your secret message here` - Create new message
|
| - `read MESSAGE_ID` - Read message (destroys it!)
|
| - `list` - Show your created messages
|
| """)
|
|
|
| with gr.Row():
|
| command_input = gr.Textbox(
|
| label="Command",
|
| placeholder="create Hello, this message will self-destruct!",
|
| scale=3
|
| )
|
| send_btn = gr.Button("Send", variant="primary", scale=1)
|
|
|
| result_output = gr.Textbox(
|
| label="Mailbox Response",
|
| lines=5,
|
| interactive=False
|
| )
|
|
|
|
|
| message_history = gr.Dataframe(
|
| headers=["Message ID", "Created", "Status", "Reads Left"],
|
| label="Your Messages",
|
| interactive=False
|
| )
|
|
|
|
|
| def handle_mailbox_command(command: str):
|
|
|
| current_players = list(game_world.players.keys())
|
| if not current_players:
|
| return "❌ No players in the game! Please join the game first.", []
|
|
|
|
|
| player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
| player_name = game_world.players[player_id].name
|
|
|
| print(f"[Read2Burn] Command '{command}' from player {player_name} ({player_id})")
|
|
|
| result = self.handle_command(player_id, command)
|
| history = self.get_player_message_history(player_id)
|
|
|
|
|
| result = f"**Player:** {player_name}\n\n{result}"
|
|
|
| return result, history
|
|
|
| send_btn.click(
|
| handle_mailbox_command,
|
| inputs=[command_input],
|
| outputs=[result_output, message_history]
|
| )
|
|
|
| command_input.submit(
|
| handle_mailbox_command,
|
| inputs=[command_input],
|
| outputs=[result_output, message_history]
|
| )
|
|
|
| return interface
|
|
|
| def handle_command(self, player_id: str, command: str) -> str:
|
| """Handle Read2Burn mailbox commands"""
|
| parts = command.strip().split(' ', 1)
|
| cmd = parts[0].lower()
|
|
|
| if cmd == "create" and len(parts) > 1:
|
| return self.create_message(player_id, parts[1])
|
| elif cmd == "read" and len(parts) > 1:
|
| return self.read_message(player_id, parts[1])
|
| elif cmd == "list":
|
| return self.list_player_messages(player_id)
|
| else:
|
| return "❓ Invalid command. Try: create <message>, read <id>, or list"
|
|
|
| def create_message(self, creator_id: str, content: str) -> str:
|
| """Create a new self-destructing message"""
|
| message_id = self.generate_message_id()
|
|
|
| self.messages[message_id] = {
|
| 'id': message_id,
|
| 'creator': creator_id,
|
| 'content': content,
|
| 'created_at': time.time(),
|
| 'expires_at': time.time() + (24 * 3600),
|
| 'reads_left': 1,
|
| 'burned': False
|
| }
|
|
|
| self.access_log.append({
|
| 'action': 'create',
|
| 'message_id': message_id,
|
| 'player_id': creator_id,
|
| 'timestamp': time.time()
|
| })
|
|
|
| return f"✅ **Message Created Successfully!**\n\n📝 **Message ID:** `{message_id}`\n🔗 Share this ID with the recipient\n⏰ Expires in 24 hours\n🔥 Burns after 1 read"
|
|
|
| def read_message(self, reader_id: str, message_id: str) -> str:
|
| """Read and burn a message"""
|
| if message_id not in self.messages:
|
| return "❌ Message not found or already burned"
|
|
|
| message = self.messages[message_id]
|
|
|
|
|
| if time.time() > message['expires_at']:
|
| del self.messages[message_id]
|
| return "❌ Message expired and has been burned"
|
|
|
|
|
| if message['burned'] or message['reads_left'] <= 0:
|
| del self.messages[message_id]
|
| return "❌ Message has already been burned"
|
|
|
|
|
| content = message['content']
|
| message['reads_left'] -= 1
|
|
|
| self.access_log.append({
|
| 'action': 'read',
|
| 'message_id': message_id,
|
| 'player_id': reader_id,
|
| 'timestamp': time.time()
|
| })
|
|
|
|
|
| if message['reads_left'] <= 0:
|
| message['burned'] = True
|
| del self.messages[message_id]
|
| burn_notice = "\n\n🔥 **This message has been BURNED and deleted forever!**"
|
| else:
|
| burn_notice = f"\n\n⚠️ **{message['reads_left']} reads remaining before burn**"
|
|
|
| return f"📖 **Message Content:**\n\n{content}{burn_notice}"
|
|
|
| def list_player_messages(self, player_id: str) -> str:
|
| """List messages created by player"""
|
| player_messages = [
|
| msg for msg in self.messages.values()
|
| if msg['creator'] == player_id
|
| ]
|
|
|
| if not player_messages:
|
| return "📭 You have no active messages"
|
|
|
| result = "📨 **Your Active Messages:**\n\n"
|
| for msg in player_messages:
|
| expires_in = int((msg['expires_at'] - time.time()) / 3600)
|
| result += f"🆔 `{msg['id']}` | ⏱️ {expires_in}h left | 👁️ {msg['reads_left']} reads\n"
|
|
|
| return result
|
|
|
| def get_player_message_history(self, player_id: str) -> List[List]:
|
| """Get message history for display"""
|
| player_messages = [
|
| msg for msg in self.messages.values()
|
| if msg['creator'] == player_id
|
| ]
|
|
|
| history = []
|
| for msg in player_messages:
|
| expires_in = int((msg['expires_at'] - time.time()) / 3600)
|
| status = "Active" if not msg['burned'] else "Burned"
|
| history.append([
|
| msg['id'][:8] + "...",
|
| datetime.fromtimestamp(msg['created_at']).strftime("%H:%M"),
|
| status,
|
| str(msg['reads_left'])
|
| ])
|
|
|
| return history
|
|
|
| def generate_message_id(self) -> str:
|
| """Generate a unique message ID"""
|
| import string
|
| chars = string.ascii_letters + string.digits
|
| return ''.join(random.choice(chars) for _ in range(12))
|
|
|
|
|
|
|
|
|
|
|
|
|
| loop = asyncio.new_event_loop()
|
| asyncio.set_event_loop(loop)
|
|
|
| class SimpleMCPClient:
|
| """MCP client for weather services"""
|
| def __init__(self):
|
| self.session = None
|
| self.connected = False
|
| self.tools = []
|
| self.exit_stack = None
|
| self.server_url = "https://chris4k-weather.hf.space/gradio_api/mcp/sse"
|
|
|
| def connect(self) -> str:
|
| """Connect to the hardcoded MCP server"""
|
| return loop.run_until_complete(self._connect())
|
|
|
| async def _connect(self) -> str:
|
| try:
|
|
|
| if self.exit_stack:
|
| await self.exit_stack.aclose()
|
|
|
| self.exit_stack = AsyncExitStack()
|
|
|
|
|
| sse_transport = await self.exit_stack.enter_async_context(
|
| sse_client(self.server_url)
|
| )
|
| read_stream, write_callable = sse_transport
|
|
|
| self.session = await self.exit_stack.enter_async_context(
|
| ClientSession(read_stream, write_callable)
|
| )
|
| await self.session.initialize()
|
|
|
|
|
| response = await self.session.list_tools()
|
| self.tools = response.tools
|
|
|
| self.connected = True
|
| tool_names = [tool.name for tool in self.tools]
|
| return f"✅ Connected to weather server!\nAvailable tools: {', '.join(tool_names)}"
|
|
|
| except Exception as e:
|
| self.connected = False
|
| return f"❌ Connection failed: {str(e)}"
|
|
|
| def get_weather(self, location: str) -> str:
|
| """Get weather for a location (city, country format)"""
|
| if not self.connected:
|
|
|
| connect_result = self.connect()
|
| if not self.connected:
|
| return f"❌ Failed to connect to weather server: {connect_result}"
|
|
|
| if not location.strip():
|
| return "❌ Please enter a location (e.g., 'Berlin, Germany')"
|
|
|
| return loop.run_until_complete(self._get_weather(location))
|
|
|
| async def _get_weather(self, location: str) -> str:
|
| try:
|
|
|
| if ',' in location:
|
| city, country = [part.strip() for part in location.split(',', 1)]
|
| else:
|
| city = location.strip()
|
| country = ""
|
|
|
|
|
| weather_tool = next((tool for tool in self.tools if 'weather' in tool.name.lower()), None)
|
| if not weather_tool:
|
| return "❌ Weather tool not found on server"
|
|
|
|
|
| params = {"city": city, "country": country}
|
| result = await self.session.call_tool(weather_tool.name, params)
|
|
|
|
|
| content_text = ""
|
| if hasattr(result, 'content') and result.content:
|
| if isinstance(result.content, list):
|
| for content_item in result.content:
|
| if hasattr(content_item, 'text'):
|
| content_text += content_item.text
|
| elif hasattr(content_item, 'content'):
|
| content_text += str(content_item.content)
|
| else:
|
| content_text += str(content_item)
|
| elif hasattr(result.content, 'text'):
|
| content_text = result.content.text
|
| else:
|
| content_text = str(result.content)
|
|
|
| if not content_text:
|
| return "❌ No content received from server"
|
|
|
| try:
|
|
|
| parsed = json.loads(content_text)
|
| if isinstance(parsed, dict):
|
| if 'error' in parsed:
|
| return f"❌ Error: {parsed['error']}"
|
|
|
|
|
| if 'current_weather' in parsed:
|
| weather = parsed['current_weather']
|
| formatted = f"🌍 **{parsed.get('location', 'Unknown')}**\n\n"
|
| formatted += f"🌡️ Temperature: {weather.get('temperature_celsius', 'N/A')}°C\n"
|
| formatted += f"🌤️ Conditions: {weather.get('weather_description', 'N/A')}\n"
|
| formatted += f"💨 Wind: {weather.get('wind_speed_kmh', 'N/A')} km/h\n"
|
| formatted += f"💧 Humidity: {weather.get('humidity_percent', 'N/A')}%\n"
|
| return formatted
|
| elif 'temperature (°C)' in parsed:
|
|
|
| formatted = f"🌍 **{parsed.get('location', 'Unknown')}**\n\n"
|
| formatted += f"🌡️ Temperature: {parsed.get('temperature (°C)', 'N/A')}°C\n"
|
| formatted += f"🌤️ Weather Code: {parsed.get('weather_code', 'N/A')}\n"
|
| formatted += f"🕐 Timezone: {parsed.get('timezone', 'N/A')}\n"
|
| formatted += f"🕒 Local Time: {parsed.get('local_time', 'N/A')}\n"
|
| return formatted
|
| else:
|
| return f"✅ Weather data:\n```json\n{json.dumps(parsed, indent=2)}\n```"
|
|
|
| except json.JSONDecodeError:
|
|
|
| return f"✅ Weather data:\n```\n{content_text}\n```"
|
|
|
| return f"✅ Raw result:\n{content_text}"
|
|
|
| except Exception as e:
|
| return f"❌ Failed to get weather: {str(e)}"
|
|
|
| class MCPAddonWrapper(NPCAddon):
|
| """Wrapper to use any MCP server as a game add-on"""
|
|
|
| def __init__(self, mcp_endpoint: str, addon_name: str):
|
| self.mcp_endpoint = mcp_endpoint
|
| self._addon_name = addon_name
|
| self._addon_id = f"mcp_{addon_name.lower().replace(' ', '_')}"
|
|
|
|
|
| self.mcp_client = SimpleMCPClient()
|
|
|
| try:
|
| connection_result = self.mcp_client.connect()
|
| print(f"[MCP] {addon_name} connection: {connection_result}")
|
| except Exception as e:
|
| print(f"[MCP] Failed to connect {addon_name}: {e}")
|
|
|
| @property
|
| def addon_id(self) -> str:
|
| return self._addon_id
|
|
|
| @property
|
| def addon_name(self) -> str:
|
| return self._addon_name
|
|
|
| def get_interface(self) -> gr.Component:
|
| with gr.Column() as interface:
|
| gr.Markdown(f"## 🌤️ {self.addon_name}")
|
| gr.Markdown("*Ask for weather in any city! Format: 'City, Country' (e.g., 'Berlin, Germany')*")
|
|
|
|
|
| connection_status = gr.HTML(
|
| value="<div style='color: green;'>🟢 Auto-connecting to weather server...</div>"
|
| )
|
|
|
| location_input = gr.Textbox(
|
| label="Location",
|
| placeholder="e.g., Berlin, Germany",
|
| lines=1
|
| )
|
|
|
| weather_output = gr.Textbox(
|
| label="Weather Information",
|
| lines=8,
|
| interactive=False
|
| )
|
|
|
| get_weather_btn = gr.Button("🌡️ Get Weather", variant="primary")
|
|
|
|
|
| with gr.Row():
|
| gr.Examples(
|
| examples=[
|
| ["Berlin, Germany"],
|
| ["Tokyo, Japan"],
|
| ["New York, USA"],
|
| ["London, UK"],
|
| ["Sydney, Australia"]
|
| ],
|
| inputs=[location_input]
|
| )
|
|
|
| def handle_weather_request(location: str):
|
|
|
| current_players = list(game_world.players.keys())
|
| if not current_players:
|
| return "❌ No players in the game! Please join the game first!"
|
|
|
| if not location.strip():
|
| return "❌ Please enter a location (e.g., 'Berlin, Germany')"
|
|
|
|
|
| player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
|
|
|
|
|
| result = self.mcp_client.get_weather(location)
|
|
|
|
|
| player_name = game_world.players[player_id].name
|
| print(f"[WEATHER] {player_name} requested weather for {location}")
|
|
|
| return result
|
|
|
| get_weather_btn.click(
|
| handle_weather_request,
|
| inputs=[location_input],
|
| outputs=[weather_output]
|
| )
|
|
|
| location_input.submit(
|
| handle_weather_request,
|
| inputs=[location_input],
|
| outputs=[weather_output]
|
| )
|
|
|
| return interface
|
|
|
| def handle_command(self, player_id: str, command: str) -> str:
|
| """Handle weather commands from private messages"""
|
|
|
| clean_command = command.strip()
|
| if clean_command.startswith('/'):
|
| clean_command = clean_command[1:]
|
|
|
|
|
| if not clean_command:
|
| return "🌤️ Weather Oracle: Please tell me a location! Format: 'City, Country' (e.g., 'Berlin, Germany')"
|
|
|
|
|
| result = self.mcp_client.get_weather(clean_command)
|
|
|
|
|
| if player_id in game_world.players:
|
| player_name = game_world.players[player_id].name
|
| print(f"[WEATHER_PM] {player_name} requested weather for {clean_command}")
|
|
|
| return f"🌤️ **Weather Oracle**: {result}"
|
|
|
|
|
|
|
|
|
|
|
| class GradioMCPTools:
|
| """MCP tools integrated directly into Gradio app"""
|
|
|
| def __init__(self, game_world: GameWorld):
|
| self.game_world = game_world
|
| self.ai_agents: Dict[str, Player] = {}
|
|
|
| def register_ai_agent(self, agent_name: str, mcp_client_id: str = None) -> str:
|
| """Register an AI agent as a player"""
|
| if mcp_client_id is None:
|
| mcp_client_id = f"ai_{uuid.uuid4().hex[:8]}"
|
|
|
| agent_id = f"ai_{uuid.uuid4().hex[:8]}"
|
|
|
| agent_player = Player(
|
| id=agent_id,
|
| name=agent_name,
|
| type="ai_agent",
|
| x=random.randint(50, 450),
|
| y=random.randint(50, 350),
|
| ai_agent_id=mcp_client_id,
|
| last_active=time.time()
|
| )
|
|
|
| if self.game_world.add_player(agent_player):
|
| self.ai_agents[mcp_client_id] = agent_player
|
| return agent_id
|
| else:
|
| raise Exception("Game is full, cannot add AI agent")
|
|
|
| def move_ai_agent(self, mcp_client_id: str, direction: str) -> Dict:
|
| """Move AI agent in the game world"""
|
| if mcp_client_id not in self.ai_agents:
|
| return {"error": "AI agent not registered"}
|
|
|
| agent = self.ai_agents[mcp_client_id]
|
| success = self.game_world.move_player(agent.id, direction)
|
|
|
| return {
|
| "success": success,
|
| "new_position": {"x": agent.x, "y": agent.y},
|
| "nearby_players": self.get_nearby_entities(agent.id),
|
| "world_events": self.game_world.world_events[-5:]
|
| }
|
|
|
| def ai_agent_chat(self, mcp_client_id: str, message: str) -> Dict:
|
| """AI agent sends chat message"""
|
| if mcp_client_id not in self.ai_agents:
|
| return {"error": "AI agent not registered"}
|
|
|
| agent = self.ai_agents[mcp_client_id]
|
| self.game_world.add_chat_message(f"🤖 {agent.name}", message)
|
|
|
| return {"success": True, "message": "Chat message sent"}
|
|
|
| def get_game_state_for_ai(self, mcp_client_id: str) -> Dict:
|
| """Get current game state for AI agent"""
|
| if mcp_client_id not in self.ai_agents:
|
| return {"error": "AI agent not registered"}
|
|
|
| agent = self.ai_agents[mcp_client_id]
|
|
|
| return {
|
| "agent_status": asdict(agent),
|
| "nearby_entities": self.get_nearby_entities(agent.id),
|
| "recent_chat": self.game_world.chat_messages[-10:],
|
| "world_events": self.game_world.world_events[-5:],
|
| "available_npcs": list(self.game_world.npcs.keys())
|
| }
|
|
|
| def get_nearby_entities(self, player_id: str, radius: int = 100) -> List[Dict]:
|
| """Get entities near a player"""
|
| player = self.game_world.players.get(player_id)
|
| if not player:
|
| return []
|
|
|
| nearby = []
|
|
|
|
|
| for other_player in self.game_world.players.values():
|
| if other_player.id == player_id:
|
| continue
|
|
|
| distance = ((player.x - other_player.x)**2 + (player.y - other_player.y)**2)**0.5
|
| if distance <= radius:
|
| nearby.append({
|
| "type": "player",
|
| "name": other_player.name,
|
| "player_type": other_player.type,
|
| "position": {"x": other_player.x, "y": other_player.y},
|
| "distance": round(distance, 1)
|
| })
|
|
|
|
|
| for npc in self.game_world.npcs.values():
|
| distance = ((player.x - npc['x'])**2 + (player.y - npc['y'])**2)**0.5
|
| if distance <= radius:
|
| nearby.append({
|
| "type": "npc",
|
| "name": npc['name'],
|
| "npc_id": npc['id'],
|
| "position": {"x": npc['x'], "y": npc['y']},
|
| "distance": round(distance, 1)
|
| })
|
|
|
| return nearby
|
|
|
|
|
| mcp_tools = GradioMCPTools(game_world)
|
|
|
|
|
|
|
|
|
|
|
| def get_player_id_from_session(session_hash: str) -> Optional[str]:
|
| """Get player ID from session hash"""
|
| for player in game_world.players.values():
|
| if player.session_hash == session_hash:
|
| return player.id
|
| return None
|
|
|
| def create_game_world_html() -> str:
|
| """Render the game world as HTML"""
|
| html = f"""
|
| <div style="width: 500px; height: 400px; background: linear-gradient(45deg, #2d5a27, #3d6b37);
|
| position: relative; border: 2px solid #8B4513; border-radius: 10px; margin: 10px auto;
|
| box-shadow: 0 4px 8px rgba(0,0,0,0.3);">
|
| """
|
|
|
|
|
| html += """
|
| <div style="position: absolute; width: 100%; height: 100%;
|
| background-image:
|
| linear-gradient(rgba(255,255,255,0.1) 1px, transparent 1px),
|
| linear-gradient(90deg, rgba(255,255,255,0.1) 1px, transparent 1px);
|
| background-size: 25px 25px;">
|
| </div>
|
| """
|
|
|
|
|
| for npc in game_world.npcs.values():
|
| html += f"""
|
| <div style="position: absolute; left: {npc['x']}px; top: {npc['y']}px;
|
| font-size: 24px; text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
| transition: all 0.3s ease;">
|
| {npc['char']}
|
| </div>
|
| <div style="position: absolute; left: {npc['x']-10}px; top: {npc['y']-20}px;
|
| background: rgba(0,0,0,0.7); color: white; padding: 2px 6px;
|
| border-radius: 3px; font-size: 10px; font-weight: bold;">{npc['name']}</div>
|
| """
|
|
|
|
|
| for player in game_world.players.values():
|
| char = "🤖" if player.type == "ai_agent" else "🧝♂️"
|
| color = "gold" if player.type == "ai_agent" else "lightblue"
|
|
|
| html += f"""
|
| <div style="position: absolute; left: {player.x}px; top: {player.y}px;
|
| font-size: 20px; text-shadow: 2px 2px 4px rgba(0,0,0,0.8);
|
| transition: all 0.3s ease; z-index: 10;">
|
| {char}
|
| </div>
|
| <div style="position: absolute; left: {player.x-15}px; top: {player.y-20}px;
|
| background: rgba(0,0,0,0.8); color: {color}; padding: 2px 6px;
|
| border-radius: 3px; font-size: 9px; font-weight: bold; z-index: 11;">
|
| {player.name} (Lv.{player.level})
|
| </div>
|
| """
|
|
|
| html += "</div>"
|
| return html
|
|
|
| def get_player_stats_display(player_id: str) -> Dict:
|
| """Get formatted player stats for display"""
|
| if not player_id or player_id not in game_world.players:
|
| return {"status": "❌ Not connected", "info": "Join the game to see your stats"}
|
|
|
| player = game_world.players[player_id]
|
| return {
|
| "status": "🟢 Connected",
|
| "name": player.name,
|
| "type": player.type,
|
| "level": player.level,
|
| "hp": f"{player.hp}/{player.max_hp}",
|
| "gold": player.gold,
|
| "experience": f"{player.experience}/{player.level * 100}",
|
| "position": f"({player.x}, {player.y})",
|
| "last_update": time.strftime("%H:%M:%S"),
|
| "session_id": player.id[:8] + "..."
|
| }
|
|
|
|
|
|
|
|
|
|
|
| def create_mmorpg_interface():
|
| """Create the complete MMORPG interface with all features"""
|
|
|
|
|
| read2burn_addon = Read2BurnMailboxAddon()
|
| game_world.addon_npcs['read2burn_mailbox'] = read2burn_addon
|
|
|
|
|
| weather_mcp_addon = MCPAddonWrapper("http://localhost:8001/mcp", "Weather Oracle")
|
| game_world.addon_npcs['weather_oracle'] = weather_mcp_addon
|
|
|
| with gr.Blocks(
|
| title="🎮 MMORPG with Complete MCP Integration"
|
| ) as demo:
|
|
|
| keyboard_status = gr.HTML(
|
| value="<div id='keyboard-status' style='background:#e8f5e8;border:1px solid #4caf50;padding:8px;border-radius:4px;margin:8px 0;'>🎮 Keyboard loading...</div>"
|
| )
|
|
|
| gr.HTML(r"""
|
| <script>
|
| const gameKeyboard = { enabled: false, buttons: {} };
|
| function initKeyboard() {
|
| const btns = document.querySelectorAll('button');
|
| btns.forEach(btn => {
|
| const t = btn.textContent.trim();
|
| if (t==='↑') gameKeyboard.buttons.up=btn;
|
| if (t==='↓') gameKeyboard.buttons.down=btn;
|
| if (t==='←') gameKeyboard.buttons.left=btn;
|
| if (t==='→') gameKeyboard.buttons.right=btn;
|
| if (t.includes('⚔️')) gameKeyboard.buttons.action=btn;
|
| });
|
| const status = document.getElementById('keyboard-status');
|
| if (Object.keys(gameKeyboard.buttons).length>=4) {
|
| status.innerHTML='<span style="color:#2e7d32;">🎮 Keyboard ready! Use WASD or arrows</span>';
|
| gameKeyboard.enabled=true;
|
| } else {
|
| status.innerHTML='<span style="color:#d32f2f;">❌ Keyboard init...found '+Object.keys(gameKeyboard.buttons).length+'/4</span>';
|
| }
|
| }
|
| document.addEventListener('DOMContentLoaded',()=>{ initKeyboard(); });
|
| document.addEventListener('keydown',e=>{
|
| if (!gameKeyboard.enabled) return;
|
| const tag=e.target.tagName.toLowerCase(); if (tag==='input'||tag==='textarea') return;
|
| let btn;
|
| switch(e.code){
|
| case'ArrowUp':case'KeyW':btn=gameKeyboard.buttons.up;break;
|
| case'ArrowDown':case'KeyS':btn=gameKeyboard.buttons.down;break;
|
| case'ArrowLeft':case'KeyA':btn=gameKeyboard.buttons.left;break;
|
| case'ArrowRight':case'KeyD':btn=gameKeyboard.buttons.right;break;
|
| case'Space':btn=gameKeyboard.buttons.action;break;
|
| }
|
| if (btn){ e.preventDefault(); btn.click(); initKeyboard(); }
|
| });
|
| </script>
|
| """)
|
|
|
| gr.Markdown("""
|
| <div style="text-align: center; padding: 20px; background: linear-gradient(90deg, #667eea 0%, #764ba2 100%);
|
| color: white; border-radius: 10px; margin-bottom: 20px;">
|
| <h1>🎮 MMORPG with Complete MCP Integration</h1>
|
| <p><strong>All features restored! Keyboard controls, NPCs, MCP, Read2Burn & more!</strong></p>
|
| <p>🌟 Real-time multiplayer • 🤖 AI agent support • 🔥 Read2Burn messaging • 🔌 MCP add-ons • ⌨️ Keyboard controls</p>
|
| </div>
|
| """)
|
|
|
|
|
| player_state = gr.State({})
|
|
|
| with gr.Tabs():
|
|
|
|
|
| with gr.Tab("🌍 Game World"):
|
| with gr.Row():
|
| with gr.Column(scale=2):
|
|
|
| with gr.Group():
|
| gr.Markdown("### 🎮 Join the Adventure")
|
| with gr.Row():
|
| player_name = gr.Textbox(
|
| label="Player Name",
|
| placeholder="Enter your character name",
|
| scale=3
|
| )
|
| join_btn = gr.Button("Join Game", variant="primary", scale=1)
|
| leave_btn = gr.Button("Leave Game", variant="secondary", scale=1)
|
|
|
|
|
| gr.Markdown("""
|
| ### ⌨️ Controls
|
| **Mouse:** Click movement buttons below
|
| **Keyboard:** Use **WASD** or **Arrow Keys** for movement
|
| 💡 *Press F12 → Console to see keyboard debug info*
|
| """)
|
|
|
|
|
| game_view = gr.HTML(
|
| value=create_game_world_html()
|
| )
|
|
|
|
|
| gr.Markdown("### 🕹️ Movement Controls")
|
| with gr.Row():
|
| gr.HTML("")
|
| move_up = gr.Button("↑", size="lg", scale=1)
|
| gr.HTML("")
|
| with gr.Row():
|
| move_left = gr.Button("←", size="lg", scale=1)
|
| action_btn = gr.Button("⚔️", size="lg", scale=1, variant="secondary")
|
| move_right = gr.Button("→", size="lg", scale=1)
|
| with gr.Row():
|
| gr.HTML("")
|
| move_down = gr.Button("↓", size="lg", scale=1)
|
| gr.HTML("")
|
|
|
| with gr.Column(scale=1):
|
|
|
| player_info = gr.JSON(
|
| label="🧝♂️ Player Stats",
|
| value={"status": "Not connected", "info": "Join the game to see your stats"}
|
| )
|
|
|
|
|
| online_players = gr.Dataframe(
|
| headers=["Name", "Type", "Level"],
|
| label="👥 Online Players",
|
| interactive=False
|
| )
|
|
|
|
|
| world_events = gr.Textbox(
|
| label="🌍 World Events & NPC Interactions",
|
| lines=4,
|
| interactive=False,
|
| placeholder="World events will appear here...\n\n💡 Tip: Walk near NPCs (📮🏪🌤️) to interact with them!\nThen visit the 'NPC Add-ons' tab to use their features."
|
| )
|
|
|
|
|
| with gr.Row():
|
| with gr.Column(scale=4):
|
| chat_display = gr.Chatbot(
|
| label="💬 Game Chat", height=200,
|
| type='messages',
|
| value=[{"role": "assistant", "content": "Welcome! Join the game to start chatting!"}]
|
| )
|
|
|
| with gr.Row():
|
| chat_input = gr.Textbox(
|
| placeholder="Type your message...",
|
| scale=4,
|
| container=False
|
| )
|
| chat_send = gr.Button("Send", scale=1, variant="primary")
|
|
|
| with gr.Column(scale=2):
|
|
|
| with gr.Row():
|
| auto_refresh_enabled = gr.Checkbox(
|
| label="Auto-refresh (2s)",
|
| value=True,
|
| info="Toggle to preserve manual selections"
|
| )
|
|
|
| proximity_info = gr.HTML(
|
| value="<div style='text-align: center; color: #666;'>🔍 Move near NPCs or players to chat privately</div>",
|
| label="📱 Nearby Entities"
|
| )
|
|
|
|
|
| with gr.Group(visible=False) as private_chat_group:
|
| nearby_entities = gr.Dropdown(
|
| label="💬 Start new chat with",
|
| choices=[],
|
| interactive=True
|
| )
|
|
|
| with gr.Row():
|
| start_chat_btn = gr.Button("Start Chat", variant="primary", scale=1)
|
| clear_all_tabs_btn = gr.Button("Clear All", variant="secondary", scale=1)
|
|
|
|
|
| with gr.Column() as chat_tabs_container:
|
|
|
| chat_tabs_state = gr.State({})
|
| active_tabs_display = gr.HTML(
|
| value="<div style='text-align: center; color: #666; padding: 10px;'>No active chats</div>",
|
| label="Active Chats"
|
| )
|
|
|
|
|
| current_chat_display = gr.Chatbot(
|
| label="🔒 Private Messages",
|
| height=150,
|
| type='messages',
|
| value=[],
|
| visible=False
|
| )
|
|
|
| with gr.Row(visible=False) as chat_input_row:
|
| private_message_input = gr.Textbox(
|
| placeholder="Type private message...",
|
| scale=4,
|
| container=False
|
| )
|
| private_send_btn = gr.Button("Send", scale=1, variant="secondary")
|
|
|
|
|
| with gr.Tab("🤖 NPC Add-ons"):
|
| gr.Markdown("## Available NPC Add-ons")
|
| gr.Markdown("*Extensible plugin system - each NPC can have unique functionality!*")
|
|
|
| with gr.Tabs() as addon_tabs:
|
|
|
| with gr.Tab("🔥 Read2Burn Mailbox"):
|
| read2burn_addon.get_interface()
|
|
|
|
|
| with gr.Tab("🌤️ Weather Oracle (MCP)"):
|
| weather_mcp_addon.get_interface()
|
|
|
|
|
| with gr.Tab("⚙️ Manage Add-ons"):
|
| gr.Markdown("### Install New Add-ons")
|
|
|
| with gr.Row():
|
| addon_type = gr.Dropdown(
|
| choices=["Python Plugin", "MCP Server", "Web Service"],
|
| label="Add-on Type",
|
| value="MCP Server"
|
| )
|
|
|
| with gr.Row():
|
| addon_url = gr.Textbox(
|
| label="Add-on URL/Path",
|
| placeholder="http://localhost:8001/mcp or path/to/plugin.py"
|
| )
|
|
|
| with gr.Row():
|
| addon_name = gr.Textbox(
|
| label="Display Name",
|
| placeholder="My Custom Add-on"
|
| )
|
|
|
| install_btn = gr.Button("Install Add-on", variant="primary")
|
| install_status = gr.Textbox(label="Status", interactive=False)
|
|
|
| def install_addon(addon_type: str, addon_url: str, addon_name: str):
|
| if not addon_url or not addon_name:
|
| return "❌ Please provide both URL and name"
|
|
|
| if addon_type == "MCP Server":
|
| try:
|
| new_addon = MCPAddonWrapper(addon_url, addon_name)
|
| game_world.addon_npcs[new_addon.addon_id] = new_addon
|
| return f"✅ Successfully installed MCP add-on '{addon_name}'"
|
| except Exception as e:
|
| return f"❌ Failed to install add-on: {str(e)}"
|
| else:
|
| return f"🚧 {addon_type} installation not yet implemented"
|
|
|
| install_btn.click(
|
| install_addon,
|
| inputs=[addon_type, addon_url, addon_name],
|
| outputs=[install_status]
|
| )
|
|
|
|
|
| with gr.Tab("🔌 MCP Integration"):
|
| gr.Markdown("""
|
| ## MCP Integration for AI Agents
|
|
|
| AI agents can connect to this game via MCP and participate as players!
|
|
|
| ### Available MCP Tools:
|
| - `register_ai_agent(name)` - Join the game as an AI player
|
| - `move_agent(direction)` - Move around the world
|
| - `send_chat(message)` - Chat with other players
|
| - `get_game_state()` - Get current world information
|
| - `interact_with_npc(npc_id, message)` - Use NPC add-ons
|
|
|
| ### API Endpoints (when launched with api_open=True):
|
| - `/api/register_ai_agent` - Register new AI agent
|
| - `/api/move_ai_agent` - Move AI agent
|
| - `/api/ai_agent_chat` - Send chat message
|
| - `/api/get_game_state_for_ai` - Get game state
|
| """)
|
|
|
|
|
| mcp_info = gr.JSON(
|
| label="MCP Server Information",
|
| value={
|
| "server_status": "🟢 Active",
|
| "server_url": "This Gradio app serves as MCP server",
|
| "tools_available": [
|
| "register_ai_agent",
|
| "move_agent",
|
| "send_chat",
|
| "get_game_state",
|
| "interact_with_npc"
|
| ],
|
| "active_ai_agents": 0,
|
| "total_players": 0
|
| }
|
| )
|
|
|
|
|
| with gr.Group():
|
| gr.Markdown("### 🧪 Test AI Agent")
|
| gr.Markdown("*Use this to simulate AI agent connections for testing*")
|
|
|
| with gr.Row():
|
| ai_name = gr.Textbox(
|
| label="AI Agent Name",
|
| placeholder="Claude the Explorer",
|
| scale=3
|
| )
|
| register_ai_btn = gr.Button("Register AI Agent", variant="primary", scale=1)
|
|
|
| with gr.Row():
|
| ai_action = gr.Dropdown(
|
| choices=["move up", "move down", "move left", "move right", "chat"],
|
| label="AI Action",
|
| scale=2
|
| )
|
|
|
| ai_message = gr.Textbox(
|
| label="AI Message (for chat)",
|
| placeholder="Hello humans!",
|
| scale=3
|
| )
|
|
|
| execute_ai_btn = gr.Button("Execute AI Action", variant="secondary")
|
| ai_result = gr.Textbox(label="AI Action Result", interactive=False, lines=5)
|
|
|
|
|
|
|
|
|
|
|
|
|
| keyboard_js = """
|
| <script>
|
| let gameKeyboard = {
|
| enabled: false,
|
| buttons: {},
|
|
|
| init: function() {
|
| // Find movement buttons after DOM loads
|
| setTimeout(function() {
|
| var buttons = document.querySelectorAll('button');
|
| buttons.forEach(function(btn) {
|
| var text = btn.textContent.trim();
|
| if (text === '↑') gameKeyboard.buttons.up = btn;
|
| else if (text === '↓') gameKeyboard.buttons.down = btn;
|
| else if (text === '←') gameKeyboard.buttons.left = btn;
|
| else if (text === '→') gameKeyboard.buttons.right = btn;
|
| else if (text.indexOf('⚔️') !== -1) gameKeyboard.buttons.action = btn;
|
| });
|
|
|
| // Update status indicator
|
| const foundButtons = Object.keys(gameKeyboard.buttons).length;
|
| const statusDiv = document.querySelector('div[style*="background: #e8f5e8"]');
|
| if (statusDiv) {
|
| if (foundButtons >= 4) {
|
| statusDiv.innerHTML = '<span style="color: #2e7d32;">🎮 Keyboard controls ready! Use WASD or Arrow Keys</span>';
|
| gameKeyboard.enabled = true;
|
| console.log('🎮 Game keyboard initialized - found', foundButtons, 'buttons');
|
| } else {
|
| statusDiv.innerHTML = '<span style="color: #d32f2f;">❌ Keyboard loading... found ' + foundButtons + '/4 buttons</span>';
|
| console.log('⏳ Still looking for movement buttons, found:', foundButtons);
|
| }
|
| }
|
| }, 1000);
|
| },
|
|
|
| handleKey: function(event) {
|
| if (!gameKeyboard.enabled) return;
|
|
|
| // Only capture keys when not typing in inputs
|
| var tagName = event.target.tagName.toLowerCase();
|
| if (tagName === 'input' || tagName === 'textarea') {
|
| return;
|
| }
|
|
|
| var button = null;
|
|
|
| switch(event.code) {
|
| case 'ArrowUp':
|
| case 'KeyW':
|
| button = gameKeyboard.buttons.up;
|
| break;
|
| case 'ArrowDown':
|
| case 'KeyS':
|
| button = gameKeyboard.buttons.down;
|
| break;
|
| case 'ArrowLeft':
|
| case 'KeyA':
|
| button = gameKeyboard.buttons.left;
|
| break;
|
| case 'ArrowRight':
|
| case 'KeyD':
|
| button = gameKeyboard.buttons.right;
|
| break;
|
| case 'Space':
|
| button = gameKeyboard.buttons.action;
|
| break;
|
| }
|
|
|
| if (button) {
|
| event.preventDefault();
|
| button.click();
|
|
|
| // Visual feedback
|
| button.style.backgroundColor = '#ff6b6b';
|
| button.style.transform = 'scale(0.95)';
|
| setTimeout(function() {
|
| button.style.backgroundColor = '';
|
| button.style.transform = '';
|
| }, 150);
|
|
|
| // Update status
|
| const statusDiv = document.querySelector('div[style*="background: #e8f5e8"]');
|
| if (statusDiv) {
|
| statusDiv.innerHTML = '<span style="color: #2e7d32;">🎮 Last key: ' + event.code + ' ✓</span>';
|
| }
|
|
|
| console.log('🎯 Key pressed: ' + event.code);
|
| }
|
| }
|
| };
|
|
|
| // Initialize when page loads
|
| document.addEventListener('DOMContentLoaded', function() {
|
| gameKeyboard.init();
|
| });
|
|
|
| // Add keyboard listener
|
| document.addEventListener('keydown', function(e) {
|
| gameKeyboard.handleKey(e);
|
| });
|
|
|
| // Reinitialize when Gradio updates (for dynamic content)
|
| setInterval(function() {
|
| if (!gameKeyboard.enabled) {
|
| gameKeyboard.init();
|
| }
|
| }, 2000);
|
| </script>
|
| """
|
|
|
| gr.HTML(keyboard_js)
|
|
|
|
|
|
|
|
|
|
|
| def join_game(name: str, current_state: Dict, request: gr.Request):
|
| """Handle player joining the game"""
|
| print(f"[JOIN] Called with name='{name}', state={current_state}")
|
|
|
| if not name.strip():
|
| return (
|
| current_state,
|
| {"status": "❌ Error", "info": "Please enter a valid name"},
|
| create_game_world_html(),
|
| [],
|
| "Enter a player name to join!"
|
| )
|
|
|
|
|
| if current_state.get("player_id"):
|
| player_id = current_state["player_id"]
|
| if player_id in game_world.players:
|
| player_stats = get_player_stats_display(player_id)
|
| return (
|
| current_state,
|
| player_stats,
|
| create_game_world_html(),
|
| [[p.name, p.type, p.level] for p in game_world.players.values()],
|
| "Already connected!"
|
| )
|
|
|
|
|
| player_id = str(uuid.uuid4())
|
| player = Player(
|
| id=player_id,
|
| name=name.strip(),
|
| type="human",
|
| session_hash=request.session_hash,
|
| last_active=time.time()
|
| )
|
|
|
| if game_world.add_player(player):
|
| new_state = {"player_id": player_id, "player_name": name}
|
| player_display = get_player_stats_display(player_id)
|
|
|
| world_html = create_game_world_html()
|
| players_list = [
|
| [p.name, p.type, p.level]
|
| for p in game_world.players.values()
|
| ]
|
| events = "\n".join([
|
| f"{e.get('event', '')}"
|
| for e in game_world.world_events[-5:]
|
| ])
|
|
|
| proximity_html, private_chat_visible, entity_choices, private_messages = get_proximity_status(new_state)
|
|
|
| return (new_state, player_display, world_html, players_list, events,
|
| proximity_html, private_chat_visible, entity_choices, private_messages)
|
| else:
|
|
|
| return (
|
| current_state,
|
| {"status": "❌ Error", "info": "Game is full (20/20 players)"},
|
| create_game_world_html(),
|
| [],
|
| "Game is full!",
|
| "<div style='text-align: center; color: #666;'>🔍 Join the game to see nearby entities</div>",
|
| gr.update(visible=False),
|
| gr.update(choices=[]),
|
| [] )
|
|
|
| def leave_game(current_state: Dict):
|
| """Handle player leaving the game"""
|
| if current_state and current_state.get("player_id"):
|
| player_id = current_state["player_id"]
|
| game_world.remove_player(player_id)
|
| return (
|
| {},
|
| {"status": "Not connected", "info": "Join the game to see your stats"},
|
| create_game_world_html(),
|
| [[p.name, p.type, p.level] for p in game_world.players.values()],
|
| "\n".join([e.get('event', '') for e in game_world.world_events[-5:]]),
|
| "<div style='text-align: center; color: #666;'>🔍 Join the game to see nearby entities</div>",
|
| gr.update(visible=False),
|
| gr.update(choices=[]),
|
| []
|
| )
|
| else:
|
| return (
|
| current_state,
|
| {"status": "Not connected", "info": "You're not in the game"},
|
| create_game_world_html(),
|
| [],
|
| "Not connected",
|
| "<div style='text-align: center; color: #666;'>🔍 Join the game to see nearby entities</div>",
|
| gr.update(visible=False),
|
| gr.update(choices=[]),
|
| [] )
|
|
|
| def handle_movement(direction: str, current_state: Dict):
|
| """Handle player movement"""
|
| print(f"[MOVE] Direction: {direction}, state: {current_state}")
|
| player_id = current_state.get("player_id")
|
| if not player_id:
|
| return current_state, create_game_world_html(), [], get_player_stats_display(None)
|
|
|
| success = game_world.move_player(player_id, direction)
|
| if success:
|
|
|
| world_html = create_game_world_html()
|
| players_list = [
|
| [p.name, p.type, p.level]
|
| for p in game_world.players.values()
|
| ]
|
| player_stats = get_player_stats_display(player_id)
|
|
|
|
|
| proximity_html, private_chat_visible, entity_choices, private_messages = get_proximity_status(current_state)
|
|
|
| return (current_state, world_html, players_list, player_stats,
|
| proximity_html, private_chat_visible, entity_choices, private_messages)
|
|
|
| proximity_html, private_chat_visible, entity_choices, private_messages = get_proximity_status(current_state)
|
| return (current_state, create_game_world_html(), [], get_player_stats_display(player_id),
|
| proximity_html, private_chat_visible, entity_choices, private_messages)
|
|
|
| def handle_chat_command(message: str, player: Player, player_id: str) -> str:
|
| """Handle chat commands like /heal, /stats, etc."""
|
| parts = message.split()
|
| command = parts[0].lower()
|
|
|
| if command == "/heal":
|
|
|
| if player.health < 100:
|
| old_health = player.health
|
| player.health = min(100, player.health + 25)
|
| game_world.add_world_event(f"✨ {player.name} used healing magic!")
|
| return f"💚 {player.name} healed for {player.health - old_health} HP! Health: {player.health}/100"
|
| else:
|
| return f"💚 {player.name} is already at full health!"
|
|
|
| elif command == "/stats":
|
|
|
| return f"📊 {player.name} - Health: {player.health}/100, Position: ({player.x}, {player.y}), Type: {player.type}"
|
|
|
| elif command == "/time":
|
|
|
| return f"🕐 Current time: {time.strftime('%H:%M:%S')}"
|
|
|
| elif command == "/players":
|
|
|
| players_list = [p.name for p in game_world.players.values()]
|
| return f"👥 Online players ({len(players_list)}): {', '.join(players_list)}"
|
|
|
| elif command == "/npcs":
|
|
|
| nearby_npcs = []
|
| for npc_id, npc in game_world.npcs.items():
|
| distance = ((player.x - npc['x'])**2 + (player.y - npc['y'])**2)**0.5
|
| if distance <= 100:
|
| nearby_npcs.append(f"{npc['name']} ({npc['type']})")
|
| if nearby_npcs:
|
| return f"🤖 Nearby NPCs: {', '.join(nearby_npcs)}"
|
| else:
|
| return "🤖 No NPCs nearby. Move around to find them!"
|
|
|
| elif command == "/help":
|
|
|
| return """📖 Available Commands:
|
| /heal - Restore 25 HP
|
| /stats - Show your player stats
|
| /time - Show current time
|
| /players - List online players
|
| /npcs - List nearby NPCs
|
| /help - Show this help"""
|
|
|
| else:
|
| return f"❓ Unknown command: {command}. Use /help to see available commands."
|
|
|
| def handle_chat(message: str, history: List, current_state: Dict):
|
| """Handle chat messages and commands"""
|
| if not message.strip():
|
| return history, ""
|
|
|
| player_id = current_state.get("player_id")
|
| if not player_id:
|
| return history, ""
|
|
|
| player = game_world.players.get(player_id)
|
| if not player:
|
| return history, ""
|
|
|
|
|
| if message.startswith('/'):
|
| command_result = handle_chat_command(message, player, player_id)
|
| if command_result:
|
| game_world.add_chat_message("🎮 System", command_result)
|
| else:
|
|
|
| game_world.add_chat_message(player.name, message.strip())
|
|
|
|
|
| formatted_history = []
|
| for msg in game_world.chat_messages[-20:]: formatted_history.append({
|
| "role": "assistant",
|
| "content": f"[{msg['timestamp']}] {msg['sender']}: {msg['message']}"
|
| })
|
|
|
| return formatted_history, ""
|
|
|
| def get_updated_private_messages(player_id: str):
|
| """Get formatted private messages for display"""
|
| if not player_id:
|
| return []
|
|
|
| private_messages = game_world.get_private_messages_for_player(player_id)
|
| formatted_private = []
|
| for msg in private_messages:
|
| formatted_private.append({
|
| "role": "assistant",
|
| "content": f"[{msg['timestamp']}] {msg['sender']}: {msg['message']}"
|
| })
|
|
|
| print(f"[PRIVATE_UPDATE] Returning {len(formatted_private)} private messages for player {player_id}")
|
| return formatted_private
|
|
|
| def handle_private_message(message: str, current_state: Dict, chat_tabs_state: Dict):
|
| """Handle private messages using the active chat tab"""
|
| if not message.strip():
|
| return [], ""
|
|
|
| if not current_state.get("player_id"):
|
| return [], ""
|
|
|
| if not chat_tabs_state:
|
| return [], ""
|
|
|
| print(f"[PRIVATE_MESSAGE_DEBUG] Chat tabs state: {chat_tabs_state}")
|
|
|
| active_entity_id = None
|
| for entity_id, tab_info in chat_tabs_state.items():
|
| print(f"[PRIVATE_MESSAGE_DEBUG] Checking tab: entity_id={entity_id}, tab_info={tab_info}")
|
| if tab_info.get('active', False):
|
| active_entity_id = entity_id
|
| break
|
|
|
| print(f"[PRIVATE_MESSAGE_DEBUG] Active entity_id: {active_entity_id}")
|
|
|
| if not active_entity_id:
|
| return [], ""
|
|
|
| player_id = current_state["player_id"]
|
| print(f"[PRIVATE_MESSAGE_DEBUG] Sending message from player {player_id} to entity {active_entity_id}: '{message.strip()}'")
|
|
|
| success, error_message = game_world.send_private_message(player_id, active_entity_id, message.strip())
|
|
|
| if success:
|
|
|
| updated_messages = get_chat_messages_for_entity(player_id, active_entity_id)
|
| return updated_messages, ""
|
| else:
|
| return [], ""
|
|
|
| def get_chat_messages_for_entity(player_id: str, entity_id: str) -> List:
|
| """Get chat messages for a specific entity"""
|
| messages = []
|
| for msg in game_world.chat_messages:
|
| msg_type = msg.get('type', '')
|
|
|
| if msg_type in ['private_to_npc', 'private_from_npc', 'private_to_player', 'private_from_player']:
|
|
|
| if ((msg.get('sender_id') == player_id and msg.get('target') == entity_id) or
|
| (msg.get('sender_id') == entity_id and msg.get('target') == player_id)):
|
| messages.append({
|
| "role": "assistant" if msg.get('sender_id') != player_id else "user",
|
| "content": f"[{msg['timestamp']}] {msg['sender']}: {msg['message']}"
|
| })
|
|
|
| print(f"[CHAT_MESSAGES] Found {len(messages)} messages between player {player_id} and entity {entity_id}")
|
| return messages
|
|
|
| def start_new_chat(entity_id: str, entity_name: str, chat_tabs_state: Dict) -> tuple:
|
| """Start a new chat tab with an entity"""
|
| if not entity_id or not entity_name:
|
| return chat_tabs_state, "No entity selected"
|
|
|
|
|
| if entity_id not in chat_tabs_state:
|
| chat_tabs_state[entity_id] = {
|
| 'name': entity_name,
|
| 'active': True,
|
| 'pinned': False,
|
| 'unread': 0
|
| }
|
|
|
| for other_id in chat_tabs_state:
|
| if other_id != entity_id:
|
| chat_tabs_state[other_id]['active'] = False
|
| else:
|
|
|
| for other_id in chat_tabs_state:
|
| chat_tabs_state[other_id]['active'] = (other_id == entity_id)
|
|
|
|
|
| tabs_html = generate_chat_tabs_html(chat_tabs_state)
|
| return chat_tabs_state, tabs_html
|
|
|
| def close_chat_tab(entity_id: str, chat_tabs_state: Dict) -> tuple:
|
| """Close a chat tab (respects pinned status)"""
|
| if entity_id in chat_tabs_state:
|
| if not chat_tabs_state[entity_id].get('pinned', False):
|
| del chat_tabs_state[entity_id]
|
|
|
| if chat_tabs_state:
|
| first_tab = next(iter(chat_tabs_state))
|
| chat_tabs_state[first_tab]['active'] = True
|
|
|
| tabs_html = generate_chat_tabs_html(chat_tabs_state)
|
| return chat_tabs_state, tabs_html
|
|
|
| def toggle_pin_tab(entity_id: str, chat_tabs_state: Dict) -> tuple:
|
| """Toggle pin status of a chat tab"""
|
| if entity_id in chat_tabs_state:
|
| chat_tabs_state[entity_id]['pinned'] = not chat_tabs_state[entity_id].get('pinned', False)
|
|
|
| tabs_html = generate_chat_tabs_html(chat_tabs_state)
|
| return chat_tabs_state, tabs_html
|
|
|
| def clear_all_chat_tabs(chat_tabs_state: Dict) -> tuple:
|
| """Clear all non-pinned chat tabs"""
|
|
|
| pinned_tabs = {k: v for k, v in chat_tabs_state.items() if v.get('pinned', False)}
|
| chat_tabs_state.clear()
|
| chat_tabs_state.update(pinned_tabs)
|
|
|
| if chat_tabs_state:
|
| first_tab = next(iter(chat_tabs_state))
|
| chat_tabs_state[first_tab]['active'] = True
|
| tabs_html = generate_chat_tabs_html(chat_tabs_state)
|
| return chat_tabs_state, tabs_html
|
|
|
|
|
| def handle_start_chat(selection: str, chat_tabs_state: Dict, current_state: Dict):
|
| """Start a chat tab and show messages and input row"""
|
| print(f"[HANDLE_START_CHAT] Called with selection='{selection}', type={type(selection)}")
|
|
|
| if not selection:
|
| print(f"[HANDLE_START_CHAT] No selection provided")
|
| return chat_tabs_state, generate_chat_tabs_html(chat_tabs_state), gr.update(value=[], visible=False), gr.update(visible=False)
|
|
|
|
|
| entity_id = selection
|
| print(f"[HANDLE_START_CHAT] Using entity_id='{entity_id}'")
|
|
|
|
|
| if not entity_id or not isinstance(entity_id, str):
|
| print(f"[HANDLE_START_CHAT] Invalid entity_id: {entity_id}")
|
| return chat_tabs_state, generate_chat_tabs_html(chat_tabs_state), gr.update(value=[], visible=False), gr.update(visible=False)
|
|
|
|
|
| entity_name = None
|
| entity_type = None
|
|
|
| if entity_id in game_world.npcs:
|
| entity_name = game_world.npcs[entity_id]['name']
|
| entity_type = "NPC"
|
| print(f"[HANDLE_START_CHAT] Found NPC: {entity_id} -> {entity_name}")
|
| elif entity_id in game_world.players:
|
| entity_name = game_world.players[entity_id].name
|
| entity_type = "Player"
|
| print(f"[HANDLE_START_CHAT] Found Player: {entity_id} -> {entity_name}")
|
| else:
|
| print(f"[HANDLE_START_CHAT] ERROR: Entity '{entity_id}' not found in NPCs or players")
|
| print(f"[HANDLE_START_CHAT] Available NPCs: {list(game_world.npcs.keys())}")
|
| print(f"[HANDLE_START_CHAT] Available Players: {list(game_world.players.keys())}")
|
|
|
| entity_name = entity_id
|
| entity_type = "Unknown"
|
|
|
| print(f"[CHAT_START] Starting chat with entity_id: '{entity_id}', entity_name: '{entity_name}', type: {entity_type}")
|
|
|
|
|
| if entity_name and "(" in entity_id and ")" in entity_id:
|
| print(f"[HANDLE_START_CHAT] WARNING: entity_id '{entity_id}' looks like a display name! This suggests a bug.")
|
|
|
|
|
| chat_tabs_state, tabs_html = start_new_chat(entity_id, entity_name, chat_tabs_state)
|
|
|
| player_id = current_state.get("player_id")
|
| messages = get_chat_messages_for_entity(player_id, entity_id) if player_id else []
|
|
|
| print(f"[HANDLE_START_CHAT] Created/switched to tab with key: '{entity_id}' and loaded {len(messages)} messages")
|
| return chat_tabs_state, tabs_html, gr.update(value=messages, visible=True), gr.update(visible=True)
|
|
|
|
|
| def generate_chat_tabs_html(chat_tabs_state: Dict) -> str:
|
| """Generate HTML for chat tabs display"""
|
| if not chat_tabs_state:
|
| return "<div style='text-align: center; color: #666; padding: 10px;'>No active chats</div>"
|
|
|
| tabs_html = "<div style='display: flex; flex-wrap: wrap; gap: 5px; padding: 5px;'>"
|
| for entity_id, tab_info in chat_tabs_state.items():
|
| active_style = "background: #e3f2fd; border: 2px solid #2196f3;" if tab_info.get('active') else "background: #f5f5f5; border: 1px solid #ccc;"
|
| pin_icon = "📌" if tab_info.get('pinned') else ""
|
| unread_badge = f" ({tab_info.get('unread', 0)})" if tab_info.get('unread', 0) > 0 else ""
|
|
|
| tabs_html += f"""
|
| <div style='{active_style} padding: 8px 12px; border-radius: 8px; cursor: pointer; display: flex; align-items: center; gap: 5px;'>
|
| <span style='flex: 1;' title='Entity ID: {entity_id}'>{tab_info['name']}{unread_badge}</span>
|
| {pin_icon}
|
| <span style='color: #f44336; font-weight: bold; margin-left: 5px; cursor: pointer;' title='Close tab'>×</span>
|
| </div>
|
| """
|
|
|
| tabs_html += "</div>"
|
| return tabs_html
|
|
|
| def switch_to_chat_tab(entity_id: str, chat_tabs_state: Dict, player_id: str) -> tuple:
|
| """Switch to a specific chat tab"""
|
| if entity_id in chat_tabs_state:
|
|
|
| for tab_id in chat_tabs_state:
|
| chat_tabs_state[tab_id]['active'] = False
|
|
|
|
|
| chat_tabs_state[entity_id]['active'] = True
|
| chat_tabs_state[entity_id]['unread'] = 0
|
|
|
|
|
| messages = get_chat_messages_for_entity(player_id, entity_id)
|
| tabs_html = generate_chat_tabs_html(chat_tabs_state)
|
|
|
| return chat_tabs_state, tabs_html, messages, True
|
|
|
| return chat_tabs_state, generate_chat_tabs_html(chat_tabs_state), [], False
|
|
|
| def get_proximity_status(current_state: Dict, preserve_selection: str = None):
|
| """Get proximity status and nearby entities"""
|
| player_id = current_state.get("player_id")
|
| print(f"[PROXIMITY_DEBUG] Called with player_id={player_id}, preserve_selection={preserve_selection}")
|
|
|
| if not player_id:
|
| print(f"[PROXIMITY_DEBUG] No player_id, returning empty")
|
| return (
|
| "<div style='text-align: center; color: #666;'>🔍 Join the game to see nearby entities</div>",
|
| gr.update(visible=False),
|
| gr.update(choices=[]),
|
| [] )
|
|
|
| nearby_entities = game_world.check_npc_proximity(player_id)
|
| print(f"[PROXIMITY_DEBUG] Found {len(nearby_entities)} nearby entities: {[e.get('name') for e in nearby_entities]}")
|
|
|
| if not nearby_entities:
|
| print(f"[PROXIMITY_DEBUG] No nearby entities, returning empty choices")
|
| return (
|
| "<div style='text-align: center; color: #666;'>🔍 Move near NPCs or players to chat privately</div>",
|
| gr.update(visible=False),
|
| gr.update(choices=[], value=None),
|
| []
|
| )
|
|
|
|
|
| entity_list = []
|
| dropdown_choices = []
|
|
|
| for entity in nearby_entities:
|
| entity_id = entity['id']
|
| entity_name = entity['name']
|
| entity_type = entity['type']
|
|
|
| if entity_type == 'npc':
|
| entity_list.append(f"🤖 {entity_name} (NPC)")
|
| else:
|
| entity_list.append(f"👤 {entity_name} (Player)")
|
|
|
|
|
|
|
| display_label = f"{entity_name} ({entity_type.upper()})"
|
| dropdown_choices.append((display_label, entity_id))
|
|
|
| print(f"[PROXIMITY_DEBUG] Added dropdown choice: Label='{display_label}' -> Value='{entity_id}'")
|
|
|
| print(f"[PROXIMITY_DEBUG] Created {len(dropdown_choices)} dropdown choices:")
|
| for choice in dropdown_choices:
|
| print(f"[PROXIMITY_DEBUG] - Label: '{choice[0]}', Value: '{choice[1]}'")
|
| print(f"[PROXIMITY_DEBUG] When user selects from dropdown, handle_start_chat will receive the Value (entity_id), not the Label")
|
|
|
| proximity_html = f"""
|
| <div style='background: #e8f5e8; border: 1px solid #4caf50; padding: 10px; border-radius: 5px;'>
|
| <div style='font-weight: bold; color: #2e7d32; margin-bottom: 5px;'>📱 Nearby for Private Chat:</div>
|
| {'<br>'.join(entity_list)}
|
| </div>
|
| """
|
|
|
|
|
| private_messages = game_world.get_private_messages_for_player(player_id)
|
| formatted_private = []
|
| for msg in private_messages:
|
| formatted_private.append({
|
| "role": "assistant",
|
| "content": f"[{msg['timestamp']}] {msg['sender']}: {msg['message']}"
|
| })
|
|
|
|
|
| dropdown_value = None
|
| if preserve_selection:
|
| valid_ids = [choice[0] for choice in dropdown_choices]
|
| if preserve_selection in valid_ids:
|
| dropdown_value = preserve_selection
|
| print(f"[PROXIMITY_DEBUG] Preserving selection: {preserve_selection}")
|
| else:
|
| print(f"[PROXIMITY_DEBUG] Cannot preserve selection '{preserve_selection}' - not in valid_ids: {valid_ids}")
|
|
|
| print(f"[PROXIMITY_DEBUG] Returning dropdown with value={dropdown_value}, choices={dropdown_choices}")
|
| return (
|
| proximity_html,
|
| gr.update(visible=True),
|
| gr.update(choices=dropdown_choices, value=dropdown_value),
|
| formatted_private
|
| )
|
|
|
| def register_test_ai_agent(ai_name: str):
|
| """Register a test AI agent"""
|
| if not ai_name.strip():
|
| return "Please enter AI agent name"
|
|
|
| try:
|
| test_client_id = f"test_{uuid.uuid4().hex[:8]}"
|
| agent_id = mcp_tools.register_ai_agent(ai_name.strip(), test_client_id)
|
| return f"✅ AI agent '{ai_name}' registered with ID: {agent_id}\nClient ID: {test_client_id}"
|
| except Exception as e:
|
| return f"❌ Failed to register AI agent: {str(e)}"
|
|
|
| def execute_ai_action(action: str, message: str):
|
| """Execute AI agent action"""
|
| if not mcp_tools.ai_agents:
|
| return "❌ No AI agents registered. Register an AI agent first!"
|
|
|
|
|
| client_id = list(mcp_tools.ai_agents.keys())[0]
|
|
|
| if action.startswith("move"):
|
| direction = action.split()[1]
|
| result = mcp_tools.move_ai_agent(client_id, direction)
|
| return f"🤖 Move result:\n{json.dumps(result, indent=2)}"
|
| elif action == "chat":
|
| if not message.strip():
|
| return "❌ Please enter a message for chat"
|
| result = mcp_tools.ai_agent_chat(client_id, message.strip())
|
| return f"💬 Chat result:\n{json.dumps(result, indent=2)}"
|
|
|
| return "❓ Unknown action"
|
|
|
| def auto_refresh(current_state: Dict, current_dropdown: str, auto_refresh_enabled: bool):
|
| """Auto-refresh game displays"""
|
| if not auto_refresh_enabled:
|
|
|
| return (gr.update(), gr.update(), gr.update(), gr.update(), gr.update(), gr.update(),
|
| gr.update(), gr.update(), gr.update(), gr.update())
|
|
|
| player_id = current_state.get("player_id") if current_state else None
|
|
|
|
|
| game_world.update_moving_npcs()
|
|
|
| world_html = create_game_world_html()
|
| players_list = [
|
| [p.name, p.type, p.level]
|
| for p in game_world.players.values()
|
| ]
|
|
|
|
|
| formatted_chat = []
|
| for msg in game_world.chat_messages[-20:]:
|
| formatted_chat.append({
|
| "role": "assistant",
|
| "content": f"[{msg['timestamp']}] {msg['sender']}: {msg['message']}"
|
| })
|
|
|
| events = "\n".join([
|
| f"{e.get('event', '')}"
|
| for e in game_world.world_events[-5:]
|
| ]) if game_world.world_events else "No recent events"
|
|
|
|
|
| player_stats = get_player_stats_display(player_id)
|
|
|
|
|
| mcp_status = {
|
| "server_status": "🟢 Active",
|
| "server_url": "This Gradio app serves as MCP server",
|
| "tools_available": [
|
| "register_ai_agent",
|
| "move_agent",
|
| "send_chat",
|
| "get_game_state",
|
| "interact_with_npc"
|
| ],
|
| "active_ai_agents": len(mcp_tools.ai_agents),
|
| "total_players": len(game_world.players) }
|
|
|
| proximity_html, private_chat_visible, entity_choices, _ = get_proximity_status(current_state, current_dropdown)
|
|
|
| return (world_html, players_list, formatted_chat, events, player_stats, mcp_status,
|
| proximity_html, private_chat_visible, entity_choices, gr.update())
|
|
|
|
|
| join_btn.click(
|
| join_game,
|
| inputs=[player_name, player_state],
|
| outputs=[player_state, player_info, game_view, online_players, world_events,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display]
|
| )
|
|
|
| leave_btn.click(
|
| leave_game,
|
| inputs=[player_state],
|
| outputs=[player_state, player_info, game_view, online_players, world_events,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display] )
|
|
|
|
|
| def move_up_handler(state):
|
| return handle_movement("up", state)
|
|
|
| def move_down_handler(state):
|
| return handle_movement("down", state)
|
|
|
| def move_left_handler(state):
|
| return handle_movement("left", state)
|
|
|
| def move_right_handler(state):
|
| return handle_movement("right", state)
|
|
|
| move_up.click(
|
| move_up_handler,
|
| inputs=[player_state],
|
| outputs=[player_state, game_view, online_players, player_info,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display]
|
| )
|
|
|
| move_down.click(
|
| move_down_handler,
|
| inputs=[player_state],
|
| outputs=[player_state, game_view, online_players, player_info,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display]
|
| )
|
|
|
| move_left.click(
|
| move_left_handler,
|
| inputs=[player_state],
|
| outputs=[player_state, game_view, online_players, player_info,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display]
|
| )
|
|
|
| move_right.click(
|
| move_right_handler,
|
| inputs=[player_state],
|
| outputs=[player_state, game_view, online_players, player_info,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display] )
|
|
|
|
|
| chat_send.click(
|
| handle_chat,
|
| inputs=[chat_input, chat_display, player_state],
|
| outputs=[chat_display, chat_input]
|
| )
|
|
|
| chat_input.submit(
|
| handle_chat,
|
| inputs=[chat_input, chat_display, player_state],
|
| outputs=[chat_display, chat_input] )
|
|
|
|
|
| private_send_btn.click(
|
| handle_private_message,
|
| inputs=[private_message_input, player_state, chat_tabs_state],
|
| outputs=[current_chat_display, private_message_input]
|
| )
|
|
|
| private_message_input.submit(
|
| handle_private_message,
|
| inputs=[private_message_input, player_state, chat_tabs_state],
|
| outputs=[current_chat_display, private_message_input]
|
| )
|
|
|
|
|
| register_ai_btn.click(
|
| register_test_ai_agent,
|
| inputs=[ai_name],
|
| outputs=[ai_result]
|
| )
|
|
|
| execute_ai_btn.click(
|
| execute_ai_action,
|
| inputs=[ai_action, ai_message],
|
| outputs=[ai_result]
|
| )
|
|
|
| start_chat_btn.click(
|
| handle_start_chat,
|
| inputs=[nearby_entities, chat_tabs_state, player_state],
|
| outputs=[chat_tabs_state, active_tabs_display, current_chat_display, chat_input_row]
|
| )
|
|
|
| clear_all_tabs_btn.click(
|
| clear_all_chat_tabs,
|
| inputs=[chat_tabs_state],
|
| outputs=[chat_tabs_state, active_tabs_display]
|
| )
|
| refresh_timer = gr.Timer(value=2)
|
| refresh_timer.tick(
|
| auto_refresh,
|
| inputs=[player_state, nearby_entities, auto_refresh_enabled],
|
| outputs=[game_view, online_players, chat_display, world_events, player_info, mcp_info,
|
| proximity_info, private_chat_group, nearby_entities, current_chat_display]
|
| )
|
|
|
| return demo.queue()
|
|
|
|
|
|
|
|
|
|
|
| if __name__ == "__main__":
|
| print("🎮 Starting COMPLETE MMORPG with ALL FEATURES...")
|
| print("\n🔧 Features Restored:")
|
| print("✅ FIXED: Deadlock issue resolved with RLock")
|
| print("✅ Human players can join and play")
|
| print("✅ AI agents can connect via MCP API")
|
| print("✅ Extensible NPC add-on system")
|
| print("✅ MCP servers can be used as add-ons")
|
| print("✅ Real-time multiplayer gameplay")
|
| print("✅ Read2Burn secure mailbox add-on")
|
| print("✅ Keyboard controls (WASD + Arrow Keys)")
|
| print("✅ Complete MCP integration")
|
| print("✅ All original features restored")
|
| print("="*60)
|
|
|
| app = create_mmorpg_interface()
|
|
|
|
|
| print("🌐 Human players: Access via web interface")
|
| print("🤖 AI agents: Connect via MCP API endpoints")
|
| print("⌨️ Keyboard: Use WASD or Arrow Keys for movement")
|
| print("🔌 MCP Tools available for AI integration")
|
| print("🔥 Read2Burn mailbox for secure messaging")
|
| print("🎯 All features working with deadlock fix!")
|
| print("="*60)
|
| app.launch(
|
| server_name="127.0.0.1",
|
| server_port=7868,
|
| mcp_server=True,
|
| share=True,
|
| show_error=True,
|
| debug=True
|
| ) |