nomeansnoquest/tools/gen_sprites.py
type-two 0976611f36 v1.2 — Rob walks: sprite-sheet ego (MRPCI v0.5 -sheet2x3 views)
rob.png → rob-sheet2x3.png: 2 walk frames x front/back/side loops
(engine mirrors side for right-facing). gen_sprites.py grows a sheet()
assembler with per-cel shape asserts. Golden replay still wins 60/60.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 13:42:50 +10:00

663 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
"""Generate NOMEANSNO QUEST sprites as PNGs — stdlib only (zlib + struct).
Sprites are ASCII grids: each char maps to an RGB in the sprite's legend,
'.' is transparent. MRPCI quantizes them per-room, so colors here are
intent, not law. Run from the repo root: python3 tools/gen_sprites.py
"""
import os
import struct
import zlib
OUT = os.path.join(os.path.dirname(__file__), "..", "sprites")
def write_png(path, rows, legend, scale=1):
rows = [r for r in rows for _ in range(scale)]
h = len(rows)
w = max(len(r) for r in rows) * scale
raw = b""
for y in range(h):
raw += b"\x00" # filter: none
row = rows[y].ljust(w // scale, ".")
for ch in row:
px = b"\x00\x00\x00\x00" if ch == "." else struct.pack("BBBB", *legend[ch], 255)
raw += px * scale
def chunk(tag, data):
c = struct.pack(">I", len(data)) + tag + data
return c + struct.pack(">I", zlib.crc32(tag + data) & 0xFFFFFFFF)
png = b"\x89PNG\r\n\x1a\n"
png += chunk(b"IHDR", struct.pack(">IIBBBBB", w, h, 8, 6, 0, 0, 0))
png += chunk(b"IDAT", zlib.compress(raw, 9))
png += chunk(b"IEND", b"")
with open(path, "wb") as f:
f.write(png)
print(f" {os.path.basename(path)} {w}x{h}")
S = {} # name -> (rows, legend)
# --- people -------------------------------------------------------------
# Rob Wright — the ego, as an MRPCI sprite sheet (`-sheet2x3`): 2 walk
# frames x 3 direction loops (front, back, side; the engine mirrors side
# for right-facing). Every cel is exactly 12x20; feet = bottom center.
ROB_LEGEND = {
"h": (72, 52, 36), # hair
"f": (214, 178, 148),# face
"2": (40, 40, 48), # eyes
"s": (52, 56, 64), # dark shirt
"p": (44, 48, 88), # jeans
"o": (30, 30, 34), # boots / strap
"b": (150, 60, 40), # bass body
"r": (220, 200, 160),# bass neck
}
ROB_FRONT0 = [
"....hhhh....",
"....hhhh....",
"....ffff....",
"....f2f2....",
"....ffff....",
".....ff.....",
"...ssssss...",
"..ssssssss..",
"..s.ssss.s..",
"..s.ssss.s..",
"..b.ssss....",
".bbbrssss...",
".bb.ssss....",
"..b.pppp....",
"....pppp....",
"....p..p....",
"....p..p....",
"....p..p....",
"....p..p....",
"...oo..oo...",
]
# Frame 2: the walk bob — boots swing in.
ROB_FRONT1 = ROB_FRONT0[:19] + ["....oo.oo..."]
ROB_BACK0 = [
"....hhhh....",
"....hhhh....",
"....hhhh....",
"....hhhh....",
".....ff.....",
"...ssssss...",
"..sossssss..",
"..s.osss.s..",
"..s.soss.s..",
"....ssos....",
"....ssso....",
"....sssso...",
"....ssss....",
"....pppp....",
"....pppp....",
"....p..p....",
"....p..p....",
"....p..p....",
"....p..p....",
"...oo..oo...",
]
ROB_BACK1 = ROB_BACK0[:19] + ["....oo.oo..."]
# Side faces LEFT (MRPCI mirrors it for right); bass rides the front hip.
ROB_SIDE0 = [
"....hhhh....",
"....hhhh....",
"....ffff....",
"....2fff....",
"....ffff....",
".....ff.....",
"....ssss....",
"...ssssss...",
"...ssssss...",
"...ssssss...",
"..brssss....",
".rbbssss....",
"..b.ssss....",
"....pppp....",
"....pppp....",
"....pp......",
"....pp......",
"....pp......",
"....pp......",
"...oooo.....",
]
ROB_SIDE1 = ROB_SIDE0[:15] + [
"...p..p.....",
"...p..p.....",
"..p....p....",
"..p....p....",
".oo....oo...",
]
def sheet(*loops):
"""Stack loops vertically, frames horizontally — the -sheetCxR layout."""
rows = []
for cels in loops:
for cel in cels:
assert len(cel) == len(cels[0]), "frames in a loop must share height"
assert all(len(r) == len(cel[0]) for r in cel), "cels must be rectangular"
for y in range(len(cels[0])):
rows.append("".join(c[y] for c in cels))
return rows
S["rob-sheet2x3"] = (
sheet([ROB_FRONT0, ROB_FRONT1], [ROB_BACK0, ROB_BACK1], [ROB_SIDE0, ROB_SIDE1]),
ROB_LEGEND,
)
# John Wright — behind the kit or beside it; sticks in hand.
S["john"] = (
[
"...hhhhh...",
"...hhhhh...",
"...ffff....",
"...f2f2....",
"...ffff....",
"....ff.....",
"..tttttt...",
".tttttttt..",
".t.tttt.t..",
".w.tttt.w..",
"...tttt....",
"...pppp....",
"...p..p....",
"...p..p....",
"...p..p....",
"..oo..oo...",
],
{
"h": (60, 44, 30),
"f": (216, 182, 152),
"2": (40, 40, 48),
"t": (140, 40, 44), # red tee
"w": (222, 214, 180),# drumsticks
"p": (40, 42, 50),
"o": (30, 30, 34),
},
)
S["mom"] = (
[
"...hhhhh...",
"..hhhhhhh..",
"...ffff....",
"...f2f2....",
"...ffff....",
"....ff.....",
"..dddddd...",
".dddddddd..",
".d.aaaa.d..",
"...aaaa....",
"...aaaa....",
"...dddd....",
"...d..d....",
"..oo..oo...",
],
{
"h": (168, 160, 150),
"f": (216, 184, 156),
"2": (50, 44, 40),
"d": (90, 110, 140), # housedress
"a": (228, 226, 214),# apron
"o": (60, 50, 44),
},
)
# Joey Shithead of D.O.A., on stage. Mohawk not optional.
S["joey"] = (
[
"....kk.....",
"...kkkk....",
"....kk.....",
"...ffff....",
"...f2f2....",
"...ffff....",
"....ff.....",
"..lllllll..",
".lllllllll.",
".l.lllll.l.",
".g.lllll...",
".gg.lll....",
"...pppp....",
"...p..p....",
"...p..p....",
"..oo..oo...",
],
{
"k": (30, 160, 60), # green mohawk
"f": (210, 172, 140),
"2": (30, 30, 36),
"l": (36, 36, 40), # leather
"g": (170, 120, 60), # guitar neck
"p": (46, 46, 52),
"o": (26, 26, 30),
},
)
S["biafra"] = (
[
"...hhhhh...",
"...hhhhh...",
"...ffff....",
"...f2f2....",
"...ffff....",
"....ff.....",
"..jjjjjj...",
".jjjjjjjj..",
".j.tttt.j..",
"...tttt....",
"...tttt....",
"...pppp....",
"...p..p....",
"...p..p....",
"..oo..oo...",
],
{
"h": (50, 38, 30),
"f": (214, 178, 148),
"2": (36, 40, 48),
"j": (70, 60, 90), # thrift blazer
"t": (200, 60, 50), # loud tee
"p": (50, 50, 58),
"o": (30, 30, 34),
},
)
S["stagehand"] = (
[
"...ccccc...",
"...ffff....",
"...f2f2....",
"...ffff....",
"....ff.....",
"..vvvvvv...",
".vvvvvvvv..",
".v.vvvv.v..",
"...vvvv....",
"...vvvv....",
"...pppp....",
"...p..p....",
"...p..p....",
"..oo..oo...",
],
{
"c": (40, 40, 48), # headset cap
"f": (208, 172, 140),
"2": (36, 36, 44),
"v": (240, 180, 40), # hi-vis
"p": (44, 46, 54),
"o": (28, 28, 32),
},
)
S["mobster"] = (
[
"...bbbbb...",
"...ffff....",
"...f2f2....",
"...ffff....",
"...ffnf....",
"....ff.....",
".ttttttttt.",
".tttttttte.",
".t.tttt.te.",
".t.tttt.t..",
"...tttt....",
"...tttt....",
"...ssss....",
"...s..s....",
"...s..s....",
"..oo..oo...",
],
{
"b": (30, 30, 34), # flat cap
"f": (206, 168, 138),
"2": (30, 34, 40),
"n": (150, 110, 90), # that nose
"t": (60, 90, 60), # tracksuit
"e": (220, 200, 80), # gold stripe
"s": (54, 56, 60),
"o": (26, 26, 30),
},
)
# --- portraits (24x24, drawn 2x in core windows, 6x in the GUI) ----------
S["john-face"] = (
[
"bbbbbbbbbbbbbbbbbbbbbbbb",
"bbbbbbbbbbbbbbbbbbbbbbbb",
"bbbbbhhhhhhhhhhhhhbbbbbb",
"bbbbhhhhhhhhhhhhhhhbbbbb",
"bbbhhhhhhhhhhhhhhhhhbbbb",
"bbbhhfffffffffffffhhbbbb",
"bbbhffffffffffffffhhbbbb",
"bbbhffffffffffffffhbbbbb",
"bbbhff22ffffff22ffhbbbbb",
"bbbhff22ffffff22ffhbbbbb",
"bbbbffffffffffffffbbbbbb",
"bbbbffffffnnffffffbbbbbb",
"bbbbffffffnnffffffbbbbbb",
"bbbbffffffffffffffbbbbbb",
"bbbbfffmmmmmmmmfffbbbbbb",
"bbbbffffmmmmmmffffbbbbbb",
"bbbbbffffffffffffbbbbbbb",
"bbbbbbffffffffffbbbbbbbb",
"bbbbbbbttttttttbbbbbbbbb",
"bbbbbbttttttttttbbbbbbbb",
"bbbbbttttttttttttbbbbbbb",
"bbbbttttttttttttttbbbbbb",
"bbbbttttttttttttttbbbbbb",
"bbbbbbbbbbbbbbbbbbbbbbbb",
],
{
"b": (24, 26, 34),
"h": (60, 44, 30),
"f": (216, 182, 152),
"2": (40, 40, 48),
"n": (190, 150, 122),
"m": (150, 90, 80),
"t": (140, 40, 44),
},
)
S["mom-face"] = (
[
"bbbbbbbbbbbbbbbbbbbbbbbb",
"bbbbbhhhhhhhhhhhhhbbbbbb",
"bbbhhhhhhhhhhhhhhhhbbbbb",
"bbhhhhhhhhhhhhhhhhhhbbbb",
"bbhhhffffffffffffhhhbbbb",
"bbhhffffffffffffffhhbbbb",
"bbhhffffffffffffffhhbbbb",
"bbhhff22ffffff22ffhhbbbb",
"bbhhff22ffffff22ffhhbbbb",
"bbhhffffffffffffffhhbbbb",
"bbhhffffffnnffffffhhbbbb",
"bbhhffffffffffffffhhbbbb",
"bbhhfffmmmmmmmmfffhhbbbb",
"bbhhffffmmmmmmffffhhbbbb",
"bbbhhffffffffffffhhbbbbb",
"bbbhhhffffffffffhhhbbbbb",
"bbbbhhhhffffffhhhhbbbbbb",
"bbbbbbddddddddddbbbbbbbb",
"bbbbbddddddddddddbbbbbbb",
"bbbbddddddddddddddbbbbbb",
"bbbbddddaaaaaaddddbbbbbb",
"bbbbdddaaaaaaaadddbbbbbb",
"bbbbdddaaaaaaaadddbbbbbb",
"bbbbbbbbbbbbbbbbbbbbbbbb",
],
{
"b": (26, 26, 32),
"h": (168, 160, 150),
"f": (216, 184, 156),
"2": (50, 44, 40),
"n": (192, 154, 128),
"m": (160, 100, 90),
"d": (90, 110, 140),
"a": (228, 226, 214),
},
)
S["biafra-face"] = (
[
"bbbbbbbbbbbbbbbbbbbbbbbb",
"bbbbhhhhhhhhhhhhhhbbbbbb",
"bbbhhhhhhhhhhhhhhhhbbbbb",
"bbbhhhhhhhhhhhhhhhhbbbbb",
"bbbhhffffffffffffhhbbbbb",
"bbbhffffffffffffffhbbbbb",
"bbbhffffffffffffffhbbbbb",
"bbbhffeeffffffeeffhbbbbb",
"bbbhff22ffffff22ffhbbbbb",
"bbbhffffffffffffffhbbbbb",
"bbbhffffffnnffffffhbbbbb",
"bbbhffffffffffffffhbbbbb",
"bbbbffffffffffffffbbbbbb",
"bbbbfffmmmmmmmmfffbbbbbb",
"bbbbffmmmmmmmmmmffbbbbbb",
"bbbbbfffmmmmmmfffbbbbbbb",
"bbbbbbffffffffffbbbbbbbb",
"bbbbbbbjjjjjjjjbbbbbbbbb",
"bbbbbjjjjjjjjjjjjbbbbbbb",
"bbbbjjjjttttttjjjjbbbbbb",
"bbbbjjjttttttttjjjbbbbbb",
"bbbbjjjttttttttjjjbbbbbb",
"bbbbjjjttttttttjjjbbbbbb",
"bbbbbbbbbbbbbbbbbbbbbbbb",
],
{
"b": (28, 24, 34),
"h": (50, 38, 30),
"f": (214, 178, 148),
"2": (36, 40, 48),
"e": (90, 90, 110), # that stare, pre-twinkle
"n": (188, 148, 120),
"m": (150, 84, 76), # the grin
"j": (70, 60, 90),
"t": (200, 60, 50),
},
)
S["mobster-face"] = (
[
"bbbbbbbbbbbbbbbbbbbbbbbb",
"bbbbkkkkkkkkkkkkkkbbbbbb",
"bbbkkkkkkkkkkkkkkkkbbbbb",
"bbbkkkkkkkkkkkkkkkkbbbbb",
"bbbbffffffffffffffbbbbbb",
"bbbfffffffffffffffbbbbbb",
"bbbff2222ffff2222ffbbbbb",
"bbbff2222ffff2222ffbbbbb",
"bbbffffffffffffffffbbbbb",
"bbbfffffnnnnnnfffffbbbbb",
"bbbfffffnnnnnnfffffbbbbb",
"bbbffffffffffffffffbbbbb",
"bbbbffffffffffffffbbbbbb",
"bbbbfffmmmmmmmmfffbbbbbb",
"bbbbffffffffffffffbbbbbb",
"bbbbbffffffffffffbbbbbbb",
"bbbbbbffffffffffbbbbbbbb",
"bbbbbbbttttttttbbbbbbbbb",
"bbbbbttttttttttttbbbbbbb",
"bbbbtttteeeeeetttbbbbbbb",
"bbbbttttttttttttttbbbbbb",
"bbbbttttttttttttttbbbbbb",
"bbbbttttttttttttttbbbbbb",
"bbbbbbbbbbbbbbbbbbbbbbbb",
],
{
"b": (24, 28, 26),
"k": (30, 30, 34),
"f": (206, 168, 138),
"2": (30, 34, 40),
"n": (150, 110, 90),
"m": (120, 70, 64),
"t": (60, 90, 60),
"e": (220, 200, 80),
},
)
# --- props ----------------------------------------------------------------
S["portastudio"] = (
[
"gggggggggggggggggggggggggg",
"gssssssssssssssssssssssssg",
"gsrrrrrssgggggggssrrrrrssg",
"gsr...rssgddddggssr...rssg",
"gsr.o.rssgddddggssr.o.rssg",
"gsr...rssgggggggssr...rssg",
"gsrrrrrssssssssssssrrrrssg",
"gsssssssskkkkkkkksssssssg.",
"gsswwswwsswwsswwsswwswwsg.",
"gsswwswwsswwsswwsswwswwsg.",
"gggggggggggggggggggggggg..",
],
{
"g": (70, 72, 78),
"s": (98, 100, 108),
"r": (50, 50, 56),
"o": (200, 190, 170),
"d": (180, 60, 50), # VU meters
"k": (36, 36, 40),
"w": (210, 208, 200),# faders
},
)
S["castlebox"] = (
[
"cccccccccccccccccccccc",
"cttttttttttttttttttttc",
"ct__CASTLE__(RIP)___tc",
"cttttttttttttttttttttc",
"cccccccccccccccccccccc",
"cccccccccccccccccccccc",
"cccccccccccccccccccccc",
"cccccccccccccccccccccc",
],
{
"c": (150, 110, 66),
"t": (120, 86, 50),
"_": (120, 86, 50),
"C": (60, 44, 30),
"A": (60, 44, 30),
"S": (60, 44, 30),
"T": (60, 44, 30),
"L": (60, 44, 30),
"E": (60, 44, 30),
"R": (60, 44, 30),
"I": (60, 44, 30),
"P": (60, 44, 30),
"(": (60, 44, 30),
")": (60, 44, 30),
},
)
S["flyer"] = (
[
"wwwwwwwwww",
"wkkkkkkkkw",
"wwwwwwwwww",
"wkkkwwkkww",
"wwwwwwwwww",
"wkkkkkwwww",
"wwwwwwwwww",
],
{"w": (226, 222, 210), "k": (40, 40, 44)},
)
S["drumkit"] = (
[
"........cc..........cc........",
".........c..........c.........",
"....hhhhhhhhh..hhhhhhhhh......",
"....h.......h..h.......h......",
"....hhhhhhhhh..hhhhhhhhh......",
"..bbbbbbbbbbbbbbbbbbbbbbbb....",
".bbwwwwwwwwwwwwwwwwwwwwwwbb...",
".bbwwwwwwwwwwwwwwwwwwwwwwbb...",
".bbwwwwwwwwwwwwwwwwwwwwwwbb...",
".bbbbbbbbbbbbbbbbbbbbbbbbbb...",
"...ss...ss........ss...ss.....",
"..ss.....ss......ss.....ss....",
],
{
"c": (210, 190, 120), # cymbals
"h": (120, 60, 50), # toms
"b": (140, 40, 44), # kick shell
"w": (230, 226, 214), # kick head
"s": (140, 140, 150),
},
)
S["rig"] = (
[
"kkkkkkkkkkkkkkkkkkkkkkkkkkkk",
"kggggggggggggkkggggggggggggk",
"kg..........gkkg..........gk",
"kg.oooooooo.gkkg.oooooooo.gk",
"kg.oooooooo.gkkg.oooooooo.gk",
"kg..........gkkg..........gk",
"kkkkkkkkkkkkkkkkkkkkkkkkkkkk",
"kggggggggggggkkggggggggggggk",
"kg.oooooooo.gkkg.oooooooo.gk",
"kg.oooooooo.gkkg.oooooooo.gk",
"kg..........gkkg..........gk",
"kkkkkkkkkkkkkkkkkkkkkkkkkkkk",
"kkrrkkkkkkkkkkkkkkkkkkkkrrkk",
],
{
"k": (30, 30, 34),
"g": (56, 56, 62),
"o": (80, 76, 70), # speaker cones
"r": (200, 60, 50), # power lamps
},
)
S["van"] = (
[
"..........wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww..........",
"........wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww........",
".......wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.......",
".......wggggggwwwwwwwwwwwwwwwwwwwwwwwwwwwggggggww.......",
".......wggggggwwwwwwwwwwwwwwwwwwwwwwwwwwwggggggww.......",
".......wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.......",
".......wwwwkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkwwwww.......",
".......wwwwkNOMEANSNOwwwwwwwwwwwwwwwwwwwwwwkwwwww.......",
".......wwwwkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkkwwwww.......",
".......wwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwwww.......",
"........bb..tttt..bbbbbbbbbbbbbbbbbb..tttt..bb...........",
"........bb.tttttt.bbbbbbbbbbbbbbbbbb.tttttt.bb...........",
"............tttt......................tttt..............",
],
{
"w": (216, 214, 206),
"g": (110, 140, 160), # windows
"k": (60, 60, 66), # hand-painted band stripe
"N": (220, 60, 50),
"O": (220, 60, 50),
"M": (220, 60, 50),
"E": (220, 60, 50),
"A": (220, 60, 50),
"S": (220, 60, 50),
"b": (70, 70, 76),
"t": (30, 30, 34),
},
)
S["dumpster"] = (
[
"gggggggggggggggggggggggggggggggggggg",
"ggllllllllllllllllllllllllllllllllgg",
"gg..............................gg..",
"ggllllllllllllllllllllllllllllllgg..",
"gg..............................gg..",
"gggggggggggggggggggggggggggggggggg..",
"..kk..........................kk....",
"..kk..........................kk....",
],
{"g": (60, 90, 70), "l": (80, 112, 88), "k": (30, 30, 34)},
)
def main():
os.makedirs(OUT, exist_ok=True)
print("writing sprites:")
for name, (rows, legend) in S.items():
# People and props render 2x (320-wide rooms); portraits and the
# tiny flyer stay 1:1.
scale = 1 if name.endswith("-face") or name == "flyer" else 2
write_png(os.path.join(OUT, f"{name}.png"), rows, legend, scale)
if __name__ == "__main__":
main()