Chris4K commited on
Commit
1e51656
·
verified ·
1 Parent(s): c2d4151

Upload 202 files

Browse files

a few minor updates

Files changed (42) hide show
  1. Documentation/NPC_Addon_Development_Guide_Updated.md +1 -1
  2. app.py +123 -126
  3. plugins/__pycache__/sample_plugin.cpython-313.pyc +0 -0
  4. src/addons/__pycache__/duckduckgo_search_oracle_addon.cpython-313.pyc +0 -0
  5. src/addons/__pycache__/example_npc_addon.cpython-313.pyc +0 -0
  6. src/addons/__pycache__/fortune_teller_addon.cpython-313.pyc +0 -0
  7. src/addons/__pycache__/generic_mcp_server_addon.cpython-313.pyc +0 -0
  8. src/addons/__pycache__/huggingface_hub_addon.cpython-313.pyc +0 -0
  9. src/addons/__pycache__/magic_shop_addon.cpython-313.pyc +0 -0
  10. src/addons/__pycache__/read2burn_addon.cpython-313.pyc +0 -0
  11. src/addons/__pycache__/searchhf_addon.cpython-313.pyc +0 -0
  12. src/addons/__pycache__/self_contained_example_addon.cpython-313.pyc +0 -0
  13. src/addons/__pycache__/simple_trader_addon.cpython-313.pyc +0 -0
  14. src/addons/__pycache__/weather_oracle_addon.cpython-313.pyc +0 -0
  15. src/addons/__pycache__/web_search_oracle_addon.cpython-313.pyc +0 -0
  16. src/addons/duckduckgo_search_oracle_addon.py +3 -3
  17. src/addons/fortune_teller_addon.py +37 -1
  18. src/addons/generic_mcp_server_addon.py +122 -0
  19. src/addons/huggingface_hub_addon.py +674 -0
  20. src/addons/magic_shop_addon.py +37 -1
  21. src/addons/searchhf_addon.py +369 -0
  22. src/addons/simple_trader_addon.py +32 -29
  23. src/addons/weather_oracle_addon.py +2 -2
  24. src/core/__pycache__/game_engine.cpython-313.pyc +0 -0
  25. src/core/__pycache__/world.cpython-313.pyc +0 -0
  26. src/core/game_engine.py +257 -260
  27. src/core/game_engine_fixed.py +266 -0
  28. src/core/world.py +321 -321
  29. src/facades/__pycache__/game_facade.cpython-313.pyc +0 -0
  30. src/interfaces/__pycache__/npc_addon.cpython-313.pyc +0 -0
  31. src/interfaces/npc_addon.py +15 -0
  32. src/legacy_addons/example_npc_addon.py +266 -0
  33. src/legacy_addons/web_search_oracle_addon.py +450 -0
  34. src/ui/__init__.py +2 -1
  35. src/ui/__pycache__/__init__.cpython-313.pyc +0 -0
  36. src/ui/__pycache__/huggingface_ui.cpython-313.pyc +0 -0
  37. src/ui/__pycache__/improved_interface_manager.cpython-313.pyc +0 -0
  38. src/ui/__pycache__/interface_manager.cpython-313.pyc +0 -0
  39. src/ui/huggingface_ui.py +463 -112
  40. src/ui/improved_interface_demo.py +138 -0
  41. src/ui/improved_interface_manager.py +1068 -0
  42. src/ui/interface_manager.py +2 -2
Documentation/NPC_Addon_Development_Guide_Updated.md CHANGED
@@ -866,7 +866,7 @@ def auto_register(game_engine):
866
  # Create the weather oracle NPC definition
867
  weather_oracle_npc = {
868
  'id': 'weather_oracle_auto',
869
- 'name': '🌤️ Weather Oracle (Auto)',
870
  'x': 300, 'y': 150,
871
  'char': '🌤️',
872
  'type': 'mcp',
 
866
  # Create the weather oracle NPC definition
867
  weather_oracle_npc = {
868
  'id': 'weather_oracle_auto',
869
+ 'name': '🌤️ Weather Oracle (MCP)',
870
  'x': 300, 'y': 150,
871
  'char': '🌤️',
872
  'type': 'mcp',
app.py CHANGED
@@ -1,126 +1,123 @@
1
- #!/usr/bin/env python3
2
- """
3
- MMORPG Application - Refactored Clean Architecture
4
- Main application entry point using clean architecture principles
5
- """
6
-
7
- import gradio as gr
8
- import asyncio
9
- import threading
10
- from typing import Optional
11
-
12
- # Import our clean architecture components
13
- from src.core.game_engine import GameEngine
14
- from src.facades.game_facade import GameFacade
15
- from src.ui.huggingface_ui import HuggingFaceUI
16
- from src.ui.interface_manager import InterfaceManager
17
- from src.mcp.mcp_tools import GradioMCPTools
18
-
19
- class MMORPGApplication:
20
- """Main application class that orchestrates the MMORPG game."""
21
-
22
- def __init__(self):
23
- """Initialize the application with all necessary components."""
24
- # Initialize core game engine (singleton)
25
- self.game_engine = GameEngine()
26
-
27
- # Initialize game facade for simplified operations
28
- self.game_facade = GameFacade()
29
- # Initialize UI components
30
- self.ui = HuggingFaceUI(self.game_facade)
31
- self.interface_manager = InterfaceManager(self.game_facade, self.ui)
32
-
33
- # Initialize MCP tools for AI agent integration
34
- self.mcp_tools = GradioMCPTools(self.game_facade)
35
-
36
- # Gradio interface reference
37
- self.gradio_interface: Optional[gr.Blocks] = None
38
-
39
- # Background tasks
40
- self._cleanup_task = None
41
- self._auto_refresh_task = None
42
-
43
- def create_gradio_interface(self) -> gr.Blocks:
44
- """Create and configure the Gradio interface."""
45
- # Use the InterfaceManager to create the complete interface with proper event handling
46
- self.interface_manager._create_interface_with_events()
47
- self.gradio_interface = self.interface_manager.interface
48
- return self.gradio_interface
49
-
50
- def start_background_tasks(self):
51
- """Start background tasks for game maintenance."""
52
-
53
- def cleanup_task():
54
- """Background task for cleaning up inactive players."""
55
- while True:
56
- try:
57
- self.game_facade.cleanup_inactive_players()
58
- threading.Event().wait(30) # Wait 30 seconds
59
- except Exception as e:
60
- print(f"Error in cleanup task: {e}")
61
- threading.Event().wait(5) # Wait before retry
62
-
63
- def auto_refresh_task():
64
- """Background task for auto-refreshing game state."""
65
- while True:
66
- try:
67
- # Trigger refresh for active sessions
68
- # This would need session tracking for real implementation
69
- threading.Event().wait(2) # Refresh every 2 seconds
70
- except Exception as e:
71
- print(f"Error in auto-refresh task: {e}")
72
- threading.Event().wait(5) # Wait before retry
73
-
74
- # Start cleanup task
75
- self._cleanup_task = threading.Thread(target=cleanup_task, daemon=True)
76
- self._cleanup_task.start()
77
-
78
- # Start auto-refresh task
79
- self._auto_refresh_task = threading.Thread(target=auto_refresh_task, daemon=True)
80
- self._auto_refresh_task.start()
81
-
82
- def run(self, share: bool = False, server_port: int = 7860):
83
- """Run the MMORPG application."""
84
- print("🎮 Starting MMORPG Application...")
85
- print("🏗️ Initializing game engine...")
86
- # Initialize game world and services
87
- if not self.game_engine.start():
88
- print(" Failed to start game engine")
89
- return
90
-
91
- print("🎨 Creating user interface...")
92
-
93
- # Create Gradio interface
94
- interface = self.create_gradio_interface()
95
-
96
- print("🔧 Starting background tasks...")
97
-
98
- # Start background maintenance tasks
99
- self.start_background_tasks()
100
-
101
- print("🚀 Launching server...")
102
- print(f"🌐 Server will be available at: http://localhost:{server_port}")
103
-
104
- if share:
105
- print("🔗 Public URL will be generated...")
106
-
107
- # Launch the interface
108
- interface.launch(
109
- share=share,
110
- debug=True,
111
- # server_port=server_port,
112
- mcp_server=True, # Enable MCP server integration
113
- show_error=True,
114
- quiet=False
115
- )
116
-
117
- def main():
118
- """Main entry point for the application."""
119
- # Create and run the application
120
- app = MMORPGApplication()
121
- # Run with default settings
122
- # Change share=True to make it publicly accessible
123
- app.run(share=True, server_port=7869)
124
-
125
- if __name__ == "__main__":
126
- main()
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ MMORPG Application - Refactored Clean Architecture
4
+ Main application entry point using clean architecture principles
5
+ """
6
+
7
+ import gradio as gr
8
+ import asyncio
9
+ import threading
10
+ from typing import Optional
11
+
12
+ # Import our clean architecture components
13
+ from src.core.game_engine import GameEngine
14
+ from src.facades.game_facade import GameFacade
15
+ from src.ui.huggingface_ui import HuggingFaceUI
16
+ from src.ui.improved_interface_manager import ImprovedInterfaceManager
17
+ from src.mcp.mcp_tools import GradioMCPTools
18
+
19
+ class MMORPGApplication:
20
+ """Main application class that orchestrates the MMORPG game."""
21
+
22
+ def __init__(self):
23
+ """Initialize the application with all necessary components."""
24
+ # Initialize core game engine (singleton)
25
+ self.game_engine = GameEngine()
26
+
27
+ # Initialize game facade for simplified operations
28
+ self.game_facade = GameFacade() # Initialize UI components
29
+ self.ui = HuggingFaceUI(self.game_facade)
30
+ self.interface_manager = ImprovedInterfaceManager(self.game_facade, self.ui)
31
+
32
+ # Initialize MCP tools for AI agent integration
33
+ self.mcp_tools = GradioMCPTools(self.game_facade)
34
+ # Gradio interface reference
35
+ self.gradio_interface: Optional[gr.Blocks] = None
36
+
37
+ # Background tasks
38
+ self._cleanup_task = None
39
+ self._auto_refresh_task = None
40
+
41
+ def create_gradio_interface(self) -> gr.Blocks:
42
+ """Create and configure the Gradio interface."""
43
+ # Use the ImprovedInterfaceManager to create the complete interface with proper event handling
44
+ self.gradio_interface = self.interface_manager.create_interface()
45
+ return self.gradio_interface
46
+
47
+ def start_background_tasks(self):
48
+ """Start background tasks for game maintenance."""
49
+
50
+ def cleanup_task():
51
+ """Background task for cleaning up inactive players."""
52
+ while True:
53
+ try:
54
+ self.game_facade.cleanup_inactive_players()
55
+ threading.Event().wait(30) # Wait 30 seconds
56
+ except Exception as e:
57
+ print(f"Error in cleanup task: {e}")
58
+ threading.Event().wait(5) # Wait before retry
59
+
60
+ def auto_refresh_task():
61
+ """Background task for auto-refreshing game state."""
62
+ while True:
63
+ try:
64
+ # Trigger refresh for active sessions
65
+ # This would need session tracking for real implementation
66
+ threading.Event().wait(2) # Refresh every 2 seconds
67
+ except Exception as e:
68
+ print(f"Error in auto-refresh task: {e}")
69
+ threading.Event().wait(5) # Wait before retry
70
+
71
+ # Start cleanup task
72
+ self._cleanup_task = threading.Thread(target=cleanup_task, daemon=True)
73
+ self._cleanup_task.start()
74
+
75
+ # Start auto-refresh task
76
+ self._auto_refresh_task = threading.Thread(target=auto_refresh_task, daemon=True)
77
+ self._auto_refresh_task.start()
78
+
79
+ def run(self, share: bool = False, server_port: int = 7860):
80
+ """Run the MMORPG application."""
81
+ print("🎮 Starting MMORPG Application...")
82
+ print("🏗️ Initializing game engine...")
83
+ # Initialize game world and services
84
+ if not self.game_engine.start():
85
+ print(" Failed to start game engine")
86
+ return
87
+
88
+ print("🎨 Creating user interface...")
89
+
90
+ # Create Gradio interface
91
+ interface = self.create_gradio_interface()
92
+
93
+ print("🔧 Starting background tasks...")
94
+
95
+ # Start background maintenance tasks
96
+ self.start_background_tasks()
97
+
98
+ print("🚀 Launching server...")
99
+ print(f"🌐 Server will be available at: http://localhost:{server_port}")
100
+
101
+ if share:
102
+ print("🔗 Public URL will be generated...")
103
+
104
+ # Launch the interface
105
+ interface.launch(
106
+ share=share,
107
+ debug=True,
108
+ server_port=server_port,
109
+ mcp_server=True, # Enable MCP server integration
110
+ show_error=True,
111
+ quiet=False
112
+ )
113
+
114
+ def main():
115
+ """Main entry point for the application."""
116
+ # Create and run the application
117
+ app = MMORPGApplication()
118
+ # Run with default settings
119
+ # Change share=True to make it publicly accessible
120
+ app.run(share=True, server_port=7869)
121
+
122
+ if __name__ == "__main__":
123
+ main()
 
 
 
plugins/__pycache__/sample_plugin.cpython-313.pyc CHANGED
Binary files a/plugins/__pycache__/sample_plugin.cpython-313.pyc and b/plugins/__pycache__/sample_plugin.cpython-313.pyc differ
 
src/addons/__pycache__/duckduckgo_search_oracle_addon.cpython-313.pyc ADDED
Binary file (21.5 kB). View file
 
src/addons/__pycache__/example_npc_addon.cpython-313.pyc CHANGED
Binary files a/src/addons/__pycache__/example_npc_addon.cpython-313.pyc and b/src/addons/__pycache__/example_npc_addon.cpython-313.pyc differ
 
src/addons/__pycache__/fortune_teller_addon.cpython-313.pyc ADDED
Binary file (13.1 kB). View file
 
src/addons/__pycache__/generic_mcp_server_addon.cpython-313.pyc ADDED
Binary file (8.32 kB). View file
 
src/addons/__pycache__/huggingface_hub_addon.cpython-313.pyc ADDED
Binary file (34.9 kB). View file
 
src/addons/__pycache__/magic_shop_addon.cpython-313.pyc ADDED
Binary file (25.1 kB). View file
 
src/addons/__pycache__/read2burn_addon.cpython-313.pyc CHANGED
Binary files a/src/addons/__pycache__/read2burn_addon.cpython-313.pyc and b/src/addons/__pycache__/read2burn_addon.cpython-313.pyc differ
 
src/addons/__pycache__/searchhf_addon.cpython-313.pyc ADDED
Binary file (20.2 kB). View file
 
src/addons/__pycache__/self_contained_example_addon.cpython-313.pyc ADDED
Binary file (11.1 kB). View file
 
src/addons/__pycache__/simple_trader_addon.cpython-313.pyc ADDED
Binary file (8.32 kB). View file
 
src/addons/__pycache__/weather_oracle_addon.cpython-313.pyc CHANGED
Binary files a/src/addons/__pycache__/weather_oracle_addon.cpython-313.pyc and b/src/addons/__pycache__/weather_oracle_addon.cpython-313.pyc differ
 
src/addons/__pycache__/web_search_oracle_addon.cpython-313.pyc ADDED
Binary file (23 kB). View file
 
src/addons/duckduckgo_search_oracle_addon.py CHANGED
@@ -65,7 +65,7 @@ class DuckDuckGoSearchOracleService(IDuckDuckGoSearchService, NPCAddon):
65
  return {
66
  'id': 'duckduckgo_search_oracle',
67
  'name': 'DuckDuckGo Search Oracle (MCP)',
68
- 'x': 450, 'y': 300,
69
  'char': '🦆',
70
  'type': 'mcp',
71
  'description': 'DuckDuckGo-powered web search information service'
@@ -408,8 +408,8 @@ def auto_register(game_engine):
408
  # Create the DuckDuckGo search oracle NPC definition
409
  duckduckgo_search_oracle_npc = {
410
  'id': 'duckduckgo_search_oracle_auto',
411
- 'name': '🦆 DuckDuckGo Search Oracle (Auto)',
412
- 'x': 450, 'y': 150,
413
  'char': '🦆',
414
  'type': 'mcp',
415
  'personality': 'duckduckgo_search_oracle',
 
65
  return {
66
  'id': 'duckduckgo_search_oracle',
67
  'name': 'DuckDuckGo Search Oracle (MCP)',
68
+ 'x': 250, 'y': 400,
69
  'char': '🦆',
70
  'type': 'mcp',
71
  'description': 'DuckDuckGo-powered web search information service'
 
408
  # Create the DuckDuckGo search oracle NPC definition
409
  duckduckgo_search_oracle_npc = {
410
  'id': 'duckduckgo_search_oracle_auto',
411
+ 'name': '🦆 DuckDuckGo Search Oracle (MCP)',
412
+ 'x': 650, 'y': 150,
413
  'char': '🦆',
414
  'type': 'mcp',
415
  'personality': 'duckduckgo_search_oracle',
src/addons/fortune_teller_addon.py CHANGED
@@ -44,7 +44,7 @@ class FortuneTellerAddon(NPCAddon):
44
  return {
45
  'id': 'fortune_teller',
46
  'name': 'Mystic Zelda',
47
- 'x': 125, 'y': 75,
48
  'char': '🔮',
49
  'type': 'addon',
50
  'description': 'Wise fortune teller who can glimpse into your future'
@@ -238,3 +238,39 @@ class FortuneTellerAddon(NPCAddon):
238
 
239
  # Auto-instantiate the addon for registration
240
  fortune_teller_addon = FortuneTellerAddon()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
  return {
45
  'id': 'fortune_teller',
46
  'name': 'Mystic Zelda',
47
+ 'x': 225, 'y': 75,
48
  'char': '🔮',
49
  'type': 'addon',
50
  'description': 'Wise fortune teller who can glimpse into your future'
 
238
 
239
  # Auto-instantiate the addon for registration
240
  fortune_teller_addon = FortuneTellerAddon()
241
+
242
+
243
+ def auto_register(game_engine):
244
+ """Auto-register the Fortune Teller addon with the game engine."""
245
+ try:
246
+ # Create addon instance
247
+ addon_instance = FortuneTellerAddon()
248
+
249
+ # Get NPC config
250
+ npc_config = {
251
+ 'id': 'fortune_teller',
252
+ 'name': '🔮 Mystic Zelda',
253
+ 'x': 150, 'y': 150,
254
+ 'char': '🔮',
255
+ 'type': 'mystic',
256
+ 'personality': 'mystical'
257
+ }
258
+
259
+ # Register NPC in world
260
+ npc_service = game_engine.get_npc_service()
261
+ npc_service.register_npc(npc_config['id'], npc_config)
262
+
263
+ # Register addon for command handling
264
+ if not hasattr(game_engine.get_world(), 'addon_npcs'):
265
+ game_engine.get_world().addon_npcs = {}
266
+ game_engine.get_world().addon_npcs[npc_config['id']] = addon_instance
267
+
268
+ # Call startup
269
+ addon_instance.on_startup()
270
+
271
+ print(f"[FortuneTellerAddon] Auto-registered successfully as self-contained addon")
272
+ return True
273
+
274
+ except Exception as e:
275
+ print(f"[FortuneTellerAddon] Error during auto-registration: {e}")
276
+ return False
src/addons/generic_mcp_server_addon.py ADDED
@@ -0,0 +1,122 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import random
2
+ import asyncio
3
+ import gradio as gr
4
+ from mcp import ClientSession
5
+ from mcp.client.sse import sse_client
6
+ from contextlib import AsyncExitStack
7
+ from ..interfaces.npc_addon import NPCAddon
8
+
9
+ # Define approximate game world dimensions
10
+ #TODO this seems wrong, should come fromt game config
11
+ GAME_WIDTH = 800
12
+ GAME_HEIGHT = 600
13
+
14
+ class GenericMCPServerAddon(NPCAddon):
15
+ """
16
+ Generic MCP Server NPC-Addon instantiated per MCP-search result.
17
+ Expected config JSON keys:
18
+ - space_id: str unique HF Space identifier
19
+ - title: str display name for the addon
20
+ - author: str author/organization of the space
21
+ - mcp_server_url: str SSE endpoint for MCP integration
22
+ - description: str optional descriptive text
23
+ - version: str optional version tag
24
+ """
25
+ def __init__(self, config: dict):
26
+ # metadata from search result
27
+ self.name = config.get("title", config.get("space_id", "MCP Server"))
28
+ self.description = config.get("description", f"MCP server NPC for {config.get('space_id', self.name)}")
29
+ self.version = config.get("version", "auto")
30
+ self.author = config.get("author", "unknown")
31
+ self.mcp_url = config.get("mcp_server_url")
32
+ # initialize tool list
33
+ self.tools = []
34
+ self.connected = False
35
+ # random placement within game world
36
+ self.character = "🔌"
37
+ x = random.randint(0, GAME_WIDTH)
38
+ y = random.randint(0, GAME_HEIGHT)
39
+ self.position = (x, y)
40
+ self.npc_name = self.name
41
+ # prepare event loop and session
42
+ try:
43
+ self.loop = asyncio.get_event_loop()
44
+ except RuntimeError:
45
+ self.loop = asyncio.new_event_loop()
46
+ self.session = None
47
+ self.exit_stack = None
48
+ # register
49
+ super().__init__()
50
+
51
+ @property
52
+ def addon_id(self) -> str:
53
+ return self.name.lower().replace(" ", "_")
54
+
55
+ @property
56
+ def ui_tab_name(self) -> str:
57
+ return f"🔌 {self.name}"
58
+
59
+ @property
60
+ def npc_config(self) -> dict:
61
+ return {
62
+ 'id': self.addon_id,
63
+ 'name': self.npc_name,
64
+ 'x': self.position[0],
65
+ 'y': self.position[1],
66
+ 'char': self.character,
67
+ 'type': 'mcp',
68
+ 'description': self.description
69
+ }
70
+
71
+ def on_startup(self):
72
+ # connect on engine startup
73
+ self.connect_to_mcp()
74
+
75
+ def connect_to_mcp(self) -> str:
76
+ """Synchronously connect to MCP server via SSE"""
77
+ try:
78
+ return self.loop.run_until_complete(self._connect())
79
+ except Exception as e:
80
+ self.connected = False
81
+ return f"❌ Connection failed: {e}"
82
+
83
+ async def _connect(self) -> str:
84
+ # setup SSE client
85
+ if self.exit_stack:
86
+ await self.exit_stack.aclose()
87
+ self.exit_stack = AsyncExitStack()
88
+ rs, ws = await self.exit_stack.enter_async_context(sse_client(self.mcp_url))
89
+ self.session = await self.exit_stack.enter_async_context(ClientSession(rs, ws))
90
+ await self.session.initialize()
91
+ resp = await self.session.list_tools()
92
+ self.tools = resp.tools
93
+ self.connected = True
94
+ return f"✅ Connected to {self.name}, {len(self.tools)} tools available"
95
+
96
+ def handle_command(self, player_id: str, command: str) -> str:
97
+ # call first tool with full command
98
+ if not self.connected:
99
+ self.connect_to_mcp()
100
+ tool = self.tools[0] if self.tools else None
101
+ if not tool:
102
+ return "❌ No tools registered"
103
+ try:
104
+ params = { 'input': command }
105
+ result = self.loop.run_until_complete(self.session.call_tool(tool.name, params))
106
+ content = getattr(result, 'content', result)
107
+ if isinstance(content, list):
108
+ return "".join(getattr(item, 'text', str(item)) for item in content)
109
+ return getattr(content, 'text', str(content))
110
+ except Exception as e:
111
+ return f"❌ Error: {e}"
112
+
113
+ def get_interface(self) -> gr.Component:
114
+ with gr.Column() as interface:
115
+ gr.Markdown(f"## 🔌 {self.name}\nConnect and interact with this MCP server NPC.")
116
+ cmd_in = gr.Textbox(label="Command to send")
117
+ out = gr.Textbox(label="Output", lines=5)
118
+ btn = gr.Button("▶️ Send")
119
+ btn.click(lambda c: self.handle_command(None, c), inputs=[cmd_in], outputs=[out])
120
+ return interface
121
+
122
+ # no manual registration needed; instantiating this class auto-registers the NPC-addon.
src/addons/huggingface_hub_addon.py ADDED
@@ -0,0 +1,674 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Hugging Face Hub Oracle Add-on - MCP-powered Hugging Face Hub integration.
3
+ """
4
+
5
+ import time
6
+ import json
7
+ import random
8
+ import string
9
+ import asyncio
10
+ from typing import Dict, List, Optional
11
+ import gradio as gr
12
+ from abc import ABC, abstractmethod
13
+ from mcp import ClientSession
14
+ from mcp.client.sse import sse_client
15
+ from contextlib import AsyncExitStack
16
+
17
+ from ..interfaces.npc_addon import NPCAddon
18
+
19
+
20
+ class IHuggingFaceHubService(ABC):
21
+ """Interface for Hugging Face Hub service operations."""
22
+
23
+ @abstractmethod
24
+ def search_models(self, query: str, task: str = None, library: str = None) -> str:
25
+ """Search for ML models."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def search_datasets(self, query: str, author: str = None, tags: str = None) -> str:
30
+ """Search for datasets."""
31
+ pass
32
+
33
+ @abstractmethod
34
+ def search_papers(self, query: str) -> str:
35
+ """Search for ML research papers."""
36
+ pass
37
+
38
+ @abstractmethod
39
+ def search_spaces(self, query: str) -> str:
40
+ """Search for AI Apps/Spaces."""
41
+ pass
42
+
43
+ @abstractmethod
44
+ def connect_to_mcp(self) -> str:
45
+ """Connect to Hugging Face Hub MCP server."""
46
+ pass
47
+
48
+
49
+ class HuggingFaceHubOracleService(IHuggingFaceHubService, NPCAddon):
50
+ """Service for managing Hugging Face Hub Oracle MCP integration."""
51
+
52
+ def __init__(self):
53
+ super().__init__()
54
+ self.connected = False
55
+ self.last_connection_attempt = 0
56
+ self.connection_cooldown = 30 # 30 seconds between connection attempts
57
+ self.server_url = "https://huggingface.co/mcp"
58
+ self.session = None
59
+ self.tools = []
60
+ self.exit_stack = None
61
+ self.hf_token = None # Will need to be configured
62
+ # Set up event loop for async operations
63
+ try:
64
+ self.loop = asyncio.get_event_loop()
65
+ except RuntimeError:
66
+ self.loop = asyncio.new_event_loop()
67
+
68
+ @property
69
+ def addon_id(self) -> str:
70
+ """Unique identifier for this add-on"""
71
+ return "huggingface_hub_oracle"
72
+
73
+ @property
74
+ def addon_name(self) -> str:
75
+ """Display name for this add-on"""
76
+ return "Hugging Face Hub Oracle (MCP)"
77
+
78
+ @property
79
+ def npc_config(self) -> Dict:
80
+ """NPC configuration for auto-placement in world"""
81
+ return {
82
+ 'id': 'huggingface_hub_oracle',
83
+ 'name': 'Hugging Face Hub Oracle (MCP)',
84
+ 'x': 500, 'y': 150,
85
+ 'char': '🤗',
86
+ 'type': 'mcp',
87
+ 'description': 'Hugging Face Hub-powered ML models, datasets, papers, and spaces search service'
88
+ }
89
+
90
+ @property
91
+ def ui_tab_name(self) -> str:
92
+ """UI tab name for this addon"""
93
+ return "Hugging Face Hub Oracle"
94
+
95
+ def get_interface(self) -> gr.Component:
96
+ """Return Gradio interface for this add-on"""
97
+ with gr.Column() as interface:
98
+ gr.Markdown("""
99
+ ## 🤗 Hugging Face Hub Oracle (MCP)
100
+
101
+ *I commune with the Hugging Face Hub spirits through the mystical MCP protocol to bring you the best ML models, datasets, papers, and AI spaces!*
102
+
103
+ **Ask me to find:**
104
+ - 🤖 **Models**: ML models with task and library filters
105
+ - 📊 **Datasets**: Datasets with author and tag filters
106
+ - 📄 **Papers**: ML research papers via semantic search
107
+ - 🚀 **Spaces**: AI Apps and demos via semantic search
108
+ - 🔍 **Details**: Detailed information about specific models/datasets
109
+
110
+ *Powered by Hugging Face Hub via Model Context Protocol (MCP)*
111
+
112
+ **Note**: Requires HF token configuration for full functionality
113
+ """)
114
+
115
+ # Connection status
116
+ connection_status = gr.HTML(
117
+ value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to Hugging Face Hub spirits' if self.connected else '🔴 Disconnected from HF Hub realm'}</div>"
118
+ )
119
+
120
+ # HF Token configuration
121
+ with gr.Row():
122
+ hf_token_input = gr.Textbox(
123
+ label="🔑 Hugging Face Token",
124
+ placeholder="hf_...",
125
+ type="password",
126
+ scale=3
127
+ )
128
+ set_token_btn = gr.Button("🔑 Set Token", variant="secondary", scale=1)
129
+
130
+ with gr.Tabs():
131
+ # Models Tab
132
+ with gr.TabItem("🤖 Models"):
133
+ with gr.Row():
134
+ model_query = gr.Textbox(
135
+ label="Model Search Query",
136
+ placeholder="e.g., text classification, image generation, language model",
137
+ scale=2
138
+ )
139
+ model_task = gr.Textbox(
140
+ label="Task Filter (optional)",
141
+ placeholder="e.g., text-classification, image-to-text",
142
+ scale=1
143
+ )
144
+ model_library = gr.Textbox(
145
+ label="Library Filter (optional)",
146
+ placeholder="e.g., transformers, diffusers, pytorch",
147
+ scale=1
148
+ )
149
+ search_models_btn = gr.Button("🤖 Search Models", variant="primary")
150
+
151
+ # Datasets Tab
152
+ with gr.TabItem("📊 Datasets"):
153
+ with gr.Row():
154
+ dataset_query = gr.Textbox(
155
+ label="Dataset Search Query",
156
+ placeholder="e.g., image classification, natural language processing",
157
+ scale=2
158
+ )
159
+ dataset_author = gr.Textbox(
160
+ label="Author Filter (optional)",
161
+ placeholder="e.g., huggingface, microsoft",
162
+ scale=1
163
+ )
164
+ dataset_tags = gr.Textbox(
165
+ label="Tags Filter (optional)",
166
+ placeholder="e.g., computer-vision, nlp",
167
+ scale=1
168
+ )
169
+ search_datasets_btn = gr.Button("📊 Search Datasets", variant="primary")
170
+
171
+ # Papers Tab
172
+ with gr.TabItem("📄 Papers"):
173
+ papers_query = gr.Textbox(
174
+ label="Research Papers Search Query",
175
+ placeholder="e.g., transformer architecture, diffusion models, reinforcement learning",
176
+ scale=3
177
+ )
178
+ search_papers_btn = gr.Button("📄 Search Papers", variant="primary")
179
+
180
+ # Spaces Tab
181
+ with gr.TabItem("🚀 Spaces"):
182
+ spaces_query = gr.Textbox(
183
+ label="AI Spaces Search Query",
184
+ placeholder="e.g., image generator, chatbot, text-to-speech",
185
+ scale=3
186
+ )
187
+ search_spaces_btn = gr.Button("🚀 Search Spaces", variant="primary")
188
+
189
+ # Results output
190
+ search_output = gr.Textbox(
191
+ label="🤗 Hugging Face Hub Wisdom",
192
+ lines=15,
193
+ interactive=False,
194
+ placeholder="Use the tabs above to search for models, datasets, papers, or spaces..."
195
+ )
196
+
197
+ # Example queries
198
+ with gr.Row():
199
+ gr.Examples(
200
+ examples=[
201
+ ["GPT language models", "text-generation", "transformers"],
202
+ ["image classification models", "image-classification", "pytorch"],
203
+ ["sentiment analysis", "text-classification", ""],
204
+ ["stable diffusion", "text-to-image", "diffusers"],
205
+ ["whisper speech recognition", "automatic-speech-recognition", ""]
206
+ ],
207
+ inputs=[model_query, model_task, model_library],
208
+ label="🤖 Try These Model Searches"
209
+ )
210
+
211
+ # Connection controls
212
+ with gr.Row():
213
+ connect_btn = gr.Button("🔗 Connect to HF Hub MCP", variant="secondary")
214
+ status_btn = gr.Button("📊 Check Status", variant="secondary")
215
+ tools_btn = gr.Button("🛠️ List Tools", variant="secondary")
216
+
217
+ def handle_set_token(token: str):
218
+ if token.strip():
219
+ self.hf_token = token.strip()
220
+ return "✅ Hugging Face token configured successfully!"
221
+ return "❌ Please provide a valid HF token"
222
+
223
+ def handle_search_models(query: str, task: str, library: str):
224
+ if not query.strip():
225
+ return "❓ Please enter a model search query."
226
+ return self.search_models(query, task if task.strip() else None, library if library.strip() else None)
227
+
228
+ def handle_search_datasets(query: str, author: str, tags: str):
229
+ if not query.strip():
230
+ return "❓ Please enter a dataset search query."
231
+ return self.search_datasets(query, author if author.strip() else None, tags if tags.strip() else None)
232
+
233
+ def handle_search_papers(query: str):
234
+ if not query.strip():
235
+ return "❓ Please enter a papers search query."
236
+ return self.search_papers(query)
237
+
238
+ def handle_search_spaces(query: str):
239
+ if not query.strip():
240
+ return "❓ Please enter a spaces search query."
241
+ return self.search_spaces(query)
242
+
243
+ def handle_connect():
244
+ result = self.connect_to_mcp()
245
+ # Update connection status
246
+ new_status = f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to Hugging Face Hub spirits' if self.connected else '🔴 Disconnected from HF Hub realm'}</div>"
247
+ return result, new_status
248
+
249
+ def handle_status():
250
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
251
+ token_status = "✅ Configured" if self.hf_token else "❌ Not set"
252
+ return f"🤗 **Hugging Face Hub Oracle Status**\n\nConnection: {status}\nHF Token: {token_status}\nLast update: {time.strftime('%H:%M')}\nAvailable: Models, Datasets, Papers, Spaces"
253
+
254
+ def handle_list_tools():
255
+ if not self.connected:
256
+ return "❌ Not connected to Hugging Face Hub MCP server. Please connect first."
257
+ if not self.tools:
258
+ return "❌ No tools available."
259
+
260
+ tool_info = "🛠️ **Available Hugging Face Hub Tools:**\n\n"
261
+ for tool in self.tools:
262
+ tool_info += f"• **{tool.name}**: {tool.description}\n"
263
+ return tool_info
264
+
265
+ # Wire up events
266
+ set_token_btn.click(
267
+ handle_set_token,
268
+ inputs=[hf_token_input],
269
+ outputs=[search_output]
270
+ )
271
+
272
+ search_models_btn.click(
273
+ handle_search_models,
274
+ inputs=[model_query, model_task, model_library],
275
+ outputs=[search_output]
276
+ )
277
+
278
+ search_datasets_btn.click(
279
+ handle_search_datasets,
280
+ inputs=[dataset_query, dataset_author, dataset_tags],
281
+ outputs=[search_output]
282
+ )
283
+
284
+ search_papers_btn.click(
285
+ handle_search_papers,
286
+ inputs=[papers_query],
287
+ outputs=[search_output]
288
+ )
289
+
290
+ search_spaces_btn.click(
291
+ handle_search_spaces,
292
+ inputs=[spaces_query],
293
+ outputs=[search_output]
294
+ )
295
+
296
+ connect_btn.click(
297
+ handle_connect,
298
+ outputs=[search_output, connection_status]
299
+ )
300
+
301
+ status_btn.click(
302
+ handle_status,
303
+ outputs=[search_output]
304
+ )
305
+
306
+ tools_btn.click(
307
+ handle_list_tools,
308
+ outputs=[search_output]
309
+ )
310
+
311
+ return interface
312
+
313
+ def connect_to_mcp(self) -> str:
314
+ """Connect to Hugging Face Hub MCP server."""
315
+ current_time = time.time()
316
+ if current_time - self.last_connection_attempt < self.connection_cooldown:
317
+ return "⏳ Please wait before retrying connection..."
318
+
319
+ self.last_connection_attempt = current_time
320
+
321
+ if not self.hf_token:
322
+ return "❌ Please configure your Hugging Face token first using the token field above."
323
+
324
+ try:
325
+ return self.loop.run_until_complete(self._connect())
326
+ except Exception as e:
327
+ self.connected = False
328
+ return f"❌ Connection failed: {str(e)}"
329
+
330
+ async def _connect(self) -> str:
331
+ """Async connect to Hugging Face Hub MCP server."""
332
+ try:
333
+ # Clean up previous connection
334
+ if self.exit_stack:
335
+ await self.exit_stack.aclose()
336
+
337
+ self.exit_stack = AsyncExitStack()
338
+
339
+ # Connect to MCP server with authorization header
340
+ headers = {
341
+ "Authorization": f"Bearer {self.hf_token}"
342
+ }
343
+
344
+ # Note: This is a simplified connection example
345
+ # The actual HF MCP server might use different connection methods
346
+ sse_transport = await self.exit_stack.enter_async_context(
347
+ sse_client(self.server_url, headers=headers)
348
+ )
349
+ read_stream, write_callable = sse_transport
350
+
351
+ self.session = await self.exit_stack.enter_async_context(
352
+ ClientSession(read_stream, write_callable)
353
+ )
354
+ await self.session.initialize()
355
+
356
+ # Get available tools
357
+ response = await self.session.list_tools()
358
+ self.tools = response.tools
359
+
360
+ self.connected = True
361
+ tool_names = [tool.name for tool in self.tools]
362
+ return f"✅ Connected to Hugging Face Hub MCP server!\nAvailable tools: {', '.join(tool_names)}"
363
+
364
+ except Exception as e:
365
+ self.connected = False
366
+ return f"❌ Connection failed: {str(e)}"
367
+
368
+ def search_models(self, query: str, task: str = None, library: str = None) -> str:
369
+ """Search for ML models using Hugging Face Hub MCP server"""
370
+ if not self.connected:
371
+ # Try to auto-connect
372
+ connect_result = self.connect_to_mcp()
373
+ if not self.connected:
374
+ return f"❌ Failed to connect to Hugging Face Hub server. {connect_result}"
375
+
376
+ try:
377
+ return self.loop.run_until_complete(self._search_models(query, task, library))
378
+ except Exception as e:
379
+ return f"❌ Error searching models: {str(e)}"
380
+
381
+ async def _search_models(self, query: str, task: str = None, library: str = None) -> str:
382
+ """Async search models using HF Hub MCP."""
383
+ try:
384
+ # Find the model search tool
385
+ search_tool = next((tool for tool in self.tools if 'model' in tool.name.lower() and 'search' in tool.name.lower()), None)
386
+
387
+ if not search_tool:
388
+ return "❌ Model search tool not found on server"
389
+
390
+ # Build parameters
391
+ params = {"query": query}
392
+ if task:
393
+ params["task"] = task
394
+ if library:
395
+ params["library"] = library
396
+
397
+ # Call the tool
398
+ result = await self.session.call_tool(search_tool.name, params)
399
+
400
+ return self._format_search_results(result, "Models", query)
401
+
402
+ except Exception as e:
403
+ return f"❌ Failed to search models: {str(e)}"
404
+
405
+ def search_datasets(self, query: str, author: str = None, tags: str = None) -> str:
406
+ """Search for datasets using Hugging Face Hub MCP server"""
407
+ if not self.connected:
408
+ connect_result = self.connect_to_mcp()
409
+ if not self.connected:
410
+ return f"❌ Failed to connect to Hugging Face Hub server. {connect_result}"
411
+
412
+ try:
413
+ return self.loop.run_until_complete(self._search_datasets(query, author, tags))
414
+ except Exception as e:
415
+ return f"❌ Error searching datasets: {str(e)}"
416
+
417
+ async def _search_datasets(self, query: str, author: str = None, tags: str = None) -> str:
418
+ """Async search datasets using HF Hub MCP."""
419
+ try:
420
+ # Find the dataset search tool
421
+ search_tool = next((tool for tool in self.tools if 'dataset' in tool.name.lower() and 'search' in tool.name.lower()), None)
422
+
423
+ if not search_tool:
424
+ return "❌ Dataset search tool not found on server"
425
+
426
+ # Build parameters
427
+ params = {"query": query}
428
+ if author:
429
+ params["author"] = author
430
+ if tags:
431
+ params["tags"] = tags
432
+
433
+ # Call the tool
434
+ result = await self.session.call_tool(search_tool.name, params)
435
+
436
+ return self._format_search_results(result, "Datasets", query)
437
+
438
+ except Exception as e:
439
+ return f"❌ Failed to search datasets: {str(e)}"
440
+
441
+ def search_papers(self, query: str) -> str:
442
+ """Search for ML research papers using Hugging Face Hub MCP server"""
443
+ if not self.connected:
444
+ connect_result = self.connect_to_mcp()
445
+ if not self.connected:
446
+ return f"❌ Failed to connect to Hugging Face Hub server. {connect_result}"
447
+
448
+ try:
449
+ return self.loop.run_until_complete(self._search_papers(query))
450
+ except Exception as e:
451
+ return f"❌ Error searching papers: {str(e)}"
452
+
453
+ async def _search_papers(self, query: str) -> str:
454
+ """Async search papers using HF Hub MCP."""
455
+ try:
456
+ # Find the papers search tool
457
+ search_tool = next((tool for tool in self.tools if 'paper' in tool.name.lower() and 'search' in tool.name.lower()), None)
458
+
459
+ if not search_tool:
460
+ return "❌ Papers search tool not found on server"
461
+
462
+ # Call the tool
463
+ params = {"query": query}
464
+ result = await self.session.call_tool(search_tool.name, params)
465
+
466
+ return self._format_search_results(result, "Papers", query)
467
+
468
+ except Exception as e:
469
+ return f"❌ Failed to search papers: {str(e)}"
470
+
471
+ def search_spaces(self, query: str) -> str:
472
+ """Search for AI Spaces using Hugging Face Hub MCP server"""
473
+ if not self.connected:
474
+ connect_result = self.connect_to_mcp()
475
+ if not self.connected:
476
+ return f"❌ Failed to connect to Hugging Face Hub server. {connect_result}"
477
+
478
+ try:
479
+ return self.loop.run_until_complete(self._search_spaces(query))
480
+ except Exception as e:
481
+ return f"❌ Error searching spaces: {str(e)}"
482
+
483
+ async def _search_spaces(self, query: str) -> str:
484
+ """Async search spaces using HF Hub MCP."""
485
+ try:
486
+ # Find the spaces search tool
487
+ search_tool = next((tool for tool in self.tools if 'space' in tool.name.lower() and 'search' in tool.name.lower()), None)
488
+
489
+ if not search_tool:
490
+ return "❌ Spaces search tool not found on server"
491
+
492
+ # Call the tool
493
+ params = {"query": query}
494
+ result = await self.session.call_tool(search_tool.name, params)
495
+
496
+ return self._format_search_results(result, "Spaces", query)
497
+
498
+ except Exception as e:
499
+ return f"❌ Failed to search spaces: {str(e)}"
500
+
501
+ def _format_search_results(self, result, search_type: str, query: str) -> str:
502
+ """Format search results from MCP response."""
503
+ try:
504
+ # Extract content properly
505
+ content_text = ""
506
+ if hasattr(result, 'content') and result.content:
507
+ if isinstance(result.content, list):
508
+ for content_item in result.content:
509
+ if hasattr(content_item, 'text'):
510
+ content_text += content_item.text
511
+ elif hasattr(content_item, 'content'):
512
+ content_text += str(content_item.content)
513
+ else:
514
+ content_text += str(content_item)
515
+ elif hasattr(result.content, 'text'):
516
+ content_text = result.content.text
517
+ else:
518
+ content_text = str(result.content)
519
+
520
+ if not content_text:
521
+ return f"❌ No {search_type.lower()} found for query: '{query}'"
522
+
523
+ # Try to parse as JSON for structured results
524
+ try:
525
+ parsed = json.loads(content_text)
526
+ if isinstance(parsed, list):
527
+ formatted = f"🤗 **{search_type} Search Results for: '{query}'**\n\n"
528
+
529
+ for i, item in enumerate(parsed[:5], 1): # Limit to top 5 results
530
+ if isinstance(item, dict):
531
+ # Format based on search type
532
+ if search_type == "Models":
533
+ name = item.get('name', item.get('modelId', 'Unknown'))
534
+ downloads = item.get('downloads', 'N/A')
535
+ task = item.get('pipeline_tag', item.get('task', 'N/A'))
536
+ formatted += f"**{i}. {name}**\n"
537
+ formatted += f"📈 Downloads: {downloads}\n"
538
+ formatted += f"🎯 Task: {task}\n"
539
+ formatted += f"🔗 https://huggingface.co/{name}\n\n"
540
+
541
+ elif search_type == "Datasets":
542
+ name = item.get('name', item.get('id', 'Unknown'))
543
+ downloads = item.get('downloads', 'N/A')
544
+ size = item.get('size', 'N/A')
545
+ formatted += f"**{i}. {name}**\n"
546
+ formatted += f"📊 Downloads: {downloads}\n"
547
+ formatted += f"💾 Size: {size}\n"
548
+ formatted += f"🔗 https://huggingface.co/datasets/{name}\n\n"
549
+
550
+ elif search_type == "Papers":
551
+ title = item.get('title', 'Unknown')
552
+ authors = item.get('authors', ['Unknown'])
553
+ paper_id = item.get('id', '')
554
+ formatted += f"**{i}. {title}**\n"
555
+ formatted += f"👥 Authors: {', '.join(authors) if isinstance(authors, list) else authors}\n"
556
+ formatted += f"🔗 https://huggingface.co/papers/{paper_id}\n\n"
557
+
558
+ elif search_type == "Spaces":
559
+ name = item.get('name', item.get('id', 'Unknown'))
560
+ sdk = item.get('sdk', 'N/A')
561
+ likes = item.get('likes', 'N/A')
562
+ formatted += f"**{i}. {name}**\n"
563
+ formatted += f"⚙️ SDK: {sdk}\n"
564
+ formatted += f"❤️ Likes: {likes}\n"
565
+ formatted += f"🔗 https://huggingface.co/spaces/{name}\n\n"
566
+
567
+ formatted += f"⏰ Search completed: {time.strftime('%H:%M')}\n🤗 **Powered by Hugging Face Hub MCP**"
568
+ return formatted
569
+
570
+ except json.JSONDecodeError:
571
+ pass
572
+
573
+ # Fallback: return formatted raw content
574
+ formatted = f"🤗 **{search_type} Search Results for: '{query}'**\n\n"
575
+ formatted += f"```\n{content_text[:1500]}{'...' if len(content_text) > 1500 else ''}\n```\n\n"
576
+ formatted += f"⏰ Search completed: {time.strftime('%H:%M')}\n🤗 **Powered by Hugging Face Hub MCP**"
577
+ return formatted
578
+
579
+ except Exception as e:
580
+ return f"❌ Error formatting {search_type.lower()} results: {str(e)}"
581
+
582
+ def handle_command(self, player_id: str, command: str) -> str:
583
+ """Handle Hugging Face Hub Oracle commands."""
584
+ parts = command.strip().split(' ', 2)
585
+ cmd = parts[0].lower()
586
+
587
+ if cmd == "models" and len(parts) > 1:
588
+ query = parts[1]
589
+ return self.search_models(query)
590
+ elif cmd == "datasets" and len(parts) > 1:
591
+ query = parts[1]
592
+ return self.search_datasets(query)
593
+ elif cmd == "papers" and len(parts) > 1:
594
+ query = parts[1]
595
+ return self.search_papers(query)
596
+ elif cmd == "spaces" and len(parts) > 1:
597
+ query = parts[1]
598
+ return self.search_spaces(query)
599
+ elif cmd == "connect":
600
+ return self.connect_to_mcp()
601
+ elif cmd == "status":
602
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
603
+ token_status = "✅ Configured" if self.hf_token else "❌ Not set"
604
+ return f"🤗 **Hugging Face Hub Oracle Status**\n\nConnection: {status}\nHF Token: {token_status}\nLast update: {time.strftime('%H:%M')}\nAvailable: Models, Datasets, Papers, Spaces"
605
+ elif cmd == "tools":
606
+ if not self.connected:
607
+ return "❌ Not connected to Hugging Face Hub MCP server. Use `connect` first."
608
+ if not self.tools:
609
+ return "❌ No tools available."
610
+
611
+ tool_info = "🛠️ **Available Hugging Face Hub Tools:**\n\n"
612
+ for tool in self.tools:
613
+ tool_info += f"• **{tool.name}**: {tool.description}\n"
614
+ return tool_info
615
+ elif cmd == "help":
616
+ return """🤗 **Hugging Face Hub Oracle Commands:**
617
+
618
+ **models** `<query>` - Search for ML models (e.g., 'models text classification')
619
+ **datasets** `<query>` - Search for datasets (e.g., 'datasets image classification')
620
+ **papers** `<query>` - Search for research papers (e.g., 'papers transformer')
621
+ **spaces** `<query>` - Search for AI spaces/apps (e.g., 'spaces chatbot')
622
+ **connect** - Connect to Hugging Face Hub MCP server
623
+ **status** - Check connection and token status
624
+ **tools** - List available MCP tools
625
+ **help** - Show this help
626
+
627
+ 🤗 **Example Commands:**
628
+ • models GPT language models
629
+ • datasets computer vision
630
+ • papers attention mechanism
631
+ • spaces image generator
632
+
633
+ 🔑 **Note**: Configure HF token via UI for full functionality"""
634
+ else:
635
+ return "❓ Invalid command. Try: `models <query>`, `datasets <query>`, `papers <query>`, `spaces <query>`, `connect`, `status`, `tools`, or `help`"
636
+
637
+
638
+ # Global Hugging Face Hub Oracle service instance
639
+ huggingface_hub_oracle_service = HuggingFaceHubOracleService()
640
+
641
+
642
+ def auto_register(game_engine):
643
+ """Auto-register the Hugging Face Hub Oracle addon with the game engine.
644
+
645
+ This function makes the addon self-contained by handling its own registration.
646
+ """
647
+ try:
648
+ # Create the Hugging Face Hub oracle NPC definition
649
+ huggingface_hub_oracle_npc = {
650
+ 'id': 'huggingface_hub_oracle_auto',
651
+ 'name': '🤗 Hugging Face Hub Oracle (MCP)',
652
+ 'x': 300, 'y': 150,
653
+ 'char': '🤗',
654
+ 'type': 'mcp',
655
+ 'personality': 'huggingface_hub_oracle',
656
+ 'description': 'Self-contained Hugging Face Hub MCP-powered ML models, datasets, papers, and spaces search service'
657
+ }
658
+
659
+ # Register the NPC with the NPC service
660
+ npc_service = game_engine.get_npc_service()
661
+ npc_service.register_npc('huggingface_hub_oracle_auto', huggingface_hub_oracle_npc)
662
+
663
+ # Register the addon for handling private message commands
664
+ world = game_engine.get_world()
665
+ if not hasattr(world, 'addon_npcs'):
666
+ world.addon_npcs = {}
667
+ world.addon_npcs['huggingface_hub_oracle_auto'] = huggingface_hub_oracle_service
668
+
669
+ print("[HuggingFaceHubOracleAddon] Auto-registered successfully as self-contained addon")
670
+ return True
671
+
672
+ except Exception as e:
673
+ print(f"[HuggingFaceHubOracleAddon] Error during auto-registration: {e}")
674
+ return False
src/addons/magic_shop_addon.py CHANGED
@@ -75,7 +75,7 @@ class MagicShopAddon(NPCAddon):
75
  return {
76
  'id': 'magic_shop_keeper',
77
  'name': '🧙‍♀️ Mystical Mage Elara',
78
- 'x': 300, 'y': 150, # Position in the game world
79
  'char': '🧙‍♀️',
80
  'type': 'magic_shop',
81
  'description': 'Wise mage selling powerful spells and magical items'
@@ -507,3 +507,39 @@ Based on your level, I recommend starting with healing magic!
507
  # Global instance - this triggers auto-registration!
508
  # Just like weather_oracle_service at the bottom of weather_oracle_addon.py
509
  magic_shop_addon = MagicShopAddon()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  return {
76
  'id': 'magic_shop_keeper',
77
  'name': '🧙‍♀️ Mystical Mage Elara',
78
+ 'x': 550, 'y': 450, # Position in the game world
79
  'char': '🧙‍♀️',
80
  'type': 'magic_shop',
81
  'description': 'Wise mage selling powerful spells and magical items'
 
507
  # Global instance - this triggers auto-registration!
508
  # Just like weather_oracle_service at the bottom of weather_oracle_addon.py
509
  magic_shop_addon = MagicShopAddon()
510
+
511
+
512
+ def auto_register(game_engine):
513
+ """Auto-register the Magic Shop addon with the game engine."""
514
+ try:
515
+ # Create addon instance
516
+ addon_instance = MagicShopAddon()
517
+
518
+ # Get NPC config
519
+ npc_config = {
520
+ 'id': 'magic_shop',
521
+ 'name': '🔮 Elara\'s Magic Shop',
522
+ 'x': 350, 'y': 100,
523
+ 'char': '🔮',
524
+ 'type': 'magic_shop',
525
+ 'personality': 'mystical'
526
+ }
527
+
528
+ # Register NPC in world
529
+ npc_service = game_engine.get_npc_service()
530
+ npc_service.register_npc(npc_config['id'], npc_config)
531
+
532
+ # Register addon for command handling
533
+ if not hasattr(game_engine.get_world(), 'addon_npcs'):
534
+ game_engine.get_world().addon_npcs = {}
535
+ game_engine.get_world().addon_npcs[npc_config['id']] = addon_instance
536
+
537
+ # Call startup
538
+ addon_instance.on_startup()
539
+
540
+ print(f"[MagicShopAddon] Auto-registered successfully as self-contained addon")
541
+ return True
542
+
543
+ except Exception as e:
544
+ print(f"[MagicShopAddon] Error during auto-registration: {e}")
545
+ return False
src/addons/searchhf_addon.py ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ SearchHF Oracle Addon - MCP-based Hugging Face search interface
3
+ """
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Dict, Any, Optional, List
8
+ import gradio as gr
9
+ import json
10
+ from mcp import ClientSession
11
+ from mcp.client.sse import sse_client
12
+ from contextlib import AsyncExitStack
13
+
14
+ from ..interfaces.npc_addon import NPCAddon
15
+
16
+ class SearchHFOracleAddon(NPCAddon):
17
+ """SearchHF Oracle Addon for searching Hugging Face using MCP."""
18
+
19
+ def __init__(self):
20
+ # Initialize properties first
21
+ self.name = "SearchHF Oracle"
22
+ self.description = "Advanced Hugging Face search using specialized MCP server. Search and add any HF mcp to the game world."
23
+ self.version = "1.0.0"
24
+ self.author = "MMOP Team"
25
+
26
+ # NPC characteristics
27
+ self.character = "🔍"
28
+ self.position = (200, 100) # Position on the game map
29
+ self.npc_name = "SearchHF Oracle"
30
+
31
+ # MCP Configuration
32
+ self.mcp_server_url = "https://chris4k-searchhfformcp.hf.space/gradio_api/mcp/sse"
33
+ self.connected = False
34
+
35
+ # Available tools
36
+ self.available_tools = []
37
+
38
+ # Logger
39
+ self.logger = logging.getLogger(__name__)
40
+
41
+ # Initialize parent (which will auto-register)
42
+ super().__init__()
43
+ # MCP client components
44
+ try:
45
+ self.loop = asyncio.get_event_loop()
46
+ except RuntimeError:
47
+ self.loop = asyncio.new_event_loop()
48
+ self.session = None
49
+ self.exit_stack = None
50
+ self.tools = []
51
+
52
+ @property
53
+ def addon_id(self) -> str:
54
+ """Unique identifier for this add-on"""
55
+ return "searchhf_oracle"
56
+
57
+ @property
58
+ def addon_name(self) -> str:
59
+ """Display name for this add-on"""
60
+ return "SearchHF Oracle"
61
+
62
+ @property
63
+ def npc_config(self) -> Dict:
64
+ """NPC configuration for auto-placement in world"""
65
+ return {
66
+ 'id': 'searchhf_oracle',
67
+ 'name': self.npc_name,
68
+ 'x': self.position[0],
69
+ 'y': self.position[1], 'char': self.character,
70
+ 'type': 'oracle',
71
+ 'personality': 'searchhf',
72
+ 'description': self.description
73
+ }
74
+
75
+ @property
76
+ def ui_tab_name(self) -> str:
77
+ """UI tab name for this addon"""
78
+ return "SearchHF Oracle"
79
+
80
+ def handle_command(self, player_id: str, command: str) -> str:
81
+ """Handle player commands via private messages"""
82
+ try:
83
+ if command.startswith("searchhf "):
84
+ query = command[9:].strip()
85
+ if not query:
86
+ return "❌ Usage: searchhf <search_query>"
87
+
88
+ result = asyncio.run(self._call_search_tool(query))
89
+ return result
90
+
91
+ elif command == "searchhf_connect":
92
+ asyncio.run(self.connect_to_mcp())
93
+ if self.connected:
94
+ return f"✅ Connected to SearchHF MCP server with {len(self.available_tools)} tools"
95
+ else:
96
+ return "❌ Failed to connect to SearchHF MCP server"
97
+
98
+ elif command == "searchhf_status":
99
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
100
+ return f"SearchHF Oracle Status: {status} | Tools: {len(self.available_tools)}"
101
+
102
+ elif command == "searchhf_tools":
103
+ if not self.available_tools:
104
+ return "❌ No tools available. Use searchhf_connect first."
105
+
106
+ tools_list = [f"- {tool.get('name', 'unknown')}: {tool.get('description', 'No description')}"
107
+ for tool in self.available_tools]
108
+ return f"🛠️ **Available SearchHF Tools:**\n" + "\n".join(tools_list)
109
+
110
+ elif command == "searchhf_help":
111
+ return f"""
112
+ 🔍 **SearchHF Oracle Help**
113
+
114
+ **Commands:**
115
+ - searchhf <query> - Search Hugging Face
116
+ - searchhf_connect - Connect to MCP server
117
+ - searchhf_status - Check status
118
+ - searchhf_tools - List tools
119
+ - searchhf_help - Show help
120
+ **Example:** searchhf BERT sentiment analysis """
121
+
122
+ return "❓ Unknown command. Use 'searchhf_help' for available commands."
123
+
124
+ except Exception as e:
125
+ self.logger.error(f"[{self.name}] Error handling command {command}: {e}")
126
+ return f"❌ Error processing command: {str(e)}"
127
+
128
+ def on_startup(self):
129
+ """Called when the addon is loaded during game startup"""
130
+ try:
131
+ # Connect to MCP server and fetch tools
132
+ result = self.connect_to_mcp()
133
+ self.logger.info(f"[{self.name}] Startup connection: {result}")
134
+
135
+ except Exception as e:
136
+ self.logger.error(f"[{self.name}] Error during startup: {e}")
137
+ self.connected = False
138
+
139
+ def connect_to_mcp(self) -> str:
140
+ """Synchronous connect to the SearchHF MCP server."""
141
+ try:
142
+ return self.loop.run_until_complete(self._connect())
143
+ except Exception as e:
144
+ self.connected = False
145
+ return f"❌ Connection failed: {e}"
146
+
147
+ async def _connect(self) -> str:
148
+ """Async MCP connection using SSE."""
149
+ try:
150
+ if self.exit_stack:
151
+ await self.exit_stack.aclose()
152
+ self.exit_stack = AsyncExitStack()
153
+ transport = await self.exit_stack.enter_async_context(sse_client(self.mcp_server_url))
154
+ read_stream, write = transport
155
+ self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream, write))
156
+ await self.session.initialize()
157
+ # list available MCP tools
158
+ response = await self.session.list_tools()
159
+ self.tools = response.tools
160
+ self.connected = True
161
+ names = [t.name for t in self.tools]
162
+ return f"✅ Connected with tools: {', '.join(names)}"
163
+ except Exception as e:
164
+ self.connected = False
165
+ return f"❌ Connection error: {e}"
166
+
167
+ def get_interface(self) -> gr.Interface:
168
+ """Create the Gradio interface for the SearchHF addon."""
169
+
170
+ def search_huggingface(query: str, search_type: str = "all") -> str:
171
+ """Search Hugging Face using the MCP server."""
172
+ if not query.strip():
173
+ return "❌ Please enter a search query"
174
+
175
+ if not self.connected:
176
+ return "❌ Not connected to SearchHF MCP server. Use 'Connect' button first."
177
+
178
+ try:
179
+ # Use the MCP service to call the search function
180
+ result = asyncio.run(self._call_search_tool(query, search_type))
181
+ return result
182
+
183
+ except Exception as e:
184
+ error_msg = f"❌ Search error: {str(e)}"
185
+ self.logger.error(f"[{self.name}] {error_msg}")
186
+ return error_msg
187
+
188
+ def connect_to_server() -> str:
189
+ """Connect to the MCP server."""
190
+ try:
191
+ result = asyncio.run(self.connect_to_mcp())
192
+ if self.connected:
193
+ return f"✅ Connected to SearchHF MCP server\n🔧 Available tools: {len(self.available_tools)}"
194
+ else:
195
+ return "❌ Failed to connect to SearchHF MCP server"
196
+ except Exception as e:
197
+ return f"❌ Connection error: {str(e)}"
198
+
199
+ def get_status() -> str:
200
+ """Get current connection status."""
201
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
202
+ return f"""
203
+ **SearchHF Oracle Status:**
204
+ - Connection: {status}
205
+ - MCP Server: {self.mcp_server_url}
206
+ - Available Tools: {len(self.available_tools)}
207
+ - Tools: {', '.join([tool.get('name', 'unknown') for tool in self.available_tools]) if self.available_tools else 'None'}
208
+ """
209
+
210
+ def list_tools() -> str:
211
+ """List available MCP tools."""
212
+ if not self.available_tools:
213
+ return "❌ No tools available. Connect to server first."
214
+
215
+ tools_info = ["**Available SearchHF Tools:**"]
216
+ for tool in self.available_tools:
217
+ name = tool.get('name', 'unknown')
218
+ description = tool.get('description', 'No description')
219
+ tools_info.append(f"- **{name}**: {description}")
220
+
221
+ return "\n".join(tools_info)
222
+
223
+ # Create the interface
224
+ with gr.Blocks(title=f"{self.character} {self.name}") as interface:
225
+ gr.Markdown(f"""
226
+ # {self.character} {self.name}
227
+
228
+ Advanced Hugging Face search using specialized MCP integration.
229
+ Search models, datasets, papers, and spaces with enhanced capabilities.
230
+
231
+ **MCP Server:** `{self.mcp_server_url}`
232
+ """)
233
+
234
+ with gr.Tab("🔍 Search"):
235
+ with gr.Row():
236
+ query_input = gr.Textbox(
237
+ label="Search Query",
238
+ placeholder="Enter your search query (e.g., 'BERT model', 'sentiment analysis', 'computer vision')",
239
+ lines=2
240
+ )
241
+ search_type = gr.Dropdown(
242
+ choices=["all", "models", "datasets", "papers", "spaces"],
243
+ label="Search Type",
244
+ value="all"
245
+ )
246
+
247
+ search_btn = gr.Button("🔍 Search Hugging Face", variant="primary")
248
+
249
+ gr.Markdown("### Examples:")
250
+ example_queries = [
251
+ "BERT sentiment analysis",
252
+ "computer vision transformers",
253
+ "text generation models",
254
+ "image classification datasets",
255
+ "machine learning papers 2024"
256
+ ]
257
+
258
+ for example in example_queries:
259
+ gr.Markdown(f"- `{example}`")
260
+
261
+ result_output = gr.Textbox(
262
+ label="Search Results",
263
+ lines=10,
264
+ max_lines=20
265
+ )
266
+
267
+ search_btn.click(
268
+ search_huggingface,
269
+ inputs=[query_input, search_type],
270
+ outputs=result_output
271
+ )
272
+
273
+ with gr.Tab("🔧 Connection"):
274
+ with gr.Row():
275
+ connect_btn = gr.Button("🔗 Connect to MCP Server", variant="secondary")
276
+ status_btn = gr.Button("📊 Get Status")
277
+ tools_btn = gr.Button("🛠️ List Tools")
278
+
279
+ connection_output = gr.Textbox(
280
+ label="Connection Status",
281
+ lines=8
282
+ )
283
+
284
+ connect_btn.click(connect_to_server, outputs=connection_output)
285
+ status_btn.click(get_status, outputs=connection_output)
286
+ tools_btn.click(list_tools, outputs=connection_output)
287
+
288
+ with gr.Tab("ℹ️ Help"):
289
+ gr.Markdown(f"""
290
+ ## {self.character} SearchHF Oracle Help
291
+
292
+ ### Chat Commands:
293
+ - `/searchhf <query>` - Search Hugging Face with your query
294
+ - `/searchhf_connect` - Connect to MCP server
295
+ - `/searchhf_status` - Check connection status
296
+ - `/searchhf_tools` - List available tools
297
+ - `/searchhf_help` - Show this help
298
+
299
+ ### Features:
300
+ - 🔍 Advanced Hugging Face search
301
+ - 🤖 Models, datasets, papers, and spaces
302
+ - 🔗 MCP-based integration
303
+ - 📊 Real-time status monitoring
304
+
305
+ ### MCP Integration:
306
+ The SearchHF Oracle uses a specialized MCP server for enhanced search capabilities:
307
+ ```
308
+ {self.mcp_server_url}
309
+ ```
310
+
311
+ ### Search Types:
312
+ - **All**: Search across all Hugging Face content - **Models**: Focus on ML models
313
+ - **Datasets**: Focus on datasets
314
+ - **Papers**: Focus on research papers
315
+ - **Spaces**: Focus on demo spaces
316
+ """)
317
+
318
+ return interface
319
+
320
+ async def _call_search_tool(self, query: str, search_type: str = "all") -> str:
321
+ """Call the search tool via MCP service and return the formatted result."""
322
+ if not self.connected:
323
+ conn = await self._connect()
324
+ if not self.connected:
325
+ return conn
326
+ # find search tool
327
+ tool = next((t for t in self.tools if 'search' in t.name.lower()), None)
328
+ if not tool:
329
+ return "❌ SearchHF tool not found on MCP server"
330
+ try:
331
+ params = {'query': query, 'search_type': search_type}
332
+ result = await self.session.call_tool(tool.name, params)
333
+ # extract text content
334
+ content = getattr(result, 'content', result)
335
+ text = ''
336
+ if isinstance(content, list):
337
+ for item in content:
338
+ text += getattr(item, 'text', str(item))
339
+ else:
340
+ text = getattr(content, 'text', str(content))
341
+ return text or "❌ Empty response from SearchHF MCP"
342
+ except Exception as e:
343
+ return f"❌ Search error: {e}"
344
+ # Auto-registration function for the game engine
345
+ def auto_register(game_engine) -> bool:
346
+ """Auto-register the SearchHF Oracle addon with the game engine."""
347
+ try:
348
+ # Create addon instance (this will auto-register via NPCAddon)
349
+ addon = SearchHFOracleAddon()
350
+
351
+ # Register NPC if needed
352
+ if addon.npc_config:
353
+ npc_service = game_engine.get_npc_service()
354
+ npc_service.register_npc(addon.npc_config['id'], addon.npc_config)
355
+
356
+ # Add to addon NPCs for command handling
357
+ if not hasattr(game_engine.get_world(), 'addon_npcs'):
358
+ game_engine.get_world().addon_npcs = {}
359
+ game_engine.get_world().addon_npcs[addon.addon_id] = addon
360
+
361
+ # Call startup
362
+ addon.on_startup()
363
+
364
+ print(f"[SearchHFOracleAddon] Auto-registered successfully as self-contained addon")
365
+ return True
366
+
367
+ except Exception as e:
368
+ print(f"[SearchHFOracleAddon] Error during auto-registration: {e}")
369
+ return False
src/addons/simple_trader_addon.py CHANGED
@@ -82,32 +82,7 @@ class SimpleTraderAddon(NPCAddon):
82
  'personality': 'trader',
83
  'description': 'A friendly trader selling useful items'
84
  }
85
-
86
- @classmethod
87
- def auto_register(cls, game_engine):
88
- """Automatically register this NPC with the game engine."""
89
- try:
90
- # Create addon instance
91
- addon_instance = cls()
92
-
93
- # Get NPC config
94
- npc_config = cls.get_npc_config()
95
-
96
- # Register NPC in world
97
- npc_service = game_engine.get_npc_service()
98
- npc_service.register_npc(npc_config['id'], npc_config)
99
-
100
- # Register addon for command handling
101
- if not hasattr(game_engine.get_world(), 'addon_npcs'):
102
- game_engine.get_world().addon_npcs = {}
103
- game_engine.get_world().addon_npcs[npc_config['id']] = addon_instance
104
-
105
- print(f"[SimpleTrader] Auto-registered NPC '{npc_config['name']}' at position ({npc_config['x']}, {npc_config['y']})")
106
- return True
107
-
108
- except Exception as e:
109
- print(f"[SimpleTrader] Auto-registration failed: {e}")
110
- return False
111
 
112
  # ===== PRIVATE METHODS =====
113
 
@@ -181,6 +156,34 @@ simple_trader_addon = SimpleTraderAddon()
181
 
182
 
183
  # Auto-registration function that can be called from game engine
184
- def register_simple_trader(game_engine):
185
- """Convenience function to register this addon."""
186
- return SimpleTraderAddon.auto_register(game_engine)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
  'personality': 'trader',
83
  'description': 'A friendly trader selling useful items'
84
  }
85
+ # ===== PRIVATE METHODS =====
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  # ===== PRIVATE METHODS =====
88
 
 
156
 
157
 
158
  # Auto-registration function that can be called from game engine
159
+ def auto_register(game_engine):
160
+ """Auto-register the Simple Trader addon with the game engine."""
161
+ try:
162
+ # Create addon instance
163
+ addon_instance = SimpleTraderAddon()
164
+
165
+ # Get NPC config
166
+ npc_config = {
167
+ 'id': 'simple_trader',
168
+ 'name': '🛒 Simple Trader',
169
+ 'x': 300, 'y': 300,
170
+ 'char': '🛒',
171
+ 'type': 'trader',
172
+ 'personality': 'friendly'
173
+ }
174
+
175
+ # Register NPC in world
176
+ npc_service = game_engine.get_npc_service()
177
+ npc_service.register_npc(npc_config['id'], npc_config)
178
+
179
+ # Register addon for command handling
180
+ if not hasattr(game_engine.get_world(), 'addon_npcs'):
181
+ game_engine.get_world().addon_npcs = {}
182
+ game_engine.get_world().addon_npcs[npc_config['id']] = addon_instance
183
+
184
+ print(f"[SimpleTrader] Auto-registered successfully as self-contained addon")
185
+ return True
186
+
187
+ except Exception as e:
188
+ print(f"[SimpleTrader] Error during auto-registration: {e}")
189
+ return False
src/addons/weather_oracle_addon.py CHANGED
@@ -354,8 +354,8 @@ def auto_register(game_engine):
354
  # Create the weather oracle NPC definition
355
  weather_oracle_npc = {
356
  'id': 'weather_oracle_auto',
357
- 'name': '🌤️ Weather Oracle (Auto)',
358
- 'x': 300, 'y': 150,
359
  'char': '🌤️',
360
  'type': 'mcp',
361
  'personality': 'weather_oracle',
 
354
  # Create the weather oracle NPC definition
355
  weather_oracle_npc = {
356
  'id': 'weather_oracle_auto',
357
+ 'name': '🌤️ Weather Oracle (MCCP)',
358
+ 'x': 200, 'y': 350,
359
  'char': '🌤️',
360
  'type': 'mcp',
361
  'personality': 'weather_oracle',
src/core/__pycache__/game_engine.cpython-313.pyc CHANGED
Binary files a/src/core/__pycache__/game_engine.cpython-313.pyc and b/src/core/__pycache__/game_engine.cpython-313.pyc differ
 
src/core/__pycache__/world.cpython-313.pyc CHANGED
Binary files a/src/core/__pycache__/world.cpython-313.pyc and b/src/core/__pycache__/world.cpython-313.pyc differ
 
src/core/game_engine.py CHANGED
@@ -1,260 +1,257 @@
1
- """
2
- Game Engine - Singleton pattern for centralized game management.
3
- """
4
-
5
- import threading
6
- from typing import Dict, Any, Optional
7
- from ..interfaces.game_interfaces import IGameEngine, IGameWorld
8
- from ..core.world import GameWorld
9
- from ..services.player_service import PlayerService
10
- from ..services.chat_service import ChatService
11
- from ..services.npc_service import NPCService
12
- from ..services.mcp_service import MCPService
13
- from ..services.plugin_service import PluginService
14
-
15
-
16
- class GameEngine(IGameEngine):
17
- """Singleton Game Engine managing all game services and state."""
18
-
19
- _instance = None
20
- _lock = threading.Lock()
21
-
22
- def __new__(cls):
23
- if cls._instance is None:
24
- with cls._lock:
25
- if cls._instance is None:
26
- cls._instance = super(GameEngine, cls).__new__(cls)
27
- return cls._instance
28
-
29
- def __init__(self):
30
- if hasattr(self, '_initialized'):
31
- return
32
-
33
- self._initialized = True
34
- self._running = False
35
- self._services: Dict[str, Any] = {}
36
-
37
- # Initialize core components
38
- self._game_world = GameWorld()
39
- # Initialize services
40
- self._player_service = PlayerService(self._game_world)
41
- self._plugin_service = PluginService("plugins")
42
- self._chat_service = ChatService(self._game_world, self._plugin_service)
43
- self._npc_service = NPCService(self._game_world)
44
- self._mcp_service = MCPService(self._player_service, self._chat_service)
45
-
46
- # Register services
47
- self._services.update({
48
- 'player': self._player_service,
49
- 'chat': self._chat_service,
50
- 'npc': self._npc_service,
51
- 'mcp': self._mcp_service,
52
- 'plugin': self._plugin_service
53
- })
54
-
55
- print("[GameEngine] Initialized with all services")
56
-
57
- def start(self) -> bool:
58
- """Start the game engine."""
59
- try:
60
- if self._running:
61
- print("[GameEngine] Already running")
62
- return True
63
-
64
- # Set up plugin context
65
- self._plugin_service.set_game_context({
66
- 'game_world': self._game_world,
67
- 'player_service': self._player_service,
68
- 'chat_service': self._chat_service,
69
- 'npc_service': self._npc_service,
70
- 'mcp_service': self._mcp_service
71
- })
72
-
73
- # Load plugins
74
- plugin_count = self._plugin_service.load_plugins()
75
- print(f"[GameEngine] Loaded {plugin_count} plugins")
76
-
77
- # Initialize default world state
78
- self._initialize_world()
79
-
80
- self._running = True
81
- print("[GameEngine] Started successfully")
82
- return True
83
-
84
- except Exception as e:
85
- print(f"[GameEngine] Error starting: {e}")
86
- return False
87
-
88
- def stop(self) -> bool:
89
- """Stop the game engine."""
90
- try:
91
- if not self._running:
92
- return True
93
- # Unload all plugins
94
- for plugin_id in list(self._plugin_service.get_loaded_plugins()):
95
- self._plugin_service.unload_plugin(plugin_id)
96
-
97
- self._running = False
98
- print("[GameEngine] Stopped successfully")
99
- return True
100
-
101
- except Exception as e:
102
- print(f"[GameEngine] Error stopping: {e}")
103
- return False
104
-
105
- def get_world(self) -> IGameWorld:
106
- """Get the game world instance."""
107
- return self._game_world
108
-
109
- def register_service(self, service_name: str, service: Any) -> bool:
110
- """Register a service with the engine."""
111
- try:
112
- self._services[service_name] = service
113
- print(f"[GameEngine] Registered service: {service_name}")
114
- return True
115
- except Exception as e:
116
- print(f"[GameEngine] Error registering service {service_name}: {e}")
117
- return False
118
-
119
- def get_service(self, service_name: str) -> Optional[Any]:
120
- """Get a registered service."""
121
- return self._services.get(service_name)
122
-
123
- # Convenience methods for common operations
124
- def get_player_service(self) -> PlayerService:
125
- """Get player service."""
126
- return self._player_service
127
-
128
- def get_chat_service(self) -> ChatService:
129
- """Get chat service."""
130
- return self._chat_service
131
-
132
- def get_npc_service(self) -> NPCService:
133
- """Get NPC service."""
134
- return self._npc_service
135
-
136
- def get_mcp_service(self) -> MCPService:
137
- """Get MCP service."""
138
- return self._mcp_service
139
-
140
- def get_plugin_service(self) -> PluginService:
141
- """Get plugin service."""
142
- return self._plugin_service
143
-
144
- def is_running(self) -> bool:
145
- """Check if engine is running."""
146
- return self._running
147
-
148
- def _initialize_world(self):
149
- """Initialize the world with default NPCs and setup."""
150
- try:
151
- # Add Donald NPC with new behavior
152
- donald_npc = {
153
- 'id': 'donald',
154
- 'name': 'Donald the Trader',
155
- 'x': 400, 'y': 200,
156
- 'char': '💼',
157
- 'type': 'trader',
158
- 'personality': 'donald'
159
- }
160
- self._npc_service.register_npc('donald', donald_npc)
161
- # Register addon NPCs
162
- self._register_addon_npcs()
163
-
164
- # Initialize default sample plugin if in dev mode
165
- self._plugin_service.create_sample_plugin()
166
-
167
- print("[GameEngine] World initialized with default content")
168
-
169
- except Exception as e:
170
- print(f"[GameEngine] Error initializing world: {e}")
171
-
172
- def _register_addon_npcs(self):
173
- """Register NPC addons that can handle private message commands."""
174
- try:
175
- # Import the Read2Burn service
176
- from ..addons.read2burn_addon import read2burn_service
177
-
178
- # Register the Read2Burn NPC first
179
- read2burn_npc = {
180
- 'id': 'read2burn',
181
- 'name': '📮 Read2Burn Service',
182
- 'x': 100, 'y': 375,
183
- 'char': '📮',
184
- 'type': 'service',
185
- 'personality': 'read2burn',
186
- 'description': 'Secure message service - send private messages that auto-delete after reading' }
187
- self._npc_service.register_npc('read2burn', read2burn_npc)
188
-
189
- # Register the addon for handling private message commands
190
- # The Read2BurnService already implements handle_command(player_id, command)
191
- if not hasattr(self._game_world, 'addon_npcs'):
192
- self._game_world.addon_npcs = {}
193
- self._game_world.addon_npcs['read2burn'] = read2burn_service
194
-
195
- print("[GameEngine] Read2Burn addon registered successfully")
196
-
197
- except ImportError as e:
198
- print(f"[GameEngine] Could not import Read2Burn service: {e}")
199
- except Exception as e:
200
- print(f"[GameEngine] Error registering Read2Burn addon: {e}")
201
- # Auto-register self-contained addons
202
- self._auto_register_addons()
203
-
204
- def _auto_register_addons(self):
205
- """Automatically register all self-contained addons."""
206
- auto_register_addons = [
207
- ('weather_oracle_addon', 'auto_register'),
208
- ('duckduckgo_search_oracle_addon', 'auto_register'),
209
- ]
210
-
211
- for addon_module, auto_register_func in auto_register_addons:
212
- try:
213
- # Use importlib for cleaner imports
214
- import importlib
215
- full_module_name = f'src.addons.{addon_module}'
216
- module = importlib.import_module(full_module_name)
217
- auto_register = getattr(module, auto_register_func)
218
-
219
- # Call the auto-register function with the game engine
220
- success = auto_register(self)
221
- if success:
222
- print(f"[GameEngine] Auto-registered {addon_module} successfully")
223
- else:
224
- print(f"[GameEngine] Failed to auto-register {addon_module}")
225
-
226
- except ImportError as e:
227
- print(f"[GameEngine] Could not import {addon_module}: {e}")
228
- except Exception as e:
229
- print(f"[GameEngine] Error auto-registering {addon_module}: {e}")
230
-
231
- def get_engine_status(self) -> Dict[str, Any]:
232
- """Get comprehensive engine status."""
233
- try:
234
- all_players = self._player_service.get_all_players()
235
- all_npcs = self._npc_service.get_all_npcs()
236
- loaded_plugins = self._plugin_service.get_loaded_plugins()
237
- recent_chat = self._chat_service.get_recent_messages(5)
238
-
239
- return {
240
- 'running': self._running,
241
- 'services': list(self._services.keys()),
242
- 'statistics': {
243
- 'total_players': len(all_players),
244
- 'active_players': len([p for p in all_players.values() if p.is_active()]),
245
- 'total_npcs': len(all_npcs),
246
- 'loaded_plugins': len(loaded_plugins),
247
- 'recent_messages': len(recent_chat)
248
- },
249
- 'loaded_plugins': loaded_plugins,
250
- 'world_state': self._game_world.get_world_state() if hasattr(self._game_world, 'get_world_state') else {}
251
- }
252
-
253
- except Exception as e:
254
- return {'error': str(e), 'running': self._running}
255
-
256
-
257
- # Convenience function to get the singleton instance
258
- def get_game_engine() -> GameEngine:
259
- """Get the singleton game engine instance."""
260
- return GameEngine()
 
1
+ """
2
+ Game Engine - Singleton pattern for centralized game management.
3
+ """
4
+
5
+ import threading
6
+ from typing import Dict, Any, Optional
7
+ from ..interfaces.game_interfaces import IGameEngine, IGameWorld
8
+ from ..core.world import GameWorld
9
+ from ..services.player_service import PlayerService
10
+ from ..services.chat_service import ChatService
11
+ from ..services.npc_service import NPCService
12
+ from ..services.mcp_service import MCPService
13
+ from ..services.plugin_service import PluginService
14
+
15
+
16
+ class GameEngine(IGameEngine):
17
+ """Singleton Game Engine managing all game services and state."""
18
+
19
+ _instance = None
20
+ _lock = threading.Lock()
21
+
22
+ def __new__(cls):
23
+ if cls._instance is None:
24
+ with cls._lock:
25
+ if cls._instance is None:
26
+ cls._instance = super(GameEngine, cls).__new__(cls)
27
+ return cls._instance
28
+
29
+ def __init__(self):
30
+ if hasattr(self, '_initialized'):
31
+ return
32
+
33
+ self._initialized = True
34
+ self._running = False
35
+ self._services: Dict[str, Any] = {}
36
+
37
+ # Initialize core components
38
+ self._game_world = GameWorld()
39
+
40
+ # Initialize services
41
+ self._player_service = PlayerService(self._game_world)
42
+ self._plugin_service = PluginService("plugins")
43
+ self._chat_service = ChatService(self._game_world, self._plugin_service)
44
+ self._npc_service = NPCService(self._game_world)
45
+ self._mcp_service = MCPService(self._player_service, self._chat_service)
46
+
47
+ # Register services
48
+ self._services.update({
49
+ 'player': self._player_service,
50
+ 'chat': self._chat_service,
51
+ 'npc': self._npc_service,
52
+ 'mcp': self._mcp_service,
53
+ 'plugin': self._plugin_service
54
+ })
55
+
56
+ print("[GameEngine] Initialized with all services")
57
+
58
+ def start(self) -> bool:
59
+ """Start the game engine."""
60
+ try:
61
+ if self._running:
62
+ print("[GameEngine] Already running")
63
+ return True
64
+
65
+ # Set up plugin context
66
+ self._plugin_service.set_game_context({
67
+ 'game_world': self._game_world,
68
+ 'player_service': self._player_service,
69
+ 'chat_service': self._chat_service,
70
+ 'npc_service': self._npc_service,
71
+ 'mcp_service': self._mcp_service
72
+ })
73
+
74
+ # Load plugins
75
+ plugin_count = self._plugin_service.load_plugins()
76
+ print(f"[GameEngine] Loaded {plugin_count} plugins")
77
+
78
+ # Initialize default world state
79
+ self._initialize_world()
80
+
81
+ self._running = True
82
+ print("[GameEngine] Started successfully")
83
+ return True
84
+
85
+ except Exception as e:
86
+ print(f"[GameEngine] Error starting: {e}")
87
+ return False
88
+
89
+ def stop(self) -> bool:
90
+ """Stop the game engine."""
91
+ try:
92
+ if not self._running:
93
+ return True
94
+
95
+ # Unload all plugins
96
+ for plugin_id in list(self._plugin_service.get_loaded_plugins()):
97
+ self._plugin_service.unload_plugin(plugin_id)
98
+
99
+ self._running = False
100
+ print("[GameEngine] Stopped successfully")
101
+ return True
102
+
103
+ except Exception as e:
104
+ print(f"[GameEngine] Error stopping: {e}")
105
+ return False
106
+
107
+ def get_world(self) -> IGameWorld:
108
+ """Get the game world instance."""
109
+ return self._game_world
110
+
111
+ def register_service(self, service_name: str, service: Any) -> bool:
112
+ """Register a service with the engine."""
113
+ try:
114
+ self._services[service_name] = service
115
+ print(f"[GameEngine] Registered service: {service_name}")
116
+ return True
117
+ except Exception as e:
118
+ print(f"[GameEngine] Error registering service {service_name}: {e}")
119
+ return False
120
+
121
+ def get_service(self, service_name: str) -> Optional[Any]:
122
+ """Get a registered service."""
123
+ return self._services.get(service_name)
124
+
125
+ # Convenience methods for common operations
126
+ def get_player_service(self) -> PlayerService:
127
+ """Get player service."""
128
+ return self._player_service
129
+
130
+ def get_chat_service(self) -> ChatService:
131
+ """Get chat service."""
132
+ return self._chat_service
133
+
134
+ def get_npc_service(self) -> NPCService:
135
+ """Get NPC service."""
136
+ return self._npc_service
137
+
138
+ def get_mcp_service(self) -> MCPService:
139
+ """Get MCP service."""
140
+ return self._mcp_service
141
+
142
+ def get_plugin_service(self) -> PluginService:
143
+ """Get plugin service."""
144
+ return self._plugin_service
145
+
146
+ def is_running(self) -> bool:
147
+ """Check if engine is running."""
148
+ return self._running
149
+
150
+ def _initialize_world(self):
151
+ """Initialize the world with default NPCs and setup."""
152
+ try:
153
+ # Add Donald NPC with new behavior
154
+ donald_npc = {
155
+ 'id': 'donald',
156
+ 'name': 'Donald the Trader',
157
+ 'x': 400, 'y': 200,
158
+ 'char': '💼',
159
+ 'type': 'trader',
160
+ 'personality': 'donald'
161
+ }
162
+ self._npc_service.register_npc('donald', donald_npc)
163
+
164
+ # Register addon NPCs
165
+ self._register_addon_npcs()
166
+ # Initialize default sample plugin if in dev mode
167
+ self._plugin_service.create_sample_plugin()
168
+
169
+ print("[GameEngine] World initialized with default content")
170
+ except Exception as e:
171
+ print(f"[GameEngine] Error initializing world: {e}")
172
+
173
+ def _register_addon_npcs(self):
174
+ """Register NPC addons that can handle private message commands."""
175
+ # Auto-register self-contained addons
176
+ self._auto_register_addons()
177
+ def _auto_register_addons(self):
178
+ """Automatically discover and register all self-contained addons."""
179
+ import os
180
+ import importlib
181
+
182
+ # Get the addons directory path
183
+ addons_dir = os.path.join(os.path.dirname(__file__), '..', 'addons')
184
+ addons_dir = os.path.abspath(addons_dir)
185
+
186
+ if not os.path.exists(addons_dir):
187
+ print(f"[GameEngine] Addons directory not found: {addons_dir}")
188
+ return
189
+
190
+ print(f"[GameEngine] Scanning for addons in: {addons_dir}")
191
+
192
+ # Scan all Python files in addons directory
193
+ for filename in os.listdir(addons_dir):
194
+ if not filename.endswith('.py') or filename.startswith('__'):
195
+ continue
196
+
197
+ addon_module_name = filename[:-3] # Remove .py extension
198
+
199
+ try:
200
+ # Import the module
201
+ full_module_name = f'src.addons.{addon_module_name}'
202
+ module = importlib.import_module(full_module_name)
203
+
204
+ # Check if module has an auto_register function
205
+ if hasattr(module, 'auto_register'):
206
+ auto_register_func = getattr(module, 'auto_register')
207
+
208
+ # Verify it's a callable function
209
+ if callable(auto_register_func):
210
+ print(f"[GameEngine] Found auto_register function in {addon_module_name}")
211
+
212
+ # Call the auto-register function with the game engine
213
+ success = auto_register_func(self)
214
+ if success:
215
+ print(f"[GameEngine] Auto-registered {addon_module_name} successfully")
216
+ else:
217
+ print(f"[GameEngine] Failed to auto-register {addon_module_name}")
218
+ else:
219
+ print(f"[GameEngine] auto_register in {addon_module_name} is not callable")
220
+ else:
221
+ print(f"[GameEngine] No auto_register function found in {addon_module_name} (skipping)")
222
+
223
+ except ImportError as e:
224
+ print(f"[GameEngine] Could not import addon {addon_module_name}: {e}")
225
+ except Exception as e:
226
+ print(f"[GameEngine] Error auto-registering {addon_module_name}: {e}")
227
+
228
+ def get_engine_status(self) -> Dict[str, Any]:
229
+ """Get comprehensive engine status."""
230
+ try:
231
+ all_players = self._player_service.get_all_players()
232
+ all_npcs = self._npc_service.get_all_npcs()
233
+ loaded_plugins = self._plugin_service.get_loaded_plugins()
234
+ recent_chat = self._chat_service.get_recent_messages(5)
235
+
236
+ return {
237
+ 'running': self._running,
238
+ 'services': list(self._services.keys()),
239
+ 'statistics': {
240
+ 'total_players': len(all_players),
241
+ 'active_players': len([p for p in all_players.values() if p.is_active()]),
242
+ 'total_npcs': len(all_npcs),
243
+ 'loaded_plugins': len(loaded_plugins),
244
+ 'recent_messages': len(recent_chat)
245
+ },
246
+ 'loaded_plugins': loaded_plugins,
247
+ 'world_state': self._game_world.get_world_state() if hasattr(self._game_world, 'get_world_state') else {}
248
+ }
249
+
250
+ except Exception as e:
251
+ return {'error': str(e), 'running': self._running}
252
+
253
+
254
+ # Convenience function to get the singleton instance
255
+ def get_game_engine() -> GameEngine:
256
+ """Get the singleton game engine instance."""
257
+ return GameEngine()
 
 
 
src/core/game_engine_fixed.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Game Engine - Singleton pattern for centralized game management.
3
+ """
4
+
5
+ import threading
6
+ from typing import Dict, Any, Optional
7
+ from ..interfaces.game_interfaces import IGameEngine, IGameWorld
8
+ from ..core.world import GameWorld
9
+ from ..services.player_service import PlayerService
10
+ from ..services.chat_service import ChatService
11
+ from ..services.npc_service import NPCService
12
+ from ..services.mcp_service import MCPService
13
+ from ..services.plugin_service import PluginService
14
+
15
+
16
+ class GameEngine(IGameEngine):
17
+ """Singleton Game Engine managing all game services and state."""
18
+
19
+ _instance = None
20
+ _lock = threading.Lock()
21
+
22
+ def __new__(cls):
23
+ if cls._instance is None:
24
+ with cls._lock:
25
+ if cls._instance is None:
26
+ cls._instance = super(GameEngine, cls).__new__(cls)
27
+ return cls._instance
28
+
29
+ def __init__(self):
30
+ if hasattr(self, '_initialized'):
31
+ return
32
+
33
+ self._initialized = True
34
+ self._running = False
35
+ self._services: Dict[str, Any] = {}
36
+
37
+ # Initialize core components
38
+ self._game_world = GameWorld()
39
+
40
+ # Initialize services
41
+ self._player_service = PlayerService(self._game_world)
42
+ self._plugin_service = PluginService("plugins")
43
+ self._chat_service = ChatService(self._game_world, self._plugin_service)
44
+ self._npc_service = NPCService(self._game_world)
45
+ self._mcp_service = MCPService(self._player_service, self._chat_service)
46
+
47
+ # Register services
48
+ self._services.update({
49
+ 'player': self._player_service,
50
+ 'chat': self._chat_service,
51
+ 'npc': self._npc_service,
52
+ 'mcp': self._mcp_service,
53
+ 'plugin': self._plugin_service
54
+ })
55
+
56
+ print("[GameEngine] Initialized with all services")
57
+
58
+ def start(self) -> bool:
59
+ """Start the game engine."""
60
+ try:
61
+ if self._running:
62
+ print("[GameEngine] Already running")
63
+ return True
64
+
65
+ # Set up plugin context
66
+ self._plugin_service.set_game_context({
67
+ 'game_world': self._game_world,
68
+ 'player_service': self._player_service,
69
+ 'chat_service': self._chat_service,
70
+ 'npc_service': self._npc_service,
71
+ 'mcp_service': self._mcp_service
72
+ })
73
+
74
+ # Load plugins
75
+ plugin_count = self._plugin_service.load_plugins()
76
+ print(f"[GameEngine] Loaded {plugin_count} plugins")
77
+
78
+ # Initialize default world state
79
+ self._initialize_world()
80
+
81
+ self._running = True
82
+ print("[GameEngine] Started successfully")
83
+ return True
84
+
85
+ except Exception as e:
86
+ print(f"[GameEngine] Error starting: {e}")
87
+ return False
88
+
89
+ def stop(self) -> bool:
90
+ """Stop the game engine."""
91
+ try:
92
+ if not self._running:
93
+ return True
94
+
95
+ # Unload all plugins
96
+ for plugin_id in list(self._plugin_service.get_loaded_plugins()):
97
+ self._plugin_service.unload_plugin(plugin_id)
98
+
99
+ self._running = False
100
+ print("[GameEngine] Stopped successfully")
101
+ return True
102
+
103
+ except Exception as e:
104
+ print(f"[GameEngine] Error stopping: {e}")
105
+ return False
106
+
107
+ def get_world(self) -> IGameWorld:
108
+ """Get the game world instance."""
109
+ return self._game_world
110
+
111
+ def register_service(self, service_name: str, service: Any) -> bool:
112
+ """Register a service with the engine."""
113
+ try:
114
+ self._services[service_name] = service
115
+ print(f"[GameEngine] Registered service: {service_name}")
116
+ return True
117
+ except Exception as e:
118
+ print(f"[GameEngine] Error registering service {service_name}: {e}")
119
+ return False
120
+
121
+ def get_service(self, service_name: str) -> Optional[Any]:
122
+ """Get a registered service."""
123
+ return self._services.get(service_name)
124
+
125
+ # Convenience methods for common operations
126
+ def get_player_service(self) -> PlayerService:
127
+ """Get player service."""
128
+ return self._player_service
129
+
130
+ def get_chat_service(self) -> ChatService:
131
+ """Get chat service."""
132
+ return self._chat_service
133
+
134
+ def get_npc_service(self) -> NPCService:
135
+ """Get NPC service."""
136
+ return self._npc_service
137
+
138
+ def get_mcp_service(self) -> MCPService:
139
+ """Get MCP service."""
140
+ return self._mcp_service
141
+
142
+ def get_plugin_service(self) -> PluginService:
143
+ """Get plugin service."""
144
+ return self._plugin_service
145
+
146
+ def is_running(self) -> bool:
147
+ """Check if engine is running."""
148
+ return self._running
149
+
150
+ def _initialize_world(self):
151
+ """Initialize the world with default NPCs and setup."""
152
+ try:
153
+ # Add Donald NPC with new behavior
154
+ donald_npc = {
155
+ 'id': 'donald',
156
+ 'name': 'Donald the Trader',
157
+ 'x': 400, 'y': 200,
158
+ 'char': '💼',
159
+ 'type': 'trader',
160
+ 'personality': 'donald'
161
+ }
162
+ self._npc_service.register_npc('donald', donald_npc)
163
+
164
+ # Register addon NPCs
165
+ self._register_addon_npcs()
166
+
167
+ # Initialize default sample plugin if in dev mode
168
+ self._plugin_service.create_sample_plugin()
169
+
170
+ print("[GameEngine] World initialized with default content")
171
+
172
+ except Exception as e:
173
+ print(f"[GameEngine] Error initializing world: {e}")
174
+
175
+ def _register_addon_npcs(self):
176
+ """Register NPC addons that can handle private message commands."""
177
+ try:
178
+ # Import the Read2Burn service
179
+ from ..addons.read2burn_addon import read2burn_service
180
+
181
+ # Register the Read2Burn NPC first
182
+ read2burn_npc = {
183
+ 'id': 'read2burn',
184
+ 'name': '📮 Read2Burn Service',
185
+ 'x': 100, 'y': 375,
186
+ 'char': '📮',
187
+ 'type': 'service',
188
+ 'personality': 'read2burn',
189
+ 'description': 'Secure message service - send private messages that auto-delete after reading'
190
+ }
191
+ self._npc_service.register_npc('read2burn', read2burn_npc)
192
+
193
+ # Register the addon for handling private message commands
194
+ # The Read2BurnService already implements handle_command(player_id, command)
195
+ if not hasattr(self._game_world, 'addon_npcs'):
196
+ self._game_world.addon_npcs = {}
197
+ self._game_world.addon_npcs['read2burn'] = read2burn_service
198
+
199
+ print("[GameEngine] Read2Burn addon registered successfully")
200
+
201
+ except ImportError as e:
202
+ print(f"[GameEngine] Could not import Read2Burn service: {e}")
203
+ except Exception as e:
204
+ print(f"[GameEngine] Error registering Read2Burn addon: {e}")
205
+
206
+ # Auto-register self-contained addons
207
+ self._auto_register_addons()
208
+
209
+ def _auto_register_addons(self):
210
+ """Automatically register all self-contained addons."""
211
+ auto_register_addons = [
212
+ ('weather_oracle_addon', 'auto_register'),
213
+ ('duckduckgo_search_oracle_addon', 'auto_register'),
214
+ ('huggingface_hub_addon', 'auto_register'),
215
+ ]
216
+
217
+ for addon_module, auto_register_func in auto_register_addons:
218
+ try:
219
+ # Use importlib for cleaner imports
220
+ import importlib
221
+ full_module_name = f'src.addons.{addon_module}'
222
+ module = importlib.import_module(full_module_name)
223
+ auto_register = getattr(module, auto_register_func)
224
+
225
+ # Call the auto-register function with the game engine
226
+ success = auto_register(self)
227
+ if success:
228
+ print(f"[GameEngine] Auto-registered {addon_module} successfully")
229
+ else:
230
+ print(f"[GameEngine] Failed to auto-register {addon_module}")
231
+
232
+ except ImportError as e:
233
+ print(f"[GameEngine] Could not import {addon_module}: {e}")
234
+ except Exception as e:
235
+ print(f"[GameEngine] Error auto-registering {addon_module}: {e}")
236
+
237
+ def get_engine_status(self) -> Dict[str, Any]:
238
+ """Get comprehensive engine status."""
239
+ try:
240
+ all_players = self._player_service.get_all_players()
241
+ all_npcs = self._npc_service.get_all_npcs()
242
+ loaded_plugins = self._plugin_service.get_loaded_plugins()
243
+ recent_chat = self._chat_service.get_recent_messages(5)
244
+
245
+ return {
246
+ 'running': self._running,
247
+ 'services': list(self._services.keys()),
248
+ 'statistics': {
249
+ 'total_players': len(all_players),
250
+ 'active_players': len([p for p in all_players.values() if p.is_active()]),
251
+ 'total_npcs': len(all_npcs),
252
+ 'loaded_plugins': len(loaded_plugins),
253
+ 'recent_messages': len(recent_chat)
254
+ },
255
+ 'loaded_plugins': loaded_plugins,
256
+ 'world_state': self._game_world.get_world_state() if hasattr(self._game_world, 'get_world_state') else {}
257
+ }
258
+
259
+ except Exception as e:
260
+ return {'error': str(e), 'running': self._running}
261
+
262
+
263
+ # Convenience function to get the singleton instance
264
+ def get_game_engine() -> GameEngine:
265
+ """Get the singleton game engine instance."""
266
+ return GameEngine()
src/core/world.py CHANGED
@@ -1,321 +1,321 @@
1
- """
2
- Game World Model
3
-
4
- This module contains the GameWorld class that manages the game state,
5
- players, NPCs, and world events.
6
- """
7
-
8
- import time
9
- import threading
10
- from typing import Dict, List, Any, Optional
11
- from dataclasses import asdict
12
-
13
- from .player import Player
14
-
15
-
16
- class GameWorld:
17
- """
18
- Central game world state manager.
19
-
20
- Manages players, NPCs, chat messages, and world events.
21
- Thread-safe operations using RLock.
22
- """
23
-
24
- def __init__(self):
25
- self.players: Dict[str, Player] = {}
26
- self.npcs: Dict[str, Dict] = self._initialize_npcs() # Tree positions for collision detection (matching UI)
27
- self.trees = [
28
- {'x':150,'y':100}, {'x':300,'y':200}, {'x':350,'y':150},
29
- {'x':200,'y':300}, {'x':100,'y':250}, {'x':400,'y':320}
30
- ]
31
- self.chat_messages: List[Dict] = []
32
- self.world_events: List[Dict] = []
33
- self.addon_npcs: Dict[str, Any] = {} # NPCAddon instances
34
- self._lock = threading.RLock() # Thread-safe operations # World configuration
35
- self.max_players = 20
36
- self.world_width = 800
37
- self.world_height = 600
38
- self.chat_history_limit = 50
39
-
40
- # Movement configuration (scale with world size)
41
- self.movement_step = min(self.world_width, self.world_height) // 20 # 25px for 500x400 world
42
- self.player_size = self.movement_step # Player collision radius
43
- self.tree_collision_radius = self.movement_step + 5 # Tree collision radius
44
-
45
- # Add initial system message
46
- self.add_chat_message("System", "🎮 Game server started!")
47
-
48
- def _initialize_npcs(self) -> Dict[str, Dict]:
49
- """Initialize the default NPCs in the world."""
50
- return {
51
- 'donald': {
52
- 'id': 'donald',
53
- 'name': 'Donald the Trader',
54
- 'x': 250, 'y': 100,
55
- 'char': '💼',
56
- 'type': 'trader',
57
- 'personality': 'donald'
58
- },
59
- 'read2burn_mailbox': {
60
- 'id': 'read2burn_mailbox',
61
- 'name': 'Secure Mailbox',
62
- 'x': 200, 'y': 150,
63
- 'char': '📮',
64
- 'type': 'addon'
65
- },
66
- 'merchant': {
67
- 'id': 'merchant',
68
- 'name': 'Tom the Trader',
69
- 'x': 300, 'y': 200,
70
- 'char': '🏪',
71
- 'type': 'basic' },
72
- 'example_merchant': {
73
- 'id': 'example_merchant',
74
- 'name': 'Example Merchant',
75
- 'x': 350, 'y': 250,
76
- 'char': '🏪',
77
- 'type': 'addon'
78
- },
79
- 'scholar': {
80
- 'id': 'scholar',
81
- 'name': 'Professor Wise',
82
- 'x': 100, 'y': 100,
83
- 'char': '📚',
84
- 'type': 'learning',
85
- 'personality': 'wise_teacher'
86
- },
87
- 'jester': {
88
- 'id': 'jester',
89
- 'name': 'Funny Pete',
90
- 'x': 400, 'y': 100,
91
- 'char': '🃏',
92
- 'type': 'entertainment',
93
- 'personality': 'comedian'
94
- },
95
- 'warrior': {
96
- 'id': 'warrior',
97
- 'name': 'Captain Steel',
98
- 'x': 350, 'y': 350,
99
- 'char': '⚔️',
100
- 'type': 'combat',
101
- 'personality': 'tough_trainer'
102
- },
103
- 'healer': {
104
- 'id': 'healer',
105
- 'name': 'Sister Grace',
106
- 'x': 50, 'y': 250,
107
- 'char': '💚',
108
- 'type': 'healing',
109
- 'personality': 'gentle_healer'
110
- },
111
- 'wanderer': {
112
- 'id': 'wanderer',
113
- 'name': 'Roaming Rick',
114
- 'x': 200, 'y': 200,
115
- 'char': '🚶',
116
- 'type': 'moving',
117
- 'personality': 'traveler',
118
- 'movement': {
119
- 'speed': 2,
120
- 'direction_x': 1,
121
- 'direction_y': 1,
122
- 'last_move': time.time()
123
- }
124
- },
125
- 'sage': {
126
- 'id': 'sage',
127
- 'name': 'Ancient Sage',
128
- 'x': 450, 'y': 250,
129
- 'char': '🧙',
130
- 'type': 'magic',
131
- 'personality': 'mystical_sage'
132
- }
133
- }
134
-
135
- # Player Management
136
- def add_player(self, player: Player) -> bool:
137
- """Add a player to the world."""
138
- print(f"[GameWorld.add_player] Adding {player.name}")
139
- with self._lock:
140
- if len(self.players) >= self.max_players:
141
- return False
142
- self.players[player.id] = player
143
- self.add_chat_message("System", f"🎮 {player.name} ({player.type}) joined the game!")
144
- print(f"[GameWorld] Player {player.name} added. Total players: {len(self.players)}")
145
- return True
146
-
147
- def remove_player(self, player_id: str) -> bool:
148
- """Remove a player from the world."""
149
- with self._lock:
150
- if player_id in self.players:
151
- player = self.players[player_id]
152
- self.add_chat_message("System", f"👋 {player.name} left the game!")
153
- del self.players[player_id]
154
- return True
155
- return False
156
-
157
- def get_player(self, player_id: str) -> Optional[Player]:
158
- """Get a player by ID."""
159
- with self._lock:
160
- return self.players.get(player_id)
161
-
162
- def get_all_players(self) -> Dict[str, Player]:
163
- """Get all players (thread-safe copy)."""
164
- with self._lock:
165
- return self.players.copy()
166
-
167
- # Movement and Positioning
168
- def move_player(self, player_id: str, direction: str) -> bool:
169
- """Move a player in the specified direction."""
170
- with self._lock:
171
- if player_id not in self.players:
172
- return False
173
- player = self.players[player_id]
174
- old_x, old_y = player.x, player.y # Determine candidate new position without moving
175
- new_x, new_y = player.x, player.y
176
- if direction == "up":
177
- new_y = max(0, player.y - self.movement_step)
178
- elif direction == "down":
179
- new_y = min(self.world_height, player.y + self.movement_step)
180
- elif direction == "left":
181
- new_x = max(0, player.x - self.movement_step)
182
- elif direction == "right":
183
- new_x = min(self.world_width, player.x + self.movement_step)
184
-
185
- # Check tree collision
186
- for tree in self.trees:
187
- if abs(new_x - tree['x']) < self.tree_collision_radius and abs(new_y - tree['y']) < self.tree_collision_radius:
188
- # Collision: do not move
189
- return False
190
- # Perform movement
191
- player.move_to(new_x, new_y)
192
- # Award experience and handle level-up
193
- if old_x != player.x or old_y != player.y:
194
- player.experience += 1
195
- if player.can_level_up():
196
- player.level_up()
197
- self.add_chat_message("System", f"🎉 {player.name} reached level {player.level}!")
198
- # Check for NPC proximity events
199
- self.check_npc_proximity(player_id)
200
- return True
201
- return False
202
-
203
- def check_npc_proximity(self, player_id: str):
204
- """Check if player is near any NPCs and trigger interactions."""
205
- player = self.players.get(player_id)
206
- if not player:
207
- return
208
-
209
- interaction_range = 50
210
- nearby_npcs = []
211
-
212
- for npc_id, npc_data in self.npcs.items():
213
- npc_x, npc_y = npc_data['x'], npc_data['y']
214
- distance = ((player.x - npc_x) ** 2 + (player.y - npc_y) ** 2) ** 0.5
215
-
216
- if distance <= interaction_range:
217
- nearby_npcs.append(npc_data['name'])
218
-
219
- if nearby_npcs:
220
- npc_list = ", ".join(nearby_npcs)
221
- self.add_world_event(f"🔍 {player.name} is near: {npc_list}")
222
-
223
- # Chat and Communication
224
- def add_chat_message(self, sender: str, message: str, message_type: str = "public",
225
- target: str = None, sender_id: str = None):
226
- """Add a chat message to the world."""
227
- with self._lock:
228
- chat_msg = {
229
- 'sender': sender,
230
- 'message': message,
231
- 'timestamp': time.strftime("%H:%M:%S"),
232
- 'id': len(self.chat_messages),
233
- 'type': message_type,
234
- 'target': target,
235
- 'sender_id': sender_id
236
- }
237
- self.chat_messages.append(chat_msg)
238
- if len(self.chat_messages) > self.chat_history_limit:
239
- self.chat_messages = self.chat_messages[-self.chat_history_limit:]
240
-
241
- def get_chat_history(self, limit: int = None) -> List[Dict]:
242
- """Get chat message history."""
243
- with self._lock:
244
- if limit:
245
- return self.chat_messages[-limit:]
246
- return self.chat_messages.copy()
247
-
248
- def add_world_event(self, event: str):
249
- """Add a world event."""
250
- with self._lock:
251
- event_data = {
252
- 'event': event,
253
- 'timestamp': time.strftime("%H:%M:%S"),
254
- 'id': len(self.world_events)
255
- }
256
- self.world_events.append(event_data)
257
- if len(self.world_events) > 20: # Keep last 20 events
258
- self.world_events = self.world_events[-20:]
259
-
260
- def get_world_events(self) -> List[Dict]:
261
- """Get world events history."""
262
- with self._lock:
263
- return self.world_events.copy()
264
-
265
- # NPC Management
266
- def add_npc(self, npc_id: str, npc_data: Dict):
267
- """Add an NPC to the world."""
268
- with self._lock:
269
- self.npcs[npc_id] = npc_data
270
-
271
- def get_npc(self, npc_id: str) -> Optional[Dict]:
272
- """Get NPC data by ID."""
273
- with self._lock:
274
- return self.npcs.get(npc_id)
275
-
276
- def get_all_npcs(self) -> Dict[str, Dict]:
277
- """Get all NPCs."""
278
- with self._lock:
279
- return self.npcs.copy()
280
-
281
- # Utility Methods
282
- def get_world_state(self) -> Dict:
283
- """Get the complete world state for debugging/monitoring."""
284
- with self._lock:
285
- return {
286
- 'players': {pid: asdict(player) for pid, player in self.players.items()},
287
- 'npcs': self.npcs.copy(),
288
- 'recent_chat': self.chat_messages[-10:],
289
- 'recent_events': self.world_events[-5:],
290
- 'stats': {
291
- 'total_players': len(self.players),
292
- 'total_npcs': len(self.npcs),
293
- 'total_messages': len(self.chat_messages),
294
- 'total_events': len(self.world_events)
295
- }
296
- }
297
-
298
- def cleanup_inactive_players(self, timeout_seconds: int = 86400):
299
- """Remove players that have been inactive for too long."""
300
- with self._lock:
301
- inactive_players = [
302
- player_id for player_id, player in self.players.items()
303
- if not player.is_active(timeout_seconds)
304
- ]
305
-
306
- for player_id in inactive_players:
307
- self.remove_player(player_id)
308
-
309
- return len(inactive_players)
310
-
311
- def configure_world_dimensions(self, width: int, height: int):
312
- """Configure world dimensions and auto-calculate movement parameters."""
313
- self.world_width = width
314
- self.world_height = height
315
-
316
- # Recalculate movement parameters based on new dimensions
317
- self.movement_step = min(self.world_width, self.world_height) // 20
318
- self.player_size = self.movement_step
319
- self.tree_collision_radius = self.movement_step + 5
320
-
321
- print(f"[GameWorld] World configured: {width}x{height}, movement_step: {self.movement_step}")
 
1
+ """
2
+ Game World Model
3
+
4
+ This module contains the GameWorld class that manages the game state,
5
+ players, NPCs, and world events.
6
+ """
7
+
8
+ import time
9
+ import threading
10
+ from typing import Dict, List, Any, Optional
11
+ from dataclasses import asdict
12
+
13
+ from .player import Player
14
+
15
+
16
+ class GameWorld:
17
+ """
18
+ Central game world state manager.
19
+
20
+ Manages players, NPCs, chat messages, and world events.
21
+ Thread-safe operations using RLock.
22
+ """
23
+
24
+ def __init__(self):
25
+ self.players: Dict[str, Player] = {}
26
+ self.npcs: Dict[str, Dict] = self._initialize_npcs() # Tree positions for collision detection (matching UI)
27
+ self.trees = [
28
+ {'x':150,'y':100}, {'x':300,'y':200}, {'x':350,'y':150},
29
+ {'x':200,'y':300}, {'x':100,'y':250}, {'x':400,'y':320}
30
+ ]
31
+ self.chat_messages: List[Dict] = []
32
+ self.world_events: List[Dict] = []
33
+ self.addon_npcs: Dict[str, Any] = {} # NPCAddon instances
34
+ self._lock = threading.RLock() # Thread-safe operations # World configuration
35
+ self.max_players = 20
36
+ self.world_width = 800
37
+ self.world_height = 600
38
+ self.chat_history_limit = 50
39
+
40
+ # Movement configuration (scale with world size)
41
+ self.movement_step = min(self.world_width, self.world_height) // 20 # 25px for 500x400 world
42
+ self.player_size = self.movement_step # Player collision radius
43
+ self.tree_collision_radius = self.movement_step + 5 # Tree collision radius
44
+
45
+ # Add initial system message
46
+ self.add_chat_message("System", "🎮 Game server started!")
47
+
48
+ def _initialize_npcs(self) -> Dict[str, Dict]:
49
+ """Initialize the default NPCs in the world."""
50
+ return {
51
+ """ 'donald': {
52
+ 'id': 'donald',
53
+ 'name': 'Donald the Trader',
54
+ 'x': 250, 'y': 100,
55
+ 'char': '💼',
56
+ 'type': 'trader',
57
+ 'personality': 'donald'
58
+ },
59
+ 'read2burn_mailbox': {
60
+ 'id': 'read2burn_mailbox',
61
+ 'name': 'Secure Mailbox',
62
+ 'x': 200, 'y': 150,
63
+ 'char': '📮',
64
+ 'type': 'addon'
65
+ },
66
+ 'merchant': {
67
+ 'id': 'merchant',
68
+ 'name': 'Tom the Trader',
69
+ 'x': 300, 'y': 200,
70
+ 'char': '🏪',
71
+ 'type': 'basic' },
72
+ 'example_merchant': {
73
+ 'id': 'example_merchant',
74
+ 'name': 'Example Merchant',
75
+ 'x': 350, 'y': 250,
76
+ 'char': '🏪',
77
+ 'type': 'addon'
78
+ }, """
79
+ 'scholar': {
80
+ 'id': 'scholar',
81
+ 'name': 'Professor Wise',
82
+ 'x': 100, 'y': 100,
83
+ 'char': '📚',
84
+ 'type': 'learning',
85
+ 'personality': 'wise_teacher'
86
+ },
87
+ """ 'jester': {
88
+ 'id': 'jester',
89
+ 'name': 'Funny Pete',
90
+ 'x': 400, 'y': 100,
91
+ 'char': '🃏',
92
+ 'type': 'entertainment',
93
+ 'personality': 'comedian'
94
+ }, """
95
+ 'warrior': {
96
+ 'id': 'warrior',
97
+ 'name': 'Captain Steel',
98
+ 'x': 350, 'y': 350,
99
+ 'char': '⚔️',
100
+ 'type': 'combat',
101
+ 'personality': 'tough_trainer'
102
+ },
103
+ """ 'healer': {
104
+ 'id': 'healer',
105
+ 'name': 'Sister Grace',
106
+ 'x': 50, 'y': 250,
107
+ 'char': '💚',
108
+ 'type': 'healing',
109
+ 'personality': 'gentle_healer'
110
+ }, """
111
+ 'wanderer': {
112
+ 'id': 'wanderer',
113
+ 'name': 'Roaming Rick',
114
+ 'x': 200, 'y': 200,
115
+ 'char': '🚶',
116
+ 'type': 'moving',
117
+ 'personality': 'traveler',
118
+ 'movement': {
119
+ 'speed': 2,
120
+ 'direction_x': 1,
121
+ 'direction_y': 1,
122
+ 'last_move': time.time()
123
+ }
124
+ },
125
+ 'sage': {
126
+ 'id': 'sage',
127
+ 'name': 'Ancient Sage',
128
+ 'x': 450, 'y': 250,
129
+ 'char': '🧙',
130
+ 'type': 'magic',
131
+ 'personality': 'mystical_sage'
132
+ }
133
+ }
134
+
135
+ # Player Management
136
+ def add_player(self, player: Player) -> bool:
137
+ """Add a player to the world."""
138
+ print(f"[GameWorld.add_player] Adding {player.name}")
139
+ with self._lock:
140
+ if len(self.players) >= self.max_players:
141
+ return False
142
+ self.players[player.id] = player
143
+ self.add_chat_message("System", f"🎮 {player.name} ({player.type}) joined the game!")
144
+ print(f"[GameWorld] Player {player.name} added. Total players: {len(self.players)}")
145
+ return True
146
+
147
+ def remove_player(self, player_id: str) -> bool:
148
+ """Remove a player from the world."""
149
+ with self._lock:
150
+ if player_id in self.players:
151
+ player = self.players[player_id]
152
+ self.add_chat_message("System", f"👋 {player.name} left the game!")
153
+ del self.players[player_id]
154
+ return True
155
+ return False
156
+
157
+ def get_player(self, player_id: str) -> Optional[Player]:
158
+ """Get a player by ID."""
159
+ with self._lock:
160
+ return self.players.get(player_id)
161
+
162
+ def get_all_players(self) -> Dict[str, Player]:
163
+ """Get all players (thread-safe copy)."""
164
+ with self._lock:
165
+ return self.players.copy()
166
+
167
+ # Movement and Positioning
168
+ def move_player(self, player_id: str, direction: str) -> bool:
169
+ """Move a player in the specified direction."""
170
+ with self._lock:
171
+ if player_id not in self.players:
172
+ return False
173
+ player = self.players[player_id]
174
+ old_x, old_y = player.x, player.y # Determine candidate new position without moving
175
+ new_x, new_y = player.x, player.y
176
+ if direction == "up":
177
+ new_y = max(0, player.y - self.movement_step)
178
+ elif direction == "down":
179
+ new_y = min(self.world_height, player.y + self.movement_step)
180
+ elif direction == "left":
181
+ new_x = max(0, player.x - self.movement_step)
182
+ elif direction == "right":
183
+ new_x = min(self.world_width, player.x + self.movement_step)
184
+
185
+ # Check tree collision
186
+ for tree in self.trees:
187
+ if abs(new_x - tree['x']) < self.tree_collision_radius and abs(new_y - tree['y']) < self.tree_collision_radius:
188
+ # Collision: do not move
189
+ return False
190
+ # Perform movement
191
+ player.move_to(new_x, new_y)
192
+ # Award experience and handle level-up
193
+ if old_x != player.x or old_y != player.y:
194
+ player.experience += 1
195
+ if player.can_level_up():
196
+ player.level_up()
197
+ self.add_chat_message("System", f"🎉 {player.name} reached level {player.level}!")
198
+ # Check for NPC proximity events
199
+ self.check_npc_proximity(player_id)
200
+ return True
201
+ return False
202
+
203
+ def check_npc_proximity(self, player_id: str):
204
+ """Check if player is near any NPCs and trigger interactions."""
205
+ player = self.players.get(player_id)
206
+ if not player:
207
+ return
208
+
209
+ interaction_range = 50
210
+ nearby_npcs = []
211
+
212
+ for npc_id, npc_data in self.npcs.items():
213
+ npc_x, npc_y = npc_data['x'], npc_data['y']
214
+ distance = ((player.x - npc_x) ** 2 + (player.y - npc_y) ** 2) ** 0.5
215
+
216
+ if distance <= interaction_range:
217
+ nearby_npcs.append(npc_data['name'])
218
+
219
+ if nearby_npcs:
220
+ npc_list = ", ".join(nearby_npcs)
221
+ self.add_world_event(f"🔍 {player.name} is near: {npc_list}")
222
+
223
+ # Chat and Communication
224
+ def add_chat_message(self, sender: str, message: str, message_type: str = "public",
225
+ target: str = None, sender_id: str = None):
226
+ """Add a chat message to the world."""
227
+ with self._lock:
228
+ chat_msg = {
229
+ 'sender': sender,
230
+ 'message': message,
231
+ 'timestamp': time.strftime("%H:%M:%S"),
232
+ 'id': len(self.chat_messages),
233
+ 'type': message_type,
234
+ 'target': target,
235
+ 'sender_id': sender_id
236
+ }
237
+ self.chat_messages.append(chat_msg)
238
+ if len(self.chat_messages) > self.chat_history_limit:
239
+ self.chat_messages = self.chat_messages[-self.chat_history_limit:]
240
+
241
+ def get_chat_history(self, limit: int = None) -> List[Dict]:
242
+ """Get chat message history."""
243
+ with self._lock:
244
+ if limit:
245
+ return self.chat_messages[-limit:]
246
+ return self.chat_messages.copy()
247
+
248
+ def add_world_event(self, event: str):
249
+ """Add a world event."""
250
+ with self._lock:
251
+ event_data = {
252
+ 'event': event,
253
+ 'timestamp': time.strftime("%H:%M:%S"),
254
+ 'id': len(self.world_events)
255
+ }
256
+ self.world_events.append(event_data)
257
+ if len(self.world_events) > 20: # Keep last 20 events
258
+ self.world_events = self.world_events[-20:]
259
+
260
+ def get_world_events(self) -> List[Dict]:
261
+ """Get world events history."""
262
+ with self._lock:
263
+ return self.world_events.copy()
264
+
265
+ # NPC Management
266
+ def add_npc(self, npc_id: str, npc_data: Dict):
267
+ """Add an NPC to the world."""
268
+ with self._lock:
269
+ self.npcs[npc_id] = npc_data
270
+
271
+ def get_npc(self, npc_id: str) -> Optional[Dict]:
272
+ """Get NPC data by ID."""
273
+ with self._lock:
274
+ return self.npcs.get(npc_id)
275
+
276
+ def get_all_npcs(self) -> Dict[str, Dict]:
277
+ """Get all NPCs."""
278
+ with self._lock:
279
+ return self.npcs.copy()
280
+
281
+ # Utility Methods
282
+ def get_world_state(self) -> Dict:
283
+ """Get the complete world state for debugging/monitoring."""
284
+ with self._lock:
285
+ return {
286
+ 'players': {pid: asdict(player) for pid, player in self.players.items()},
287
+ 'npcs': self.npcs.copy(),
288
+ 'recent_chat': self.chat_messages[-10:],
289
+ 'recent_events': self.world_events[-5:],
290
+ 'stats': {
291
+ 'total_players': len(self.players),
292
+ 'total_npcs': len(self.npcs),
293
+ 'total_messages': len(self.chat_messages),
294
+ 'total_events': len(self.world_events)
295
+ }
296
+ }
297
+
298
+ def cleanup_inactive_players(self, timeout_seconds: int = 300):
299
+ """Remove players that have been inactive for too long."""
300
+ with self._lock:
301
+ inactive_players = [
302
+ player_id for player_id, player in self.players.items()
303
+ if not player.is_active(timeout_seconds)
304
+ ]
305
+
306
+ for player_id in inactive_players:
307
+ self.remove_player(player_id)
308
+
309
+ return len(inactive_players)
310
+
311
+ def configure_world_dimensions(self, width: int, height: int):
312
+ """Configure world dimensions and auto-calculate movement parameters."""
313
+ self.world_width = width
314
+ self.world_height = height
315
+
316
+ # Recalculate movement parameters based on new dimensions
317
+ self.movement_step = min(self.world_width, self.world_height) // 20
318
+ self.player_size = self.movement_step
319
+ self.tree_collision_radius = self.movement_step + 5
320
+
321
+ print(f"[GameWorld] World configured: {width}x{height}, movement_step: {self.movement_step}")
src/facades/__pycache__/game_facade.cpython-313.pyc CHANGED
Binary files a/src/facades/__pycache__/game_facade.cpython-313.pyc and b/src/facades/__pycache__/game_facade.cpython-313.pyc differ
 
src/interfaces/__pycache__/npc_addon.cpython-313.pyc CHANGED
Binary files a/src/interfaces/__pycache__/npc_addon.cpython-313.pyc and b/src/interfaces/__pycache__/npc_addon.cpython-313.pyc differ
 
src/interfaces/npc_addon.py CHANGED
@@ -85,3 +85,18 @@ def clear_addon_registry():
85
  """Clear the addon registry (used for testing)"""
86
  global _addon_registry
87
  _addon_registry = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
85
  """Clear the addon registry (used for testing)"""
86
  global _addon_registry
87
  _addon_registry = {}
88
+
89
+ def list_active_addons() -> list:
90
+ """Return all currently registered addon IDs."""
91
+ return list(_addon_registry.keys())
92
+
93
+ def remove_addon(addon_id: str) -> bool:
94
+ """Unregister and shut down an addon by its ID."""
95
+ addon = _addon_registry.pop(addon_id, None)
96
+ if addon:
97
+ try:
98
+ addon.on_shutdown()
99
+ except Exception:
100
+ pass
101
+ return True
102
+ return False
src/legacy_addons/example_npc_addon.py ADDED
@@ -0,0 +1,266 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Example NPC Addon - Template for creating new NPCs with custom functionality
3
+ """
4
+
5
+ import gradio as gr
6
+ from typing import Dict, Any
7
+ from src.interfaces.npc_addon import NPCAddon
8
+
9
+
10
+ class ExampleNPCAddon(NPCAddon):
11
+ """Example NPC addon demonstrating the complete NPC creation process."""
12
+
13
+ def __init__(self):
14
+ super().__init__()
15
+ # Initialize any state or data your NPC needs
16
+ self.npc_inventory = {
17
+ "health_potion": {"name": "Health Potion", "price": 25, "stock": 10},
18
+ "magic_scroll": {"name": "Magic Scroll", "price": 50, "stock": 5},
19
+ "steel_sword": {"name": "Steel Sword", "price": 100, "stock": 3}
20
+ }
21
+ self.greeting_messages = [
22
+ "Welcome to my shop, adventurer!",
23
+ "Looking for some fine wares?",
24
+ "I have the best deals in town!",
25
+ "What can I help you with today?"
26
+ ]
27
+
28
+ @property
29
+ def addon_id(self) -> str:
30
+ """Unique identifier for this addon - must match NPC ID in world."""
31
+ return "example_merchant"
32
+
33
+ @property
34
+ def addon_name(self) -> str:
35
+ """Display name shown in the interface."""
36
+ return "Example Merchant"
37
+
38
+ def get_interface(self) -> gr.Component:
39
+ """Create the Gradio interface for this NPC."""
40
+ with gr.Column() as interface:
41
+ gr.Markdown("""
42
+ ## 🏪 Example Merchant
43
+
44
+ *Welcome to my humble shop! I offer fine wares for brave adventurers.*
45
+
46
+ ### Available Services:
47
+ - **Shop**: Browse and purchase items
48
+ - **Appraisal**: Get item value estimates
49
+ - **Information**: Learn about the local area
50
+
51
+ *Walk near me in the game world to unlock full functionality!*
52
+ """)
53
+
54
+ with gr.Tabs():
55
+ # Shop Tab
56
+ with gr.Tab("🛒 Shop"):
57
+ gr.Markdown("### Available Items")
58
+
59
+ with gr.Row():
60
+ item_select = gr.Dropdown(
61
+ label="Select Item",
62
+ choices=list(self.npc_inventory.keys()),
63
+ value=None
64
+ )
65
+ quantity = gr.Number(
66
+ label="Quantity",
67
+ value=1,
68
+ minimum=1,
69
+ maximum=10
70
+ )
71
+
72
+ with gr.Row():
73
+ buy_btn = gr.Button("💰 Purchase", variant="primary")
74
+ refresh_btn = gr.Button("🔄 Refresh Stock", variant="secondary")
75
+
76
+ purchase_result = gr.Textbox(
77
+ label="Transaction Result",
78
+ lines=3,
79
+ interactive=False
80
+ )
81
+ # Shop display
82
+ shop_display = gr.JSON(
83
+ label="Current Inventory",
84
+ value=self.npc_inventory
85
+ )
86
+
87
+ def handle_purchase(item_key, qty):
88
+ if not item_key:
89
+ return "❌ Please select an item first!"
90
+
91
+ # Get current player (simplified for example)
92
+ from ..core.game_engine import GameEngine
93
+ game_engine = GameEngine()
94
+ game_world = game_engine.get_world()
95
+ current_players = list(game_world.players.keys())
96
+ if not current_players:
97
+ return "❌ You must be in the game to make purchases!"
98
+
99
+ player_id = max(current_players, key=lambda pid: game_world.players[pid].last_active)
100
+ return self.purchase_item(player_id, item_key, int(qty))
101
+
102
+ def refresh_shop():
103
+ return self.npc_inventory
104
+
105
+ # Wire up the interface
106
+ buy_btn.click(handle_purchase, [item_select, quantity], purchase_result)
107
+ refresh_btn.click(refresh_shop, outputs=shop_display)
108
+
109
+ # Information Tab
110
+ with gr.Tab("ℹ️ Information"):
111
+ gr.Markdown("""
112
+ ### 📍 Local Area Information
113
+
114
+ - **Location**: Village Market Square
115
+ - **Trading Hours**: Always open for brave adventurers
116
+ - **Specialties**: Weapons, potions, and magical items
117
+ - **Payment**: Gold coins accepted
118
+
119
+ ### 🗺️ Nearby Locations
120
+ - **Village Elder**: North of here, near the fountain
121
+ - **Training Grounds**: East side of village
122
+ - **Mystic Oracle**: In the tower to the south
123
+ """)
124
+
125
+ info_btn = gr.Button("📋 Get Quest Information")
126
+ quest_info = gr.Textbox(
127
+ label="Available Quests",
128
+ lines=5,
129
+ interactive=False
130
+ )
131
+
132
+ def get_quest_info():
133
+ return """
134
+ 🗡️ **Available Quests:**
135
+
136
+ 1. **Goblin Problem** (Level 1-3)
137
+ - Clear goblins from the eastern caves
138
+ - Reward: 100 gold + basic equipment
139
+
140
+ 2. **Herb Collection** (Level 1-2)
141
+ - Gather 10 healing herbs from the forest
142
+ - Reward: 50 gold + health potion
143
+
144
+ 3. **Lost Merchant** (Level 3-5)
145
+ - Find the missing merchant on the trade route
146
+ - Reward: 200 gold + rare item
147
+ """
148
+
149
+ info_btn.click(get_quest_info, outputs=quest_info)
150
+
151
+ return interface
152
+
153
+ def handle_command(self, player_id: str, command: str) -> str:
154
+ """Handle commands sent via private messages to this NPC."""
155
+ command_lower = command.lower().strip()
156
+ # Get player info safely
157
+ from ..core.game_engine import GameEngine
158
+ game_engine = GameEngine()
159
+ game_world = game_engine.get_world()
160
+ player = game_world.players.get(player_id)
161
+ if not player:
162
+ return "❌ Player not found!"
163
+
164
+ player_name = player.name
165
+
166
+ # Command parsing
167
+ if any(word in command_lower for word in ['hello', 'hi', 'greeting', 'hey']):
168
+ import random
169
+ return f"🏪 {random.choice(self.greeting_messages)} {player_name}!"
170
+
171
+ elif any(word in command_lower for word in ['shop', 'buy', 'purchase', 'item']):
172
+ return self._get_shop_summary()
173
+
174
+ elif any(word in command_lower for word in ['quest', 'mission', 'task']):
175
+ return "📜 I have information about local quests! Visit my Information tab for details."
176
+
177
+ elif any(word in command_lower for word in ['help', 'commands']):
178
+ return """
179
+ 🏪 **Available Commands:**
180
+ - **hello/hi**: Friendly greeting
181
+ - **shop/buy**: View available items
182
+ - **quest**: Learn about available quests
183
+ - **help**: Show this help message
184
+
185
+ *Visit the NPC Add-ons tab for my full interface!*
186
+ """
187
+
188
+ else:
189
+ return f"🤔 Interesting words, {player_name}! Try 'help' to see what I can do, or visit my shop interface in the NPC Add-ons tab!"
190
+
191
+ def purchase_item(self, player_id: str, item_key: str, quantity: int) -> str:
192
+ """Handle item purchase logic."""
193
+ if item_key not in self.npc_inventory:
194
+ return "❌ Item not found in inventory!"
195
+
196
+ item = self.npc_inventory[item_key]
197
+ total_cost = item["price"] * quantity
198
+
199
+ if item["stock"] < quantity:
200
+ return f"❌ Not enough stock! Only {item['stock']} available."
201
+
202
+ # Here you would check player's gold and deduct it
203
+ # For this example, we'll simulate the transaction
204
+
205
+ # Update inventory
206
+ self.npc_inventory[item_key]["stock"] -= quantity
207
+
208
+ return f"""
209
+ ✅ **Purchase Successful!**
210
+
211
+ **Item**: {item['name']} x{quantity}
212
+ **Cost**: {total_cost} gold
213
+ **Remaining Stock**: {self.npc_inventory[item_key]['stock']}
214
+
215
+ *Thank you for your business, adventurer!*
216
+ """
217
+
218
+ def _get_shop_summary(self) -> str:
219
+ """Get a summary of available shop items."""
220
+ summary = "🏪 **Shop Inventory:**\n\n"
221
+ for key, item in self.npc_inventory.items():
222
+ if item["stock"] > 0:
223
+ summary += f"• **{item['name']}** - {item['price']} gold (Stock: {item['stock']})\n"
224
+
225
+ summary += "\n*Visit my shop interface in the NPC Add-ons tab to purchase items!*"
226
+ return summary
227
+
228
+
229
+ # Global instance for auto-registration
230
+ example_merchant_addon = ExampleNPCAddon()
231
+
232
+
233
+ def auto_register(game_engine):
234
+ """Auto-register the Example NPC addon with the game engine."""
235
+ try:
236
+ # Create addon instance
237
+ addon_instance = ExampleNPCAddon()
238
+
239
+ # Get NPC config
240
+ npc_config = {
241
+ 'id': 'example_merchant',
242
+ 'name': '🏪 Example Merchant',
243
+ 'x': 300, 'y': 250,
244
+ 'char': '🏪',
245
+ 'type': 'trader',
246
+ 'personality': 'friendly'
247
+ }
248
+
249
+ # Register NPC in world
250
+ npc_service = game_engine.get_npc_service()
251
+ npc_service.register_npc(npc_config['id'], npc_config)
252
+
253
+ # Register addon for command handling
254
+ if not hasattr(game_engine.get_world(), 'addon_npcs'):
255
+ game_engine.get_world().addon_npcs = {}
256
+ game_engine.get_world().addon_npcs[npc_config['id']] = addon_instance
257
+
258
+ # Call startup
259
+ addon_instance.on_startup()
260
+
261
+ print(f"[ExampleNPCAddon] Auto-registered successfully as self-contained addon")
262
+ return True
263
+
264
+ except Exception as e:
265
+ print(f"[ExampleNPCAddon] Error during auto-registration: {e}")
266
+ return False
src/legacy_addons/web_search_oracle_addon.py ADDED
@@ -0,0 +1,450 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ DuckDuckGo Search Oracle Add-on - MCP-powered DuckDuckGo search information system.
3
+ """
4
+
5
+ import time
6
+ import json
7
+ import random
8
+ import string
9
+ import asyncio
10
+ from typing import Dict, List, Optional
11
+ import gradio as gr
12
+ from abc import ABC, abstractmethod
13
+ from mcp import ClientSession
14
+ from mcp.client.sse import sse_client
15
+ from contextlib import AsyncExitStack
16
+
17
+ from ..interfaces.npc_addon import NPCAddon
18
+
19
+
20
+ class IDuckDuckGoSearchService(ABC):
21
+ """Interface for DuckDuckGo Search service operations."""
22
+
23
+ @abstractmethod
24
+ def search_web(self, query: str) -> str:
25
+ """Search the web using DuckDuckGo."""
26
+ pass
27
+
28
+ @abstractmethod
29
+ def connect_to_mcp(self) -> str:
30
+ """Connect to DuckDuckGo MCP server."""
31
+ pass
32
+
33
+
34
+ class DuckDuckGoSearchOracleService(IDuckDuckGoSearchService, NPCAddon):
35
+ """Service for managing DuckDuckGo Search Oracle MCP integration."""
36
+
37
+ def __init__(self):
38
+ super().__init__()
39
+ self.connected = False
40
+ self.last_connection_attempt = 0
41
+ self.connection_cooldown = 30 # 30 seconds between connection attempts
42
+ self.server_url = "https://agents-mcp-hackathon-duckduckgo-mcp-server.hf.space/gradio_api/mcp/sse"
43
+ self.session = None
44
+ self.tools = []
45
+ self.exit_stack = None
46
+ # Set up event loop for async operations
47
+ try:
48
+ self.loop = asyncio.get_event_loop()
49
+ except RuntimeError:
50
+ self.loop = asyncio.new_event_loop()
51
+ @property
52
+ def addon_id(self) -> str:
53
+ """Unique identifier for this add-on"""
54
+ return "duckduckgo_search_oracle"
55
+
56
+ @property
57
+ def addon_name(self) -> str:
58
+ """Display name for this add-on"""
59
+ return "DuckDuckGo Search Oracle (MCP)"
60
+
61
+ @property
62
+ def npc_config(self) -> Dict:
63
+ """NPC configuration for auto-placement in world"""
64
+ return {
65
+ 'id': 'duckduckgo_search_oracle',
66
+ 'name': 'DuckDuckGo Search Oracle (MCP)',
67
+ 'x': 450, 'y': 300,
68
+ 'char': '🦆',
69
+ 'type': 'mcp',
70
+ 'description': 'DuckDuckGo-powered web search information service'
71
+ }
72
+
73
+ @property
74
+ def ui_tab_name(self) -> str:
75
+ """UI tab name for this addon"""
76
+ return "DuckDuckGo Search Oracle"
77
+
78
+ def get_interface(self) -> gr.Component:
79
+ """Return Gradio interface for this add-on"""
80
+ with gr.Column() as interface:
81
+ gr.Markdown("""
82
+ ## 🦆 DuckDuckGo Search Oracle (MCP)
83
+
84
+ *I commune with the DuckDuckGo search spirits through the mystical MCP protocol to bring you knowledge from across the web!*
85
+
86
+ **Ask me to search for anything:**
87
+ - Current news and information
88
+ - Technical documentation
89
+ - Research topics
90
+ - General knowledge queries
91
+ - Powered by DuckDuckGo via Model Context Protocol (MCP)
92
+
93
+ *Privacy-focused search powered by DuckDuckGo*
94
+ """)
95
+
96
+ # Connection status
97
+ connection_status = gr.HTML(
98
+ value=f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to DuckDuckGo spirits' if self.connected else '🔴 Disconnected from DuckDuckGo realm'}</div>"
99
+ )
100
+
101
+ with gr.Row():
102
+ query_input = gr.Textbox(
103
+ label="Search Query",
104
+ placeholder="e.g., latest news about AI, Python programming tutorials",
105
+ scale=3 )
106
+ search_btn = gr.Button("🦆 Consult DuckDuckGo Spirits", variant="primary", scale=1)
107
+
108
+ with gr.Row():
109
+ max_results = gr.Slider(
110
+ minimum=1,
111
+ maximum=10,
112
+ value=5,
113
+ step=1,
114
+ label="Max Results",
115
+ scale=2
116
+ )
117
+ engine_dropdown = gr.Dropdown(
118
+ choices=["DuckDuckGo"],
119
+ value="DuckDuckGo",
120
+ label="Search Engine",
121
+ scale=1
122
+ )
123
+
124
+ search_output = gr.Textbox(
125
+ label="🔍 Search Wisdom",
126
+ lines=12,
127
+ interactive=False,
128
+ placeholder="Enter a search query and I will consult the search spirits..."
129
+ )
130
+
131
+ # Example queries
132
+ with gr.Row():
133
+ gr.Examples(
134
+ examples=[
135
+ ["latest AI developments"],
136
+ ["Python programming best practices"],
137
+ ["climate change news 2024"],
138
+ ["machine learning tutorials"],
139
+ ["cryptocurrency market trends"],
140
+ ["space exploration news"],
141
+ ["renewable energy technologies"]
142
+ ],
143
+ inputs=[query_input],
144
+ label="🌐 Try These Searches"
145
+ )
146
+
147
+ # Connection controls
148
+ with gr.Row():
149
+ connect_btn = gr.Button("🔗 Connect to MCP", variant="secondary")
150
+ status_btn = gr.Button("📊 Check Status", variant="secondary")
151
+ tools_btn = gr.Button("🛠️ List Tools", variant="secondary")
152
+
153
+ def handle_search_request(query: str, engine: str, max_results: int):
154
+ if not query.strip():
155
+ return "❓ Please enter a search query to find information."
156
+ return self.search_web(query, engine, max_results)
157
+
158
+ def handle_connect():
159
+ result = self.connect_to_mcp()
160
+ # Update connection status
161
+ new_status = f"<div style='color: {'green' if self.connected else 'red'};'>{'🟢 Connected to search spirits' if self.connected else '🔴 Disconnected from search realm'}</div>"
162
+ return result, new_status
163
+
164
+ def handle_status():
165
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
166
+ return f"🔍 **Web Search Oracle Status**\n\nConnection: {status}\nLast update: {time.strftime('%H:%M')}\nAvailable engines: {', '.join(self.search_engines)}"
167
+
168
+ def handle_list_tools():
169
+ if not self.connected:
170
+ return "❌ Not connected to MCP server. Please connect first."
171
+ if not self.tools:
172
+ return "❌ No tools available."
173
+
174
+ tool_info = "🛠️ **Available Search Tools:**\n\n"
175
+ for tool in self.tools:
176
+ tool_info += f"• **{tool.name}**: {tool.description}\n"
177
+ return tool_info
178
+
179
+ # Wire up events
180
+ search_btn.click(
181
+ handle_search_request,
182
+ inputs=[query_input, engine_dropdown, max_results],
183
+ outputs=[search_output]
184
+ )
185
+
186
+ query_input.submit(
187
+ handle_search_request,
188
+ inputs=[query_input, engine_dropdown, max_results],
189
+ outputs=[search_output]
190
+ )
191
+
192
+ connect_btn.click(
193
+ handle_connect,
194
+ outputs=[search_output, connection_status]
195
+ )
196
+
197
+ status_btn.click(
198
+ handle_status,
199
+ outputs=[search_output]
200
+ )
201
+
202
+ tools_btn.click(
203
+ handle_list_tools,
204
+ outputs=[search_output]
205
+ )
206
+
207
+ return interface
208
+
209
+ def connect_to_mcp(self) -> str:
210
+ """Connect to MCP web search server."""
211
+ current_time = time.time()
212
+ if current_time - self.last_connection_attempt < self.connection_cooldown:
213
+ return "⏳ Please wait before retrying connection..."
214
+
215
+ self.last_connection_attempt = current_time
216
+
217
+ try:
218
+ return self.loop.run_until_complete(self._connect())
219
+ except Exception as e:
220
+ self.connected = False
221
+ return f"❌ Connection failed: {str(e)}"
222
+
223
+ async def _connect(self) -> str:
224
+ """Async connect to MCP server using SSE."""
225
+ try:
226
+ # Clean up previous connection
227
+ if self.exit_stack:
228
+ await self.exit_stack.aclose()
229
+
230
+ self.exit_stack = AsyncExitStack()
231
+
232
+ # Connect to SSE MCP server
233
+ sse_transport = await self.exit_stack.enter_async_context(
234
+ sse_client(self.server_url)
235
+ )
236
+ read_stream, write_callable = sse_transport
237
+
238
+ self.session = await self.exit_stack.enter_async_context(
239
+ ClientSession(read_stream, write_callable)
240
+ )
241
+ await self.session.initialize()
242
+
243
+ # Get available tools
244
+ response = await self.session.list_tools()
245
+ self.tools = response.tools
246
+
247
+ self.connected = True
248
+ tool_names = [tool.name for tool in self.tools]
249
+ return f"✅ Connected to web search MCP server!\nAvailable tools: {', '.join(tool_names)}"
250
+
251
+ except Exception as e:
252
+ self.connected = False
253
+ return f"❌ Connection failed: {str(e)}"
254
+
255
+ def search_web(self, query: str, engine: str = "brave", max_results: int = 5) -> str:
256
+ """Search the web using actual MCP server"""
257
+ if not self.connected:
258
+ # Try to auto-connect
259
+ connect_result = self.connect_to_mcp()
260
+ if not self.connected:
261
+ return f"❌ Failed to connect to search server. {connect_result}"
262
+
263
+ if not query.strip():
264
+ return "❌ Please enter a search query"
265
+
266
+ try:
267
+ return self.loop.run_until_complete(self._search_web(query, engine, max_results))
268
+ except Exception as e:
269
+ return f"❌ Error performing search: {str(e)}"
270
+
271
+ async def _search_web(self, query: str, engine: str, max_results: int) -> str:
272
+ """Async search web using MCP."""
273
+ try:
274
+ # Find the appropriate search tool based on engine
275
+ tool_name = f"web_search_mcp_{engine}_search"
276
+ search_tool = next((tool for tool in self.tools if tool.name == tool_name), None)
277
+
278
+ if not search_tool:
279
+ # Try to find any search tool if specific engine not found
280
+ search_tool = next((tool for tool in self.tools if 'search' in tool.name.lower()), None)
281
+ if not search_tool:
282
+ return "❌ No search tools found on server"
283
+
284
+ # Call the tool
285
+ params = {"query": query}
286
+ result = await self.session.call_tool(search_tool.name, params)
287
+
288
+ # Extract content properly
289
+ content_text = ""
290
+ if hasattr(result, 'content') and result.content:
291
+ if isinstance(result.content, list):
292
+ for content_item in result.content:
293
+ if hasattr(content_item, 'text'):
294
+ content_text += content_item.text
295
+ elif hasattr(content_item, 'content'):
296
+ content_text += str(content_item.content)
297
+ else:
298
+ content_text += str(content_item)
299
+ elif hasattr(result.content, 'text'):
300
+ content_text = result.content.text
301
+ else:
302
+ content_text = str(result.content)
303
+
304
+ if not content_text:
305
+ return "❌ No content received from server"
306
+
307
+ try:
308
+ # Try to parse as JSON
309
+ parsed = json.loads(content_text)
310
+ if isinstance(parsed, dict):
311
+ if 'error' in parsed:
312
+ return f"❌ Error: {parsed['error']}"
313
+
314
+ # Format search results nicely
315
+ formatted = f"🔍 **Search Results for: '{query}'**\n"
316
+ formatted += f"🌐 **Engine: {engine.title()}**\n\n"
317
+
318
+ # Handle different response formats
319
+ if 'data' in parsed and isinstance(parsed['data'], list):
320
+ results = parsed['data'][:max_results]
321
+ for i, result_item in enumerate(results, 1):
322
+ if isinstance(result_item, list) and len(result_item) >= 3:
323
+ title, link, body = result_item[0], result_item[1], result_item[2]
324
+ formatted += f"**{i}. {title}**\n"
325
+ formatted += f"🔗 {link}\n"
326
+ formatted += f"📝 {body[:200]}{'...' if len(body) > 200 else ''}\n\n"
327
+ elif isinstance(result_item, dict):
328
+ title = result_item.get('title', 'No title')
329
+ link = result_item.get('link', 'No link')
330
+ body = result_item.get('body', 'No description')
331
+ formatted += f"**{i}. {title}**\n"
332
+ formatted += f"🔗 {link}\n"
333
+ formatted += f"📝 {body[:200]}{'...' if len(body) > 200 else ''}\n\n"
334
+ elif 'results' in parsed:
335
+ results = parsed['results'][:max_results]
336
+ for i, result_item in enumerate(results, 1):
337
+ title = result_item.get('title', 'No title')
338
+ link = result_item.get('url', result_item.get('link', 'No link'))
339
+ body = result_item.get('snippet', result_item.get('body', 'No description'))
340
+ formatted += f"**{i}. {title}**\n"
341
+ formatted += f"🔗 {link}\n"
342
+ formatted += f"📝 {body[:200]}{'...' if len(body) > 200 else ''}\n\n"
343
+ else:
344
+ # Fallback to raw JSON display
345
+ formatted += f"✅ Raw search data:\n```json\n{json.dumps(parsed, indent=2)}\n```"
346
+
347
+ formatted += f"\n⏰ Search completed: {time.strftime('%H:%M')}\n⚡ **Powered by MCP**"
348
+ return formatted
349
+
350
+ except json.JSONDecodeError:
351
+ # If not JSON, try to format as text
352
+ formatted = f"🔍 **Search Results for: '{query}'**\n"
353
+ formatted += f"🌐 **Engine: {engine.title()}**\n\n"
354
+ formatted += f"✅ Search data:\n```\n{content_text[:1000]}{'...' if len(content_text) > 1000 else ''}\n```\n\n"
355
+ formatted += f"⏰ Search completed: {time.strftime('%H:%M')}\n⚡ **Powered by MCP**"
356
+ return formatted
357
+
358
+ return f"🔍 **Search Results for: '{query}'**\n\n✅ Raw result:\n{content_text[:1000]}{'...' if len(content_text) > 1000 else ''}\n\n⚡ **Powered by MCP**"
359
+
360
+ except Exception as e:
361
+ return f"❌ Failed to search: {str(e)}"
362
+
363
+ def handle_command(self, player_id: str, command: str) -> str:
364
+ """Handle Web Search Oracle commands."""
365
+ parts = command.strip().split(' ', 2)
366
+ cmd = parts[0].lower()
367
+
368
+ if cmd == "search" and len(parts) > 1:
369
+ query = parts[1]
370
+ engine = "brave" # default
371
+ if len(parts) > 2 and parts[2] in self.search_engines:
372
+ engine = parts[2]
373
+ return self.search_web(query, engine)
374
+ elif cmd == "engines":
375
+ return f"🔍 **Available Search Engines:**\n\n{', '.join(self.search_engines)}\n\n**Usage:** `search <query> [engine]`"
376
+ elif cmd == "connect":
377
+ return self.connect_to_mcp()
378
+ elif cmd == "status":
379
+ status = "🟢 Connected" if self.connected else "🔴 Disconnected"
380
+ return f"🔍 **Web Search Oracle Status**\n\nConnection: {status}\nLast update: {time.strftime('%H:%M')}\nAvailable engines: {', '.join(self.search_engines)}"
381
+ elif cmd == "tools":
382
+ if not self.connected:
383
+ return "❌ Not connected to MCP server. Use `connect` first."
384
+ if not self.tools:
385
+ return "❌ No tools available."
386
+
387
+ tool_info = "🛠️ **Available Search Tools:**\n\n"
388
+ for tool in self.tools:
389
+ tool_info += f"• **{tool.name}**: {tool.description}\n"
390
+ return tool_info
391
+ elif cmd == "help":
392
+ return """🔍 **Web Search Oracle Commands:**
393
+
394
+ **search** `<query>` `[engine]` - Search the web (e.g., 'search AI news brave')
395
+ **engines** - List available search engines
396
+ **connect** - Connect to MCP search server
397
+ **status** - Check connection status
398
+ **tools** - List available MCP tools
399
+ **help** - Show this help
400
+
401
+ 🌐 **Available Engines:**
402
+ {engines}
403
+
404
+ 🔍 **Example Commands:**
405
+ • search latest AI developments
406
+ • search Python tutorials duckduckgo
407
+ • search climate change news searxng
408
+
409
+ ⚡ **Powered by MCP (Model Context Protocol)**""".format(engines=', '.join(self.search_engines))
410
+ else:
411
+ return "❓ Invalid command. Try: `search <query>`, `engines`, `connect`, `status`, `tools`, or `help`"
412
+
413
+
414
+ # Global Web Search Oracle service instance
415
+ web_search_oracle_service = DuckDuckGoSearchOracleService()
416
+
417
+
418
+ def auto_register(game_engine):
419
+ """Auto-register the Web Search Oracle addon with the game engine.
420
+
421
+ This function makes the addon self-contained by handling its own registration.
422
+ """
423
+ try:
424
+ # Create the web search oracle NPC definition
425
+ web_search_oracle_npc = {
426
+ 'id': 'web_search_oracle_auto',
427
+ 'name': '🔍 Web Search Oracle (Auto)',
428
+ 'x': 550, 'y': 100,
429
+ 'char': '🔍',
430
+ 'type': 'mcp',
431
+ 'personality': 'web_search_oracle',
432
+ 'description': 'Self-contained MCP-powered web search information service'
433
+ }
434
+
435
+ # Register the NPC with the NPC service
436
+ npc_service = game_engine.get_npc_service()
437
+ npc_service.register_npc('web_search_oracle_auto', web_search_oracle_npc)
438
+
439
+ # Register the addon for handling private message commands
440
+ world = game_engine.get_world()
441
+ if not hasattr(world, 'addon_npcs'):
442
+ world.addon_npcs = {}
443
+ world.addon_npcs['web_search_oracle_auto'] = web_search_oracle_service
444
+
445
+ print("[WebSearchOracleAddon] Auto-registered successfully as self-contained addon")
446
+ return True
447
+
448
+ except Exception as e:
449
+ print(f"[WebSearchOracleAddon] Error during auto-registration: {e}")
450
+ return False
src/ui/__init__.py CHANGED
@@ -5,7 +5,8 @@ This module contains all user interface components for the MMORPG application.
5
  """
6
 
7
  from .interface_manager import InterfaceManager
 
8
  from .huggingface_ui import HuggingFaceUI
9
  from .documentation_tabs import DocumentationTabs
10
 
11
- __all__ = ['InterfaceManager', 'HuggingFaceUI', 'DocumentationTabs']
 
5
  """
6
 
7
  from .interface_manager import InterfaceManager
8
+ from .improved_interface_manager import ImprovedInterfaceManager
9
  from .huggingface_ui import HuggingFaceUI
10
  from .documentation_tabs import DocumentationTabs
11
 
12
+ __all__ = ['InterfaceManager', 'ImprovedInterfaceManager', 'HuggingFaceUI', 'DocumentationTabs']
src/ui/__pycache__/__init__.cpython-313.pyc CHANGED
Binary files a/src/ui/__pycache__/__init__.cpython-313.pyc and b/src/ui/__pycache__/__init__.cpython-313.pyc differ
 
src/ui/__pycache__/huggingface_ui.cpython-313.pyc CHANGED
Binary files a/src/ui/__pycache__/huggingface_ui.cpython-313.pyc and b/src/ui/__pycache__/huggingface_ui.cpython-313.pyc differ
 
src/ui/__pycache__/improved_interface_manager.cpython-313.pyc ADDED
Binary file (50.3 kB). View file
 
src/ui/__pycache__/interface_manager.cpython-313.pyc CHANGED
Binary files a/src/ui/__pycache__/interface_manager.cpython-313.pyc and b/src/ui/__pycache__/interface_manager.cpython-313.pyc differ
 
src/ui/huggingface_ui.py CHANGED
@@ -32,12 +32,11 @@ class HuggingFaceUI:
32
  radius_size="md",
33
  spacing_size="md"
34
  )
35
-
36
- # Custom CSS for HuggingFace-like styling
37
  self.custom_css = """
38
- /* HuggingFace-inspired styling */
39
  .main-container {
40
- max-width: 1200px;
41
  margin: 0 auto;
42
  padding: 20px;
43
  }
@@ -49,12 +48,14 @@ class HuggingFaceUI:
49
  border-radius: 12px;
50
  margin-bottom: 30px;
51
  text-align: center;
 
52
  }
53
 
54
  .hero-title {
55
  font-size: 2.5em;
56
  font-weight: bold;
57
  margin-bottom: 10px;
 
58
  }
59
 
60
  .hero-subtitle {
@@ -72,67 +73,87 @@ class HuggingFaceUI:
72
  background: white;
73
  border-radius: 12px;
74
  padding: 20px;
75
- box-shadow: 0 2px 10px rgba(0,0,0,0.1);
76
  margin-bottom: 20px;
 
77
  }
78
 
79
  .stats-card {
80
- background: #f8f9fa;
81
- border: 1px solid #e9ecef;
82
- border-radius: 8px;
83
- padding: 15px;
84
- margin: 10px 0;
 
85
  }
86
 
87
  .movement-grid {
88
  display: grid;
89
- grid-template-columns: 1fr 1fr 1fr;
90
- gap: 10px;
91
- max-width: 200px;
92
- margin: 0 auto;
 
93
  }
94
 
95
  .movement-btn {
96
- padding: 12px;
97
- font-size: 20px;
98
- border-radius: 8px;
99
  border: 2px solid #007bff;
100
- background: #f8f9fa;
 
 
101
  cursor: pointer;
102
- transition: all 0.2s;
 
 
 
 
103
  }
104
 
105
  .movement-btn:hover {
106
- background: #007bff;
107
  color: white;
 
 
 
 
 
 
 
108
  }
109
 
110
  .keyboard-status {
111
- background: #e8f5e8;
112
  border: 2px solid #4caf50;
113
  border-radius: 8px;
114
  padding: 12px;
115
  margin: 10px 0;
116
  text-align: center;
117
  font-weight: bold;
 
118
  }
119
 
120
  .tab-content {
121
- padding: 20px;
122
  min-height: 600px;
 
 
123
  }
124
 
125
  .doc-section {
126
  margin-bottom: 30px;
127
- padding: 20px;
128
  border-left: 4px solid #007bff;
129
- background: #f8f9fa;
130
- border-radius: 0 8px 8px 0;
 
131
  }
132
 
133
  .feature-grid {
134
  display: grid;
135
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
136
  gap: 20px;
137
  margin: 20px 0;
138
  }
@@ -140,55 +161,73 @@ class HuggingFaceUI:
140
  .feature-card {
141
  background: white;
142
  border: 1px solid #e9ecef;
143
- border-radius: 8px;
144
- padding: 20px;
145
- box-shadow: 0 2px 5px rgba(0,0,0,0.05);
 
 
 
 
 
 
146
  }
147
 
148
  .feature-icon {
149
- font-size: 2em;
150
- margin-bottom: 10px;
151
  display: block;
 
152
  }
153
 
154
  .chat-tab {
155
- background: #f5f5f5;
156
- border: 1px solid #ccc;
157
  border-radius: 8px;
158
- padding: 8px 12px;
159
- margin: 2px;
160
  cursor: pointer;
161
  display: inline-flex;
162
  align-items: center;
163
- gap: 5px;
164
- transition: all 0.2s;
 
165
  }
166
 
167
  .chat-tab.active {
168
- background: #e3f2fd;
169
  border-color: #2196f3;
170
- border-width: 2px;
 
171
  }
172
 
173
  .chat-tab:hover {
174
- background: #e9ecef;
 
 
175
  }
176
 
177
  .npc-addon-panel {
178
- border: 1px solid #e9ecef;
179
- border-radius: 8px;
180
- padding: 15px;
181
- margin: 10px 0;
182
- background: #fafafa;
 
183
  }
184
 
185
  .plugin-card {
186
  background: white;
187
- border: 1px solid #e9ecef;
188
- border-radius: 8px;
189
- padding: 15px;
190
- margin: 10px 0;
191
- box-shadow: 0 1px 3px rgba(0,0,0,0.1);
 
 
 
 
 
 
192
  }
193
 
194
  .status-indicator {
@@ -197,16 +236,54 @@ class HuggingFaceUI:
197
  height: 12px;
198
  border-radius: 50%;
199
  margin-right: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
200
  }
201
 
202
- .status-online { background-color: #4caf50; }
203
- .status-offline { background-color: #f44336; }
204
- .status-idle { background-color: #ff9800; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
205
 
206
  /* Responsive design */
207
  @media (max-width: 768px) {
208
  .main-container {
209
- padding: 10px;
210
  }
211
 
212
  .hero-section {
@@ -220,6 +297,37 @@ class HuggingFaceUI:
220
  .feature-grid {
221
  grid-template-columns: 1fr;
222
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  }
224
  """
225
 
@@ -446,13 +554,47 @@ class HuggingFaceUI:
446
  )
447
 
448
  def _generate_world_html(self, width: int = 800, height: int = 600) -> str:
449
- """Generate HTML for the game world visualization with bigger map."""
450
  # Enhanced game world with tree collision detection
451
- # Dynamically generate NPCs from GameFacade
452
  npc_html = ""
453
  for npc in self.game_facade.get_all_npcs().values():
454
- npc_html += f"<div style=\"position: absolute; left: {npc['x']}px; top: {npc['y']}px; font-size: 25px;\" title=\"{npc['name']}\">{npc['char']}</div>"
455
- # Base world template
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
456
  world_html = f"""
457
  <div style="position: relative; width: {width}px; height: {height}px;
458
  border: 3px solid #333; border-radius: 12px;
@@ -460,44 +602,137 @@ class HuggingFaceUI:
460
  margin: 20px auto; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.2);">
461
 
462
  <!-- Trees for collision detection -->
463
- <div style="position: absolute; left: 150px; top: 100px; font-size: 30px;">🌳</div>
464
- <div style="position: absolute; left: 300px; top: 200px; font-size: 30px;">🌳</div>
465
- <div style="position: absolute; left: 500px; top: 150px; font-size: 30px;">🌳</div>
466
- <div style="position: absolute; left: 650px; top: 300px; font-size: 30px;">🌳</div>
467
- <div style="position: absolute; left: 200px; top: 400px; font-size: 30px;">🌳</div>
468
- <div style="position: absolute; left: 450px; top: 450px; font-size: 30px;">🌳</div>
469
- <div style="position: absolute; left: 600px; top: 500px; font-size: 30px;">🌳</div>
470
 
471
- <!-- NPCs -->
472
  {npc_html}
473
 
474
- <!-- Decorative elements -->
475
- <div style="position: absolute; left: 50px; top: 50px; font-size: 20px;">🏔️</div>
476
- <div style="position: absolute; right: 50px; top: 50px; font-size: 20px;">🏔️</div>
477
- <div style="position: absolute; left: 50px; bottom: 50px; font-size: 20px;">🌊</div>
478
- <div style="position: absolute; right: 50px; bottom: 50px; font-size: 20px;">🌊</div>
479
 
480
  <!-- Players will be dynamically added here -->
481
- <div id="players-container"></div>
482
-
483
- <!-- Legend
484
- <div style="position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.9);
485
- padding: 10px; border-radius: 8px; font-size: 12px; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
486
- <strong>🗺️ Game World ({width}x{height})</strong><br>
487
- 🧑‍🦰 Players | 📮🏪🚶🧙🌤️ NPCs | 🌳 Trees (collision)
488
- </div>
489
- -->
490
-
491
- <!-- Coordinates display
492
- <div style="position: absolute; bottom: 10px; right: 10px; background: rgba(0,0,0,0.7);
493
- color: white; padding: 8px; border-radius: 5px; font-size: 11px;">
494
- <span id="coordinates">Move to see coordinates</span>
495
  </div>
496
- -->
497
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
498
  """
499
  return world_html
500
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
501
  def create_interface(self) -> gr.Blocks:
502
  """Create the complete HuggingFace-style interface."""
503
 
@@ -1044,32 +1279,148 @@ class HuggingFaceUI:
1044
  )
1045
 
1046
  with gr.Tab("🧪 AI Agent Testing"):
1047
- gr.Markdown("## 🧪 Test AI Agent Integration")
1048
- gr.Markdown("*Use this to simulate AI agent connections for testing*")
1049
-
1050
- with gr.Row():
1051
- ai_name = gr.Textbox(
1052
- label="AI Agent Name",
1053
- placeholder="Claude the Explorer",
1054
- scale=3
1055
- )
1056
- register_ai_btn = gr.Button("Register AI Agent", variant="primary", scale=1)
1057
-
1058
- with gr.Row():
1059
- ai_action = gr.Dropdown(
1060
- choices=["move up", "move down", "move left", "move right", "chat"],
1061
- label="AI Action",
1062
- scale=2
1063
- )
1064
- ai_message = gr.Textbox(
1065
- label="AI Message (for chat)",
1066
- placeholder="Hello humans!",
1067
- scale=3
1068
- )
1069
-
1070
- execute_ai_btn = gr.Button("Execute AI Action", variant="secondary")
1071
- ai_result = gr.Textbox(label="AI Action Result", interactive=False, lines=5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1072
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1073
  def _create_architecture_content(self) -> None:
1074
  """Create architecture documentation."""
1075
  gr.Markdown("""
 
32
  radius_size="md",
33
  spacing_size="md"
34
  )
35
+ # Custom CSS for HuggingFace-like styling with enhanced design
 
36
  self.custom_css = """
37
+ /* HuggingFace-inspired styling with modern enhancements */
38
  .main-container {
39
+ max-width: 1400px;
40
  margin: 0 auto;
41
  padding: 20px;
42
  }
 
48
  border-radius: 12px;
49
  margin-bottom: 30px;
50
  text-align: center;
51
+ box-shadow: 0 4px 20px rgba(0,0,0,0.1);
52
  }
53
 
54
  .hero-title {
55
  font-size: 2.5em;
56
  font-weight: bold;
57
  margin-bottom: 10px;
58
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
59
  }
60
 
61
  .hero-subtitle {
 
73
  background: white;
74
  border-radius: 12px;
75
  padding: 20px;
76
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
77
  margin-bottom: 20px;
78
+ border: 1px solid #e9ecef;
79
  }
80
 
81
  .stats-card {
82
+ background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
83
+ border: 1px solid #dee2e6;
84
+ border-radius: 12px;
85
+ padding: 20px;
86
+ margin: 15px 0;
87
+ box-shadow: 0 2px 10px rgba(0,0,0,0.08);
88
  }
89
 
90
  .movement-grid {
91
  display: grid;
92
+ grid-template-columns: repeat(3, 60px);
93
+ gap: 12px;
94
+ max-width: 220px;
95
+ margin: 15px auto;
96
+ justify-content: center;
97
  }
98
 
99
  .movement-btn {
100
+ width: 60px;
101
+ height: 60px;
102
+ border-radius: 12px;
103
  border: 2px solid #007bff;
104
+ background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
105
+ font-size: 18px;
106
+ font-weight: bold;
107
  cursor: pointer;
108
+ transition: all 0.3s ease;
109
+ display: flex;
110
+ align-items: center;
111
+ justify-content: center;
112
+ box-shadow: 0 2px 8px rgba(0,123,255,0.2);
113
  }
114
 
115
  .movement-btn:hover {
116
+ background: linear-gradient(135deg, #007bff 0%, #0056b3 100%);
117
  color: white;
118
+ transform: translateY(-2px);
119
+ box-shadow: 0 4px 12px rgba(0,123,255,0.4);
120
+ }
121
+
122
+ .movement-btn:active {
123
+ transform: translateY(0px);
124
+ box-shadow: 0 2px 6px rgba(0,123,255,0.3);
125
  }
126
 
127
  .keyboard-status {
128
+ background: linear-gradient(135deg, #e8f5e8 0%, #d4edda 100%);
129
  border: 2px solid #4caf50;
130
  border-radius: 8px;
131
  padding: 12px;
132
  margin: 10px 0;
133
  text-align: center;
134
  font-weight: bold;
135
+ box-shadow: 0 2px 8px rgba(76,175,80,0.2);
136
  }
137
 
138
  .tab-content {
139
+ padding: 25px;
140
  min-height: 600px;
141
+ background: #fafafa;
142
+ border-radius: 8px;
143
  }
144
 
145
  .doc-section {
146
  margin-bottom: 30px;
147
+ padding: 25px;
148
  border-left: 4px solid #007bff;
149
+ background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
150
+ border-radius: 0 12px 12px 0;
151
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
152
  }
153
 
154
  .feature-grid {
155
  display: grid;
156
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
157
  gap: 20px;
158
  margin: 20px 0;
159
  }
 
161
  .feature-card {
162
  background: white;
163
  border: 1px solid #e9ecef;
164
+ border-radius: 12px;
165
+ padding: 25px;
166
+ box-shadow: 0 4px 15px rgba(0,0,0,0.08);
167
+ transition: all 0.3s ease;
168
+ }
169
+
170
+ .feature-card:hover {
171
+ transform: translateY(-5px);
172
+ box-shadow: 0 8px 25px rgba(0,0,0,0.15);
173
  }
174
 
175
  .feature-icon {
176
+ font-size: 2.5em;
177
+ margin-bottom: 15px;
178
  display: block;
179
+ text-align: center;
180
  }
181
 
182
  .chat-tab {
183
+ background: linear-gradient(135deg, #f5f5f5 0%, #e9ecef 100%);
184
+ border: 2px solid #dee2e6;
185
  border-radius: 8px;
186
+ padding: 10px 16px;
187
+ margin: 3px;
188
  cursor: pointer;
189
  display: inline-flex;
190
  align-items: center;
191
+ gap: 8px;
192
+ transition: all 0.3s ease;
193
+ font-weight: 500;
194
  }
195
 
196
  .chat-tab.active {
197
+ background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
198
  border-color: #2196f3;
199
+ color: #1976d2;
200
+ box-shadow: 0 2px 8px rgba(33,150,243,0.3);
201
  }
202
 
203
  .chat-tab:hover {
204
+ background: linear-gradient(135deg, #e9ecef 0%, #dee2e6 100%);
205
+ transform: translateY(-1px);
206
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
207
  }
208
 
209
  .npc-addon-panel {
210
+ border: 2px solid #e9ecef;
211
+ border-radius: 12px;
212
+ padding: 20px;
213
+ margin: 15px 0;
214
+ background: linear-gradient(135deg, #fafafa 0%, #ffffff 100%);
215
+ box-shadow: 0 2px 10px rgba(0,0,0,0.05);
216
  }
217
 
218
  .plugin-card {
219
  background: white;
220
+ border: 2px solid #e9ecef;
221
+ border-radius: 12px;
222
+ padding: 20px;
223
+ margin: 15px 0;
224
+ box-shadow: 0 2px 10px rgba(0,0,0,0.08);
225
+ transition: all 0.3s ease;
226
+ }
227
+
228
+ .plugin-card:hover {
229
+ border-color: #007bff;
230
+ box-shadow: 0 4px 15px rgba(0,123,255,0.15);
231
  }
232
 
233
  .status-indicator {
 
236
  height: 12px;
237
  border-radius: 50%;
238
  margin-right: 8px;
239
+ box-shadow: 0 0 0 2px white, 0 0 0 3px currentColor;
240
+ }
241
+
242
+ .status-online {
243
+ background-color: #4caf50;
244
+ animation: pulse-green 2s infinite;
245
+ }
246
+ .status-offline {
247
+ background-color: #f44336;
248
+ }
249
+ .status-idle {
250
+ background-color: #ff9800;
251
  }
252
 
253
+ @keyframes pulse-green {
254
+ 0% { box-shadow: 0 0 0 2px white, 0 0 0 3px #4caf50; }
255
+ 50% { box-shadow: 0 0 0 2px white, 0 0 0 6px rgba(76,175,80,0.5); }
256
+ 100% { box-shadow: 0 0 0 2px white, 0 0 0 3px #4caf50; }
257
+ }
258
+
259
+ /* Enhanced buttons */
260
+ .gradio-button {
261
+ border-radius: 8px !important;
262
+ font-weight: 600 !important;
263
+ transition: all 0.3s ease !important;
264
+ }
265
+
266
+ .gradio-button:hover {
267
+ transform: translateY(-1px) !important;
268
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15) !important;
269
+ }
270
+
271
+ /* Enhanced form controls */
272
+ .gradio-textbox, .gradio-dropdown, .gradio-slider {
273
+ border-radius: 8px !important;
274
+ border: 2px solid #e9ecef !important;
275
+ transition: all 0.3s ease !important;
276
+ }
277
+
278
+ .gradio-textbox:focus, .gradio-dropdown:focus {
279
+ border-color: #007bff !important;
280
+ box-shadow: 0 0 0 3px rgba(0,123,255,0.1) !important;
281
+ }
282
 
283
  /* Responsive design */
284
  @media (max-width: 768px) {
285
  .main-container {
286
+ padding: 15px;
287
  }
288
 
289
  .hero-section {
 
297
  .feature-grid {
298
  grid-template-columns: 1fr;
299
  }
300
+
301
+ .movement-grid {
302
+ grid-template-columns: repeat(3, 50px);
303
+ gap: 8px;
304
+ }
305
+
306
+ .movement-btn {
307
+ width: 50px;
308
+ height: 50px;
309
+ font-size: 16px;
310
+ }
311
+ }
312
+
313
+ /* Dark mode support */
314
+ @media (prefers-color-scheme: dark) {
315
+ .doc-section {
316
+ background: linear-gradient(135deg, #2d3748 0%, #4a5568 100%);
317
+ color: white;
318
+ }
319
+
320
+ .feature-card {
321
+ background: #2d3748;
322
+ border-color: #4a5568;
323
+ color: white;
324
+ }
325
+
326
+ .npc-addon-panel, .plugin-card {
327
+ background: #2d3748;
328
+ border-color: #4a5568;
329
+ color: white;
330
+ }
331
  }
332
  """
333
 
 
554
  )
555
 
556
  def _generate_world_html(self, width: int = 800, height: int = 600) -> str:
557
+ """Generate HTML for the game world visualization with enhanced tooltips."""
558
  # Enhanced game world with tree collision detection
559
+ # Dynamically generate NPCs from GameFacade with enhanced tooltips from addons
560
  npc_html = ""
561
  for npc in self.game_facade.get_all_npcs().values():
562
+ # Get rich tooltip information from addon system
563
+ tooltip_info = self._get_npc_tooltip_info(npc)
564
+
565
+ npc_html += f"""
566
+ <div style="position: absolute; left: {npc['x']}px; top: {npc['y']}px; text-align: center;"
567
+ title="{tooltip_info}"
568
+ class="npc-hover-element">
569
+ <div style="font-size: 25px; line-height: 1;">{npc['char']}</div>
570
+ <div style="font-size: 7px; font-weight: bold; color: #333; background: rgba(255,255,255,0.8);
571
+ padding: 1px 3px; border-radius: 3px; margin-top: 2px; white-space: nowrap;">{npc['name']}</div>
572
+ </div>"""
573
+
574
+ # Generate player tooltips with enhanced information
575
+ players_html = ""
576
+ try:
577
+ players = self.game_facade.get_all_players()
578
+ for player_id, player in players.items():
579
+ player_tooltip = self._get_player_tooltip_info(player)
580
+ players_html += f"""
581
+ <div style="position: absolute; left: {player.x}px; top: {player.y}px;
582
+ font-size: 20px; z-index: 10; border: 2px solid yellow; border-radius: 50%;
583
+ text-shadow: 2px 2px 4px rgba(0,0,0,0.5);"
584
+ title="{player_tooltip}"
585
+ class="player-hover-element">
586
+ 🧑‍🦰
587
+ </div>
588
+
589
+ <div style="position: absolute; left: {player.x - 15}px; top: {player.y - 15}px;
590
+ background: rgba(255,215,0,0.9); color: black; padding: 1px 4px;
591
+ border-radius: 3px; font-size: 8px; font-weight: bold; z-index: 11;">
592
+ {player.name} (Lv.{player.level})
593
+ </div>"""
594
+ except Exception as e:
595
+ print(f"[UI] Error generating player tooltips: {e}")
596
+
597
+ # Base world template with enhanced tooltip styling
598
  world_html = f"""
599
  <div style="position: relative; width: {width}px; height: {height}px;
600
  border: 3px solid #333; border-radius: 12px;
 
602
  margin: 20px auto; overflow: hidden; box-shadow: 0 4px 15px rgba(0,0,0,0.2);">
603
 
604
  <!-- Trees for collision detection -->
605
+ <div style="position: absolute; left: 150px; top: 100px; font-size: 30px;" title="Ancient Oak - Blocks movement">🌳</div>
606
+ <div style="position: absolute; left: 300px; top: 200px; font-size: 30px;" title="Forest Pine - Collision object">🌳</div>
607
+ <div style="position: absolute; left: 500px; top: 150px; font-size: 30px;" title="Mystical Tree - Impassable">🌳</div>
608
+ <div style="position: absolute; left: 650px; top: 300px; font-size: 30px;" title="Old Growth Tree - Blocks path">🌳</div>
609
+ <div style="position: absolute; left: 200px; top: 400px; font-size: 30px;" title="Willow Tree - Natural barrier">🌳</div>
610
+ <div style="position: absolute; left: 450px; top: 450px; font-size: 30px;" title="Sacred Grove Tree - Protected">🌳</div>
611
+ <div style="position: absolute; left: 600px; top: 500px; font-size: 30px;" title="Elder Tree - Wise and immovable">🌳</div>
612
 
613
+ <!-- NPCs with enhanced tooltips -->
614
  {npc_html}
615
 
616
+ <!-- Decorative elements with tooltips -->
617
+ <div style="position: absolute; left: 50px; top: 50px; font-size: 20px;" title="Northern Mountains - Unexplored peaks">🏔️</div>
618
+ <div style="position: absolute; right: 50px; top: 50px; font-size: 20px;" title="Eastern Peaks - Dragon territory">🏔️</div>
619
+ <div style="position: absolute; left: 50px; bottom: 50px; font-size: 20px;" title="Western Waters - Crystal clear lakes">🌊</div>
620
+ <div style="position: absolute; right: 50px; bottom: 50px; font-size: 20px;" title="Southern Seas - Endless horizon">🌊</div>
621
 
622
  <!-- Players will be dynamically added here -->
623
+ <div id="players-container">
624
+ {players_html}
 
 
 
 
 
 
 
 
 
 
 
 
625
  </div>
626
+ <!-- Enhanced tooltip styling -->
627
+ <style>
628
+ .npc-hover-element, .player-hover-element {{
629
+ cursor: pointer;
630
+ transition: transform 0.2s ease;
631
+ }}
632
+ .npc-hover-element:hover, .player-hover-element:hover {{
633
+ transform: scale(1.1);
634
+ z-index: 100;
635
+ }}
636
+
637
+ /* Enhanced tooltip styling */
638
+ [title] {{
639
+ position: relative;
640
+ }}
641
+
642
+ /* Custom tooltip appearance - wider and more readable */
643
+ div[title]:hover::after {{
644
+ content: attr(title);
645
+ position: absolute;
646
+ bottom: 100%;
647
+ left: 50%;
648
+ transform: translateX(-50%);
649
+ background: rgba(0, 0, 0, 0.9);
650
+ color: white;
651
+ padding: 12px 16px;
652
+ border-radius: 8px;
653
+ font-size: 14px;
654
+ white-space: pre-line;
655
+ min-width: 200px;
656
+ max-width: 400px;
657
+ z-index: 1000;
658
+ box-shadow: 0 4px 12px rgba(0,0,0,0.3);
659
+ border: 1px solid #555;
660
+ text-align: center;
661
+ line-height: 1.4;
662
+ }}
663
+ </style>
664
+ <div style="position: absolute; top: 10px; left: 10px; background: rgba(255,255,255,0.9);
665
+ padding: 10px; border-radius: 8px; font-size: 12px; box-shadow: 0 2px 5px rgba(0,0,0,0.2);">
666
+ 🌍 <strong>Fantasy Realm</strong> |
667
+ Players: {len(players) if 'players' in locals() else 0}/20 |
668
+ Hover over NPCs and players for information
669
+ <br>
670
+ 🧑‍🦰 Players | 📮🏪🚶🧙🌤️ NPCs | 🌳 Trees
671
+ </div> </div>
672
  """
673
  return world_html
674
 
675
+ def _get_npc_tooltip_info(self, npc: dict) -> str:
676
+ """Generate simple tooltip information for an NPC from addon data."""
677
+ try:
678
+ # Get addon information if available
679
+ from ..interfaces.npc_addon import get_registered_addons
680
+ registered_addons = get_registered_addons()
681
+
682
+ # Try to find matching addon
683
+ addon = None
684
+ npc_id = npc.get('id', '')
685
+
686
+ # Search for addon by NPC ID or name matching
687
+ for addon_id, registered_addon in registered_addons.items():
688
+ if hasattr(registered_addon, 'npc_config') and registered_addon.npc_config:
689
+ addon_npc_id = registered_addon.npc_config.get('id', '')
690
+ if addon_npc_id == npc_id or addon_npc_id in npc_id:
691
+ addon = registered_addon
692
+ break
693
+
694
+ # Use description from addon's npc_config if available
695
+ if addon and hasattr(addon, 'npc_config') and addon.npc_config:
696
+ description = addon.npc_config.get('description', '')
697
+ if description:
698
+ return description
699
+
700
+ # Fallback to basic NPC description
701
+ return npc.get('description', f"{npc['name']} - Interactive NPC")
702
+
703
+ except Exception as e:
704
+ print(f"[UI] Error generating NPC tooltip for {npc.get('name', 'Unknown')}: {e}")
705
+ return f"{npc['name']} - Interactive NPC"
706
+
707
+ def _get_player_tooltip_info(self, player) -> str:
708
+ """Generate rich tooltip information for a player."""
709
+ try:
710
+ tooltip = f"👤 {player.name}\n"
711
+ tooltip += f"⭐ Level: {player.level}\n"
712
+ tooltip += f"❤️ Health: {player.hp}/{player.max_hp}\n"
713
+ tooltip += f"💰 Gold: {getattr(player, 'gold', 0)}\n"
714
+ tooltip += f"🎯 XP: {player.experience}\n"
715
+ tooltip += f"📍 Position: ({player.x}, {player.y})\n"
716
+
717
+ # Add status information
718
+ if hasattr(player, 'last_active'):
719
+ import time
720
+ time_diff = time.time() - player.last_active
721
+ if time_diff < 30:
722
+ tooltip += "🟢 Status: Active"
723
+ elif time_diff < 300:
724
+ tooltip += "🟡 Status: Away"
725
+ else:
726
+ tooltip += "🔴 Status: Idle"
727
+
728
+ tooltip += "\n\n💬 Walk near to start private chat"
729
+
730
+ return tooltip
731
+
732
+ except Exception as e:
733
+ print(f"[UI] Error generating player tooltip: {e}")
734
+ return f"{getattr(player, 'name', 'Player')}\n💬 Interactive player - Walk near to chat"
735
+
736
  def create_interface(self) -> gr.Blocks:
737
  """Create the complete HuggingFace-style interface."""
738
 
 
1279
  )
1280
 
1281
  with gr.Tab("🧪 AI Agent Testing"):
1282
+ self._create_ai_testing_tab()
1283
+
1284
+ def _create_ai_testing_tab(self):
1285
+ """Create enhanced AI testing tab."""
1286
+ gr.Markdown("## 🧪 Test AI Agent Integration")
1287
+ gr.Markdown("*Use this to simulate AI agent connections for testing*")
1288
+
1289
+ with gr.Row():
1290
+ ai_name = gr.Textbox(
1291
+ label="AI Agent Name",
1292
+ placeholder="Claude the Explorer",
1293
+ scale=3
1294
+ )
1295
+ register_ai_btn = gr.Button("Register AI Agent", variant="primary", scale=1)
1296
+
1297
+ with gr.Row():
1298
+ ai_action = gr.Dropdown(
1299
+ choices=["move up", "move down", "move left", "move right", "chat"],
1300
+ label="AI Action",
1301
+ scale=2
1302
+ )
1303
+ ai_message = gr.Textbox(
1304
+ label="AI Message (for chat)",
1305
+ placeholder="Hello humans!",
1306
+ scale=3
1307
+ )
1308
+
1309
+ execute_ai_btn = gr.Button("Execute AI Action", variant="secondary")
1310
+ ai_result = gr.Textbox(label="AI Action Result", interactive=False, lines=5)
1311
+
1312
+ # Raw Data Testing Section
1313
+ gr.Markdown("### 🧪 Test Raw Data Access")
1314
+ gr.Markdown("*Test accessing comprehensive world data without HTML parsing*")
1315
+
1316
+ with gr.Row():
1317
+ test_raw_data_btn = gr.Button("Get Raw World Data", variant="secondary")
1318
+ test_available_npcs_btn = gr.Button("Get Available NPCs", variant="secondary")
1319
+
1320
+ raw_data_result = gr.JSON(label="Raw Data Output", visible=True)
1321
+
1322
+ # Wire up handlers (simplified versions of original handlers)
1323
+ register_ai_btn.click(
1324
+ self._handle_register_ai_agent,
1325
+ inputs=[ai_name],
1326
+ outputs=[ai_result]
1327
+ )
1328
+
1329
+ execute_ai_btn.click(
1330
+ self._handle_execute_ai_action,
1331
+ inputs=[ai_action, ai_message],
1332
+ outputs=[ai_result]
1333
+ )
1334
+
1335
+ test_raw_data_btn.click(
1336
+ self._handle_test_raw_world_data,
1337
+ inputs=[],
1338
+ outputs=[raw_data_result]
1339
+ )
1340
+
1341
+ test_available_npcs_btn.click(
1342
+ self._handle_test_available_npcs,
1343
+ inputs=[],
1344
+ outputs=[raw_data_result]
1345
+ )
1346
+ # =================================================================
1347
+
1348
 
1349
+ def _handle_register_ai_agent(self, ai_name: str) -> str:
1350
+ """Handle AI agent registration."""
1351
+ if not ai_name or not ai_name.strip():
1352
+ return "❌ Please enter a valid AI agent name"
1353
+
1354
+ try:
1355
+ agent_id = f"test_agent_{uuid.uuid4().hex[:8]}"
1356
+ result = self.game_facade.register_ai_agent(agent_id, ai_name.strip())
1357
+
1358
+ if result:
1359
+ if not hasattr(self, 'registered_agents'):
1360
+ self.registered_agents = {}
1361
+ self.registered_agents[agent_id] = ai_name.strip()
1362
+ return f"✅ AI agent '{ai_name.strip()}' registered successfully!\nAgent ID: {agent_id}"
1363
+ else:
1364
+ return f"❌ Failed to register AI agent '{ai_name.strip()}'"
1365
+ except Exception as e:
1366
+ return f"❌ Error registering AI agent: {str(e)}"
1367
+
1368
+ def _handle_execute_ai_action(self, action: str, message: str) -> str:
1369
+ """Handle AI agent action execution."""
1370
+ if not hasattr(self, 'registered_agents') or not self.registered_agents:
1371
+ return "❌ No AI agents registered. Please register an AI agent first!"
1372
+
1373
+ if not action:
1374
+ return "❌ Please select an action to execute"
1375
+
1376
+ try:
1377
+ agent_id = list(self.registered_agents.keys())[0]
1378
+ agent_name = self.registered_agents[agent_id]
1379
+
1380
+ if action.startswith("move"):
1381
+ direction = action.split()[1] if len(action.split()) > 1 else action
1382
+ result = self.game_facade.move_ai_agent(agent_id, direction)
1383
+
1384
+ if result.get("success"):
1385
+ position = result.get("position", {})
1386
+ return f"🤖 {agent_name} moved {direction} successfully!\nNew position: ({position.get('x', '?')}, {position.get('y', '?')})"
1387
+ else:
1388
+ error_msg = result.get("error", "Movement failed")
1389
+ return f"❌ {agent_name} movement failed: {error_msg}"
1390
+ elif action == "chat":
1391
+ if not message or not message.strip():
1392
+ return "❌ Please enter a chat message"
1393
+
1394
+ result = self.game_facade.ai_agent_chat(agent_id, message.strip())
1395
+
1396
+ if result.get("success"):
1397
+ return f"💬 {agent_name} sent chat message: '{message.strip()}'"
1398
+ else:
1399
+ error_msg = result.get("error", "Chat failed")
1400
+ return f"❌ {agent_name} chat failed: {error_msg}"
1401
+ else:
1402
+ return f"❓ Unknown action: {action}"
1403
+ except Exception as e:
1404
+ return f"❌ Error executing AI action: {str(e)}"
1405
+ except Exception as e:
1406
+ return f"❌ Error executing AI action: {str(e)}"
1407
+
1408
+ def _handle_test_raw_world_data(self) -> Dict:
1409
+ """Handle testing raw world data access."""
1410
+ try:
1411
+ raw_data = self.game_facade.get_raw_world_data()
1412
+ return raw_data
1413
+ except Exception as e:
1414
+ return {"error": f"Failed to get raw world data: {str(e)}"}
1415
+
1416
+ def _handle_test_available_npcs(self) -> List[Dict]:
1417
+ """Handle testing available NPCs data access."""
1418
+ try:
1419
+ npcs_data = self.game_facade.get_available_npcs()
1420
+ return npcs_data
1421
+ except Exception as e:
1422
+ return [{"error": f"Failed to get NPCs data: {str(e)}"}]
1423
+
1424
  def _create_architecture_content(self) -> None:
1425
  """Create architecture documentation."""
1426
  gr.Markdown("""
src/ui/improved_interface_demo.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Demo: How to use the Improved Interface Manager
3
+
4
+ This file demonstrates how to integrate the improved interface manager
5
+ into your MMORPG application for a better user experience.
6
+ """
7
+
8
+ from improved_interface_manager import ImprovedInterfaceManager
9
+ from .huggingface_ui import HuggingFaceUI
10
+ from ..facades.game_facade import GameFacade
11
+
12
+
13
+ def create_improved_interface():
14
+ """
15
+ Create an improved interface with enhanced UX.
16
+
17
+ This replaces the original interface_manager with a cleaner,
18
+ more organized version that includes:
19
+
20
+ 1. ✅ Login in one line
21
+ 2. ✅ Controls next to gameboard
22
+ 3. ✅ Better player stats form
23
+ 4. ✅ Unified chat system
24
+ 5. ✅ Consistent styling throughout
25
+
26
+ Returns:
27
+ gradio.Blocks: The improved interface
28
+ """
29
+
30
+ # Initialize the game facade (you'll need this from your existing setup)
31
+ game_facade = GameFacade() # Use your existing game facade instance
32
+
33
+ # Create the enhanced UI manager
34
+ ui = HuggingFaceUI(game_facade)
35
+
36
+ # Create the improved interface manager
37
+ improved_manager = ImprovedInterfaceManager(game_facade, ui)
38
+
39
+ # Create the interface
40
+ interface = improved_manager.create_interface()
41
+
42
+ return interface
43
+
44
+
45
+ def main():
46
+ """
47
+ Launch the improved interface.
48
+
49
+ Replace your existing main() function with this to use the improved interface.
50
+ """
51
+
52
+ # Create the improved interface
53
+ interface = create_improved_interface()
54
+
55
+ # Launch with optimized settings
56
+ interface.launch(
57
+ server_name="0.0.0.0",
58
+ server_port=7860,
59
+ share=False,
60
+ inbrowser=True,
61
+ show_error=True,
62
+ debug=True
63
+ )
64
+
65
+
66
+ if __name__ == "__main__":
67
+ main()
68
+
69
+
70
+ """
71
+ Key Improvements Made:
72
+
73
+ 1. 🎯 **One-Line Login**:
74
+ - Moved player name input and join/leave buttons to a single horizontal row
75
+ - Added better visual styling with gradient background
76
+ - More compact and intuitive
77
+
78
+ 2. 🎮 **Controls Next to Gameboard**:
79
+ - Repositioned movement controls to be adjacent to the game world
80
+ - Compact 3x3 grid layout that doesn't take up too much space
81
+ - Enhanced visual feedback with hover effects
82
+
83
+ 3. 📊 **Enhanced Player Stats**:
84
+ - Replaced plain JSON display with styled HTML cards
85
+ - Added visual hierarchy with colors and gradients
86
+ - Shows health, level, position in a more readable format
87
+ - Dynamic status updates with better formatting
88
+
89
+ 4. 💬 **Unified Chat System**:
90
+ - Combined public and private chat in one clean interface
91
+ - Radio button toggle between chat modes
92
+ - Better visual separation and organization
93
+ - Maintains all original functionality
94
+
95
+ 5. 🎨 **Consistent Styling**:
96
+ - Enhanced CSS with modern gradients and shadows
97
+ - Consistent color scheme throughout
98
+ - Better spacing and typography
99
+ - Responsive design for different screen sizes
100
+ - Hover effects and smooth transitions
101
+ - Professional HuggingFace-inspired design
102
+
103
+ 6. 🚀 **Performance Optimizations**:
104
+ - Optimized game world size (650x450 vs 800x600)
105
+ - Better component organization
106
+ - Cleaner event handler structure
107
+
108
+ 7. 📱 **Better UX**:
109
+ - More intuitive layout with logical grouping
110
+ - Enhanced visual feedback
111
+ - Better error handling and status messages
112
+ - Improved accessibility
113
+
114
+ Usage Instructions:
115
+
116
+ 1. Replace your existing interface_manager import:
117
+ ```python
118
+ # OLD:
119
+ from .interface_manager import InterfaceManager
120
+
121
+ # NEW:
122
+ from .improved_interface_manager import ImprovedInterfaceManager
123
+ ```
124
+
125
+ 2. Update your interface creation:
126
+ ```python
127
+ # OLD:
128
+ manager = InterfaceManager(game_facade, ui)
129
+
130
+ # NEW:
131
+ manager = ImprovedInterfaceManager(game_facade, ui)
132
+ ```
133
+
134
+ 3. All existing functionality is preserved - the interface just looks and feels better!
135
+
136
+ The improved interface maintains 100% compatibility with your existing MCP server
137
+ functionality while providing a much cleaner and more professional user experience.
138
+ """
src/ui/improved_interface_manager.py ADDED
@@ -0,0 +1,1068 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Improved Interface Manager Module
3
+
4
+ This module provides an enhanced, cleaner version of the interface manager
5
+ with better layout, unified styling, and improved user experience.
6
+ """
7
+ from typing import Dict, Any, List, Optional, Tuple
8
+ from dataclasses import asdict
9
+ import time
10
+ import uuid
11
+ import gradio as gr
12
+
13
+ from ..core.game_engine import GameEngine
14
+ from ..core.player import Player
15
+ from ..core.world import GameWorld
16
+ from ..facades.game_facade import GameFacade
17
+ from .huggingface_ui import HuggingFaceUI
18
+
19
+
20
+ class ImprovedInterfaceManager:
21
+ """Enhanced interface manager with cleaner design and better UX."""
22
+
23
+ def __init__(self, game_facade: GameFacade, ui: HuggingFaceUI):
24
+ from ..core.game_engine import get_game_engine
25
+ self.game_engine = get_game_engine()
26
+ self.game_facade = game_facade
27
+ self.ui = ui
28
+
29
+ # Interface state
30
+ self.interface = None
31
+
32
+ # Auto-refresh timers
33
+ self.auto_refresh_interval = 2.0
34
+
35
+ # Entity name to ID mapping for private chat
36
+ self.entity_name_to_id = {}
37
+
38
+ # Current player tracking for enhanced features
39
+ self.current_player_id = None
40
+
41
+ def create_interface(self) -> gr.Blocks:
42
+ """Create and configure the complete enhanced interface."""
43
+ self._create_enhanced_interface()
44
+ return self.interface
45
+
46
+ def _create_enhanced_interface(self) -> None:
47
+ """Create the enhanced interface with improved layout and styling.""" # Enhanced CSS for better styling
48
+ enhanced_css = self.ui.custom_css + """
49
+ /* Enhanced styling for improved interface */
50
+ .login-section {
51
+ background: transparent;
52
+ color: #333;
53
+ padding: 15px;
54
+ border-radius: 8px;
55
+ margin-bottom: 20px;
56
+ border: 1px solid #e9ecef;
57
+ }
58
+
59
+ /* Remove gray gradient from join section */
60
+ .join-section {
61
+ background: transparent !important;
62
+ }
63
+
64
+ .game-layout {
65
+ display: flex;
66
+ gap: 20px;
67
+ margin-top: 20px;
68
+ }
69
+
70
+ .game-world-container {
71
+ flex: 3;
72
+ background: white;
73
+ border-radius: 12px;
74
+ padding: 15px;
75
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
76
+ }
77
+
78
+ .controls-stats-panel {
79
+ background: #f8f9fa;
80
+ border: 2px solid #e9ecef;
81
+ border-radius: 12px;
82
+ padding: 15px;
83
+ margin-left: 15px;
84
+ } .controls-stats-panel {
85
+ background: #f8f9fa;
86
+ border: 2px solid #e9ecef;
87
+ border-radius: 12px;
88
+ padding: 15px;
89
+ margin-left: 15px;
90
+ }
91
+
92
+ .controls-grid {
93
+ display: grid;
94
+ grid-template-columns: repeat(3, 50px);
95
+ gap: 8px;
96
+ justify-content: center;
97
+ margin-top: 10px;
98
+ }
99
+
100
+ .control-btn {
101
+ width: 50px;
102
+ height: 50px;
103
+ border-radius: 8px;
104
+ border: 2px solid #007bff;
105
+ background: #ffffff;
106
+ font-size: 16px;
107
+ font-weight: bold;
108
+ cursor: pointer;
109
+ transition: all 0.2s;
110
+ }
111
+
112
+ .control-btn:hover {
113
+ background: #007bff;
114
+ color: white;
115
+ transform: scale(1.05);
116
+ }
117
+
118
+ .stats-panel {
119
+ flex: 1;
120
+ background: white;
121
+ border-radius: 12px;
122
+ padding: 20px;
123
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
124
+ margin-top: 20px;
125
+ }
126
+
127
+ .player-stats-card {
128
+ background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
129
+ color: white;
130
+ padding: 20px;
131
+ border-radius: 12px;
132
+ margin-bottom: 20px;
133
+ text-align: center;
134
+ }
135
+
136
+ .stat-item {
137
+ display: flex;
138
+ justify-content: space-between;
139
+ align-items: center;
140
+ padding: 8px 0;
141
+ border-bottom: 1px solid rgba(255,255,255,0.2);
142
+ }
143
+
144
+ .stat-label {
145
+ font-weight: bold;
146
+ opacity: 0.9;
147
+ }
148
+
149
+ .stat-value {
150
+ background: rgba(255,255,255,0.2);
151
+ padding: 4px 8px;
152
+ border-radius: 6px;
153
+ font-weight: bold;
154
+ }
155
+
156
+ .events-panel {
157
+ background: #fff3cd;
158
+ border: 1px solid #ffeaa7;
159
+ border-radius: 8px;
160
+ padding: 15px;
161
+ margin-top: 15px;
162
+ }
163
+
164
+ .events-content {
165
+ font-size: 14px;
166
+ line-height: 1.5;
167
+ }
168
+
169
+ .tip {
170
+ background: #d4edda;
171
+ border: 1px solid #c3e6cb;
172
+ border-radius: 6px;
173
+ padding: 10px;
174
+ margin-top: 10px;
175
+ font-size: 12px;
176
+ }
177
+
178
+ .chat-unified {
179
+ background: white;
180
+ border-radius: 12px;
181
+ padding: 20px;
182
+ margin-top: 20px;
183
+ box-shadow: 0 4px 15px rgba(0,0,0,0.1);
184
+ }
185
+
186
+ .chat-tabs {
187
+ display: flex;
188
+ gap: 10px;
189
+ margin-bottom: 15px;
190
+ border-bottom: 2px solid #e9ecef;
191
+ padding-bottom: 10px;
192
+ }
193
+
194
+ .chat-tab {
195
+ padding: 8px 16px;
196
+ background: #f8f9fa;
197
+ border: 1px solid #e9ecef;
198
+ border-radius: 8px 8px 0 0;
199
+ cursor: pointer;
200
+ transition: all 0.2s;
201
+ }
202
+
203
+ .chat-tab.active {
204
+ background: #007bff;
205
+ color: white;
206
+ border-color: #007bff;
207
+ }
208
+
209
+ .chat-tab:hover {
210
+ background: #e9ecef;
211
+ }
212
+ """
213
+
214
+ # Get keyboard script for head injection
215
+ keyboard_js = self.ui.get_keyboard_script()
216
+
217
+ with gr.Blocks(
218
+ title="🎮 MMORPG with MCP Integration",
219
+ theme=self.ui.theme,
220
+ css=enhanced_css,
221
+ head=keyboard_js
222
+ ) as interface:
223
+
224
+ # Hero section
225
+ self.ui.create_hero_section()
226
+
227
+ # Player state management
228
+ player_state = gr.State({})
229
+
230
+ with gr.Tabs():
231
+ # Top-level Game World tab with nested sub-tabs
232
+ with gr.Tab("🌍 Game World"):
233
+ with gr.Tabs():
234
+ # Game Tab: world view, player status, movement
235
+ with gr.Tab("🎮 Game"):
236
+
237
+ gr.Markdown("**NPC Add-ons:** Walk near NPCs to unlock their features!")
238
+
239
+ # One-line join section
240
+ with gr.Group(elem_classes=["join-section"]):
241
+ gr.Markdown("### 🎮 Join the Adventure")
242
+ with gr.Row():
243
+ player_name = gr.Textbox(
244
+ label="Player Name",
245
+ placeholder="Enter your character name",
246
+ scale=4,
247
+ container=False
248
+ )
249
+ join_btn = gr.Button("Join Game 🎮", variant="primary", scale=1)
250
+ leave_btn = gr.Button("Leave Game 🚪", variant="secondary", scale=1)
251
+
252
+ # Game layout and controls
253
+ with gr.Row():
254
+ # Left side: Game world spanning 4 columns for better visibility
255
+ with gr.Column(scale=4):
256
+ game_view = gr.HTML(
257
+ value=self.ui._generate_world_html(900,600),
258
+ label="🌍 Game World",
259
+ elem_classes=["game-world-container"]
260
+ )
261
+
262
+ # Right side: Combined controls and stats in 1 column
263
+ with gr.Column(scale=1, elem_classes=["controls-stats-panel"]):
264
+ # Controls section
265
+ with gr.Group():
266
+ gr.Markdown("**🕹️ Controls**")
267
+ gr.Markdown("*WASD or Arrow Keys*")
268
+
269
+ # Enhanced movement controls in grid
270
+ with gr.Row():
271
+ gr.HTML("") # spacer
272
+ move_up = gr.Button("↑", elem_classes=["control-btn"])
273
+ gr.HTML("") # spacer
274
+ with gr.Row():
275
+ move_left = gr.Button("←", elem_classes=["control-btn"])
276
+ action_btn = gr.Button("⚔️", elem_classes=["control-btn"], variant="secondary")
277
+ move_right = gr.Button("→", elem_classes=["control-btn"])
278
+ with gr.Row():
279
+ gr.HTML("") # spacer
280
+ move_down = gr.Button("↓", elem_classes=["control-btn"])
281
+ gr.HTML("") # spacer
282
+
283
+ # Player status panel below controls
284
+ player_info = gr.HTML(label="Player Information")
285
+
286
+ # Chat Tab: unified public and private chat
287
+ with gr.Tab("💬 Chat"):
288
+ with gr.Group(elem_classes=["chat-unified"]):
289
+ gr.Markdown("### 💬 Communication Hub")
290
+
291
+ # Chat type selector
292
+ with gr.Row():
293
+ chat_mode = gr.Radio(
294
+ choices=["🌍 Public Chat", "🔒 Private Messages"],
295
+ value="🌍 Public Chat",
296
+ label="Chat Mode",
297
+ scale=2
298
+ )
299
+ auto_refresh_enabled = gr.Checkbox(
300
+ label="Auto-refresh (2s)",
301
+ value=True,
302
+ scale=1
303
+ )
304
+
305
+ public_chat_group = gr.Group()
306
+ with public_chat_group:
307
+ chat_display = gr.Chatbot(
308
+ label="💬 Game Chat",
309
+ height=250,
310
+ type='messages',
311
+ value=[{"role": "assistant", "content": "Welcome! Join the game to start chatting!"}]
312
+ )
313
+
314
+ with gr.Row():
315
+ chat_input = gr.Textbox(
316
+ placeholder="Type your message... (use /help for commands)",
317
+ scale=4,
318
+ container=False
319
+ )
320
+ chat_send = gr.Button("Send", scale=1, variant="primary")
321
+
322
+ private_chat_group = gr.Group(visible=False)
323
+ with private_chat_group:
324
+ proximity_info = gr.HTML(
325
+ value="<div style='text-align: center; color: #666;'>🔍 Move near NPCs or players to chat privately</div>"
326
+ )
327
+
328
+ nearby_entities = gr.Dropdown(
329
+ label="💬 Start new chat with",
330
+ choices=[],
331
+ interactive=True
332
+ )
333
+
334
+ with gr.Row():
335
+ start_chat_btn = gr.Button("Start Chat", variant="primary", scale=1)
336
+ clear_all_tabs_btn = gr.Button("Clear All", variant="secondary", scale=1)
337
+
338
+ # Chat tabs and current conversation
339
+ chat_tabs_state = gr.State({})
340
+ active_tabs_display = gr.HTML(
341
+ value="<div style='text-align: center; color: #666; padding: 10px;'>No active chats</div>"
342
+ )
343
+
344
+ current_chat_display = gr.Chatbot(
345
+ label="🔒 Private Messages",
346
+ height=200,
347
+ type='messages',
348
+ value=[],
349
+ visible=False
350
+ )
351
+
352
+ with gr.Row(visible=False) as chat_input_row:
353
+ private_message_input = gr.Textbox(
354
+ placeholder="Type private message...",
355
+ scale=4,
356
+ container=False
357
+ )
358
+ private_send_btn = gr.Button("Send", scale=1, variant="secondary")
359
+
360
+ # Status Tab: online players and world events
361
+ with gr.Tab("📊 Status"):
362
+ online_players = gr.Dataframe(
363
+ headers=["Name","Type","Level"],
364
+ label="👥 Online Players",
365
+ elem_classes=["players-table"]
366
+ )
367
+
368
+ world_events = gr.HTML(label="Game Events")
369
+
370
+ # NPC Add-ons Tab
371
+ with gr.Tab("🤖 NPC Add-ons"):
372
+ self.ui._create_npc_addons_content()
373
+
374
+ # Plugin System Tab
375
+ with gr.Tab("🔌 Plugin System"):
376
+ gr.Markdown("### 🔧 Plugin Manager")
377
+ refresh_plugins_btn = gr.Button("🔄 Refresh Plugins", variant="primary")
378
+ reload_all_btn = gr.Button("🔄 Reload All", variant="secondary")
379
+ plugin_status = gr.JSON(value={
380
+ "total_plugins": 0,
381
+ "active_plugins": 0,
382
+ "failed_plugins": 0,
383
+ "last_scan": "Never"
384
+ }, label="Plugin Status")
385
+ # Wire up plugin manager actions
386
+ refresh_plugins_btn.click(
387
+ self._handle_refresh_plugins,
388
+ inputs=[],
389
+ outputs=[plugin_status]
390
+ )
391
+ reload_all_btn.click(
392
+ self._handle_reload_all_plugins,
393
+ inputs=[],
394
+ outputs=[plugin_status]
395
+ )
396
+
397
+ # MCP Integration Tab
398
+ with gr.Tab("🔗 MCP Integration"):
399
+ self.ui._create_mcp_integration_content()
400
+
401
+ # AI Agent Testing Tab
402
+ with gr.Tab("🧪 AI Agent Testing"):
403
+ self._create_ai_testing_tab()
404
+
405
+ # Architecture Tab
406
+ with gr.Tab("🏗️ Architecture"):
407
+ self.ui._create_architecture_content()
408
+
409
+
410
+
411
+ # =================================================================
412
+ # EVENT HANDLERS
413
+ # =================================================================
414
+
415
+ # Chat mode switching
416
+ def switch_chat_mode(mode):
417
+ if mode == "🌍 Public Chat":
418
+ # Show public chat, hide private chat and input row
419
+ return gr.update(visible=True), gr.update(visible=False), gr.update(visible=False)
420
+ else:
421
+ # Hide public chat, show private chat and input row
422
+ return gr.update(visible=False), gr.update(visible=True), gr.update(visible=True)
423
+
424
+ chat_mode.change(
425
+ switch_chat_mode,
426
+ inputs=[chat_mode],
427
+ outputs=[public_chat_group, private_chat_group, chat_input_row]
428
+ )
429
+
430
+ # Join/Leave game handlers
431
+ join_btn.click(
432
+ self._handle_join_game,
433
+ inputs=[player_name, player_state],
434
+ outputs=[player_state, player_info, online_players, game_view]
435
+ )
436
+
437
+ leave_btn.click(
438
+ self._handle_leave_game,
439
+ inputs=[player_state],
440
+ outputs=[player_state, player_info, online_players, game_view]
441
+ )
442
+
443
+ # Movement handlers
444
+ move_up.click(
445
+ self._handle_movement,
446
+ inputs=[gr.State("up"), player_state],
447
+ outputs=[player_state, game_view, world_events]
448
+ )
449
+
450
+ move_down.click(
451
+ self._handle_movement,
452
+ inputs=[gr.State("down"), player_state],
453
+ outputs=[player_state, game_view, world_events]
454
+ )
455
+
456
+ move_left.click(
457
+ self._handle_movement,
458
+ inputs=[gr.State("left"), player_state],
459
+ outputs=[player_state, game_view, world_events]
460
+ )
461
+
462
+ move_right.click(
463
+ self._handle_movement,
464
+ inputs=[gr.State("right"), player_state],
465
+ outputs=[player_state, game_view, world_events]
466
+ )
467
+
468
+ action_btn.click(
469
+ self._handle_action,
470
+ inputs=[player_state],
471
+ outputs=[world_events]
472
+ )
473
+
474
+ # Chat handlers
475
+ chat_send.click(
476
+ self._handle_chat_message,
477
+ inputs=[chat_input, player_state],
478
+ outputs=[chat_display, chat_input]
479
+ )
480
+
481
+ chat_input.submit(
482
+ self._handle_chat_message,
483
+ inputs=[chat_input, player_state],
484
+ outputs=[chat_display, chat_input]
485
+ )
486
+
487
+ # Private chat handlers
488
+ start_chat_btn.click(
489
+ self._handle_start_chat,
490
+ inputs=[nearby_entities, chat_tabs_state, player_state],
491
+ outputs=[chat_tabs_state, active_tabs_display, current_chat_display, chat_input_row]
492
+ )
493
+
494
+ private_send_btn.click(
495
+ self._handle_private_message,
496
+ inputs=[private_message_input, chat_tabs_state, player_state],
497
+ outputs=[current_chat_display, private_message_input]
498
+ )
499
+
500
+ clear_all_tabs_btn.click(
501
+ self._handle_clear_all_chats,
502
+ inputs=[chat_tabs_state],
503
+ outputs=[chat_tabs_state, active_tabs_display, current_chat_display, chat_input_row]
504
+ )
505
+
506
+ # Auto-refresh handlers
507
+ refresh_timer = gr.Timer(value=self.auto_refresh_interval)
508
+ refresh_timer.tick(
509
+ self._auto_refresh_game_state,
510
+ inputs=[player_state, auto_refresh_enabled],
511
+ outputs=[player_info, online_players, game_view, proximity_info, nearby_entities,
512
+ private_chat_group, chat_display, world_events]
513
+ )
514
+
515
+ self.interface = interface
516
+
517
+ def _create_ai_testing_tab(self):
518
+ """Create enhanced AI testing tab."""
519
+ gr.Markdown("## 🧪 Test AI Agent Integration")
520
+ gr.Markdown("*Use this to simulate AI agent connections for testing*")
521
+
522
+ with gr.Row():
523
+ ai_name = gr.Textbox(
524
+ label="AI Agent Name",
525
+ placeholder="Claude the Explorer",
526
+ scale=3
527
+ )
528
+ register_ai_btn = gr.Button("Register AI Agent", variant="primary", scale=1)
529
+
530
+ with gr.Row():
531
+ ai_action = gr.Dropdown(
532
+ choices=["move up", "move down", "move left", "move right", "chat"],
533
+ label="AI Action",
534
+ scale=2
535
+ )
536
+ ai_message = gr.Textbox(
537
+ label="AI Message (for chat)",
538
+ placeholder="Hello humans!",
539
+ scale=3
540
+ )
541
+
542
+ execute_ai_btn = gr.Button("Execute AI Action", variant="secondary")
543
+ ai_result = gr.Textbox(label="AI Action Result", interactive=False, lines=5)
544
+
545
+ # Raw Data Testing Section
546
+ gr.Markdown("### 🧪 Test Raw Data Access")
547
+ gr.Markdown("*Test accessing comprehensive world data without HTML parsing*")
548
+
549
+ with gr.Row():
550
+ test_raw_data_btn = gr.Button("Get Raw World Data", variant="secondary")
551
+ test_available_npcs_btn = gr.Button("Get Available NPCs", variant="secondary")
552
+
553
+ raw_data_result = gr.JSON(label="Raw Data Output", visible=True)
554
+
555
+ # Wire up handlers (simplified versions of original handlers)
556
+ register_ai_btn.click(
557
+ self._handle_register_ai_agent,
558
+ inputs=[ai_name],
559
+ outputs=[ai_result]
560
+ )
561
+
562
+ execute_ai_btn.click(
563
+ self._handle_execute_ai_action,
564
+ inputs=[ai_action, ai_message],
565
+ outputs=[ai_result]
566
+ )
567
+
568
+ test_raw_data_btn.click(
569
+ self._handle_test_raw_world_data,
570
+ inputs=[],
571
+ outputs=[raw_data_result]
572
+ )
573
+
574
+ test_available_npcs_btn.click(
575
+ self._handle_test_available_npcs,
576
+ inputs=[],
577
+ outputs=[raw_data_result]
578
+ )
579
+ # =================================================================
580
+ # EVENT HANDLER METHODS (Reusing from original interface manager)
581
+ # =================================================================
582
+
583
+ def _handle_join_game(self, name: str, current_state: Dict) -> Tuple[Dict, str, List[List[str]], str]:
584
+ """Handle player joining the game with enhanced UI feedback."""
585
+ if not name or not name.strip():
586
+ return current_state, self._create_error_stats_html("Please enter a valid name"), [], self.ui._generate_world_html(900, 600)
587
+
588
+ player_id = self.game_facade.join_game(name.strip())
589
+ if not player_id:
590
+ return current_state, self._create_error_stats_html("Failed to join game"), [], self.ui._generate_world_html(900, 600)
591
+
592
+ current_state.update({"player_id": player_id, "joined": True, "name": name.strip()})
593
+ self.current_player_id = player_id
594
+ # Enhanced player stats HTML
595
+ stats_html = self._create_player_stats_html(player_id, name.strip())
596
+ rows = self._get_online_players_data()
597
+ world_html = self._generate_world_html_with_players()
598
+
599
+ return current_state, stats_html, rows, world_html
600
+
601
+ def _handle_leave_game(self, current_state: Dict) -> Tuple[Dict, str, List[List[str]], str]:
602
+ """Handle player leaving the game with enhanced UI feedback."""
603
+ player_id = current_state.get("player_id")
604
+ success = self.game_facade.leave_game(player_id)
605
+
606
+ new_state: Dict = {}
607
+ self.current_player_id = None
608
+
609
+ stats_html = self._create_default_stats_html()
610
+ rows = self._get_online_players_data()
611
+ world_html = self._generate_world_html_with_players()
612
+
613
+ return new_state, stats_html, rows, world_html
614
+
615
+ def _handle_movement(self, direction: str, current_state: Dict) -> Tuple[Dict, str, str]:
616
+ """Handle player movement with enhanced feedback."""
617
+ if not current_state.get("joined"):
618
+ return current_state, self._generate_world_html_with_players(), self._create_events_html("Join the game to move!")
619
+
620
+ try:
621
+ player_id = current_state.get("player_id")
622
+ success, new_position, events = self.game_facade.move_player(player_id, direction)
623
+
624
+ if success:
625
+ current_state.update(new_position)
626
+ world_html = self._generate_world_html_with_players()
627
+ events_html = self._create_events_html(events or f"Moved {direction}")
628
+ return current_state, world_html, events_html
629
+ else:
630
+ events_html = self._create_events_html(f"Cannot move {direction} - blocked!")
631
+ return current_state, self._generate_world_html_with_players(), events_html
632
+
633
+ except Exception as e:
634
+ return current_state, self._generate_world_html_with_players(), self._create_events_html(f"Movement error: {str(e)}")
635
+
636
+ def _handle_action(self, current_state: Dict) -> str:
637
+ """Handle special action button with enhanced feedback."""
638
+ if not current_state.get("joined"):
639
+ return self._create_events_html("Join the game to use actions!")
640
+
641
+ try:
642
+ player_id = current_state.get("player_id")
643
+ result = self.game_facade.handle_action(player_id)
644
+ return self._create_events_html(result)
645
+ except Exception as e:
646
+ return self._create_events_html(f"Action error: {str(e)}")
647
+
648
+ def _handle_chat_message(self, message: str, current_state: Dict) -> Tuple[List[Dict], str]:
649
+ """Handle public chat messages."""
650
+ if not message or not message.strip() or not current_state.get("joined"):
651
+ return [], ""
652
+
653
+ try:
654
+ player_id = current_state.get("player_id")
655
+ self.game_facade.send_chat_message(player_id, message.strip())
656
+ history = self.game_facade.get_chat_history(20)
657
+ formatted = self._format_chat_messages(history)
658
+ return formatted, ""
659
+ except Exception:
660
+ return [{"role": "assistant", "content": "Chat error"}], ""
661
+
662
+ def _handle_start_chat(self, entity_selection: str, chat_tabs_state: Dict, current_state: Dict):
663
+ """Handle starting a private chat - proper implementation."""
664
+ if not entity_selection or not current_state.get("joined"):
665
+ return chat_tabs_state, "", gr.update(visible=False), gr.update(visible=False)
666
+
667
+ try:
668
+ # Convert display name back to entity ID using the stored mapping
669
+ entity_id = self.entity_name_to_id.get(entity_selection, entity_selection)
670
+ entity_name = entity_selection # We already have the display name
671
+
672
+ if entity_id not in chat_tabs_state:
673
+ chat_tabs_state[entity_id] = {
674
+ "name": entity_name,
675
+ "active": True,
676
+ "pinned": False,
677
+ "unread": 0
678
+ }
679
+
680
+ # Set as active
681
+ for tab_id in chat_tabs_state:
682
+ chat_tabs_state[tab_id]["active"] = (tab_id == entity_id)
683
+
684
+ # Get chat messages for this entity
685
+ player_id = current_state.get("player_id")
686
+ messages = self.game_facade.get_private_messages(player_id, entity_id)
687
+ tabs_html = self._generate_chat_tabs_html(chat_tabs_state)
688
+ return chat_tabs_state, gr.update(value=tabs_html), gr.update(value=messages, visible=True), gr.update(visible=True)
689
+
690
+ except Exception as e:
691
+ return chat_tabs_state, gr.update(value=f"Error: {str(e)}"), gr.update(visible=False), gr.update(visible=False)
692
+
693
+ def _handle_private_message(self, message: str, chat_tabs_state: Dict, current_state: Dict):
694
+ """Handle private chat messages - proper implementation."""
695
+ # Validate input and state
696
+ if not message or not message.strip() or not current_state.get("joined"):
697
+ return [], ""
698
+
699
+ # Determine active entity for private chat
700
+ entity_id = next((eid for eid, info in chat_tabs_state.items() if info.get("active")), None)
701
+ if not entity_id:
702
+ return [], ""
703
+
704
+ print(f"[ImprovedInterfaceManager] Sending private message from {current_state.get('player_id')} to {entity_id}: '{message.strip()}'")
705
+
706
+ # Send private message via game facade
707
+ player_id = current_state.get("player_id")
708
+ player_name = current_state.get("name", "Unknown") # Get player name from state
709
+ success = self.game_facade.send_private_message(player_id, player_name, entity_id, message.strip())
710
+
711
+ if success:
712
+ # Fetch updated messages
713
+ messages = self.game_facade.get_private_messages(player_id, entity_id)
714
+ # Format messages for Gradio Chatbot
715
+ formatted = []
716
+ for msg in messages[-20:]: # last 20 messages
717
+ # Determine role based on sender_id vs current player
718
+ role = "user" if msg.get("sender_id") == player_id else "assistant"
719
+ # Convert newlines to HTML breaks for proper display
720
+ message_content = msg.get('message', '').replace('\n', '<br>')
721
+ content = f"[{msg.get('timestamp', '')}] {msg.get('sender', 'Unknown')}: {message_content}"
722
+ formatted.append({"role": role, "content": content})
723
+
724
+ print(f"[ImprovedInterfaceManager] Formatted {len(formatted)} private messages for display")
725
+ return formatted, ""
726
+ else:
727
+ print(f"[ImprovedInterfaceManager] Failed to send private message")
728
+ return [], ""
729
+
730
+ def _handle_clear_all_chats(self, chat_tabs_state: Dict):
731
+ """Handle clearing all private chat tabs - proper implementation."""
732
+ chat_tabs_state.clear()
733
+ tabs_html = self._generate_chat_tabs_html(chat_tabs_state)
734
+ return chat_tabs_state, tabs_html, gr.update(visible=False), gr.update(visible=False)
735
+
736
+ def _handle_register_ai_agent(self, ai_name: str) -> str:
737
+ """Handle AI agent registration."""
738
+ if not ai_name or not ai_name.strip():
739
+ return "❌ Please enter a valid AI agent name"
740
+
741
+ try:
742
+ agent_id = f"test_agent_{uuid.uuid4().hex[:8]}"
743
+ result = self.game_facade.register_ai_agent(agent_id, ai_name.strip())
744
+
745
+ if result:
746
+ if not hasattr(self, 'registered_agents'):
747
+ self.registered_agents = {}
748
+ self.registered_agents[agent_id] = ai_name.strip()
749
+ return f"✅ AI agent '{ai_name.strip()}' registered successfully!\nAgent ID: {agent_id}"
750
+ else:
751
+ return f"❌ Failed to register AI agent '{ai_name.strip()}'"
752
+ except Exception as e:
753
+ return f"❌ Error registering AI agent: {str(e)}"
754
+
755
+ def _handle_execute_ai_action(self, action: str, message: str) -> str:
756
+ """Handle AI agent action execution."""
757
+ if not hasattr(self, 'registered_agents') or not self.registered_agents:
758
+ return "❌ No AI agents registered. Please register an AI agent first!"
759
+
760
+ if not action:
761
+ return "❌ Please select an action to execute"
762
+
763
+ try:
764
+ agent_id = list(self.registered_agents.keys())[0]
765
+ agent_name = self.registered_agents[agent_id]
766
+
767
+ if action.startswith("move"):
768
+ direction = action.split()[1] if len(action.split()) > 1 else action
769
+ result = self.game_facade.move_ai_agent(agent_id, direction)
770
+
771
+ if result.get("success"):
772
+ position = result.get("position", {})
773
+ return f"🤖 {agent_name} moved {direction} successfully!\nNew position: ({position.get('x', '?')}, {position.get('y', '?')})"
774
+ else:
775
+ error_msg = result.get("error", "Movement failed")
776
+ return f"❌ {agent_name} movement failed: {error_msg}"
777
+ elif action == "chat":
778
+ if not message or not message.strip():
779
+ return "❌ Please enter a chat message"
780
+
781
+ result = self.game_facade.ai_agent_chat(agent_id, message.strip())
782
+
783
+ if result.get("success"):
784
+ return f"💬 {agent_name} sent chat message: '{message.strip()}'"
785
+ else:
786
+ error_msg = result.get("error", "Chat failed")
787
+ return f"❌ {agent_name} chat failed: {error_msg}"
788
+ else:
789
+ return f"❓ Unknown action: {action}"
790
+ except Exception as e:
791
+ return f"❌ Error executing AI action: {str(e)}"
792
+ except Exception as e:
793
+ return f"❌ Error executing AI action: {str(e)}"
794
+
795
+ def _handle_test_raw_world_data(self) -> Dict:
796
+ """Handle testing raw world data access."""
797
+ try:
798
+ raw_data = self.game_facade.get_raw_world_data()
799
+ return raw_data
800
+ except Exception as e:
801
+ return {"error": f"Failed to get raw world data: {str(e)}"}
802
+
803
+ def _handle_test_available_npcs(self) -> List[Dict]:
804
+ """Handle testing available NPCs data access."""
805
+ try:
806
+ npcs_data = self.game_facade.get_available_npcs()
807
+ return npcs_data
808
+ except Exception as e:
809
+ return [{"error": f"Failed to get NPCs data: {str(e)}"}]
810
+
811
+ def _handle_refresh_plugins(self):
812
+ """Refresh plugin list and status metrics."""
813
+ from datetime import datetime
814
+ plugin_ids = self.game_facade.get_loaded_plugins()
815
+ active = 0
816
+ failed = 0
817
+ for pid in plugin_ids:
818
+ status = self.game_facade.get_plugin_status(pid) or {}
819
+ if status.get('enabled', False):
820
+ active += 1
821
+ else:
822
+ failed += 1
823
+ return {
824
+ 'total_plugins': len(plugin_ids),
825
+ 'active_plugins': active,
826
+ 'failed_plugins': failed,
827
+ 'last_scan': datetime.now().isoformat()
828
+ }
829
+
830
+ def _handle_reload_all_plugins(self):
831
+ """Hot-reload all discovered plugins."""
832
+ plugin_ids = self.game_facade.get_loaded_plugins()
833
+ for pid in plugin_ids:
834
+ try:
835
+ self.game_facade.reload_plugin(pid)
836
+ except Exception:
837
+ pass
838
+ # return refreshed status
839
+ return self._handle_refresh_plugins()
840
+
841
+ def _auto_refresh_game_state(self, current_state: Dict, auto_refresh: bool):
842
+ """Auto-refresh game state with enhanced UI updates."""
843
+ if not auto_refresh or not current_state.get("joined"):
844
+ return (gr.update(),) * 8
845
+
846
+ try:
847
+ player_id = current_state.get("player_id")
848
+ player_name = current_state.get("name", "Unknown")
849
+
850
+ # Enhanced player stats
851
+ stats_html = self._create_player_stats_html(player_id, player_name)
852
+ online_players_data = self._get_online_players_data()
853
+ world_html = self._generate_world_html_with_players()
854
+
855
+ # Proximity and chat info
856
+ proximity_html, nearby_choices, show_private_chat = self._get_proximity_info(player_id)
857
+ chat_history = self.game_facade.get_chat_history(20)
858
+ formatted_chat = self._format_chat_messages(chat_history)
859
+ events_html = self._get_formatted_world_events()
860
+
861
+ return (
862
+ gr.update(value=stats_html),
863
+ gr.update(value=online_players_data),
864
+ gr.update(value=world_html),
865
+ gr.update(value=proximity_html),
866
+ gr.update(choices=nearby_choices),
867
+ gr.update(visible=show_private_chat),
868
+ gr.update(value=formatted_chat),
869
+ gr.update(value=events_html),
870
+ )
871
+ except Exception as e:
872
+ print(f"Auto-refresh error: {e}")
873
+ return (gr.update(),) * 8
874
+
875
+ # =================================================================
876
+ # HELPER METHODS FOR ENHANCED UI
877
+ # =================================================================
878
+
879
+ def _create_player_stats_html(self, player_id: str, player_name: str) -> str:
880
+ """Create enhanced player stats HTML."""
881
+ try:
882
+ player_data = self.game_facade.get_player_stats(player_id)
883
+
884
+ return f"""
885
+ <div class="player-stats-card">
886
+ <h3>🧝‍♂️ {player_name}</h3>
887
+ <div class="stat-item">
888
+ <span class="stat-label">Status:</span>
889
+ <span class="stat-value">Online</span>
890
+ </div>
891
+ <div class="stat-item">
892
+ <span class="stat-label">Level:</span>
893
+ <span class="stat-value">{player_data.get('level', 1)}</span>
894
+ </div>
895
+ <div class="stat-item">
896
+ <span class="stat-label">Health:</span>
897
+ <span class="stat-value">{player_data.get('hp', 100)}/{player_data.get('max_hp', 100)}</span>
898
+ </div>
899
+ <div class="stat-item">
900
+ <span class="stat-label">XP:</span>
901
+ <span class="stat-value">{player_data.get('experience', 0)}</span>
902
+ </div>
903
+ <div class="stat-item">
904
+ <span class="stat-label">Gold:</span>
905
+ <span class="stat-value">{player_data.get('gold', 0)}</span>
906
+ </div>
907
+ <div class="stat-item">
908
+ <span class="stat-label">Position:</span>
909
+ <span class="stat-value">{player_data.get('position', '(0,0)')}</span>
910
+ </div>
911
+ </div>
912
+ """
913
+ except Exception:
914
+ return self._create_default_stats_html()
915
+
916
+ def _create_default_stats_html(self) -> str:
917
+ """Create default stats HTML when not connected."""
918
+ return """
919
+ <div class="player-stats-card">
920
+ <h3>🧝‍♂️ Player Stats</h3>
921
+ <div class="stat-item">
922
+ <span class="stat-label">Status:</span>
923
+ <span class="stat-value">Not connected</span>
924
+ </div>
925
+ <div class="info-text" style="margin-top: 10px; opacity: 0.9;">
926
+ Join the game to see your stats
927
+ </div>
928
+ </div>
929
+ """
930
+
931
+ def _create_error_stats_html(self, error_message: str) -> str:
932
+ """Create error stats HTML."""
933
+ return f"""
934
+ <div class="player-stats-card" style="background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%);">
935
+ <h3>❌ Error</h3>
936
+ <div class="info-text" style="margin-top: 10px; opacity: 0.9;">
937
+ {error_message}
938
+ </div>
939
+ </div>
940
+ """
941
+
942
+ def _create_events_html(self, events_text: str) -> str:
943
+ """Create formatted events HTML."""
944
+ return f"""
945
+ <div class="events-panel">
946
+ <h4>🌍 World Events</h4>
947
+ <div class="events-content">
948
+ <p>{events_text}</p>
949
+ <div class="tip">
950
+ 💡 <strong>Tip:</strong> Walk near NPCs (📮🏪🌤️) to interact!<br>
951
+ Then visit the 'NPC Add-ons' tab to use their features.
952
+ </div>
953
+ </div>
954
+ </div>
955
+ """
956
+
957
+ def _generate_chat_tabs_html(self, chat_tabs_state: Dict) -> str:
958
+ """Generate HTML for chat tabs display."""
959
+ if not chat_tabs_state:
960
+ return "<div style='text-align: center; color: #666; padding: 10px;'>No active chats</div>"
961
+
962
+ tabs_html = "<div style='display: flex; flex-wrap: wrap; gap: 5px; padding: 5px;'>"
963
+ for entity_id, tab_info in chat_tabs_state.items():
964
+ active_class = "active" if tab_info.get('active') else ""
965
+ pin_icon = "📌" if tab_info.get('pinned') else ""
966
+ unread_badge = f" ({tab_info.get('unread', 0)})" if tab_info.get('unread', 0) > 0 else ""
967
+
968
+ tabs_html += f'''
969
+ <div class="chat-tab {active_class}"
970
+ style="display: flex; align-items: center; gap: 5px; padding: 8px 12px;
971
+ background: {'#007bff' if tab_info.get('active') else '#f8f9fa'};
972
+ color: {'white' if tab_info.get('active') else '#333'};
973
+ border-radius: 8px; border: 1px solid #e9ecef;"
974
+ title="Entity ID: {entity_id}">
975
+ <span>{tab_info['name']}{unread_badge}</span>
976
+ {pin_icon}
977
+ <span style="color: #f44336; font-weight: bold; cursor: pointer; margin-left: 5px;" title="Close tab">×</span>
978
+ </div>
979
+ '''
980
+ tabs_html += "</div>"
981
+ return tabs_html
982
+
983
+ # Import helper methods from original interface manager
984
+ def _get_online_players_data(self):
985
+ """Get online players data - imported from original."""
986
+ try:
987
+ players_data = []
988
+ for player in self.game_facade.get_all_players().values(): players_data.append([
989
+ player.name,
990
+ "🤖 AI" if player.type == "ai_agent" else "👤 Human",
991
+ str(player.level)
992
+ ])
993
+ return players_data
994
+ except Exception:
995
+ return []
996
+
997
+ def _generate_world_html_with_players(self):
998
+ """Generate world HTML with enhanced size for better visibility."""
999
+ return self.ui._generate_world_html(900, 600)
1000
+
1001
+ def _format_chat_messages(self, messages):
1002
+ """Format chat messages - imported from original."""
1003
+ formatted = []
1004
+ for msg in messages[-20:]:
1005
+ role = "assistant" if msg.get("sender") == "System" else "user"
1006
+ message_content = msg.get('message', '').replace('\n', '<br>')
1007
+ content = f"[{msg.get('timestamp', '')}] {msg.get('sender', 'Unknown')}: {message_content}"
1008
+ formatted.append({"role": role, "content": content})
1009
+ return formatted
1010
+
1011
+ def _get_proximity_info(self, player_id):
1012
+ """Get proximity info - imported from original."""
1013
+ try:
1014
+ proximity_data = self.game_facade.get_proximity_info(player_id)
1015
+ nearby_entities = proximity_data.get("nearby_entities", [])
1016
+
1017
+ if nearby_entities:
1018
+ proximity_html = f"<div style='color: #4caf50;'>📡 Found {len(nearby_entities)} nearby entities</div>"
1019
+ choices = []
1020
+ self.entity_name_to_id = {}
1021
+
1022
+ for entity in nearby_entities:
1023
+ entity_id = entity['id']
1024
+ entity_name = self._get_entity_name(entity_id)
1025
+ self.entity_name_to_id[entity_name] = entity_id
1026
+ choices.append(entity_name)
1027
+
1028
+ return proximity_html, choices, True
1029
+ else:
1030
+ proximity_html = "<div style='color: #666;'>🔍 Move near NPCs or players to chat privately</div>"
1031
+ return proximity_html, [], False
1032
+ except Exception as e:
1033
+ return f"<div style='color: #f44336;'>Error: {str(e)}</div>", [], False
1034
+
1035
+ def _get_entity_name(self, entity_id):
1036
+ """Get entity name - imported from original."""
1037
+ try:
1038
+ world = self.game_engine.get_world()
1039
+
1040
+ npc = world.npcs.get(entity_id)
1041
+ if npc:
1042
+ return npc.get("name", entity_id)
1043
+
1044
+ player = world.players.get(entity_id)
1045
+ if player:
1046
+ return player.name
1047
+
1048
+ return entity_id
1049
+ except Exception:
1050
+ return entity_id
1051
+
1052
+ def _get_formatted_world_events(self):
1053
+ """Get formatted world events - imported from original."""
1054
+ try:
1055
+ world_events = self.game_facade.get_world_events()
1056
+ if not world_events:
1057
+ return self._create_events_html("No recent world events...")
1058
+
1059
+ formatted_events = []
1060
+ for event in world_events[-10:]:
1061
+ timestamp = event.get('timestamp', '')
1062
+ event_text = event.get('event', '')
1063
+ formatted_events.append(f"[{timestamp}] {event_text}")
1064
+
1065
+ return self._create_events_html("\\n".join(formatted_events))
1066
+ except Exception as e:
1067
+ print(f"Error formatting world events: {e}")
1068
+ return self._create_events_html("Error loading world events...")
src/ui/interface_manager.py CHANGED
@@ -204,7 +204,6 @@ class InterfaceManager:
204
  container=False
205
  )
206
  private_send_btn = gr.Button("Send", scale=1, variant="secondary")
207
-
208
  # Documentation Tab
209
  with gr.Tab("📚 Documentation"):
210
  self.ui._create_documentation_content()
@@ -215,7 +214,8 @@ class InterfaceManager:
215
 
216
  # Plugin System Tab
217
  with gr.Tab("🔌 Plugin System"):
218
- self.ui._create_plugin_system_content() # MCP Integration Tab
 
219
  with gr.Tab("🔗 MCP Integration"):
220
  self.ui._create_mcp_integration_content()
221
 
 
204
  container=False
205
  )
206
  private_send_btn = gr.Button("Send", scale=1, variant="secondary")
 
207
  # Documentation Tab
208
  with gr.Tab("📚 Documentation"):
209
  self.ui._create_documentation_content()
 
214
 
215
  # Plugin System Tab
216
  with gr.Tab("🔌 Plugin System"):
217
+ self.ui._create_plugin_system_content()
218
+ # MCP Integration Tab
219
  with gr.Tab("🔗 MCP Integration"):
220
  self.ui._create_mcp_integration_content()
221