Pokemon-Battle-Sim / src /utils /pokemon_api.py
github-actions
Deploy to Hugging Face Spaces
6c7a453
import requests
from typing import Dict, Any, List, Optional
from functools import lru_cache
import os
import json
import re
# Resolve data path robustly
BASE_DIR = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
DATA_DIR = os.path.join(BASE_DIR, 'data')
# Load name-to-ID mapping
POKEMON_ID_MAP = {}
try:
id_map_path = os.path.join(DATA_DIR, 'pokemon_ids.json')
if os.path.exists(id_map_path):
with open(id_map_path, 'r') as f:
POKEMON_ID_MAP = json.load(f)
except Exception:
pass
# We import POKEAPI_NAME_MAP inside functions to avoid circular imports if needed,
# but here it's fine as long as we don't import pokemon_api from pokemon_utils.
from .pokemon_utils import POKEAPI_NAME_MAP
def get_best_sprite(data, side='front', shiny=False):
# data['name'] is usually the PokeAPI name (e.g., "wishiwashi-solo" or "bulbasaur")
name = data.get('name', '').lower()
# Custom Overrides for Mega Zygarde (which lacks GBA sprites)
if 'zygarde-mega' in name:
if side == 'back':
return "https://pbs.twimg.com/media/G7U70olbIAAtYXa?format=png&name=360x360"
return "https://pbs.twimg.com/media/G3lRUbgXMAA3jdG?format=png&name=small"
# 1. Base species that Showdown expects WITHOUT hyphens
# Format megas for showdown (e.g. charizard-mega-y -> charizard-megay)
if '-mega-' in name:
name = name.replace('-mega-x', '-megax').replace('-mega-y', '-megay')
FLATTEN_BASE = [
'ho-oh', 'porygon-z', 'jangmo-o', 'hakamo-o', 'kommo-o',
'sirfetch’d', 'farfetch’d', 'sirfetchd', 'farfetchd',
'mr-mime', 'mr-rime', 'mime-jr', 'type-null',
'tapu-koko', 'tapu-lele', 'tapu-bulu', 'tapu-fini',
'nidoran-m', 'nidoran-f'
]
# 2. Specific form overrides for Showdown
SHOWDOWN_OVERRIDES = {
'aegislash-shield': 'aegislash',
'aegislash-blade': 'aegislash-blade',
'basculin-red-striped': 'basculin',
'basculin-blue-striped': 'basculin-blue',
'basculin-white-striped': 'basculin-white',
'darmanitan-standard': 'darmanitan',
'darmanitan-galar-standard': 'darmanitan-galar',
'deoxys-normal': 'deoxys',
'giratina-altered': 'giratina',
'gourgeist-average': 'gourgeist',
'keldeo-ordinary': 'keldeo',
'landorus-incarnate': 'landorus',
'thundurus-incarnate': 'thundurus',
'tornadus-incarnate': 'tornadus',
'meloetta-aria': 'meloetta',
'mimikyu-disguised': 'mimikyu',
'morpeko-full-belly': 'morpeko',
'morpeko-hangry': 'morpeko-hangry',
'oricorio-baile': 'oricorio',
'pumpkaboo-average': 'pumpkaboo',
'shaymin-land': 'shaymin',
'toxtricity-amped': 'toxtricity',
'urshifu-single-strike': 'urshifu',
'wormadam-plant': 'wormadam',
'zygarde-50': 'zygarde',
'minior-red-meteor': 'minior',
'minior-red': 'minior-red',
'wishiwashi-solo': 'wishiwashi',
'wishiwashi-school': 'wishiwashi-school',
'toxtricity-low-key': 'toxtricity-lowkey',
'gastrodon-west': 'gastrodon',
'lycanroc-midday': 'lycanroc'
}
pokemon_name = name
# Check for direct overrides first
if name in SHOWDOWN_OVERRIDES:
pokemon_name = SHOWDOWN_OVERRIDES[name]
elif name in FLATTEN_BASE:
pokemon_name = name.replace('-', '')
else:
# Default behavior: remove ' (for things like Farfetch'd) but keep - for forms
pokemon_name = name.replace("'", "").replace("’", "")
# If it's a simple name without forms, remove spaces
if ' ' in pokemon_name:
pokemon_name = pokemon_name.replace(' ', '')
# Special case: Showdown uses 'mrmime', 'mrrime', etc.
if pokemon_name.startswith('mr-'):
pokemon_name = pokemon_name.replace('-', '')
if pokemon_name.endswith('-jr'):
pokemon_name = pokemon_name.replace('-', '')
back_suffix = "-back" if side == 'back' else ""
shiny_suffix = "-shiny" if shiny else ""
# Try static PNG from Gen 5 (best compatibility)
url_pixel_static = f"https://play.pokemonshowdown.com/sprites/gen5{back_suffix}{shiny_suffix}/{pokemon_name}.png"
return url_pixel_static
@lru_cache(maxsize=1000)
def get_pokemon_data(pokemon_name):
"""Fetch Pokemon data from PokeAPI with caching and name normalization."""
# 1. Try normalizing to map (e.g. "Giratina Origin" -> "giratinaorigin" -> "giratina-origin")
normalized_name = re.sub(r'[^a-z0-9]', '', pokemon_name.lower())
api_name = POKEAPI_NAME_MAP.get(normalized_name, None)
# 2. If not in map, but input has hyphens, it might already be a PokeAPI name (e.g. "stunfisk-galar")
if not api_name:
# Strip special chars like % and then check if it looks like a PokeAPI name
cleaned_name = pokemon_name.lower().replace('%', '').strip()
api_name = cleaned_name if '-' in cleaned_name else normalized_name
# 3. Check for ID in our map (ID is more reliable)
pokemon_id = POKEMON_ID_MAP.get(api_name) or POKEMON_ID_MAP.get(normalized_name)
identifier = str(pokemon_id) if pokemon_id else api_name
url = f'https://pokeapi.co/api/v2/pokemon/{identifier}'
try:
response = requests.get(url, timeout=5)
if response.status_code == 200:
return response.json()
except Exception:
pass
# Try common form patterns if direct hit fails
patterns = [
f"{api_name}-galar", f"{api_name}-alola", f"{api_name}-hisui",
f"{api_name}-origin", f"{api_name}-altered", f"{api_name}-single-strike",
f"{api_name}-amped", f"{api_name}-low-key"
]
for p_name in patterns:
try:
res = requests.get(f'https://pokeapi.co/api/v2/pokemon/{p_name}', timeout=5)
if res.status_code == 200:
return res.json()
except:
continue
# Final fallback: try stripping everything after the first hyphen (e.g. silvally-fairy -> silvally)
if '-' in api_name:
base_name = api_name.split('-')[0]
try:
res = requests.get(f'https://pokeapi.co/api/v2/pokemon/{base_name}', timeout=5)
if res.status_code == 200:
return res.json()
except:
pass
return None
def get_forme_data(species_name: str, side='front', shiny=False):
"""Helper for mid-battle form changes to get all necessary transformation data."""
data = get_pokemon_data(species_name)
if not data:
return None
primary_ability = next((a['ability']['name'].replace('-', '').lower() for a in data.get('abilities', []) if not a.get('is_hidden')), '')
if not primary_ability and data.get('abilities'):
primary_ability = data['abilities'][0]['ability']['name'].replace('-', '').lower()
return {
'name': data['name'],
'types': [t['type']['name'] for t in data['types']],
'sprite_url': get_best_sprite(data, side=side, shiny=shiny),
'cry_url': data.get('cries', {}).get('latest', ''),
'stats': {s['stat']['name'].replace('-', '_'): s['base_stat'] for s in data['stats']},
'ability': primary_ability
}
def to_display_name(name: str) -> str:
"""Format a Pokémon name for display (e.g. 'tapu-koko' -> 'Tapu-Koko')."""
# Special cases
if name.lower() == 'urshifu-single-strike': return 'Urshifu-Single-Strike'
if name.lower() == 'urshifu': return 'Urshifu-Single-Strike'
if name.lower() == 'giratina-altered': return 'Giratina-Altered'
if name.lower() == 'giratina': return 'Giratina-Altered'
# General rule: capitalize parts
parts = re.split(r'[- ]', name)
return '-'.join(p.capitalize() for p in parts)