Skip to content

Instantly share code, notes, and snippets.

@0xpizza
Last active September 5, 2019 22:41
Show Gist options
  • Save 0xpizza/4e4cac9916e805cf832f398d9fb053d7 to your computer and use it in GitHub Desktop.
Save 0xpizza/4e4cac9916e805cf832f398d9fb053d7 to your computer and use it in GitHub Desktop.
National Pokedex according to Bulbapedia
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