#!/usr/bin/env python3
"""
Generate SVG vector assets for the *Rats!* game.
Each original 20x20 PNG is reproduced as a clean scalable SVG with smooth
vector shapes, proper shading, and a 64x64 design grid (wider for UI banners).
Output: assets/Rat/svg/
"""
import os, math
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
OUT_DIR = os.path.join(BASE_DIR, "assets", "Rat", "svg")
os.makedirs(OUT_DIR, exist_ok=True)
# ─── low-level helpers ────────────────────────────────────────────────────────
def save(name: str, svg: str) -> None:
path = os.path.join(OUT_DIR, f"{name}.svg")
with open(path, "w", encoding="utf-8") as fh:
fh.write(svg)
def _build_svg(vw: int, vh: int, defs: str, body: str,
sw: int = None, sh: int = None) -> str:
"""Assemble a complete SVG document."""
w = sw or vw * 4
h = sh or vh * 4
defs_block = f"\n \n{defs}\n " if defs else ""
return (f'\n'
f'\n')
def mk(vw: int, vh: int, body: str, defs: str = "",
sw: int = None, sh: int = None) -> str:
return _build_svg(vw, vh, defs, body, sw, sh)
# ─── colour palettes ──────────────────────────────────────────────────────────
# player: body, highlight, shadow, ear-inner, tail
PC = {
1: ("#33DD33", "#88FF88", "#1A941A", "#FF9999", "#116611"),
2: ("#FFE033", "#FFF0AA", "#B8920A", "#FFB8B8", "#7A6008"),
3: ("#AAAACC", "#CCCCEE", "#555577", "#FFBBCC", "#333355"),
4: ("#EE4444", "#FF9090", "#AA1111", "#FFAAAA", "#771111"),
}
# terrain: bg, accent, dark, bright
TC = {
1: ("#1ECC1E", "#28DD28", "#128012", "#88FF88"), # green
2: ("#DDB800", "#FFD700", "#8A7000", "#FFE860"), # yellow
3: ("#666688", "#7777AA", "#333355", "#9999BB"), # slate
4: ("#CC2222", "#EE3333", "#881111", "#FF6666"), # red
}
# ─── rat shape (facing East, 64×64 viewBox) ──────────────────────────────────
def _rat_defs() -> str:
return "" # no gradient defs needed (simpler look)
def _rat_body(bc: str, hi: str, sh: str, ec: str, tc: str) -> str:
"""SVG group innards for a rat facing East (→) in a 64×64 canvas."""
return f"""\
"""
def rat_svg(player: int, angle: float, frame: int = 0) -> str:
bc, hi, sh, ec, tc = PC[player]
tr = f' transform="rotate({angle:.0f} 32 32)"' if angle else ""
# frame=1: offset feet slightly for walk-cycle variety
body = _rat_body(bc, hi, sh, ec, tc)
return mk(64, 64,
f' \n{body}\n ',
sw=128, sh=128)
# direction name → rotation angle (East=0°, clockwise positive in SVG)
DIRS_8 = {
"E": 0, "NE": -45, "N": -90, "NW": -135,
"W": 180, "WS": 135, "S": 90, "SE": 45,
}
# animation-frame aliases (same angle, second walk frame)
DIRS_ALT = {"EN": -45, "ES": 45, "WN": -135, "SW": 135}
ALL_DIRS = {**DIRS_8, **DIRS_ALT}
CARD4_ANGLES = {"DOWN": 90, "UP": -90, "LEFT": 180, "RIGHT": 0}
# ── generate all player-rat sprites ──────────────────────────────────────────
def _gas_overlay() -> str:
return """\
☠"""
def _explosion_overlay() -> str:
return """\
"""
def _cave_overlay() -> str:
return """\
"""
for p_num in range(1, 5):
bc, hi, sh, ec, tc = PC[p_num]
# 12-direction movement sprites
for dir_name, angle in ALL_DIRS.items():
tr = f' transform="rotate({angle:.0f} 32 32)"' if angle else ""
body = _rat_body(bc, hi, sh, ec, tc)
save(f"BMP_{p_num}_{dir_name}",
mk(64, 64, f' \n{body}\n ', sw=128, sh=128))
# CAVE, GAS, EXPLOSION (4 cardinal directions)
for dir_name, angle in CARD4_ANGLES.items():
tr = f' transform="rotate({angle:.0f} 32 32)"' if angle else ""
rat_grp = f' \n{_rat_body(bc, hi, sh, ec, tc)}\n '
for suffix, overlay in [("CAVE", _cave_overlay()),
("GAS", _gas_overlay()),
("EXPLOSION", _explosion_overlay())]:
save(f"BMP_{p_num}_{suffix}_{dir_name}",
mk(64, 64, rat_grp + "\n" + overlay, sw=128, sh=128))
# ─── MALE enemy rat ──────────────────────────────────────────────────────────
MB = "#5588EE"; MH = "#99BBFF"; MS = "#2244AA"; ME = "#FFB8C8"; MT = "#1A2F7A"
def _male_icon() -> str:
return """\
"""
save("BMP_MALE",
mk(64, 64,
f' \n{_rat_body(MB, MH, MS, ME, MT)}\n \n{_male_icon()}',
sw=128, sh=128))
for d, a in CARD4_ANGLES.items():
tr = f' transform="rotate({a:.0f} 32 32)"' if a else ""
save(f"BMP_MALE_{d}",
mk(64, 64, f' \n{_rat_body(MB, MH, MS, ME, MT)}\n ',
sw=128, sh=128))
# ─── FEMALE enemy rat ────────────────────────────────────────────────────────
FB = "#DD55BB"; FH = "#FF99DD"; FS = "#992288"; FE = "#FFCCEE"; FT = "#661155"
def _female_icon() -> str:
return """\
"""
save("BMP_FEMALE",
mk(64, 64,
f' \n{_rat_body(FB, FH, FS, FE, FT)}\n \n{_female_icon()}',
sw=128, sh=128))
for d, a in CARD4_ANGLES.items():
tr = f' transform="rotate({a:.0f} 32 32)"' if a else ""
save(f"BMP_FEMALE_{d}",
mk(64, 64, f' \n{_rat_body(FB, FH, FS, FE, FT)}\n ',
sw=128, sh=128))
# ─── BABY rat (rounder, cuter) ───────────────────────────────────────────────
def _baby_body() -> str:
return """\
"""
for d, a in CARD4_ANGLES.items():
tr = f' transform="rotate({a:.0f} 32 32)"' if a else ""
save(f"BMP_BABY_{d}",
mk(64, 64, f' \n{_baby_body()}\n ', sw=128, sh=128))
# ─── BLOCKS (wall/barrier tiles, 0=intact to 3=heavily damaged) ──────────────
BLOCK_CRACKS = [
"",
' ',
' \n'
' \n'
' ',
' \n'
' \n'
' \n'
' \n'
' ',
]
for i in range(4):
inner_fill = "#FFDDDD" if i == 3 else "#FFFFFF"
body = f"""\
{BLOCK_CRACKS[i]}"""
save(f"BMP_BLOCK_{i}", mk(64, 64, body, sw=128, sh=128))
# ─── BOMB ────────────────────────────────────────────────────────────────────
BOMB_DEFS = """\
"""
def bomb_body(number=None) -> str:
glow = (f' \n'
if number is not None else "")
num_svg = ""
if number is not None:
num_svg = (f' \n'
f' {number}\n')
return f"""{glow}\
{num_svg}"""
save("BMP_BOMB0", mk(64, 64, bomb_body(), BOMB_DEFS, sw=128, sh=128))
for i in range(1, 5):
save(f"BMP_BOMB{i}", mk(64, 64, bomb_body(i), BOMB_DEFS, sw=128, sh=128))
# ─── EXPLOSION ───────────────────────────────────────────────────────────────
EXPL_DEFS = """\
"""
expl_center_body = """\
"""
save("BMP_EXPLOSION", mk(64, 64, expl_center_body, EXPL_DEFS, sw=128, sh=128))
# directional explosion: flame pointing UP then rotated
expl_dir_body = """\
"""
# UP=0°, RIGHT=90°, DOWN=180°, LEFT=270° (flame points toward direction, starts from centre)
for d, a in {"UP": 0, "RIGHT": 90, "DOWN": 180, "LEFT": 270}.items():
tr = f' transform="rotate({a} 32 32)"' if a else ""
save(f"BMP_EXPLOSION_{d}",
mk(64, 64, f' \n{expl_dir_body}\n ',
EXPL_DEFS, sw=128, sh=128))
# ─── GAS ─────────────────────────────────────────────────────────────────────
GAS_DEFS = """\
"""
gas_center_body = """\
☠"""
save("BMP_GAS", mk(64, 64, gas_center_body, GAS_DEFS, sw=128, sh=128))
gas_dir_body = """\
"""
for d, a in {"UP": 0, "RIGHT": 90, "DOWN": 180, "LEFT": 270}.items():
tr = f' transform="rotate({a} 32 32)"' if a else ""
save(f"BMP_GAS_{d}",
mk(64, 64, f' \n{gas_dir_body}\n ',
GAS_DEFS, sw=128, sh=128))
# ─── TUNNEL ──────────────────────────────────────────────────────────────────
save("BMP_TUNNEL", mk(64, 64, """\
""",
sw=128, sh=128))
# ─── NUCLEAR ─────────────────────────────────────────────────────────────────
def _trefoil_blade(cx, cy, r_in, r_out, angle_deg):
a = math.radians(angle_deg)
hw = math.radians(30) # half-width of each blade sector (60° wide blades, 60° gaps)
a1, a2 = a - hw, a + hw
ix1, iy1 = cx + r_in * math.cos(a1), cy + r_in * math.sin(a1)
ix2, iy2 = cx + r_in * math.cos(a2), cy + r_in * math.sin(a2)
ox1, oy1 = cx + r_out * math.cos(a1), cy + r_out * math.sin(a1)
ox2, oy2 = cx + r_out * math.cos(a2), cy + r_out * math.sin(a2)
return (f"M {ix1:.2f},{iy1:.2f} "
f"L {ox1:.2f},{oy1:.2f} "
f"A {r_out},{r_out} 0 0,1 {ox2:.2f},{oy2:.2f} "
f"L {ix2:.2f},{iy2:.2f} "
f"A {r_in},{r_in} 0 0,0 {ix1:.2f},{iy1:.2f} Z")
blades = [_trefoil_blade(32, 32, 8, 27, a) for a in [-90, 30, 150]]
blade_paths = "\n ".join(f'' for p in blades)
save("BMP_NUCLEAR", mk(64, 64, f"""\
{blade_paths}
""",
sw=128, sh=128))
# ─── POISON (skull & crossbones) ─────────────────────────────────────────────
save("BMP_POISON", mk(64, 64, """\
""",
sw=128, sh=128))
# ─── MINE ────────────────────────────────────────────────────────────────────
MINE_DEFS = """\
"""
spike_lines = "\n ".join(
f''
for a in range(0, 360, 45)
)
spike_caps = "\n ".join(
f''
for a in range(0, 360, 45)
)
save("mine", mk(64, 64, f"""\
{spike_lines}
{spike_caps}
""",
MINE_DEFS, sw=128, sh=128))
# ─── GRASS TILES (4 players × 4 variants) ────────────────────────────────────
# Blade coords per variant: list of (x1,y1,x2,y2)
_BLADE_SETS = {
1: [(12, 64, 14, 44), (20, 64, 22, 40), (10, 64, 6, 43)],
2: [(30, 64, 28, 38), (24, 64, 20, 42), (38, 64, 40, 42)],
3: [(48, 64, 50, 40), (44, 64, 42, 44), (54, 64, 58, 42)],
4: [(20, 64, 16, 40), (36, 64, 38, 38), (44, 64, 48, 44), (10, 64, 12, 46)],
}
def grass_svg(player: int, variant: int) -> str:
bg, ac, dk, br = TC[player]
blades = "\n".join(
f' '
for x1, y1, x2, y2 in _BLADE_SETS.get(variant, _BLADE_SETS[1])
)
# variant speckle
sx = variant * 13 % 50 + 6
sy = variant * 11 % 50 + 6
# Player 3: distinctive diagonal hatch pattern
p3_hatch = ""
p3_defs = ""
if player == 3:
pid = f"hatch3v{variant}"
p3_defs = f'''
'''
p3_hatch = f' '
body = f"""\
{p3_hatch}
{blades}"""
return mk(64, 64, body, p3_defs, sw=128, sh=128)
for p in range(1, 5):
for v in range(1, 5):
save(f"BMP_{p}_GRASS_{v}", grass_svg(p, v))
# ─── FLOWER TILES (4 players × 4 variants) ───────────────────────────────────
_FLOWER_COLORS = {
1: ("#FFFFFF", "#FFEE88"), # white petals, yellow centre
2: ("#FFCCAA", "#FF8800"), # peach petals, orange centre
3: ("#CCDDFF", "#FFDD00"), # pale-blue petals, gold centre
4: ("#FFAACC", "#FF4466"), # pink petals, hot-pink centre
}
def flower_svg(player: int, variant: int) -> str:
bg, ac, dk, br = TC[player]
pc, cc = _FLOWER_COLORS[variant]
# offset flower position per variant
ox = [32, 22, 42, 28][variant - 1]
oy = [32, 36, 28, 40][variant - 1]
# 8 petals
petals = "\n".join(
f' '
for a in range(0, 360, 45)
)
body = f"""\
{petals}
"""
return mk(64, 64, body, sw=128, sh=128)
for p in range(1, 5):
for v in range(1, 5):
save(f"BMP_{p}_FLOWER_{v}", flower_svg(p, v))
# ─── CAVE TILES (player-colored cave entrance, 4 cardinal dirs) ──────────────
def cave_tile_svg(player: int, direction: str) -> str:
"""Rat entering cave – large dark hole with player colour rim, facing dir."""
bg, ac, dk, br = TC[player]
angle = CARD4_ANGLES[direction]
# Opening faces the direction the rat exits toward
tr = f' transform="rotate({angle} 32 32)"' if angle else ""
body = f"""\
"""
return mk(64, 64, body, sw=128, sh=128)
for p in range(1, 5):
for d in ("DOWN", "UP", "LEFT", "RIGHT"):
save(f"BMP_{p}_CAVE_{d}", cave_tile_svg(p, d))
# ─── ARROWS (UI elements) ─────────────────────────────────────────────────────
def arrow_svg(direction: str) -> str:
"""Bold arrow; designed pointing UP, rotated."""
a = {"UP": 0, "DOWN": 180, "RIGHT": 90, "LEFT": 270}[direction]
tr = f' transform="rotate({a} 32 32)"' if a else ""
body = f"""\
"""
return mk(64, 64, body, sw=128, sh=128)
for d in ("DOWN", "UP", "LEFT", "RIGHT"):
save(f"BMP_ARROW_{d}", arrow_svg(d))
# ─── TITLE ("Rats!") 100×40 original → 200×80 viewBox ───────────────────────
save("BMP_TITLE", mk(200, 80, """\
Rats!
Rats!""",
sw=400, sh=160))
# ─── VERMINATORS banner ───────────────────────────────────────────────────────
save("BMP_VERMINATORS", mk(200, 32, """\
Top Verminators:""",
sw=400, sh=64))
# ─── WEWIN screen ─────────────────────────────────────────────────────────────
# Original showed a blue/purple mouse character with wings or limbs
save("BMP_WEWIN", mk(64, 64, f"""\
""",
sw=128, sh=128))
# ─── START tiles ─────────────────────────────────────────────────────────────
# Original revealed unique art per player:
# P1: large white flower on green background
# P2: cactus on yellow background
# P3: penguin on gray-blue hatched background
# P4: flame pattern on red background
def start_svg(player: int, state: str = "normal") -> str:
bg, ac, dk, br = TC[player]
opacity = "0.45" if state == "shaded" else "1"
border_w = "5" if state == "down" else "4"
decorations = {
1: f"""\
{''.join(
f''
for a in range(0, 360, 45)
)}
""",
2: f"""\
""",
3: f"""\
""",
4: f"""\
""",
}
body = f"""\
{decorations[player]}"""
return mk(64, 64, body, sw=128, sh=128)
for p in range(1, 5):
save(f"BMP_START_{p}", start_svg(p, "normal"))
save(f"BMP_START_{p}_DOWN", start_svg(p, "down"))
save(f"BMP_START_{p}_SHADED", start_svg(p, "shaded"))
# ─── BONUS numbers ────────────────────────────────────────────────────────────
BONUS_COLORS = {
5: ("#FFEEAA", "#AA8800"),
10: ("#AAFFAA", "#008800"),
20: ("#AADDFF", "#0044AA"),
40: ("#FFAACC", "#AA0044"),
80: ("#FFCCAA", "#AA4400"),
160: ("#DDAAFF", "#6600AA"),
}
for val, (fg, sh) in BONUS_COLORS.items():
fs = 28 if val < 100 else 22
ty = 34 + (33 - fs)
body = f"""\
{val}
{val}"""
save(f"BMP_BONUS_{val}", mk(64, 64, body, sw=128, sh=128))
# ─── done ─────────────────────────────────────────────────────────────────────
files = [f for f in os.listdir(OUT_DIR) if f.endswith(".svg")]
print(f"Generated {len(files)} SVG files in {OUT_DIR}")