Skip to content
Open
  •  
  •  
  •  
157 changes: 120 additions & 37 deletions scripts/showdown.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@

# NOTE: Doesn't account for females, refer this and manually check them in later https://bulbapedia.bulbagarden.net/wiki/List_of_Pok%C3%A9mon_with_gender_differences

DRY_RUN = True
DRY_RUN = False
SHOWDOWN_DIR = pathlib.Path(__file__).parent.parent / "sprites" / "pokemon" / "other" / "showdown"
SHOWDOWN_BASE_URL = "https://play.pokemonshowdown.com/sprites/ani"

Expand All @@ -19,19 +19,42 @@ class PokemonRecord(t.TypedDict):


def list_pokemon() -> dict[str, str]:
"""Retrieve a list of all Pokémon from the PokéAPI."""
api_url = "https://pokeapi.co/api/v2/pokemon?limit=10000"
"""Retrieve a list of all Pokémon from the PokéAPI.
The result is a mapping of Pokémon IDs to their names.

<hr>

**MAIN PROBLEM**: naming scheme for alt forms is different between Showdown and PokéAPI

- *Current solution*: try to map names using fuzzy matching where exact match is not found
️- *Possible future solution*: maintain a manual mapping file for known mismatches
"""
api_url = "https://pokeapi.co/api/v2/pokemon?limit=10000" # MAIN PROBLEM : naming scheme for alt forms is different between Showdown and PokéAPI
response = requests.get(api_url)

if response.status_code != 200:
raise Exception(f"Failed to retrieve Pokémon list (Status {response.status_code})")

data = response.json()
return {i["url"].split("/")[-2]: i["name"] for i in data["results"]}
results = {i["url"].split("/")[-2]: i["name"] for i in data["results"]}
above_10000 = {k: v for k, v in results.items() if int(k) > 10000}
for k, v in above_10000.items():
print(f"Found sprite ID > 10000 in PokéAPI listing: {k} -> {v}")
print(" -> Possibly an alt form placeholder - trying to search for base form instead")
base_form_name = v.split("-", 1)[0]
base_form_id = next((id_ for id_, name in results.items() if name.split("-", 1)[0] == base_form_name), None)
if base_form_id is not None:
print(f" -> Found base form '{base_form_name}' with ID {base_form_id}, remapping")
# build new name as "{base_id}-{suffix}", where suffix is whatever came after
# the first '-' in the original alt-form name
suffix = v.split("-", 1)[1] if "-" in v else ""
results[k] = f"{base_form_id}-{suffix}" if suffix else results[base_form_id]
print(f" -> New mapping: {k} -> {results[k]}")
return results


def list_showdown_images(folder: pathlib.Path) -> set[str]:
"""List all Pokémon images available in the Showdown directory."""
"""List all Pokémon images available in the local directory."""
image_files = {f.stem for f in folder.glob("*.gif") if f.is_file()}
return image_files

Expand Down Expand Up @@ -71,8 +94,56 @@ def download_image(id: str, name: str, folder: pathlib.Path, pokemon_url: str) -
print(f"Downloaded image for {name} to {folder / f'{id}.gif'}")


def resolve_alt_form_name(name: str) -> tuple[str, str]:
"""Return the base form name and the alt form suffix from a Showdown image name."""
if "-" in name:
return tuple(name.split("-", 1))
return name, ""


def normalize_showdown_name(showdown_name: str, known_names: set[str]) -> str:
"""Map a Showdown sprite name to the most likely PokéAPI species name."""
if showdown_name in known_names:
return showdown_name
base, _ = resolve_alt_form_name(showdown_name)
if base in known_names:
return base
match = difflib.get_close_matches(showdown_name, list(known_names), n=1, cutoff=0.8)
if match:
return match[0]
match = difflib.get_close_matches(base, list(known_names), n=1, cutoff=0.8)
if match:
return match[0]
return showdown_name


def build_showdown_to_species_map(showdown_index: set[str], name_to_id: dict[str, str]) -> dict[str, str]:
names_set = set(name_to_id.keys())
return {sname: normalize_showdown_name(sname, names_set) for sname in showdown_index}


def resolve_save_id(pid: str, sprite_name: str, name_to_id: dict[str, str]) -> str:
"""Return the id string to use when saving the sprite file.
If pid refers to an alternate-form placeholder (>10000), map to the base form id when available."""
try:
pid_int = int(pid)
except ValueError:
return pid
if pid_int > 10000:
base_name, _ = resolve_alt_form_name(sprite_name)
print(f"Mapping alt form ID {pid} to base form name '{base_name}'")
base_name_id = name_to_id.get(base_name.split("-", 1)[0])
print(f" -> base form ID is '{base_name_id}'")
if base_name_id is None:
print(f"Error: Could not find base form ID for alt form '{base_name}' (sprite name '{sprite_name}').")
exit(1)
return name_to_id.get(base_name, pid)
return pid


if __name__ == "__main__":
pokemon_list = list_pokemon()
name_to_id = {v: k for k, v in pokemon_list.items()}

showdown_folders = (
SHOWDOWN_DIR,
Expand All @@ -89,46 +160,58 @@ def download_image(id: str, name: str, folder: pathlib.Path, pokemon_url: str) -
shiny = "shiny" in folder.parts

showdown_index = showdown_sprite_index(back=back, shiny=shiny)
remote_to_species = build_showdown_to_species_map(showdown_index, name_to_id)

print(f"\n{'=' * 40}\nMissing images in folder: {folder}\n{'=' * 40}\n")

remaining: set[str] = set()

for pid, name in pokemon_list.items():
if pid in missing_images and not DRY_RUN:
if name in showdown_index:
download_image(
pid,
name,
folder,
f"{_construct_showdown_url(back=back, shiny=shiny)}/{name}.gif",
)
else:
print(f"Exact name not found in Showdown index: {name}")
closest_matches = difflib.get_close_matches(name, showdown_index, n=3, cutoff=0.7)
if closest_matches:
print("\n".join([str(n) + ") " + m for n, m in enumerate(closest_matches, start=1)]))
print(
"Enter to skip downloading this image, or enter the number of the closest match to download that image."

candidates = [remote for remote, species in remote_to_species.items() if species == name]

if candidates:
# Download all matching candidates (no interactive prompt)
chosen_list = candidates
for chosen in chosen_list:
save_id = resolve_save_id(pid, chosen, name_to_id)
if int(save_id) > 10000:
print(f"WARNING: Saving alt form with alt form ID {save_id}, consider mapping to base form ID instead.")
suffix = f"-{chosen.split('-', 1)[1]}" if '-' in chosen else ''
print(f"SAVED FILE NAME: {save_id}{suffix}.gif")
if not DRY_RUN:
download_image(
f"{save_id}{suffix}",
chosen,
folder,
f"{_construct_showdown_url(back=back, shiny=shiny)}/{chosen}.gif",
)
user_input = input("Your choice: ").strip()
try:
choice = int(user_input)
if 1 <= choice <= len(closest_matches):
selected_name = closest_matches[choice - 1]
download_image(
pid,
selected_name,
folder,
f"{_construct_showdown_url(back=back, shiny=shiny)}/{selected_name}.gif",
)
else:
print("Invalid choice. Skipping download.")
remaining.add(pid)
except ValueError:
print("Skipping download.")
remaining.add(pid)
else:
print(f"No Showdown sprite found for {name} (ID={pid})")
base_form = name.split("-", 1)
base_form_name = base_form[0]
base_candidates = [remote for remote, species in remote_to_species.items() if species == base_form_name]
if base_candidates:
print(f" -> but found base form sprite(s): {', '.join(base_candidates)}\n")
# Accept and download all base candidates automatically (no prompts)
for chosen in base_candidates:
print(f" Accepted alternate form mapping '{name}' -> '{chosen}'")
save_id = resolve_save_id(pid, chosen, name_to_id)
if int(save_id) > 10000:
print(f"WARNING: Saving alt form with alt form ID {save_id}, consider mapping to base form ID instead.")
suffix = f"-{name.split('-', 1)[1]}" if '-' in name else ''
print(f"SAVED FILE NAME: {save_id}{suffix}.gif")
if not DRY_RUN:
download_image(
f"{save_id}{suffix}",
chosen,
folder,
f"{_construct_showdown_url(back=back, shiny=shiny)}/{chosen}.gif",
)
else:
remaining.add(pid)

print(f"\nSummary for folder: {folder}\n{'-' * 40}\n")
table = tabulate.tabulate(
[(pid, pname) for pid, pname in pokemon_list.items() if pid in (missing_images if DRY_RUN else remaining)],
headers=["Pokémon ID", "Pokémon Name"],
Expand Down
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
File renamed without changes
Binary file added sprites/pokemon/other/showdown/100-hisui.gif
Binary file added sprites/pokemon/other/showdown/101-hisui.gif
Binary file added sprites/pokemon/other/showdown/1012-artisan.gif
Binary file added sprites/pokemon/other/showdown/103-alola.gif
Binary file added sprites/pokemon/other/showdown/105-alola.gif
Binary file added sprites/pokemon/other/showdown/105-totem.gif
Binary file added sprites/pokemon/other/showdown/110-galar.gif
Binary file added sprites/pokemon/other/showdown/112-f.gif
Binary file added sprites/pokemon/other/showdown/115-mega.gif
Binary file added sprites/pokemon/other/showdown/12-gmax.gif
Binary file added sprites/pokemon/other/showdown/121-mega.gif
Binary file added sprites/pokemon/other/showdown/122-galar.gif
Binary file added sprites/pokemon/other/showdown/123-f.gif
Binary file added sprites/pokemon/other/showdown/127-mega.gif
Binary file added sprites/pokemon/other/showdown/128-paldeaaqua.gif
Binary file added sprites/pokemon/other/showdown/129-f.gif
Binary file added sprites/pokemon/other/showdown/130-mega.gif
Binary file added sprites/pokemon/other/showdown/131-gmax.gif
Binary file added sprites/pokemon/other/showdown/133-gmax.gif
Binary file added sprites/pokemon/other/showdown/133-starter.gif
Binary file added sprites/pokemon/other/showdown/142-mega.gif
Binary file added sprites/pokemon/other/showdown/143-gmax.gif
Binary file added sprites/pokemon/other/showdown/144-galar.gif
Binary file added sprites/pokemon/other/showdown/145-galar.gif
Binary file added sprites/pokemon/other/showdown/146-galar.gif
Binary file added sprites/pokemon/other/showdown/149-mega.gif
Binary file added sprites/pokemon/other/showdown/15-mega.gif
Binary file added sprites/pokemon/other/showdown/150-megax.gif
Binary file added sprites/pokemon/other/showdown/150-megay.gif
Binary file added sprites/pokemon/other/showdown/154-f.gif
Binary file added sprites/pokemon/other/showdown/154-mega.gif
Binary file added sprites/pokemon/other/showdown/157-hisui.gif
Binary file added sprites/pokemon/other/showdown/160-mega.gif
Binary file added sprites/pokemon/other/showdown/165-f.gif
Binary file added sprites/pokemon/other/showdown/166-f.gif
Binary file added sprites/pokemon/other/showdown/178-f.gif
Binary file added sprites/pokemon/other/showdown/18-mega.gif
Binary file added sprites/pokemon/other/showdown/181-mega.gif
Binary file added sprites/pokemon/other/showdown/185-f.gif
Binary file added sprites/pokemon/other/showdown/186-f.gif
Binary file added sprites/pokemon/other/showdown/19-alola.gif
Binary file added sprites/pokemon/other/showdown/19-f.gif
Binary file added sprites/pokemon/other/showdown/190-f.gif
Binary file added sprites/pokemon/other/showdown/194-f.gif
Binary file modified sprites/pokemon/other/showdown/194-paldea.gif
Binary file added sprites/pokemon/other/showdown/195-f.gif
Binary file added sprites/pokemon/other/showdown/198-f.gif
Binary file added sprites/pokemon/other/showdown/199-galar.gif
Binary file added sprites/pokemon/other/showdown/20-alola.gif
Binary file added sprites/pokemon/other/showdown/20-f.gif
Binary file added sprites/pokemon/other/showdown/20-totem-a.gif
Binary file modified sprites/pokemon/other/showdown/201-b.gif
Binary file modified sprites/pokemon/other/showdown/201-c.gif
Binary file modified sprites/pokemon/other/showdown/201-d.gif
Binary file modified sprites/pokemon/other/showdown/201-e.gif
Binary file modified sprites/pokemon/other/showdown/201-exclamation.gif
Binary file modified sprites/pokemon/other/showdown/201-f.gif
Binary file modified sprites/pokemon/other/showdown/201-g.gif
Binary file modified sprites/pokemon/other/showdown/201-h.gif
Binary file modified sprites/pokemon/other/showdown/201-i.gif
Binary file modified sprites/pokemon/other/showdown/201-j.gif
Binary file modified sprites/pokemon/other/showdown/201-k.gif
Binary file modified sprites/pokemon/other/showdown/201-l.gif
Loading