Add Pokedex app — Gen 1 Pokemon viewer with shiny sprites#93
Conversation
Gen 1 Pokedex with all 151 Pokemon and local gen1.json database. Sprites fetched via fetch_sprites.py (not bundled due to copyright). Shiny toggle with sparkle effects, Pokedex-inspired UI design.
There was a problem hiding this comment.
Pull request overview
Adds a new Gen 1 Pokédex app for the Universe badge, including a local Pokémon database and sprite download tooling.
Changes:
- Added
pokedexapp runtime (__init__.py) with navigation, shiny toggle, and sprite caching/fetching. - Added a compact local Gen 1 database (
gen1.json) and bundled app icon/easter-egg assets. - Added a desktop helper script (
fetch_sprites.py) plus several screenshot PNGs.
Reviewed changes
Copilot reviewed 3 out of 10 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| badge/apps/pokedex/init.py | Main Pokédex app logic: UI, input handling, DB load, sprite load/cache/fetch, shiny effects |
| badge/apps/pokedex/gen1.json | Local Gen 1 dataset (151 entries + #000 easter egg) |
| badge/apps/pokedex/fetch_sprites.py | Desktop script to download normal/shiny sprites to the badge filesystem |
| badge/apps/pokedex/icon.png | App icon for launcher grid |
| badge/apps/pokedex/keefymon.png | Easter egg sprite asset (normal) |
| badge/apps/pokedex/keefymon_shiny.png | Easter egg sprite asset (shiny) |
| badge/apps/pokedex/screenshot_charmander.png | Screenshot artifact (docs) |
| badge/apps/pokedex/screenshot_pikachu.png | Screenshot artifact (docs) |
| badge/apps/pokedex/screenshot_shiny_pikachu.png | Screenshot artifact (docs) |
| badge/apps/pokedex/screenshot_mewtwo.png | Screenshot artifact (docs) |
| return "/system/shiny/{}.png".format(pokemon_id) | ||
| return "/system/sprites/{}.png".format(pokemon_id) |
There was a problem hiding this comment.
sprite_path() points at /system/sprites and /system/shiny, but fetch_sprites.py (and the PR setup instructions) download into sprites/ and shiny/ at the badge root. As-is, the app won't find pre-fetched sprites and will always try WiFi. Align these paths (and consider using the writable root filesystem rather than /system/, since cached sprites are user data).
| return "/system/shiny/{}.png".format(pokemon_id) | |
| return "/system/sprites/{}.png".format(pokemon_id) | |
| return "/shiny/{}.png".format(pokemon_id) | |
| return "/sprites/{}.png".format(pokemon_id) |
| def sprite_path(pokemon_id, is_shiny=False): | ||
| if is_shiny: | ||
| return "/system/shiny/{}.png".format(pokemon_id) | ||
| return "/system/sprites/{}.png".format(pokemon_id) | ||
|
|
There was a problem hiding this comment.
The hidden easter egg #000 can't load its bundled sprites: the repo includes keefymon.png / keefymon_shiny.png, but sprite_path() only ever looks for {id}.png in the sprite cache and will attempt to download id=0 from PokeAPI (likely 404). Add a special-case mapping for pokemon_id == 0 to these local PNGs (or ship them as 0.png in the cache directories).
| try: | ||
| sys.path.insert(0, "/") | ||
| import secrets | ||
| sys.path.pop(0) | ||
| WIFI_SSID = getattr(secrets, "WIFI_SSID", None) | ||
| WIFI_PASSWORD = getattr(secrets, "WIFI_PASSWORD", None) | ||
| del secrets | ||
| except ImportError: | ||
| WIFI_PASSWORD = None | ||
| WIFI_SSID = None |
There was a problem hiding this comment.
get_wifi_details() inserts "/" into sys.path but only pops it on the success path; if secrets.py exists but raises a non-ImportError (e.g., syntax error), the path entry will be left behind and can affect later imports. Use a try/finally to ensure sys.path.pop(0) always happens after sys.path.insert(0, "/").
| try: | |
| sys.path.insert(0, "/") | |
| import secrets | |
| sys.path.pop(0) | |
| WIFI_SSID = getattr(secrets, "WIFI_SSID", None) | |
| WIFI_PASSWORD = getattr(secrets, "WIFI_PASSWORD", None) | |
| del secrets | |
| except ImportError: | |
| WIFI_PASSWORD = None | |
| WIFI_SSID = None | |
| sys.path.insert(0, "/") | |
| try: | |
| import secrets | |
| WIFI_SSID = getattr(secrets, "WIFI_SSID", None) | |
| WIFI_PASSWORD = getattr(secrets, "WIFI_PASSWORD", None) | |
| del secrets | |
| except ImportError: | |
| WIFI_PASSWORD = None | |
| WIFI_SSID = None | |
| finally: | |
| sys.path.pop(0) |
|
|
||
| # Clear screen | ||
| screen.brush = dark_bg | ||
| screen.draw(shapes.rectangle(0, 0, 160, 120)) |
There was a problem hiding this comment.
Clearing the frame by drawing a full-screen rectangle is noticeably slower than using screen.clear() on the badge. Set screen.brush = dark_bg and call screen.clear() here (this pattern is used elsewhere in the codebase and avoids extra shape work each frame).
| screen.draw(shapes.rectangle(0, 0, 160, 120)) | |
| screen.clear() |
| # Force sprite refresh on A+C hold | ||
| if io.BUTTON_A in io.held and io.BUTTON_C in io.held: | ||
| changed = True | ||
| try: | ||
| os.remove(sprite_path(current_id, shiny)) | ||
| except: | ||
| pass | ||
|
|
There was a problem hiding this comment.
The A+C “force refresh” runs every frame while the buttons are held, repeatedly deleting the file and triggering full reload + gc.collect() in the same frame loop. This can cause stutter and makes it hard to complete a refresh. Gate this action to a single edge (e.g., track a refresh_held flag, or trigger when the combo becomes held for the first time).
| def fetch_sprite(pokemon_id, is_shiny=False): | ||
| path = sprite_path(pokemon_id, is_shiny) | ||
| if file_exists(path): | ||
| return | ||
| try: | ||
| url_path = "shiny/" if is_shiny else "" | ||
| response = urlopen(SPRITE_URL.format(id=pokemon_id, path=url_path), headers={"User-Agent": "Pokedex Badge"}) | ||
| data = bytearray(512) | ||
| with open(path, "wb") as f: | ||
| while True: | ||
| length = response.readinto(data) | ||
| if length == 0: | ||
| break | ||
| f.write(data[:length]) | ||
| yield | ||
| del data | ||
| del response | ||
| except Exception as e: | ||
| try: | ||
| os.remove(path) | ||
| except: | ||
| pass | ||
| raise RuntimeError("Sprite fetch failed: {}".format(e)) | ||
| finally: | ||
| gc.collect() |
There was a problem hiding this comment.
fetch_sprite() can block indefinitely in urlopen() / readinto() if the network stalls; since this runs inside update(), it can freeze the UI. Consider adding an io.ticks-based timeout (similar to async_fetch_to_disk() in badge/apps/badge/__init__.py) and ensure partial files are cleaned up on timeout/failure.
| # Fixed bar layout | ||
| BAR_X = 97 | ||
| BAR_W = 36 | ||
|
|
||
| def draw_stat_bar(name, value, x, y, max_val=154): | ||
| fill = min(int((value / max_val) * BAR_W), BAR_W) | ||
|
|
||
| screen.font = small_font | ||
| screen.brush = faded | ||
| screen.text(name, x, y) | ||
|
|
||
| # Bar background | ||
| screen.brush = brushes.color(40, 40, 50) | ||
| screen.draw(shapes.rounded_rectangle(BAR_X, y + 2, BAR_W, 5, 2)) | ||
|
|
There was a problem hiding this comment.
draw_stat_bar() takes an x argument but the bar itself is always drawn at the global BAR_X (only the label uses x). Either use x for the bar geometry too, or remove the parameter to avoid misleading callers.
| result = subprocess.run( | ||
| ["curl", "-sf", url, "-o", out_path], | ||
| capture_output=True, timeout=30, | ||
| ) | ||
| return pokemon_id, shiny, result.returncode == 0 | ||
| except Exception: | ||
| return pokemon_id, shiny, False |
There was a problem hiding this comment.
This script shells out to curl, which isn't available by default on all platforms (notably some Windows setups) and makes error handling harder. Consider using Python's standard library (urllib.request) for downloads so the setup flow works anywhere python3 runs.
Summary
fetch_sprites.py(not bundled — Nintendo copyright)Controls
Setup
python3 fetch_sprites.py /Volumes/BADGERto download spritesScreenshots
Charmander (#4)
Pikachu (#25)
Shiny Pikachu
Mewtwo (#150)