Last active
September 5, 2019 22:41
-
-
Save 0xpizza/4e4cac9916e805cf832f398d9fb053d7 to your computer and use it in GitHub Desktop.
National Pokedex according to Bulbapedia
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import urllib | |
from dataclasses import dataclass | |
from tkinter import * | |
from tkinter.ttk import * | |
import requests | |
from bs4 import BeautifulSoup | |
from PIL import Image, ImageTk | |
# https://gist.github.com/azai91/31e3b31cbd3992a1cc679017f850a022 | |
ALL_POKEMON = ['Bulbasaur', 'Ivysaur', 'Venusaur', 'Charmander', 'Charmeleon', | |
'Charizard', 'Squirtle', 'Wartortle', 'Blastoise', 'Caterpie', 'Metapod', | |
'Butterfree', 'Weedle', 'Kakuna', 'Beedrill', 'Pidgey', 'Pidgeotto', 'Pidgeot', | |
'Rattata', 'Raticate', 'Spearow', 'Fearow', 'Ekans', 'Arbok', 'Pikachu', | |
'Raichu', 'Sandshrew', 'Sandslash', 'Nidoran♀', 'Nidorina', 'Nidoqueen', | |
'Nidoran♂', 'Nidorino', 'Nidoking', 'Clefairy', 'Clefable', 'Vulpix', | |
'Ninetales', 'Jigglypuff', 'Wigglytuff', 'Zubat', 'Golbat', 'Oddish', 'Gloom', | |
'Vileplume', 'Paras', 'Parasect', 'Venonat', 'Venomoth', 'Diglett', 'Dugtrio', | |
'Meowth', 'Persian', 'Psyduck', 'Golduck','Mankey', 'Primeape', 'Growlithe', | |
'Arcanine', 'Poliwag', 'Poliwhirl', 'Poliwrath', 'Abra', 'Kadabra', 'Alakazam', | |
'Machop', 'Machoke', 'Machamp', 'Bellsprout', 'Weepinbell', 'Victreebel', | |
'Tentacool', 'Tentacruel', 'Geodude', 'Graveler', 'Golem', 'Ponyta', 'Rapidash', | |
'Slowpoke', 'Slowbro', 'Magnemite', 'Magneton', 'Farfetch’d', 'Doduo', | |
'Dodrio', 'Seel', 'Dewgong', 'Grimer', 'Muk', 'Shellder', 'Cloyster', 'Gastly', | |
'Haunter', 'Gengar', 'Onix', 'Drowzee', 'Hypno', 'Krabby','Kingler', 'Voltorb', | |
'Electrode', 'Exeggcute', 'Exeggutor', 'Cubone', 'Marowak', 'Hitmonlee', | |
'Hitmonchan', 'Lickitung', 'Koffing', 'Weezing', 'Rhyhorn', 'Rhydon', | |
'Chansey', 'Tangela', 'Kangaskhan', 'Horsea', 'Seadra', 'Goldeen', 'Seaking', | |
'Staryu', 'Starmie', 'Mr. Mime', 'Scyther', 'Jynx', 'Electabuzz', 'Magmar', | |
'Pinsir', 'Tauros', 'Magikarp', 'Gyarados', 'Lapras', 'Ditto', 'Eevee', | |
'Vaporeon', 'Jolteon', 'Flareon', 'Porygon', 'Omanyte', 'Omastar', 'Kabuto', | |
'Kabutops','Aerodactyl', 'Snorlax', 'Articuno', 'Zapdos', 'Moltres', 'Dratini', | |
'Dragonair', 'Dragonite', 'Mewtwo', 'Mew', 'Chikorita', 'Bayleef', 'Meganium', | |
'Cyndaquil', 'Quilava', 'Typhlosion', 'Totodile', 'Croconaw', 'Feraligatr', | |
'Sentret', 'Furret', 'Hoothoot', 'Noctowl', 'Ledyba', 'Ledian', 'Spinarak', | |
'Ariados', 'Crobat', 'Chinchou', 'Lanturn', 'Pichu', 'Cleffa', 'Igglybuff', | |
'Togepi', 'Togetic', 'Natu', 'Xatu', 'Mareep', 'Flaaffy', 'Ampharos', | |
'Bellossom', 'Marill', 'Azumarill', 'Sudowoodo', 'Politoed', 'Hoppip', | |
'Skiploom', 'Jumpluff', 'Aipom', 'Sunkern', 'Sunflora', 'Yanma', 'Wooper', | |
'Quagsire', 'Espeon', 'Umbreon', 'Murkrow', 'Slowking', 'Misdreavus', 'Unown', | |
'Wobbuffet', 'Girafarig', 'Pineco', 'Forretress', 'Dunsparce', 'Gligar', | |
'Steelix', 'Snubbull', 'Granbull', 'Qwilfish', 'Scizor', 'Shuckle', | |
'Heracross', 'Sneasel', 'Teddiursa', 'Ursaring', 'Slugma', 'Magcargo', | |
'Swinub', 'Piloswine', 'Corsola', 'Remoraid', 'Octillery', 'Delibird', | |
'Mantine', 'Skarmory', 'Houndour', 'Houndoom', 'Kingdra', 'Phanpy', 'Donphan', | |
'Porygon2', 'Stantler', 'Smeargle', 'Tyrogue', 'Hitmontop', 'Smoochum', | |
'Elekid', 'Magby', 'Miltank', 'Blissey', 'Raikou', 'Entei', 'Suicune', | |
'Larvitar', 'Pupitar', 'Tyranitar', 'Lugia', 'Ho-Oh', 'Celebi', 'Treecko', | |
'Grovyle', 'Sceptile', 'Torchic', 'Combusken', 'Blaziken', 'Mudkip', | |
'Marshtomp', 'Swampert', 'Poochyena', 'Mightyena', 'Zigzagoon', 'Linoone', | |
'Wurmple', 'Silcoon', 'Beautifly', 'Cascoon', 'Dustox', 'Lotad', 'Lombre', | |
'Ludicolo', 'Seedot', 'Nuzleaf', 'Shiftry', 'Taillow', 'Swellow', 'Wingull', | |
'Pelipper', 'Ralts', 'Kirlia', 'Gardevoir', 'Surskit', 'Masquerain', | |
'Shroomish', 'Breloom', 'Slakoth', 'Vigoroth', 'Slaking', 'Nincada', | |
'Ninjask', 'Shedinja', 'Whismur', 'Loudred', 'Exploud', 'Makuhita', 'Hariyama', | |
'Azurill', 'Nosepass', 'Skitty', 'Delcatty', 'Sableye', 'Mawile', 'Aron', | |
'Lairon', 'Aggron', 'Meditite', 'Medicham', 'Electrike', 'Manectric', 'Plusle', | |
'Minun', 'Volbeat', 'Illumise', 'Roselia', 'Gulpin', 'Swalot', 'Carvanha', | |
'Sharpedo', 'Wailmer', 'Wailord', 'Numel', 'Camerupt', 'Torkoal', 'Spoink', | |
'Grumpig', 'Spinda', 'Trapinch', 'Vibrava', 'Flygon', 'Cacnea', 'Cacturne', | |
'Swablu', 'Altaria', 'Zangoose', 'Seviper', 'Lunatone', 'Solrock', 'Barboach', | |
'Whiscash', 'Corphish', 'Crawdaunt', 'Baltoy', 'Claydol', 'Lileep', 'Cradily', | |
'Anorith', 'Armaldo', 'Feebas', 'Milotic', 'Castform', 'Kecleon', 'Shuppet', | |
'Banette', 'Duskull', 'Dusclops', 'Tropius', 'Chimecho', 'Absol', 'Wynaut', | |
'Snorunt', 'Glalie', 'Spheal', 'Sealeo', 'Walrein', 'Clamperl', 'Huntail', | |
'Gorebyss', 'Relicanth', 'Luvdisc', 'Bagon', 'Shelgon', 'Salamence', 'Beldum', | |
'Metang', 'Metagross', 'Regirock', 'Regice', 'Registeel', 'Latias', 'Latios', | |
'Kyogre', 'Groudon', 'Rayquaza', 'Jirachi', 'Deoxys', 'Turtwig', 'Grotle', | |
'Torterra', 'Chimchar', 'Monferno', 'Infernape', 'Piplup', 'Prinplup', | |
'Empoleon', 'Starly', 'Staravia', 'Staraptor', 'Bidoof', 'Bibarel', | |
'Kricketot', 'Kricketune', 'Shinx', 'Luxio', 'Luxray', 'Budew', 'Roserade', | |
'Cranidos', 'Rampardos', 'Shieldon', 'Bastiodon', 'Burmy', 'Wormadam', | |
'Mothim', 'Combee', 'Vespiquen', 'Pachirisu', 'Buizel', 'Floatzel', 'Cherubi', | |
'Cherrim', 'Shellos', 'Gastrodon', 'Ambipom', 'Drifloon', 'Drifblim', | |
'Buneary', 'Lopunny', 'Mismagius', 'Honchkrow', 'Glameow', 'Purugly', | |
'Chingling', 'Stunky', 'Skuntank', 'Bronzor', 'Bronzong', 'Bonsly', | |
'Mime Jr.', 'Happiny','Chatot', 'Spiritomb', 'Gible', 'Gabite', 'Garchomp', | |
'Munchlax', 'Riolu', 'Lucario', 'Hippopotas', 'Hippowdon', 'Skorupi', | |
'Drapion', 'Croagunk', 'Toxicroak','Carnivine', 'Finneon', 'Lumineon', | |
'Mantyke', 'Snover', 'Abomasnow', 'Weavile', 'Magnezone', 'Lickilicky', | |
'Rhyperior', 'Tangrowth', 'Electivire', 'Magmortar', 'Togekiss', 'Yanmega', | |
'Leafeon', 'Glaceon', 'Gliscor', 'Mamoswine', 'Porygon-Z', 'Gallade', | |
'Probopass', 'Dusknoir', 'Froslass', 'Rotom', 'Uxie', 'Mesprit', 'Azelf', | |
'Dialga', 'Palkia', 'Heatran', 'Regigigas', 'Giratina', 'Cresselia', | |
'Phione', 'Manaphy', 'Darkrai', 'Shaymin', 'Arceus', 'Victini', 'Snivy', | |
'Servine', 'Serperior', 'Tepig', 'Pignite', 'Emboar', 'Oshawott', 'Dewott', | |
'Samurott', 'Patrat', 'Watchog', 'Lillipup', 'Herdier', 'Stoutland', | |
'Purrloin', 'Liepard', 'Pansage', 'Simisage', 'Pansear', 'Simisear', | |
'Panpour', 'Simipour', 'Munna', 'Musharna', 'Pidove', 'Tranquill', 'Unfezant', | |
'Blitzle', 'Zebstrika', 'Roggenrola', 'Boldore', 'Gigalith', 'Woobat', | |
'Swoobat', 'Drilbur', 'Excadrill', 'Audino', 'Timburr', 'Gurdurr', | |
'Conkeldurr', 'Tympole', 'Palpitoad', 'Seismitoad', 'Throh', 'Sawk', | |
'Sewaddle', 'Swadloon', 'Leavanny', 'Venipede', 'Whirlipede', 'Scolipede', | |
'Cottonee', 'Whimsicott', 'Petilil', 'Lilligant', 'Basculin', 'Sandile', | |
'Krokorok', 'Krookodile', 'Darumaka', 'Darmanitan', 'Maractus', 'Dwebble', | |
'Crustle', 'Scraggy', 'Scrafty', 'Sigilyph', 'Yamask', 'Cofagrigus', | |
'Tirtouga', 'Carracosta', 'Archen', 'Archeops', 'Trubbish', 'Garbodor', | |
'Zorua', 'Zoroark', 'Minccino', 'Cinccino', 'Gothita', 'Gothorita', | |
'Gothitelle', 'Solosis', 'Duosion', 'Reuniclus', 'Ducklett', 'Swanna', | |
'Vanillite', 'Vanillish', 'Vanilluxe', 'Deerling', 'Sawsbuck', 'Emolga', | |
'Karrablast', 'Escavalier', 'Foongus', 'Amoonguss', 'Frillish', 'Jellicent', | |
'Alomomola', 'Joltik', 'Galvantula', 'Ferroseed', 'Ferrothorn', 'Klink', | |
'Klang', 'Klinklang', 'Tynamo', 'Eelektrik', 'Eelektross', 'Elgyem', | |
'Beheeyem', 'Litwick', 'Lampent', 'Chandelure', 'Axew', 'Fraxure', 'Haxorus', | |
'Cubchoo', 'Beartic', 'Cryogonal', 'Shelmet', 'Accelgor', 'Stunfisk', | |
'Mienfoo', 'Mienshao', 'Druddigon', 'Golett', 'Golurk', 'Pawniard', 'Bisharp', | |
'Bouffalant', 'Rufflet', 'Braviary', 'Vullaby', 'Mandibuzz', 'Heatmor', | |
'Durant', 'Deino', 'Zweilous', 'Hydreigon', 'Larvesta', 'Volcarona', | |
'Cobalion', 'Terrakion', 'Virizion', 'Tornadus', 'Thundurus', 'Reshiram', | |
'Zekrom', 'Landorus', 'Kyurem', 'Keldeo', 'Meloetta', 'Genesect', 'Chespin', | |
'Quilladin', 'Chesnaught', 'Fennekin', 'Braixen', 'Delphox', 'Froakie', | |
'Frogadier', 'Greninja', 'Bunnelby', 'Diggersby', 'Fletchling', 'Fletchinder', | |
'Talonflame', 'Scatterbug', 'Spewpa', 'Vivillon', 'Litleo', 'Pyroar', | |
'Flabébé', 'Floette', 'Florges', 'Skiddo', 'Gogoat', 'Pancham', 'Pangoro', | |
'Furfrou', 'Espurr', 'Meowstic', 'Honedge', 'Doublade', 'Aegislash', | |
'Spritzee', 'Aromatisse', 'Swirlix', 'Slurpuff', 'Inkay', 'Malamar', | |
'Binacle', 'Barbaracle', 'Skrelp', 'Dragalge', 'Clauncher', 'Clawitzer', | |
'Helioptile', 'Heliolisk', 'Tyrunt', 'Tyrantrum', 'Amaura', 'Aurorus', | |
'Sylveon', 'Hawlucha', 'Dedenne', 'Carbink', 'Goomy', 'Sliggoo', 'Goodra', | |
'Klefki', 'Phantump', 'Trevenant', 'Pumpkaboo', 'Gourgeist', 'Bergmite', | |
'Avalugg', 'Noibat', 'Noivern', 'Xerneas', 'Yveltal', 'Zygarde', 'Diancie', | |
'Hoopa', 'Volcanion', 'Rowlet', 'Dartrix', 'Decidueye', 'Litten', 'Torracat', | |
'Incineroar', 'Popplio', 'Brionne','Primarina', 'Pikipek', 'Trumbeak', | |
'Toucannon', 'Yungoos', 'Gumshoos', 'Grubbin', 'Charjabug', 'Vikavolt', | |
'Crabrawler', 'Crabominable', 'Oricorio', 'Cutiefly', 'Ribombee', 'Rockruff', | |
'Lycanroc', 'Wishiwashi', 'Mareanie', 'Toxapex', 'Mudbray', 'Mudsdale', | |
'Dewpider', 'Araquanid', 'Fomantis', 'Lurantis', 'Morelull', 'Shiinotic', | |
'Salandit', 'Salazzle', 'Stufful', 'Bewear', 'Bounsweet', 'Steenee', | |
'Tsareena', 'Comfey', 'Oranguru', 'Passimian', 'Wimpod', 'Golisopod', | |
'Sandygast', 'Palossand', 'Pyukumuku', 'Type: Null', 'Silvally', 'Minior', | |
'Komala', 'Turtonator', 'Togedemaru', 'Mimikyu', 'Bruxish', 'Drampa', | |
'Dhelmise', 'Jangmo-o','Hakamo-o', 'Kommo-o', 'Tapu Koko', 'Tapu Lele', | |
'Tapu Bulu', 'Tapu Fini', 'Cosmog', 'Cosmoem', 'Solgaleo', 'Lunala', | |
'Nihilego', 'Buzzwole', 'Pheromosa', 'Xurkitree', 'Celesteela', 'Kartana', | |
'Guzzlord', 'Necrozma', 'Magearna', 'Marshadow'] | |
# i have no idea how this thing works but it sure works great!! | |
# https://en.wikibooks.org/wiki/Algorithm_Implementation/Strings/Levenshtein_distance#Python | |
def levenshtein_distance(s1, s2): | |
if len(s1) < len(s2): | |
s1, s2 = s2, s1 | |
# len(s1) >= len(s2) | |
if len(s2) == 0: | |
return len(s1) | |
previous_row = range(len(s2) + 1) | |
for i, c1 in enumerate(s1): | |
current_row = [i + 1] | |
for j, c2 in enumerate(s2): | |
insertions = previous_row[j + 1] + 1 # j+1 instead of j since previous_row and current_row are one character longer | |
deletions = current_row[j] + 1 # than s2 | |
substitutions = previous_row[j] + (c1 != c2) | |
current_row.append(min(insertions, deletions, substitutions)) | |
previous_row = current_row | |
return previous_row[-1] | |
@dataclass | |
class Pokemon(): | |
''' | |
Most of this information is taken from the big panel on the right of the | |
bulbapedia webpage. | |
''' | |
name : str | |
number : str | |
type : tuple | |
description : str | |
picture : Image | |
class Pokedex(): | |
''' | |
This class helps you find pokemon by lookinng up their Bulbapedia entry. | |
It also helps you with spelling in case you missed the male symbol in | |
"Nidoran♂" or the accents in "Flabébé", or if you don't know how to spell | |
stupid Pokemon names like "Yveltal". | |
''' | |
def _search(self, name): | |
''' | |
Search global Pokemon catalog for approximate matches to given name. | |
I invented this search algorithm so it's probably bad. | |
''' | |
distance = levenshtein_distance # minor namespace lookup optimization | |
good_matches = [] | |
better_matches = [] | |
best_matches = [] | |
in_matches = [] | |
title_name = name.title() # pokemon names should be in title format | |
lower_name = name.casefold() # used this for our ``in'' operator | |
# do fuzzy match | |
for pokemon in ALL_POKEMON: | |
if lower_name in pokemon.casefold(): | |
in_matches.append(pokemon) | |
else: | |
d = distance(title_name, pokemon) | |
if d < 5: | |
good_matches.append(pokemon) | |
if d < 3: | |
better_matches.append(pokemon) | |
if d <= 2: | |
best_matches.append(pokemon) | |
# select most fit match set | |
matches = good_matches | |
if len(matches) > len(better_matches) > 0: | |
matches = better_matches | |
if len(matches) > len(best_matches) > 0: | |
matches = best_matches | |
# give priority to lazy matches since they are most likely what the person intended | |
return (in_matches + matches)[:5] # 5 should results be accurate enough | |
def lookup(self, name): | |
''' | |
Try to fuzzy match the name. Success defined by returning a | |
case-corrected name that can be used to query Bulbapedia. On error, | |
ValueError is raised and a list of valid suggestions is provided. | |
''' | |
suggestions = self._search(name) | |
if name.title() == suggestions[0]: | |
return suggestions | |
else: | |
raise ValueError( | |
'Invalid Pokemon name.', | |
suggestions | |
) | |
def url_for(self, name): | |
url_fmt = 'https://bulbapedia.bulbagarden.net/wiki/{name}_(Pok%C3%A9mon)' | |
name = urllib.parse.quote(name.replace(' ', '_')) | |
url = url_fmt.format(name=name) | |
return url | |
def query_bulbapedia(self, url): | |
''' | |
WARNING: this function does not cloak/cache its requests in any way. | |
This means that: | |
- Bulbapedia knows you are using python requests to query it | |
- You are more likely to get booted off (bots requests don't earn clicks) | |
- Content is not saved, so each function call is another request | |
Please be nice to bulbapedia and save your Pokemon objects! | |
''' | |
http_response = requests.get(url) | |
if not http_response.ok: | |
raise RuntimeError( | |
url + '\n' + | |
f'Web query failed with HTTP {http_response.status_code}' | |
) | |
soup = BeautifulSoup(http_response.content, 'html.parser') | |
pk_entry = soup.find('table', {'class':'roundy'}) # get first roundy class table | |
# God bless CSS selectors | |
pk_name = pk_entry.select('td big big b')[0].text | |
pk_number = pk_entry.select('big big a span')[0].text | |
pk_type = tuple(map(lambda t: t.text, pk_entry.select('tr td a span b')))[:2] | |
if pk_type[1] == 'Unknown': | |
pk_type = (pk_type[0],) | |
pk_description = pk_entry.select('a[href*=category] span')[0].text | |
pk_picture = pk_entry.select('a.image img')[0]['src'] | |
# this is a url. we need to clean it up and request the image | |
url = urllib.parse.urlparse(pk_picture) | |
if not url.scheme: | |
url = url._replace(scheme='https') | |
pk_picture = url.geturl() | |
r = requests.get(pk_picture, stream=True) | |
if not r.ok: | |
raise RuntimeError( | |
f'Web query failed with HTTP {r.status_code}' | |
) | |
pk_picture = Image.open(r.raw) # It's a png! | |
pokemon = Pokemon( | |
name = pk_name, | |
number = pk_number, | |
type = pk_type, | |
description = pk_description, | |
picture = pk_picture | |
) | |
return pokemon | |
def identify(self, name): | |
''' | |
This is the main function that will be used to identify pokemon. It | |
simplifies the search API, though, so be sure to pass it accurate names! | |
''' | |
try: | |
name = self.lookup(name)[0] | |
except ValueError as e: | |
# ignore exception and just take the first, most likely result | |
try: | |
name = e.args[1][0] | |
except IndexError: | |
raise e # if this happens its beyond fubar. ABORT!!! | |
url = self.url_for(name) | |
pokemon = self.query_bulbapedia(url) | |
return pokemon | |
class PokedexGUI(Frame): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
#self.configure(style='DexTFrame') | |
self.pokedex = Pokedex() | |
self.pokemon_frame = Frame(self) | |
self.pokemon_frame.pack(expand=True, fill=BOTH) | |
search_area = Frame(self) | |
search_area.pack() | |
self.searchbar = Entry(search_area, width=20) | |
self.searchbar.grid(row=0, column=0) | |
self.searchbar.bind('<Return>', self.identify_pokemon) | |
Button( | |
search_area, | |
text='Search', | |
command=self.identify_pokemon | |
).grid(row=0, column=1) | |
# matches pokemon names to past objects | |
self.search_history = {} | |
def identify_pokemon(self, _event=None): | |
''' | |
Get text from GUI and pass it through pokedex object to get a new | |
Pokemon object from the internet! Then, save the object (in case of | |
subsequent look ups for that pokemon) and render it to the screen | |
''' | |
name = self.searchbar.get().strip() | |
#todo: fix the pokedex search API to not be so BAD!!! | |
try: | |
name = self.pokedex.lookup(name)[0] | |
except ValueError as e: | |
try: | |
name = e.args[1][0] | |
except IndexError: | |
raise e | |
if name in self.search_history: | |
pokemon = self.search_history[name] | |
else: | |
pokemon = self.pokedex.identify(name) | |
self.search_history[name] = pokemon | |
self.show_pokemon(pokemon) | |
def show_pokemon(self, pokemon): | |
''' | |
This is the main class for rendering pokemon objects to the GUI. | |
''' | |
# for now, add text the bad-looking, lazy way | |
for w in self.pokemon_frame.winfo_children(): | |
w.pack_forget() | |
Label(self.pokemon_frame, text=pokemon.name).pack() | |
Label(self.pokemon_frame, text=pokemon.number).pack() | |
Label(self.pokemon_frame, text=pokemon.type).pack() | |
Label(self.pokemon_frame, text=pokemon.description).pack() | |
# We need to keep a reference to this object | |
self._ph = ImageTk.PhotoImage(pokemon.picture) | |
Label( | |
self.pokemon_frame, | |
image=self._ph | |
).pack(fill=Y, expand=True) | |
class App(Tk): | |
def __init__(self, *args, **kwargs): | |
super().__init__(*args, **kwargs) | |
self.geometry('280x500') | |
self.style = Style(self) | |
#self.style.configure('DexTFrame' | |
#background='red' | |
#) | |
PokedexGUI(self).pack(expand=True, fill=BOTH) | |
def main(): | |
App().mainloop() | |
if __name__ == '__main__': | |
main() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment