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 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()

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:

micro badge mainsite badge