From 888dfa4528eae8b4ad4fd7d7bd976803b4ee35bd Mon Sep 17 00:00:00 2001 From: jtc268 <89586838+jtc268@users.noreply.github.com> Date: Wed, 27 May 2026 00:35:57 -0400 Subject: [PATCH] Add bounty metro map pixel art --- assets/pixel-art/bounty-metro-map.png | Bin 0 -> 927 bytes demos/bounty-metro-map-demo.mp4 | Bin 0 -> 11407 bytes scripts/generate_bounty_metro_map.py | 167 ++++++++++++++++++++++++++ 3 files changed, 167 insertions(+) create mode 100644 assets/pixel-art/bounty-metro-map.png create mode 100644 demos/bounty-metro-map-demo.mp4 create mode 100644 scripts/generate_bounty_metro_map.py diff --git a/assets/pixel-art/bounty-metro-map.png b/assets/pixel-art/bounty-metro-map.png new file mode 100644 index 0000000000000000000000000000000000000000..9e7006f5a251255f29bf983a2c3e4e73ae198489 GIT binary patch literal 927 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 GjQ<~NwpV2U literal 0 HcmV?d00001 diff --git a/scripts/generate_bounty_metro_map.py b/scripts/generate_bounty_metro_map.py new file mode 100644 index 000000000..8d9f2b373 --- /dev/null +++ b/scripts/generate_bounty_metro_map.py @@ -0,0 +1,167 @@ +#!/usr/bin/env python3 +"""Generate original pixel art for bounty issue #80. + +The image is written without third-party dependencies so the asset can be +reproduced from a clean Python installation. +""" + +from __future__ import annotations + +import struct +import zlib +from pathlib import Path + + +WIDTH = 128 +HEIGHT = 128 +OUT = Path("assets/pixel-art/bounty-metro-map.png") + + +PALETTE = { + "ink": (13, 18, 30), + "deep": (22, 31, 50), + "grid": (37, 52, 82), + "muted": (80, 102, 136), + "paper": (222, 229, 236), + "gold": (248, 192, 67), + "lime": (72, 214, 141), + "cyan": (77, 203, 234), + "rose": (242, 93, 124), + "violet": (166, 120, 255), + "orange": (247, 132, 76), + "blue": (92, 143, 255), + "shadow": (8, 12, 22), +} + + +pixels = [[PALETTE["ink"] for _ in range(WIDTH)] for _ in range(HEIGHT)] + + +def rect(x: int, y: int, w: int, h: int, color: tuple[int, int, int]) -> None: + for yy in range(max(0, y), min(HEIGHT, y + h)): + for xx in range(max(0, x), min(WIDTH, x + w)): + pixels[yy][xx] = color + + +def line_h(x1: int, x2: int, y: int, color: tuple[int, int, int], thick: int = 3) -> None: + rect(min(x1, x2), y - thick // 2, abs(x2 - x1) + 1, thick, color) + + +def line_v(x: int, y1: int, y2: int, color: tuple[int, int, int], thick: int = 3) -> None: + rect(x - thick // 2, min(y1, y2), thick, abs(y2 - y1) + 1, color) + + +def station(x: int, y: int, color: tuple[int, int, int]) -> None: + rect(x - 4, y - 4, 9, 9, PALETTE["paper"]) + rect(x - 3, y - 3, 7, 7, color) + rect(x - 1, y - 1, 3, 3, PALETTE["ink"]) + + +def ticket(x: int, y: int, color: tuple[int, int, int]) -> None: + rect(x, y, 19, 13, PALETTE["paper"]) + rect(x + 2, y + 2, 15, 9, color) + rect(x + 5, y + 5, 9, 2, PALETTE["paper"]) + + +def draw_background() -> None: + rect(0, 0, WIDTH, HEIGHT, PALETTE["ink"]) + rect(7, 7, 114, 114, PALETTE["deep"]) + rect(10, 10, 108, 108, PALETTE["ink"]) + for x in range(14, 116, 8): + rect(x, 12, 1, 104, PALETTE["grid"]) + for y in range(14, 116, 8): + rect(12, y, 104, 1, PALETTE["grid"]) + rect(12, 12, 104, 1, PALETTE["muted"]) + rect(12, 115, 104, 1, PALETTE["muted"]) + rect(12, 12, 1, 104, PALETTE["muted"]) + rect(115, 12, 1, 104, PALETTE["muted"]) + + +def draw_routes() -> None: + line_h(24, 56, 35, PALETTE["cyan"], 5) + line_v(56, 35, 73, PALETTE["cyan"], 5) + line_h(56, 100, 73, PALETTE["cyan"], 5) + + line_v(31, 33, 90, PALETTE["rose"], 5) + line_h(31, 72, 90, PALETTE["rose"], 5) + line_v(72, 54, 90, PALETTE["rose"], 5) + + line_h(23, 98, 55, PALETTE["gold"], 5) + line_v(98, 55, 94, PALETTE["gold"], 5) + + for x, y, c in ( + (24, 35, PALETTE["blue"]), + (56, 35, PALETTE["cyan"]), + (56, 73, PALETTE["lime"]), + (100, 73, PALETTE["orange"]), + (31, 90, PALETTE["rose"]), + (72, 90, PALETTE["violet"]), + (72, 55, PALETTE["gold"]), + (98, 55, PALETTE["gold"]), + (98, 94, PALETTE["lime"]), + ): + station(x, y, c) + + +def draw_icons() -> None: + ticket(17, 20, PALETTE["blue"]) + ticket(88, 87, PALETTE["lime"]) + + # Checkmark. + rect(89, 35, 5, 5, PALETTE["lime"]) + rect(94, 40, 5, 5, PALETTE["lime"]) + rect(99, 31, 5, 14, PALETTE["lime"]) + rect(104, 26, 5, 5, PALETTE["lime"]) + + # Pull request fork shape. + rect(43, 100, 5, 5, PALETTE["violet"]) + rect(60, 100, 5, 5, PALETTE["violet"]) + line_h(47, 61, 102, PALETTE["violet"], 3) + line_v(52, 86, 102, PALETTE["violet"], 3) + station(52, 84, PALETTE["violet"]) + + # Small payout coin stack. + rect(83, 17, 23, 5, PALETTE["gold"]) + rect(80, 22, 29, 5, PALETTE["orange"]) + rect(83, 27, 23, 5, PALETTE["gold"]) + rect(88, 19, 3, 11, PALETTE["paper"]) + rect(99, 19, 3, 11, PALETTE["paper"]) + + # Four status lights. + for i, color in enumerate((PALETTE["blue"], PALETTE["cyan"], PALETTE["violet"], PALETTE["lime"])): + rect(19 + i * 12, 110, 7, 4, color) + rect(19 + i * 12, 114, 7, 2, PALETTE["shadow"]) + + +def write_png(path: Path) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + raw_rows = [] + for row in pixels: + raw_rows.append(b"\x00" + b"".join(bytes(px) for px in row)) + raw = b"".join(raw_rows) + + def chunk(kind: bytes, data: bytes) -> bytes: + return ( + struct.pack(">I", len(data)) + + kind + + data + + struct.pack(">I", zlib.crc32(kind + data) & 0xFFFFFFFF) + ) + + png = b"\x89PNG\r\n\x1a\n" + png += chunk("IHDR".encode(), struct.pack(">IIBBBBB", WIDTH, HEIGHT, 8, 2, 0, 0, 0)) + png += chunk("IDAT".encode(), zlib.compress(raw, level=9)) + png += chunk("IEND".encode(), b"") + path.write_bytes(png) + + +def main() -> None: + draw_background() + draw_routes() + draw_icons() + write_png(OUT) + print(OUT) + + +if __name__ == "__main__": + main()