Name the Pokemon demo, and land a seed rename on its own row

`seed.py` matches a seed file to its scenario by title, so renaming one
inserted a second public scenario and stranded the first. The stranded row
stays public forever and has to be deleted by hand on every deployment, which
is what happened to "Road to the Champion". A seed file now lists its old
titles under `previous_titles`, and the rename updates the existing row.

The cover art is a PNG data URI. `app/images.py` accepts raster formats only,
because SVG can carry script and the bytes are served from the app's own
origin, so an SVG stores but yields an empty `image_url`.
`tools/make_pokeball.py` draws the ball with `zlib` alone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PacdRuPXSkQQy4ZYdH32hF
This commit is contained in:
parththakkar106
2026-08-28 18:42:06 +05:30
co-authored by Claude Opus 5
parent ab546d6f3c
commit ae39514d1e
3 changed files with 130 additions and 13 deletions
+89
View File
@@ -0,0 +1,89 @@
"""Draws a Pokeball as a PNG and prints it as a base64 data URI.
`app/images.py` accepts only raster data URIs, so the seed file cannot carry an
SVG. This script writes the ball with `zlib` alone, so the seed's cover art
needs no image library and no build step.
Run it, then paste `pokeball_uri.txt` into the seed file's `image` field:
python make_pokeball.py
"""
import base64
import math
import struct
import zlib
SIZE = 192 # Drawn at `SCALE` times this size, then averaged down.
SCALE = 4
N = SIZE * SCALE
RED = (238, 27, 44)
WHITE = (247, 247, 247)
BLACK = (26, 26, 26)
def sample(x: float, y: float):
"""Returns the `(r, g, b, a)` of the ball at one point of the canvas."""
cx = cy = N / 2
dx, dy = x - cx, y - cy
dist = math.hypot(dx, dy)
radius = N / 2 - N * 0.02
if dist > radius:
return (0, 0, 0, 0)
if dist > radius - N * 0.05: # Outer rim.
return (*BLACK, 255)
if dist < N * 0.10: # Center button.
return (*WHITE, 255)
if dist < N * 0.15: # Ring around the button.
return (*BLACK, 255)
if abs(dy) < N * 0.04: # Belt across the middle.
return (*BLACK, 255)
return (*(RED if dy < 0 else WHITE), 255)
def render():
"""Returns `SIZE` rows of `SIZE` RGBA pixels, box filtered from `SCALE`."""
rows = []
for py in range(SIZE):
row = bytearray()
for px in range(SIZE):
r = g = b = a = 0
for sy in range(SCALE):
for sx in range(SCALE):
pr, pg, pb, pa = sample(px * SCALE + sx + 0.5, py * SCALE + sy + 0.5)
# Premultiply, so a transparent sample adds no color.
r += pr * pa
g += pg * pa
b += pb * pa
a += pa
if a:
row += bytes((r // a, g // a, b // a, a // (SCALE * SCALE)))
else:
row += b"\0\0\0\0"
rows.append(bytes(row))
return rows
def png(rows) -> bytes:
raw = b"".join(b"\0" + r for r in rows)
def chunk(tag: bytes, data: bytes) -> bytes:
return (struct.pack(">I", len(data)) + tag + data
+ struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF))
header = struct.pack(">IIBBBBB", SIZE, SIZE, 8, 6, 0, 0, 0)
return (b"\x89PNG\r\n\x1a\n"
+ chunk(b"IHDR", header)
+ chunk(b"IDAT", zlib.compress(raw, 9))
+ chunk(b"IEND", b""))
if __name__ == "__main__":
data = png(render())
uri = "data:image/png;base64," + base64.b64encode(data).decode("ascii")
with open("pokeball.png", "wb") as fh:
fh.write(data)
with open("pokeball_uri.txt", "w", encoding="utf-8") as fh:
fh.write(uri)
print(len(data), "bytes;", len(uri), "chars")