Animated Badges

Silly automation of a 15-minute task.

In the process of 'beautifying' this site, I was left wanting for an animated badge that represents both this site and the mainsite.

They absolutely deserve to be animated, so I picked the first thing that came to mind: a snake game! But two frames into animating it, I realized that automating would be a lot better. So I made a brief Python script to play an untimed game of snake, and render each board state as a numbered PNG. This was then easy to import as layers into GIMP and overlay onto the badge design I initially wanted.

View Snake Code
#!/usr/bin/env python3
# 'play' a snake game that exports itself as a series of images
# used for creating a badge on theokrueger.dev/micro
from enum import Enum
import random
from datetime import datetime
from PIL import Image

random.seed(datetime.now().timestamp())


class E(Enum):
    N = (" ", "#eee8aaff")
    S = ("░", "#cd69c9ff")
    F = ("*", "#ff0000ff")


# row, col
GAME_DIMS = (4, 8)
IMG_DIMS = (15, 80)
GAME_OFFSET = (3, 3)
GAME_SCALE = 2

field = [[E.N] * GAME_DIMS[1] for _ in range(GAME_DIMS[0])]
pos = (2, 0)
tails = [(2, 7)]
field[pos[0]][pos[1]] = E.S
for p in tails:
    field[p[0]][p[1]] = E.S
alive = True
score = 1
steps = 0


def fp(
    np=None,  # (int,int)
    nv=None,  # E
) -> (int, int):
    q = pos

    if not np is None:
        q = np

    if not nv is None:
        field[q[0]][q[1]] = nv

    return field[q[0]][q[1]]


def wrap(n: int, m: int) -> int:
    if n < 0:
        return m + n
    if n >= m:
        return n - m
    return n


def print_field():
    print("/" + "-" * GAME_DIMS[1] + "\\")
    for r in field:
        print("|", end="")
        for c in r:
            print(c.value[0], end="")
        print("|")
    print("\\" + "-" * GAME_DIMS[1] + "/")
    print("turn:", steps, "score:", score)


def spawn_food():
    while True:
        r = random.randint(0, GAME_DIMS[0] - 1)
        c = random.randint(0, GAME_DIMS[1] - 1)
        if field[r][c] == E.N:
            field[r][c] = E.F
            return


dirs = ["l", "r", "u", "d"]
opp = ["r", "l", "d", "u"]
tfs = [(0, -1), (0, 1), (-1, 0), (1, 0)]
opp_last_dir = ""


def step():
    global alive, pos, score, opp_last_dir, steps
    print_field()
    d = ""
    while not d in dirs or d == opp_last_dir:
        d = input("enter u/d/l/r: ")
    opp_last_dir = opp[dirs.index(d)]
    tf = tfs[dirs.index(d)]
    np = (wrap(pos[0] + tf[0], GAME_DIMS[0]), wrap(pos[1] + tf[1], GAME_DIMS[1]))
    tails.append(pos)
    ns = fp(np=np)
    if ns == E.F:
        score += 1
        spawn_food()
    elif ns == E.S:
        alive = False
        print("you died")
        print("score:", score)
    else:
        t = tails.pop(0)
        field[t[0]][t[1]] = E.N

    pos = np
    fp(nv=E.S)
    steps += 1


def hex_to_rgba(h):
    h = h.lstrip("#")
    return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4, 6))


def field_to_png():
    img = Image.new("RGBA", (IMG_DIMS[1], IMG_DIMS[0]), hex_to_rgba("#00000000"))
    for r in range(GAME_DIMS[0]):
        for c in range(GAME_DIMS[1]):
            cell = field[r][c]
            color = hex_to_rgba(cell.value[1])
            for x in range(GAME_SCALE):
                for y in range(GAME_SCALE):
                    img.putpixel(
                        (
                            (GAME_OFFSET[1] + c * (GAME_SCALE - 1)) + c + x,
                            (GAME_OFFSET[0] + r * (GAME_SCALE - 1)) + r + y,
                        ),
                        color,
                    )
    img.save("field-" + str(steps) + ".png")


field[2][5] = E.F
field_to_png()
while alive:
    step()
    field_to_png()
View Fireworks Code
#!/usr/bin/env python3
# create some firework effects
# used for creating a badge on theokrueger.dev/micro
from enum import Enum
import random
from dataclasses import dataclass
from datetime import datetime
from PIL import Image

random.seed(datetime.now().timestamp())

COLORS = ["#bfba50ff", "#938f3cff", "#bfba50ff", "#bfba50ff", "#938f3cff"]

N_FIREWORKS = 8
N_FRAMES = 200
FIREWORK_CHANCE = 0.4

# row, col
CANVAS_DIMS = (12, 52)
IMG_DIMS = (31, 88)
CANVAS_OFFSET = (5, 28)

works = [None] * N_FIREWORKS
frames = 0

FW_FRAMES = [
    [
        "     ",
        "     ",
        "  *  ",
        "     ",
        "     ",
    ],
    [
        "     ",
        "  *  ",
        " * * ",
        "  *  ",
        "     ",
    ],
    [
        "  *  ",
        "  *  ",
        "** **",
        "  *  ",
        "  *  ",
    ],
    [
        "  *  ",
        "     ",
        "*   *",
        "     ",
        "  *  ",
    ],
]
N_FW_FRAMES = len(FW_FRAMES)


@dataclass
class Firework:
    frame: int
    offset: (int, int)
    color: string

    def new(r, c) -> self:
        return Firework(0, (r, c), random.choice(COLORS))

    def pixels(self) -> [(int, int)]:
        fr = FW_FRAMES[self.frame]
        px = []
        for r in range(len(fr)):
            for c in range(len(fr[r])):
                if fr[r][c] == "*":
                    px.append((r + self.offset[0] - 2, c + self.offset[1] - 2))
        return px

    def advance(self) -> bool:
        self.frame += 1
        return self.frame >= N_FW_FRAMES


def frame():
    global frames
    for i in range(N_FIREWORKS):
        if works[i] != None and works[i].advance() == True:
            works[i] = None

    while random.random() < FIREWORK_CHANCE:
        for i in range(N_FIREWORKS):
            if works[i] == None:
                while True:
                    r_o = random.randint(2, CANVAS_DIMS[0] - 3) + CANVAS_OFFSET[0]
                    c_o = random.randint(2, CANVAS_DIMS[1] - 3) + CANVAS_OFFSET[1]

                    cont = False
                    for w in works:
                        if w != None:
                            tr, tc = w.offset
                            if not (
                                (r_o < tr - 4 or r_o > tr + 5)
                                or (c_o < tc - 4 or c_o > tc + 5)
                            ):
                                cont = True
                                break
                    if cont:
                        continue

                    works[i] = Firework.new(r_o, c_o)
                    break
                break

    print("frame", frames, "done")
    frames += 1


def hex_to_rgba(h):
    h = h.lstrip("#")
    return tuple(int(h[i : i + 2], 16) for i in (0, 2, 4, 6))


def canvas_to_png():
    img = Image.open("./base.png")
    for w in works:
        if w == None:
            continue

        for r, c in w.pixels():
            color = hex_to_rgba(w.color)
            img.putpixel((c, r), color)

    img.save("canvas-" + str(frames) + ".png")


for _ in range(N_FRAMES):
    frame()
    canvas_to_png()

In retrospect this is the type of thing perfectly suited for vibecoding, but it was too enjoyable a bikeshed to not build by hand. The results speak for themselves: