Jofthomas commited on
Commit
d6782f1
·
1 Parent(s): 0ab60e6
Files changed (2) hide show
  1. app.py +44 -227
  2. utils/pokemon_utils.py +76 -64
app.py CHANGED
@@ -2,13 +2,14 @@ import os
2
  import asyncio
3
  from dotenv import load_dotenv
4
  from mcp.server.fastmcp import FastMCP
5
- from utils.agent_factory import create_agent, get_supported_agent_types, get_default_models
6
  from utils.pokemon_utils import (
7
- start_ladder_battle, start_battle_against_agent, start_battle_against_player,
8
  submit_move_for_battle, get_battle_state, list_active_battles,
9
  download_battle_replay, cleanup_completed_battles, format_battle_state,
10
  check_recent_battles, debug_move_attributes, active_battles, player_instances
11
  )
 
12
 
13
  load_dotenv()
14
 
@@ -25,124 +26,6 @@ current_session = {
25
  'active_battle_id': None
26
  }
27
 
28
- # --- MCP Tools ---
29
-
30
- @mcp.tool()
31
- def start_ladder_match(username: str = "MCPTrainer") -> dict:
32
- """
33
- Start a ladder battle on Pokemon Showdown.
34
- Args:
35
- username (str): Username for the MCP-controlled player
36
- Returns:
37
- dict: Battle information including battle ID and initial state
38
- """
39
- global current_session
40
- try:
41
- # Start ladder search (fire-and-forget)
42
- ladder_result = start_ladder_battle(username)
43
-
44
- current_session['username'] = username
45
- # Don't set active_battle_id yet since battle hasn't started
46
-
47
- return {
48
- "status": "ladder_search_queued",
49
- "message": f"Ladder search queued for {username}. Waiting for opponent match.",
50
- "instructions": "Use find_recent_battles() or get_player_status() in a few seconds to get the battle ID and viewing link once a match is found.",
51
- "note": "Check server console for 'Ladder search started' confirmation."
52
- }
53
- except Exception as e:
54
- return {
55
- "status": "error",
56
- "message": f"Failed to start ladder battle: {str(e)}"
57
- }
58
-
59
- # @mcp.tool()
60
- # async def battle_agent(agent_type: str, username: str = "MCPTrainer", api_key: str = None, model: str = None) -> dict:
61
- # """
62
- # Start a battle against an AI agent.
63
- # Args:
64
- # agent_type (str): Type of agent ('openai', 'gemini', 'mistral', 'maxdamage', 'random')
65
- # username (str): Username for the MCP-controlled player
66
- # api_key (str, optional): API key for AI agents (required for openai, gemini, mistral)
67
- # model (str, optional): Specific model to use (will use default if not specified)
68
- # Returns:
69
- # dict: Battle information including battle ID and initial state
70
- # """
71
- # global current_session
72
- # try:
73
- # # Validate agent type
74
- # if agent_type.lower() not in get_supported_agent_types():
75
- # return {
76
- # "status": "error",
77
- # "message": f"Unsupported agent type. Supported types: {get_supported_agent_types()}"
78
- # }
79
- #
80
- # # Create opponent agent
81
- # opponent = create_agent(agent_type, api_key=api_key, model=model)
82
- #
83
- # # Start battle
84
- # battle_id = await start_battle_against_agent(username, opponent)
85
- #
86
- # current_session['username'] = username
87
- # current_session['active_battle_id'] = battle_id
88
- #
89
- # # Get initial battle state
90
- # battle_info = get_battle_state(battle_id)
91
- #
92
- # return {
93
- # "status": "success",
94
- # "battle_id": battle_id,
95
- # "message": f"Battle started against {agent_type} agent",
96
- # "opponent": opponent.username,
97
- # "battle_url": battle_info.get('battle_url', f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}"),
98
- # "battle_state": battle_info['battle_state'],
99
- # "waiting_for_move": battle_info['waiting_for_move']
100
- # }
101
- # except Exception as e:
102
- # return {
103
- # "status": "error",
104
- # "message": f"Failed to start battle against agent: {str(e)}"
105
- # }
106
-
107
- @mcp.tool()
108
- def setup_battle() -> dict:
109
- """
110
- Get setup information for playing Pokemon battles.
111
- Provides instructions on how to join and play battles on the platform.
112
- Returns:
113
- dict: Setup instructions and platform information
114
- """
115
- return {
116
- "status": "success",
117
- "message": "Pokemon Battle Setup Information",
118
- "instructions": {
119
- "how_to_play": [
120
- "1. Visit the Pokemon Showdown platform at: https://huggingface.co/spaces/Jofthomas/Pokemon_showdown",
121
- "2. Choose a random username for yourself",
122
- "3. Use the MCP tools to start battles, make moves, and interact with the game",
123
- "4. You can start ladder matches, challenge specific players, or join existing battles"
124
- ],
125
- "platform_url": "https://huggingface.co/spaces/Jofthomas/Pokemon_showdown",
126
- "username_tip": "Choose any random username you like - it will be your identity in the Pokemon battle arena",
127
- "available_battle_types": [
128
- "Ladder battles (automatic matchmaking)",
129
- "Player vs Player challenges",
130
- "Spectate ongoing battles"
131
- ]
132
- },
133
- "next_steps": [
134
- "Visit the platform and choose your username",
135
- "Use start_ladder_match() to find a random opponent",
136
- "Use battle_player() to challenge a specific player",
137
- "Use get_current_battle_state() to see your battle status"
138
- ],
139
- "platform_info": {
140
- "name": "Pokemon Showdown on Hugging Face",
141
- "url": "https://huggingface.co/spaces/Jofthomas/Pokemon_showdown",
142
- "type": "Web-based Pokemon battle simulator",
143
- "access": "Free and open to all users"
144
- }
145
- }
146
 
147
  @mcp.tool()
148
  def battle_player(opponent_username: str, username: str = "MCPTrainer") -> dict:
@@ -166,6 +49,8 @@ def battle_player(opponent_username: str, username: str = "MCPTrainer") -> dict:
166
  "status": "challenge_queued",
167
  "message": f"Challenge to {opponent_username} queued. Battle will start when accepted.",
168
  "opponent": opponent_username,
 
 
169
  "instructions": "Use find_recent_battles() or get_player_status() in a few seconds to get the battle ID and viewing link once the battle starts.",
170
  "note": "Check server console for 'Challenge sent' confirmation."
171
  }
@@ -342,25 +227,6 @@ async def download_replay(battle_id: str = None) -> dict:
342
  "message": f"Failed to download replay: {str(e)}"
343
  }
344
 
345
- @mcp.tool()
346
- def get_supported_agents() -> dict:
347
- """
348
- Get information about supported agent types and their default models.
349
- Returns:
350
- dict: Information about available agents
351
- """
352
- return {
353
- "status": "success",
354
- "supported_agents": get_supported_agent_types(),
355
- "default_models": get_default_models(),
356
- "description": {
357
- "openai": "OpenAI GPT models (requires API key) - Deterministic decisions",
358
- "gemini": "Google Gemini models (requires API key) - Deterministic decisions",
359
- "mistral": "Mistral AI models (requires API key) - Deterministic decisions",
360
- "maxdamage": "Simple agent that chooses moves with highest base power - Deterministic decisions"
361
- }
362
- }
363
-
364
  @mcp.tool()
365
  def get_player_status(username: str = None) -> dict:
366
  """
@@ -381,34 +247,54 @@ def get_player_status(username: str = None) -> dict:
381
  "message": "No username provided and no active session."
382
  }
383
 
384
- if target_username not in player_instances:
 
 
 
 
 
385
  return {
386
  "status": "no_player",
387
  "username": target_username,
388
  "message": f"No player instance found for {target_username}"
389
  }
390
 
391
- player = player_instances[target_username]
392
- player_battles = []
393
-
394
- for battle_id, battle in player.battles.items():
395
- battle_info = {
396
- 'battle_id': battle_id,
397
- 'battle_url': f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}",
398
- 'turn': getattr(battle, 'turn', 0),
399
- 'finished': getattr(battle, 'finished', False),
400
- 'won': getattr(battle, 'won', None)
401
- }
402
- player_battles.append(battle_info)
 
 
 
 
 
 
 
 
403
 
404
  return {
405
  "status": "success",
406
- "username": target_username,
407
- "player_username": player.username,
408
- "total_battles": len(player.battles),
409
- "battles": player_battles,
410
- "n_won_battles": getattr(player, 'n_won_battles', 0),
411
- "n_finished_battles": getattr(player, 'n_finished_battles', 0)
 
 
 
 
 
 
 
412
  }
413
  except Exception as e:
414
  return {
@@ -456,75 +342,6 @@ def find_recent_battles(username: str = None) -> dict:
456
  "status": "error",
457
  "message": f"Failed to check recent battles: {str(e)}"
458
  }
459
-
460
- @mcp.tool()
461
- def debug_battle_objects(username: str = None, battle_id: str = None) -> dict:
462
- """
463
- Debug tool to inspect battle objects and their attributes.
464
- Useful for troubleshooting formatting errors.
465
- Args:
466
- username (str, optional): Username to debug (uses current session if not provided)
467
- battle_id (str, optional): Specific battle ID to debug
468
- Returns:
469
- dict: Debug information about battle objects
470
- """
471
- global current_session
472
- try:
473
- # Use provided username or current session username
474
- target_username = username or current_session.get('username')
475
- target_battle_id = battle_id or current_session.get('active_battle_id')
476
-
477
- if not target_username:
478
- return {
479
- "status": "error",
480
- "message": "No username provided and no active session."
481
- }
482
-
483
- if target_username not in player_instances:
484
- return {
485
- "status": "error",
486
- "message": f"No player instance found for {target_username}"
487
- }
488
-
489
- player = player_instances[target_username]
490
- debug_result = {
491
- "status": "success",
492
- "username": target_username,
493
- "player_username": player.username,
494
- "total_battles": len(player.battles),
495
- "battles_debug": []
496
- }
497
-
498
- battles_to_debug = []
499
- if target_battle_id and target_battle_id in player.battles:
500
- battles_to_debug = [(target_battle_id, player.battles[target_battle_id])]
501
- else:
502
- # Debug all battles (limit to 3 most recent)
503
- battles_to_debug = list(player.battles.items())[-3:]
504
-
505
- for bid, battle in battles_to_debug:
506
- try:
507
- battle_debug = {
508
- "battle_id": bid,
509
- "battle_type": type(battle).__name__,
510
- "battle_attributes": [attr for attr in dir(battle) if not attr.startswith('_')],
511
- "move_debug": debug_move_attributes(battle)
512
- }
513
- debug_result["battles_debug"].append(battle_debug)
514
- except Exception as e:
515
- debug_result["battles_debug"].append({
516
- "battle_id": bid,
517
- "error": f"Error debugging battle: {e}"
518
- })
519
-
520
- return debug_result
521
-
522
- except Exception as e:
523
- return {
524
- "status": "error",
525
- "message": f"Failed to debug battle objects: {str(e)}"
526
- }
527
-
528
  @mcp.tool()
529
  def cleanup_battles() -> dict:
530
  """
 
2
  import asyncio
3
  from dotenv import load_dotenv
4
  from mcp.server.fastmcp import FastMCP
5
+
6
  from utils.pokemon_utils import (
7
+ start_ladder_battle, start_battle_against_player,
8
  submit_move_for_battle, get_battle_state, list_active_battles,
9
  download_battle_replay, cleanup_completed_battles, format_battle_state,
10
  check_recent_battles, debug_move_attributes, active_battles, player_instances
11
  )
12
+ from utils.pokemon_utils import player_instances_by_unique
13
 
14
  load_dotenv()
15
 
 
26
  'active_battle_id': None
27
  }
28
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
29
 
30
  @mcp.tool()
31
  def battle_player(opponent_username: str, username: str = "MCPTrainer") -> dict:
 
49
  "status": "challenge_queued",
50
  "message": f"Challenge to {opponent_username} queued. Battle will start when accepted.",
51
  "opponent": opponent_username,
52
+ "player_base_username": challenge_result.get('player_base_username'),
53
+ "player_unique_username": challenge_result.get('player_unique_username'),
54
  "instructions": "Use find_recent_battles() or get_player_status() in a few seconds to get the battle ID and viewing link once the battle starts.",
55
  "note": "Check server console for 'Challenge sent' confirmation."
56
  }
 
227
  "message": f"Failed to download replay: {str(e)}"
228
  }
229
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  @mcp.tool()
231
  def get_player_status(username: str = None) -> dict:
232
  """
 
247
  "message": "No username provided and no active session."
248
  }
249
 
250
+ players_to_check = []
251
+ if target_username in player_instances:
252
+ players_to_check = list(player_instances[target_username].values())
253
+ elif target_username in player_instances_by_unique:
254
+ players_to_check = [player_instances_by_unique[target_username]]
255
+ else:
256
  return {
257
  "status": "no_player",
258
  "username": target_username,
259
  "message": f"No player instance found for {target_username}"
260
  }
261
 
262
+ all_battles = []
263
+ total_battles = 0
264
+ total_won = 0
265
+ total_finished = 0
266
+
267
+ for p in players_to_check:
268
+ total_battles += len(p.battles)
269
+ total_won += getattr(p, 'n_won_battles', 0)
270
+ total_finished += getattr(p, 'n_finished_battles', 0)
271
+ for battle_id, battle in p.battles.items():
272
+ battle_info = {
273
+ 'battle_id': battle_id,
274
+ 'battle_url': f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}",
275
+ 'turn': getattr(battle, 'turn', 0),
276
+ 'finished': getattr(battle, 'finished', False),
277
+ 'won': getattr(battle, 'won', None),
278
+ 'player_unique_username': p.username,
279
+ 'player_base_username': getattr(p, 'base_username', target_username)
280
+ }
281
+ all_battles.append(battle_info)
282
 
283
  return {
284
  "status": "success",
285
+ "query_username": target_username,
286
+ "resolved_scope": "base" if target_username in player_instances else "unique",
287
+ "players": [
288
+ {
289
+ 'player_unique_username': p.username,
290
+ 'player_base_username': getattr(p, 'base_username', target_username),
291
+ 'n_battles': len(p.battles)
292
+ } for p in players_to_check
293
+ ],
294
+ "total_battles": total_battles,
295
+ "battles": all_battles,
296
+ "n_won_battles": total_won,
297
+ "n_finished_battles": total_finished
298
  }
299
  except Exception as e:
300
  return {
 
342
  "status": "error",
343
  "message": f"Failed to check recent battles: {str(e)}"
344
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
345
  @mcp.tool()
346
  def cleanup_battles() -> dict:
347
  """
utils/pokemon_utils.py CHANGED
@@ -18,14 +18,15 @@ custom_config = ServerConfiguration(CUSTOM_SERVER_URL, CUSTOM_ACTION_URL)
18
  # Global battle state management
19
  active_battles = {}
20
  player_instances = {}
 
21
 
22
  class MCPPokemonAgent(LLMAgentBase):
23
  """
24
  Special Pokemon agent controlled by MCP that allows external move selection.
25
  """
26
- def __init__(self, username: str, *args, **kwargs):
27
  # Add random suffix to make username unique
28
- unique_username = f"{username}_{random.randint(1000, 9999)}"
29
  account_config = AccountConfiguration(unique_username, None)
30
  super().__init__(
31
  account_configuration=account_config,
@@ -36,6 +37,8 @@ class MCPPokemonAgent(LLMAgentBase):
36
  log_level=25, # Reduce logging verbosity
37
  *args, **kwargs
38
  )
 
 
39
  self.external_move_queue = asyncio.Queue()
40
  self.battle_state_callback = None
41
  self.current_battle_id = None
@@ -102,6 +105,13 @@ class MCPPokemonAgent(LLMAgentBase):
102
  else:
103
  raise ValueError("Must specify either move_name or pokemon_name")
104
 
 
 
 
 
 
 
 
105
  def normalize_name(name: str) -> str:
106
  """Lowercase and remove non-alphanumeric characters."""
107
  return "".join(filter(str.isalnum, name)).lower()
@@ -266,11 +276,9 @@ def start_ladder_battle(username: str) -> Dict[str, str]:
266
  Returns:
267
  dict: Status information about the ladder request
268
  """
269
- if username in player_instances:
270
- player = player_instances[username]
271
- else:
272
- player = MCPPokemonAgent(username)
273
- player_instances[username] = player
274
 
275
  # Start ladder battle (this will connect to showdown and find a match)
276
  try:
@@ -280,14 +288,12 @@ def start_ladder_battle(username: str) -> Dict[str, str]:
280
  # Return immediately
281
  return {
282
  'status': 'ladder_search_queued',
283
- 'player_username': username,
 
284
  'message': f'Ladder search queued for {username}. Use find_recent_battles() or get_player_status() to check when match is found.'
285
  }
286
 
287
  except Exception as e:
288
- # Clean up player instance on failure
289
- if username in player_instances:
290
- del player_instances[username]
291
  raise Exception(f"Failed to queue ladder search: {str(e)}")
292
 
293
  async def start_battle_against_agent(username: str, opponent_agent, battle_format: str = "gen9randombattle") -> str:
@@ -302,11 +308,9 @@ async def start_battle_against_agent(username: str, opponent_agent, battle_forma
302
  Returns:
303
  str: Battle ID
304
  """
305
- if username in player_instances:
306
- player = player_instances[username]
307
- else:
308
- player = MCPPokemonAgent(username)
309
- player_instances[username] = player
310
 
311
  try:
312
  # Start battle against opponent
@@ -329,7 +333,9 @@ async def start_battle_against_agent(username: str, opponent_agent, battle_forma
329
  # Store battle info
330
  active_battles[battle_id] = {
331
  'type': 'agent',
332
- 'player_username': username,
 
 
333
  'opponent': opponent_agent.username,
334
  'battle_state': format_battle_state(battle),
335
  'waiting_for_move': False,
@@ -364,11 +370,9 @@ def start_battle_against_player(username: str, opponent_username: str) -> Dict[s
364
  Returns:
365
  dict: Status information about the challenge request
366
  """
367
- if username in player_instances:
368
- player = player_instances[username]
369
- else:
370
- player = MCPPokemonAgent(username)
371
- player_instances[username] = player
372
 
373
  try:
374
  # Start challenge in background task (fire-and-forget)
@@ -377,15 +381,13 @@ def start_battle_against_player(username: str, opponent_username: str) -> Dict[s
377
  # Return immediately
378
  return {
379
  'status': 'challenge_queued',
380
- 'player_username': username,
 
381
  'opponent': opponent_username,
382
  'message': f'Challenge to {opponent_username} queued. Use find_recent_battles() or get_player_status() to check if battle started.'
383
  }
384
 
385
  except Exception as e:
386
- # Clean up player instance on failure
387
- if username in player_instances:
388
- del player_instances[username]
389
  raise Exception(f"Failed to queue challenge to {opponent_username}: {str(e)}")
390
 
391
  async def submit_move_for_battle(battle_id: str, move_name: str = None, pokemon_name: str = None) -> str:
@@ -404,12 +406,12 @@ async def submit_move_for_battle(battle_id: str, move_name: str = None, pokemon_
404
  raise ValueError(f"Battle {battle_id} not found")
405
 
406
  battle_info = active_battles[battle_id]
407
- username = battle_info['player_username']
408
 
409
- if username not in player_instances:
410
- raise ValueError(f"Player {username} not found")
411
 
412
- player = player_instances[username]
413
 
414
  # Find the battle object
415
  battle = None
@@ -458,6 +460,8 @@ def list_active_battles() -> List[Dict[str, Any]]:
458
  'opponent': info['opponent'],
459
  'waiting_for_move': info['waiting_for_move'],
460
  'completed': info['completed'],
 
 
461
  'battle_url': info.get('battle_url', f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}")
462
  }
463
  for battle_id, info in active_battles.items()
@@ -468,42 +472,50 @@ def check_recent_battles(username: str) -> List[Dict[str, Any]]:
468
  Check for recent battles that may have started after a timeout.
469
 
470
  Args:
471
- username (str): Username to check battles for
472
 
473
  Returns:
474
  list: List of recent battle info
475
  """
476
- if username not in player_instances:
 
 
 
 
 
 
 
 
477
  return []
478
 
479
- player = player_instances[username]
480
- recent_battles = []
481
-
482
- for battle_id, battle in player.battles.items():
483
- if battle_id not in active_battles:
484
- # This is a new battle that wasn't tracked yet
485
- battle_url = f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}"
486
- battle_info = {
487
- 'battle_id': battle_id,
488
- 'battle_url': battle_url,
489
- 'opponent': getattr(battle, 'opponent_username', 'unknown'),
490
- 'format': battle.format if hasattr(battle, 'format') else 'unknown',
491
- 'turn': battle.turn if hasattr(battle, 'turn') else 0,
492
- 'battle_state': format_battle_state(battle)
493
- }
494
-
495
- # Add to active battles tracking
496
- active_battles[battle_id] = {
497
- 'type': 'recovered',
498
- 'player_username': username,
499
- 'opponent': battle_info['opponent'],
500
- 'battle_state': battle_info['battle_state'],
501
- 'waiting_for_move': False,
502
- 'completed': False,
503
- 'battle_url': battle_url
504
- }
505
-
506
- recent_battles.append(battle_info)
507
 
508
  return recent_battles
509
 
@@ -521,12 +533,12 @@ async def download_battle_replay(battle_id: str) -> str:
521
  raise ValueError(f"Battle {battle_id} not found")
522
 
523
  battle_info = active_battles[battle_id]
524
- username = battle_info['player_username']
525
 
526
- if username not in player_instances:
527
- raise ValueError(f"Player {username} not found")
528
 
529
- player = player_instances[username]
530
 
531
  # Find the battle object
532
  battle = None
 
18
  # Global battle state management
19
  active_battles = {}
20
  player_instances = {}
21
+ player_instances_by_unique = {}
22
 
23
  class MCPPokemonAgent(LLMAgentBase):
24
  """
25
  Special Pokemon agent controlled by MCP that allows external move selection.
26
  """
27
+ def __init__(self, base_username: str, *args, **kwargs):
28
  # Add random suffix to make username unique
29
+ unique_username = f"{base_username}_{random.randint(1000, 9999)}"
30
  account_config = AccountConfiguration(unique_username, None)
31
  super().__init__(
32
  account_configuration=account_config,
 
37
  log_level=25, # Reduce logging verbosity
38
  *args, **kwargs
39
  )
40
+ self.base_username = base_username
41
+ self.unique_username = unique_username
42
  self.external_move_queue = asyncio.Queue()
43
  self.battle_state_callback = None
44
  self.current_battle_id = None
 
105
  else:
106
  raise ValueError("Must specify either move_name or pokemon_name")
107
 
108
+ # Helper to register instances under base and unique usernames
109
+ def register_player_instance(base_username: str, player: MCPPokemonAgent) -> None:
110
+ if base_username not in player_instances:
111
+ player_instances[base_username] = {}
112
+ player_instances[base_username][player.username] = player
113
+ player_instances_by_unique[player.username] = player
114
+
115
  def normalize_name(name: str) -> str:
116
  """Lowercase and remove non-alphanumeric characters."""
117
  return "".join(filter(str.isalnum, name)).lower()
 
276
  Returns:
277
  dict: Status information about the ladder request
278
  """
279
+ # Always create a new unique player instance for this request
280
+ player = MCPPokemonAgent(username)
281
+ register_player_instance(username, player)
 
 
282
 
283
  # Start ladder battle (this will connect to showdown and find a match)
284
  try:
 
288
  # Return immediately
289
  return {
290
  'status': 'ladder_search_queued',
291
+ 'player_base_username': username,
292
+ 'player_unique_username': player.username,
293
  'message': f'Ladder search queued for {username}. Use find_recent_battles() or get_player_status() to check when match is found.'
294
  }
295
 
296
  except Exception as e:
 
 
 
297
  raise Exception(f"Failed to queue ladder search: {str(e)}")
298
 
299
  async def start_battle_against_agent(username: str, opponent_agent, battle_format: str = "gen9randombattle") -> str:
 
308
  Returns:
309
  str: Battle ID
310
  """
311
+ # Always create a new unique player instance for this request
312
+ player = MCPPokemonAgent(username)
313
+ register_player_instance(username, player)
 
 
314
 
315
  try:
316
  # Start battle against opponent
 
333
  # Store battle info
334
  active_battles[battle_id] = {
335
  'type': 'agent',
336
+ 'player_base_username': player.base_username,
337
+ 'player_unique_username': player.username,
338
+ 'player_username': player.base_username,
339
  'opponent': opponent_agent.username,
340
  'battle_state': format_battle_state(battle),
341
  'waiting_for_move': False,
 
370
  Returns:
371
  dict: Status information about the challenge request
372
  """
373
+ # Always create a new unique player instance for this request
374
+ player = MCPPokemonAgent(username)
375
+ register_player_instance(username, player)
 
 
376
 
377
  try:
378
  # Start challenge in background task (fire-and-forget)
 
381
  # Return immediately
382
  return {
383
  'status': 'challenge_queued',
384
+ 'player_base_username': username,
385
+ 'player_unique_username': player.username,
386
  'opponent': opponent_username,
387
  'message': f'Challenge to {opponent_username} queued. Use find_recent_battles() or get_player_status() to check if battle started.'
388
  }
389
 
390
  except Exception as e:
 
 
 
391
  raise Exception(f"Failed to queue challenge to {opponent_username}: {str(e)}")
392
 
393
  async def submit_move_for_battle(battle_id: str, move_name: str = None, pokemon_name: str = None) -> str:
 
406
  raise ValueError(f"Battle {battle_id} not found")
407
 
408
  battle_info = active_battles[battle_id]
409
+ unique_username = battle_info.get('player_unique_username') or battle_info.get('player_username')
410
 
411
+ if unique_username not in player_instances_by_unique:
412
+ raise ValueError(f"Player {unique_username} not found")
413
 
414
+ player = player_instances_by_unique[unique_username]
415
 
416
  # Find the battle object
417
  battle = None
 
460
  'opponent': info['opponent'],
461
  'waiting_for_move': info['waiting_for_move'],
462
  'completed': info['completed'],
463
+ 'player_base_username': info.get('player_base_username', info.get('player_username')),
464
+ 'player_unique_username': info.get('player_unique_username'),
465
  'battle_url': info.get('battle_url', f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}")
466
  }
467
  for battle_id, info in active_battles.items()
 
472
  Check for recent battles that may have started after a timeout.
473
 
474
  Args:
475
+ username (str): Username to check battles for (base or unique)
476
 
477
  Returns:
478
  list: List of recent battle info
479
  """
480
+ recent_battles: List[Dict[str, Any]] = []
481
+
482
+ # Determine which player instances to inspect
483
+ players_to_check: List[MCPPokemonAgent] = []
484
+ if username in player_instances:
485
+ players_to_check = list(player_instances[username].values())
486
+ elif username in player_instances_by_unique:
487
+ players_to_check = [player_instances_by_unique[username]]
488
+ else:
489
  return []
490
 
491
+ for player in players_to_check:
492
+ for battle_id, battle in player.battles.items():
493
+ if battle_id not in active_battles:
494
+ # This is a new battle that wasn't tracked yet
495
+ battle_url = f"https://jofthomas.com/play.pokemonshowdown.com/testclient.html#battle-{battle_id}"
496
+ battle_info = {
497
+ 'battle_id': battle_id,
498
+ 'battle_url': battle_url,
499
+ 'opponent': getattr(battle, 'opponent_username', 'unknown'),
500
+ 'format': battle.format if hasattr(battle, 'format') else 'unknown',
501
+ 'turn': battle.turn if hasattr(battle, 'turn') else 0,
502
+ 'battle_state': format_battle_state(battle)
503
+ }
504
+
505
+ # Add to active battles tracking
506
+ active_battles[battle_id] = {
507
+ 'type': 'recovered',
508
+ 'player_base_username': player.base_username,
509
+ 'player_unique_username': player.username,
510
+ 'player_username': player.base_username,
511
+ 'opponent': battle_info['opponent'],
512
+ 'battle_state': battle_info['battle_state'],
513
+ 'waiting_for_move': False,
514
+ 'completed': False,
515
+ 'battle_url': battle_url
516
+ }
517
+
518
+ recent_battles.append(battle_info)
519
 
520
  return recent_battles
521
 
 
533
  raise ValueError(f"Battle {battle_id} not found")
534
 
535
  battle_info = active_battles[battle_id]
536
+ unique_username = battle_info.get('player_unique_username') or battle_info.get('player_username')
537
 
538
+ if unique_username not in player_instances_by_unique:
539
+ raise ValueError(f"Player {unique_username} not found")
540
 
541
+ player = player_instances_by_unique[unique_username]
542
 
543
  # Find the battle object
544
  battle = None