Spaces:
Runtime error
Runtime error
| """Procedural pixel-art sprites for plants and the app favicon. | |
| Each sprite is built from a 16x16 grid: a 10-row plant "archetype" stacked on | |
| top of a 6-row pot. Rows are authored as 8-character halves and mirrored | |
| (`half + half[::-1]`) to get a symmetric 16-wide row, then the whole grid is | |
| upscaled with nearest-neighbour resizing for a crisp pixel-art look. | |
| """ | |
| import functools | |
| from pathlib import Path | |
| import pandas as pd | |
| from PIL import Image | |
| GROWTH_CSV = "data/growth_csv/growth_ds.csv" | |
| STATIC_DIR = Path("static") | |
| SPRITES_DIR = STATIC_DIR / "sprites" | |
| FAVICON_PATH = STATIC_DIR / "favicon.png" | |
| # ββ Palette ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PALETTE = { | |
| ".": (0, 0, 0, 0), | |
| "G": (46, 125, 50, 255), # dark leaf green | |
| "g": (102, 187, 106, 255), # mid green | |
| "l": (197, 225, 165, 255), # light green / highlight | |
| "y": (255, 213, 79, 255), # yellow flower | |
| "o": (255, 152, 0, 255), # orange flower center | |
| "t": (121, 85, 72, 255), # trunk brown | |
| "S": (109, 76, 65, 255), # soil | |
| } | |
| POT_STYLES = { | |
| "terracotta": {"P": (216, 124, 90, 255), "Q": (173, 96, 68, 255), "R": (235, 165, 138, 255)}, | |
| "white": {"P": (245, 245, 245, 255), "Q": (210, 210, 210, 255), "R": (255, 255, 255, 255)}, | |
| "ceramic_blue": {"P": (84, 153, 199, 255), "Q": (55, 109, 148, 255), "R": (165, 214, 237, 255)}, | |
| "charcoal": {"P": (74, 74, 74, 255), "Q": (45, 45, 45, 255), "R": (115, 115, 115, 255)}, | |
| } | |
| POT_NAMES = list(POT_STYLES.keys()) | |
| # ββ Plant archetypes (10 half-rows, 8 chars each) βββββββββββββββββββββββββββ | |
| PLANT_SPRITES = { | |
| "cactus": [ | |
| "........", | |
| "........", | |
| "......gg", | |
| "......gG", | |
| "..gg..gG", | |
| "..gG..gG", | |
| "..gg..gG", | |
| "......gG", | |
| "......gg", | |
| "......gl", | |
| ], | |
| "succulent": [ | |
| "........", | |
| ".......l", | |
| "......gl", | |
| ".....Ggl", | |
| "....gGgl", | |
| "...ggGgl", | |
| "..gggGgl", | |
| ".ggggGgl", | |
| "ggggggGl", | |
| "gggggggl", | |
| ], | |
| "fern": [ | |
| "........", | |
| "...gGg..", | |
| "..glGgl.", | |
| ".gGglGg.", | |
| "gGlgGgl.", | |
| "gglGglg.", | |
| ".gGlGg..", | |
| "...g.g..", | |
| "....g...", | |
| "....gg..", | |
| ], | |
| "flower": [ | |
| "......y.", | |
| ".....yoy", | |
| "......y.", | |
| ".......g", | |
| "..g....g", | |
| ".gG....g", | |
| "..g....g", | |
| ".......g", | |
| ".......g", | |
| "......gg", | |
| ], | |
| "palm": [ | |
| "..l...l.", | |
| ".gg...gg", | |
| "..gGGg..", | |
| "...g.g..", | |
| ".......t", | |
| ".......t", | |
| ".......t", | |
| ".......t", | |
| "......tt", | |
| "......tt", | |
| ], | |
| "trailing": [ | |
| "........", | |
| ".gGggGl.", | |
| "gGggGgl.", | |
| "Gggggl..", | |
| "gl......", | |
| "Gl......", | |
| "gl......", | |
| "Gl......", | |
| "gl......", | |
| "gl......", | |
| ], | |
| } | |
| ARCHETYPES = list(PLANT_SPRITES.keys()) | |
| # ββ Pot (6 half-rows, 8 chars each) βββββββββββββββββββββββββββββββββββββββββ | |
| POT_BASE = [ | |
| "...SSSSS", | |
| "..RPPPPP", | |
| "..QPPPPP", | |
| "...QPPPP", | |
| "....QPPP", | |
| ".....QPP", | |
| ] | |
| # ββ Genus -> (archetype, pot) mapping βββββββββββββββββββββββββββββββββββββββ | |
| def _growth_table() -> pd.DataFrame: | |
| return pd.read_csv(GROWTH_CSV) | |
| def _stable_hash(text: str) -> int: | |
| """Deterministic hash (stable across runs, unlike builtin hash() for str).""" | |
| h = 0 | |
| for ch in text: | |
| h = (h * 31 + ord(ch)) & 0xFFFFFFFF | |
| return h | |
| def genus_to_sprite_key(genus: str) -> tuple[str, str]: | |
| """Map a genus to a deterministic (plant archetype, pot style) pair.""" | |
| df = _growth_table() | |
| row = df[df["Genus"] == genus] | |
| if row.empty: | |
| h = _stable_hash(genus) | |
| archetype = ARCHETYPES[h % len(ARCHETYPES)] | |
| pot = POT_NAMES[(h // len(ARCHETYPES)) % len(POT_NAMES)] | |
| return archetype, pot | |
| growth = str(row["Growth"].iloc[0]).lower() | |
| soil = str(row["Soil"].iloc[0]).lower() | |
| sunlight = str(row["Sunlight"].iloc[0]).lower() | |
| if "sandy" in soil and "full" in sunlight: | |
| archetype = "cactus" if growth == "slow" else "succulent" | |
| elif "indirect" in sunlight and growth in ("slow", "moderate"): | |
| archetype = "fern" if ("well-drained" in soil or "loamy" in soil) else "trailing" | |
| elif growth == "fast" and "full" in sunlight: | |
| archetype = "flower" | |
| elif growth == "slow" and "well-drained" in soil: | |
| archetype = "palm" | |
| else: | |
| archetype = "fern" | |
| pot = {"sandy": "terracotta", "well-drained": "white", "loamy": "charcoal"}.get(soil, "ceramic_blue") | |
| return archetype, pot | |
| # ββ Rendering ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _build_image(half_rows: list[str], palette: dict) -> Image.Image: | |
| rows = [half + half[::-1] for half in half_rows] | |
| size = len(rows) | |
| img = Image.new("RGBA", (size, size), (0, 0, 0, 0)) | |
| for y, row in enumerate(rows): | |
| for x, ch in enumerate(row): | |
| img.putpixel((x, y), palette.get(ch, (0, 0, 0, 0))) | |
| return img | |
| def render_sprite(genus: str, scale: int = 8) -> Image.Image: | |
| """Render the pixel-art (plant + pot) sprite for a genus.""" | |
| archetype, pot_style = genus_to_sprite_key(genus) | |
| half_rows = PLANT_SPRITES[archetype] + POT_BASE | |
| palette = {**PALETTE, **POT_STYLES[pot_style]} | |
| img = _build_image(half_rows, palette) | |
| return img.resize((img.width * scale, img.height * scale), Image.NEAREST) | |
| def _slugify(genus: str) -> str: | |
| return "".join(ch.lower() if ch.isalnum() else "_" for ch in genus) | |
| def get_sprite_path(genus: str) -> str: | |
| """Return the path to the (lazily rendered + cached) sprite for a genus.""" | |
| SPRITES_DIR.mkdir(parents=True, exist_ok=True) | |
| path = SPRITES_DIR / f"{_slugify(genus)}.png" | |
| if not path.exists(): | |
| render_sprite(genus).save(path) | |
| return str(path) | |
| def ensure_favicon() -> str: | |
| """Return the path to the (lazily rendered + cached) app favicon.""" | |
| STATIC_DIR.mkdir(parents=True, exist_ok=True) | |
| if not FAVICON_PATH.exists(): | |
| half_rows = PLANT_SPRITES["fern"] + POT_BASE | |
| palette = {**PALETTE, **POT_STYLES["terracotta"]} | |
| img = _build_image(half_rows, palette) | |
| img = img.resize((img.width * 4, img.height * 4), Image.NEAREST) | |
| img.save(FAVICON_PATH) | |
| return str(FAVICON_PATH) | |