Files
mice/tools/generate_svgs.py
T
enne2 02202e4d3d Add new PNG images for clean and preview outputs
- Added `image_clean.png` to the output directory for the clean image representation.
- Added `image_clean_preview.png` for the preview of the clean image.
- Introduced `image_svg_clean.png` for the SVG clean image representation.
2026-03-27 23:40:27 +01:00

865 lines
39 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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 <defs>\n{defs}\n </defs>" if defs else ""
return (f'<?xml version="1.0" encoding="UTF-8"?>\n'
f'<svg xmlns="http://www.w3.org/2000/svg" '
f'width="{w}" height="{h}" viewBox="0 0 {vw} {vh}">'
f'{defs_block}\n{body}\n</svg>\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"""\
<!-- Tail: curves gently upward from rear -->
<path d="M14,40 Q5,36 4,28 Q3,19 13,17"
fill="none" stroke="{tc}" stroke-width="2.8"
stroke-linecap="round"/>
<!-- Body -->
<ellipse cx="34" cy="40" rx="20" ry="12" fill="{bc}"/>
<ellipse cx="34" cy="36" rx="15" ry="7" fill="{hi}" opacity="0.30"/>
<ellipse cx="34" cy="46" rx="16" ry="6" fill="{sh}" opacity="0.25"/>
<!-- Neck -->
<ellipse cx="50" cy="36" rx="8" ry="9" fill="{bc}"/>
<!-- Head -->
<ellipse cx="55" cy="31" rx="12" ry="11" fill="{bc}"/>
<ellipse cx="53" cy="27" rx="7" ry="5" fill="{hi}" opacity="0.30"/>
<!-- Ear outer -->
<ellipse cx="52" cy="21" rx="7" ry="6.5" fill="{sh}"/>
<!-- Ear inner -->
<ellipse cx="52" cy="21" rx="4.5" ry="4" fill="{ec}"/>
<!-- Snout -->
<ellipse cx="65" cy="34" rx="7" ry="5.5" fill="{sh}"/>
<!-- Nose -->
<ellipse cx="71" cy="33.5" rx="3.5" ry="2.8" fill="#CC5566"/>
<!-- Nose highlight -->
<circle cx="70" cy="32.5" r="1.1" fill="white" opacity="0.6"/>
<!-- Eye -->
<circle cx="60" cy="27" r="3.2" fill="{tc}"/>
<circle cx="61" cy="26" r="1.3" fill="white"/>
<!-- Whiskers (3 lines from snout) -->
<line x1="63" y1="31" x2="76" y2="27" stroke="{sh}" stroke-width="0.8" stroke-linecap="round" opacity="0.8"/>
<line x1="63" y1="33" x2="76" y2="33" stroke="{sh}" stroke-width="0.8" stroke-linecap="round" opacity="0.8"/>
<line x1="63" y1="35" x2="76" y2="39" stroke="{sh}" stroke-width="0.8" stroke-linecap="round" opacity="0.8"/>
<!-- Front foot near -->
<ellipse cx="52" cy="50" rx="7" ry="4" fill="{sh}"/>
<ellipse cx="57" cy="54" rx="4.5" ry="2.8" fill="{sh}"/>
<!-- Front foot far (slightly darker) -->
<ellipse cx="43" cy="51" rx="6" ry="3.5" fill="{tc}" opacity="0.45"/>
<!-- Rear foot near -->
<ellipse cx="22" cy="50" rx="7" ry="4" fill="{sh}"/>
<ellipse cx="17" cy="54" rx="4.5" ry="2.8" fill="{sh}"/>
<!-- Rear foot far -->
<ellipse cx="30" cy="51" rx="6" ry="3.5" fill="{tc}" opacity="0.45"/>"""
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' <g{tr}>\n{body}\n </g>',
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 """\
<!-- Gas cloud overlay -->
<circle cx="32" cy="32" r="26" fill="#44BB33" opacity="0.45"/>
<circle cx="22" cy="24" r="9" fill="#66CC44" opacity="0.40"/>
<circle cx="40" cy="20" r="8" fill="#55BB33" opacity="0.35"/>
<circle cx="44" cy="32" r="10" fill="#44AA22" opacity="0.35"/>
<text x="32" y="37" font-family="sans-serif" font-size="16"
text-anchor="middle" fill="#113300" opacity="0.5">☠</text>"""
def _explosion_overlay() -> str:
return """\
<!-- Explosion overlay -->
<circle cx="32" cy="32" r="28" fill="#FF6600" opacity="0.45"/>
<polygon points="32,14 35,28 48,26 37,36 44,50 32,42 20,50 27,36 16,26 29,28"
fill="#FFCC00" opacity="0.6"/>
<circle cx="32" cy="32" r="10" fill="#FFFFFF" opacity="0.5"/>"""
def _cave_overlay() -> str:
return """\
<!-- Cave hole overlay -->
<ellipse cx="32" cy="38" rx="22" ry="14" fill="#222222" opacity="0.80"/>
<ellipse cx="32" cy="38" rx="16" ry="10" fill="#111111" opacity="0.90"/>
<ellipse cx="32" cy="39" rx="11" ry="7" fill="#000000"/>"""
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' <g{tr}>\n{body}\n </g>', 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' <g{tr}>\n{_rat_body(bc, hi, sh, ec, tc)}\n </g>'
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 """\
<!-- Male (♂) symbol -->
<circle cx="54" cy="11" r="5" fill="none" stroke="#5588EE" stroke-width="2"/>
<line x1="58" y1="7" x2="64" y2="2" stroke="#5588EE" stroke-width="2" stroke-linecap="round"/>
<line x1="59" y1="2" x2="64" y2="2" stroke="#5588EE" stroke-width="2" stroke-linecap="round"/>
<line x1="64" y1="2" x2="64" y2="7" stroke="#5588EE" stroke-width="2" stroke-linecap="round"/>"""
save("BMP_MALE",
mk(64, 64,
f' <g>\n{_rat_body(MB, MH, MS, ME, MT)}\n </g>\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' <g{tr}>\n{_rat_body(MB, MH, MS, ME, MT)}\n </g>',
sw=128, sh=128))
# ─── FEMALE enemy rat ────────────────────────────────────────────────────────
FB = "#DD55BB"; FH = "#FF99DD"; FS = "#992288"; FE = "#FFCCEE"; FT = "#661155"
def _female_icon() -> str:
return """\
<!-- Female (♀) symbol -->
<circle cx="54" cy="11" r="5" fill="none" stroke="#DD55BB" stroke-width="2"/>
<line x1="54" y1="16" x2="54" y2="22" stroke="#DD55BB" stroke-width="2"/>
<line x1="50" y1="19" x2="58" y2="19" stroke="#DD55BB" stroke-width="2"/>"""
save("BMP_FEMALE",
mk(64, 64,
f' <g>\n{_rat_body(FB, FH, FS, FE, FT)}\n </g>\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' <g{tr}>\n{_rat_body(FB, FH, FS, FE, FT)}\n </g>',
sw=128, sh=128))
# ─── BABY rat (rounder, cuter) ───────────────────────────────────────────────
def _baby_body() -> str:
return """\
<!-- Baby rat facing East -->
<!-- Tail -->
<path d="M16,40 Q9,36 9,30 Q9,22 17,20"
fill="none" stroke="#223377" stroke-width="2" stroke-linecap="round"/>
<!-- Body (rounder) -->
<circle cx="34" cy="42" r="13" fill="#6699EE"/>
<ellipse cx="34" cy="38" rx="9" ry="6" fill="#AACCFF" opacity="0.30"/>
<!-- Head (large relative to body - baby proportions) -->
<circle cx="50" cy="33" r="13" fill="#6699EE"/>
<ellipse cx="48" cy="28" rx="8" ry="5" fill="#AACCFF" opacity="0.28"/>
<!-- Ear -->
<ellipse cx="47" cy="21" rx="8" ry="7" fill="#3355AA"/>
<ellipse cx="47" cy="21" rx="5" ry="4" fill="#FFBBCC"/>
<!-- Snout -->
<ellipse cx="62" cy="36" rx="7" ry="5.5" fill="#3355AA"/>
<ellipse cx="68" cy="36" rx="3.5" ry="2.8" fill="#CC5566"/>
<!-- Eye (big, cute) -->
<circle cx="56" cy="29" r="4.5" fill="#000033"/>
<circle cx="57.5" cy="27.5" r="1.8" fill="white"/>
<circle cx="55.5" cy="30.5" r="1.0" fill="white" opacity="0.4"/>
<!-- Front foot -->
<ellipse cx="52" cy="52" rx="7" ry="4" fill="#3355AA"/>
<ellipse cx="57" cy="56" rx="4" ry="2.5" fill="#3355AA"/>
<!-- Rear foot -->
<ellipse cx="22" cy="52" rx="7" ry="4" fill="#3355AA"/>
<ellipse cx="17" cy="56" rx="4" ry="2.5" fill="#3355AA"/>"""
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' <g{tr}>\n{_baby_body()}\n </g>', sw=128, sh=128))
# ─── BLOCKS (wall/barrier tiles, 0=intact to 3=heavily damaged) ──────────────
BLOCK_CRACKS = [
"",
' <line x1="18" y1="18" x2="28" y2="36" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>',
' <line x1="18" y1="18" x2="28" y2="36" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>\n'
' <line x1="40" y1="14" x2="34" y2="34" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>\n'
' <line x1="14" y1="42" x2="30" y2="40" stroke="#660000" stroke-width="2.5" stroke-linecap="round"/>',
' <line x1="18" y1="18" x2="28" y2="36" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n'
' <line x1="40" y1="14" x2="34" y2="34" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n'
' <line x1="14" y1="42" x2="30" y2="40" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n'
' <line x1="44" y1="36" x2="36" y2="50" stroke="#660000" stroke-width="3" stroke-linecap="round"/>\n'
' <line x1="22" y1="46" x2="32" y2="54" stroke="#660000" stroke-width="3" stroke-linecap="round"/>',
]
for i in range(4):
inner_fill = "#FFDDDD" if i == 3 else "#FFFFFF"
body = f"""\
<!-- Block damage level {i} -->
<rect x="1" y="1" width="62" height="62" rx="6" fill="#CC3333"/>
<rect x="3" y="3" width="58" height="58" rx="4" fill="#881111" opacity="0.45"/>
<!-- Centre ring -->
<circle cx="32" cy="32" r="23" fill="{inner_fill}" opacity="0.92"/>
<circle cx="32" cy="32" r="18" fill="#CC3333" opacity="0.55"/>
<!-- Corner highlight -->
<line x1="5" y1="5" x2="16" y2="5" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
<line x1="5" y1="5" x2="5" y2="16" stroke="white" stroke-width="2" stroke-linecap="round" opacity="0.45"/>
{BLOCK_CRACKS[i]}"""
save(f"BMP_BLOCK_{i}", mk(64, 64, body, sw=128, sh=128))
# ─── BOMB ────────────────────────────────────────────────────────────────────
BOMB_DEFS = """\
<radialGradient id="bomb_g" cx="38%" cy="33%" r="60%">
<stop offset="0%" stop-color="#666666"/>
<stop offset="55%" stop-color="#2A2A2A"/>
<stop offset="100%" stop-color="#000000"/>
</radialGradient>"""
def bomb_body(number=None) -> str:
glow = (f' <circle cx="32" cy="36" r="27" fill="#FF6600" opacity="0.28"/>\n'
if number is not None else "")
num_svg = ""
if number is not None:
num_svg = (f' <circle cx="32" cy="36" r="13" fill="#FF8800" opacity="0.55"/>\n'
f' <text x="32" y="41" font-family="Arial Black,sans-serif" '
f'font-size="13" font-weight="bold" text-anchor="middle" '
f'fill="white" stroke="#660000" stroke-width="0.8">{number}</text>\n')
return f"""{glow}\
<!-- Fuse -->
<path d="M32,14 Q41,7 39,16 Q37,22 32,20"
fill="none" stroke="#886633" stroke-width="2.5" stroke-linecap="round"/>
<!-- Fuse spark -->
<circle cx="39" cy="13" r="3.2" fill="#FFEE22"/>
<circle cx="40" cy="12" r="1.5" fill="white" opacity="0.75"/>
<!-- Bomb shell -->
<circle cx="32" cy="36" r="22" fill="#1A1A1A"/>
<circle cx="32" cy="36" r="21" fill="url(#bomb_g)"/>
<!-- Shine -->
<ellipse cx="23" cy="26" rx="7" ry="5" fill="white" opacity="0.22"/>
{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 = """\
<radialGradient id="expl_c" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#FFFFFF"/>
<stop offset="30%" stop-color="#FFFF00"/>
<stop offset="65%" stop-color="#FF8800"/>
<stop offset="100%" stop-color="#FF2200" stop-opacity="0"/>
</radialGradient>
<radialGradient id="expl_d" cx="50%" cy="80%" r="60%">
<stop offset="0%" stop-color="#FFFF88"/>
<stop offset="50%" stop-color="#FF8800"/>
<stop offset="100%" stop-color="#FF2200" stop-opacity="0"/>
</radialGradient>"""
expl_center_body = """\
<!-- Outer glow -->
<circle cx="32" cy="32" r="30" fill="#FF4400" opacity="0.28"/>
<!-- 8-point star burst -->
<polygon points="32,4 35,26 56,18 42,34 56,46 35,38 40,60 32,44 24,60 29,38 8,46 22,34 8,18 29,26"
fill="#FFCC00" opacity="0.88"/>
<!-- Core gradient disc -->
<circle cx="32" cy="32" r="19" fill="url(#expl_c)"/>"""
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 = """\
<!-- Directional flame pointing UP then rotated -->
<path d="M32,54 Q18,42 20,22 Q24,7 32,3 Q40,7 44,22 Q46,42 32,54Z"
fill="url(#expl_d)"/>
<path d="M32,50 Q24,40 26,24 Q29,14 32,10 Q35,14 38,24 Q40,40 32,50Z"
fill="#FFEE88" opacity="0.75"/>
<circle cx="32" cy="32" r="9" fill="#FFFF00" opacity="0.88"/>"""
# 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' <g{tr}>\n{expl_dir_body}\n </g>',
EXPL_DEFS, sw=128, sh=128))
# ─── GAS ─────────────────────────────────────────────────────────────────────
GAS_DEFS = """\
<radialGradient id="gas_c" cx="50%" cy="50%" r="50%">
<stop offset="0%" stop-color="#AAFF77"/>
<stop offset="65%" stop-color="#44AA22"/>
<stop offset="100%" stop-color="#44AA22" stop-opacity="0"/>
</radialGradient>
<radialGradient id="gas_d" cx="50%" cy="70%" r="60%">
<stop offset="0%" stop-color="#CCFF99"/>
<stop offset="55%" stop-color="#44AA22"/>
<stop offset="100%" stop-color="#44AA22" stop-opacity="0"/>
</radialGradient>"""
gas_center_body = """\
<!-- Gas puff cloud -->
<circle cx="32" cy="32" r="29" fill="url(#gas_c)" opacity="0.80"/>
<circle cx="22" cy="24" r="12" fill="#88EE44" opacity="0.65"/>
<circle cx="40" cy="20" r="10" fill="#77DD33" opacity="0.60"/>
<circle cx="46" cy="35" r="12" fill="#66CC22" opacity="0.62"/>
<circle cx="24" cy="42" r="10" fill="#88EE44" opacity="0.58"/>
<!-- Chemical symbol hint -->
<text x="32" y="38" font-family="monospace" font-size="17"
text-anchor="middle" fill="#0A3300" opacity="0.45" font-weight="bold">☠</text>"""
save("BMP_GAS", mk(64, 64, gas_center_body, GAS_DEFS, sw=128, sh=128))
gas_dir_body = """\
<!-- Gas flowing upward (rotated for each direction) -->
<ellipse cx="32" cy="32" rx="15" ry="27" fill="url(#gas_d)" opacity="0.75"/>
<circle cx="24" cy="16" r="9" fill="#88DD44" opacity="0.60"/>
<circle cx="40" cy="11" r="8" fill="#77CC33" opacity="0.55"/>
<circle cx="32" cy="6" r="10" fill="#66BB22" opacity="0.50"/>
<circle cx="32" cy="42" r="10" fill="#44AA22" opacity="0.45"/>"""
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' <g{tr}>\n{gas_dir_body}\n </g>',
GAS_DEFS, sw=128, sh=128))
# ─── TUNNEL ──────────────────────────────────────────────────────────────────
save("BMP_TUNNEL", mk(64, 64, """\
<!-- Stone/earth surround -->
<rect x="0" y="0" width="64" height="64" rx="4" fill="#888888"/>
<rect x="2" y="2" width="60" height="60" rx="3" fill="#AAAAAA"/>
<!-- Ground ring -->
<ellipse cx="32" cy="40" rx="26" ry="17" fill="#777777"/>
<ellipse cx="32" cy="40" rx="22" ry="13" fill="#333333"/>
<ellipse cx="32" cy="40" rx="17" ry="10" fill="#1A1A1A"/>
<ellipse cx="32" cy="40" rx="12" ry="7" fill="#000000"/>
<!-- Ground highlight -->
<ellipse cx="26" cy="30" rx="9" ry="5" fill="white" opacity="0.22"/>
<!-- Stone texture flecks -->
<rect x="8" y="8" width="5" height="5" rx="1" fill="#999999" opacity="0.5"/>
<rect x="50" y="12" width="4" height="4" rx="1" fill="#999999" opacity="0.5"/>
<rect x="10" y="50" width="4" height="4" rx="1" fill="#999999" opacity="0.5"/>
<rect x="50" y="48" width="5" height="5" rx="1" fill="#999999" opacity="0.5"/>""",
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'<path d="{p}" fill="black"/>' for p in blades)
save("BMP_NUCLEAR", mk(64, 64, f"""\
<!-- Yellow background disc -->
<circle cx="32" cy="32" r="31" fill="#FFDD00"/>
<circle cx="32" cy="32" r="29" fill="#FFE800"/>
<!-- Three trefoil blades -->
{blade_paths}
<!-- Centre yellow circle -->
<circle cx="32" cy="32" r="7" fill="#FFDD00"/>
<!-- Centre black dot -->
<circle cx="32" cy="32" r="4" fill="black"/>
<!-- Outer black ring -->
<circle cx="32" cy="32" r="31" fill="none" stroke="black" stroke-width="2"/>""",
sw=128, sh=128))
# ─── POISON (skull & crossbones) ─────────────────────────────────────────────
save("BMP_POISON", mk(64, 64, """\
<!-- Dark background -->
<rect x="0" y="0" width="64" height="64" fill="#111111"/>
<!-- Skull cranium -->
<ellipse cx="32" cy="25" rx="19" ry="17" fill="white"/>
<!-- Skull jaw -->
<rect x="22" y="34" width="20" height="11" rx="4" fill="white"/>
<!-- Eye sockets -->
<ellipse cx="24" cy="26" rx="5.5" ry="6.5" fill="#111111"/>
<ellipse cx="40" cy="26" rx="5.5" ry="6.5" fill="#111111"/>
<!-- Nose hole -->
<path d="M29,34 L32,30 L35,34 Z" fill="#111111"/>
<!-- Teeth gaps -->
<rect x="24" y="38" width="4" height="6" rx="2" fill="#111111"/>
<rect x="30" y="38" width="4" height="6" rx="2" fill="#111111"/>
<rect x="36" y="38" width="4" height="6" rx="2" fill="#111111"/>
<!-- Crossbones: two crossing bones -->
<line x1="8" y1="58" x2="56" y2="52" stroke="white" stroke-width="5" stroke-linecap="round"/>
<line x1="56" y1="58" x2="8" y2="52" stroke="white" stroke-width="5" stroke-linecap="round"/>
<!-- Bone knobs -->
<circle cx="8" cy="58" r="5" fill="white"/>
<circle cx="56" cy="58" r="5" fill="white"/>
<circle cx="8" cy="52" r="5" fill="white"/>
<circle cx="56" cy="52" r="5" fill="white"/>""",
sw=128, sh=128))
# ─── MINE ────────────────────────────────────────────────────────────────────
MINE_DEFS = """\
<radialGradient id="mine_g" cx="40%" cy="35%" r="60%">
<stop offset="0%" stop-color="#777777"/>
<stop offset="100%" stop-color="#222222"/>
</radialGradient>"""
spike_lines = "\n ".join(
f'<line x1="32" y1="32" '
f'x2="{32+28*math.cos(math.radians(a)):.1f}" '
f'y2="{32+28*math.sin(math.radians(a)):.1f}" '
f'stroke="#2A2A2A" stroke-width="4" stroke-linecap="round"/>'
for a in range(0, 360, 45)
)
spike_caps = "\n ".join(
f'<circle cx="{32+28*math.cos(math.radians(a)):.1f}" '
f'cy="{32+28*math.sin(math.radians(a)):.1f}" r="4.5" fill="#1E1E1E"/>'
for a in range(0, 360, 45)
)
save("mine", mk(64, 64, f"""\
<!-- Spikes -->
{spike_lines}
{spike_caps}
<!-- Main body -->
<circle cx="32" cy="32" r="18" fill="url(#mine_g)"/>
<!-- Shine -->
<ellipse cx="26" cy="25" rx="5.5" ry="3.8" fill="white" opacity="0.20"/>
<!-- Red trigger button -->
<circle cx="32" cy="20" r="4.8" fill="#CC1111"/>
<circle cx="31" cy="19" r="2.2" fill="#FF5555" opacity="0.70"/>""",
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' <line x1="{x1}" y1="{y1}" x2="{x2}" y2="{y2}" '
f'stroke="{dk}" stroke-width="2.5" stroke-linecap="round"/>'
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'''
<pattern id="{pid}" width="8" height="8" patternUnits="userSpaceOnUse"
patternTransform="rotate(45)">
<line x1="0" y1="0" x2="0" y2="8" stroke="{dk}" stroke-width="3"
stroke-opacity="0.55"/>
</pattern>'''
p3_hatch = f' <rect x="0" y="0" width="64" height="64" fill="url(#{pid})"/>'
body = f"""\
<!-- Grass tile P{player} V{variant} -->
<rect x="0" y="0" width="64" height="64" fill="{bg}"/>
<rect x="0" y="0" width="64" height="64" fill="{ac}" opacity="0.20"/>
{p3_hatch}
<!-- Texture speckles -->
<circle cx="{sx}" cy="{sy}" r="2.5" fill="{dk}" opacity="0.30"/>
<circle cx="{64-sx}" cy="{64-sy}" r="2" fill="{br}" opacity="0.25"/>
<!-- Grass blades -->
{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' <ellipse cx="{ox + 11*math.cos(math.radians(a)):.1f}" '
f'cy="{oy + 11*math.sin(math.radians(a)):.1f}" '
f'rx="7" ry="4.5" fill="{pc}" '
f'transform="rotate({a:.0f} '
f'{ox + 11*math.cos(math.radians(a)):.1f} '
f'{oy + 11*math.sin(math.radians(a)):.1f})"/>'
for a in range(0, 360, 45)
)
body = f"""\
<!-- Flower tile P{player} V{variant} -->
<rect x="0" y="0" width="64" height="64" fill="{bg}"/>
{petals}
<!-- Flower centre -->
<circle cx="{ox}" cy="{oy}" r="8" fill="{cc}"/>
<circle cx="{ox-2}" cy="{oy-2}" r="3" fill="white" opacity="0.45"/>"""
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"""\
<!-- Cave tile P{player} dir={direction} -->
<rect x="0" y="0" width="64" height="64" fill="{bg}"/>
<g{tr}>
<!-- Arch stone surround -->
<ellipse cx="32" cy="44" rx="24" ry="18" fill="{dk}"/>
<!-- Tunnel mouth shadow -->
<ellipse cx="32" cy="44" rx="19" ry="14" fill="#1A1A1A"/>
<!-- Dark depths -->
<ellipse cx="32" cy="44" rx="14" ry="10" fill="#000000"/>
<!-- Player-coloured rim highlight -->
<ellipse cx="32" cy="44" rx="24" ry="18" fill="none"
stroke="{ac}" stroke-width="2"/>
<!-- Ground highlight -->
<ellipse cx="26" cy="34" rx="7" ry="4" fill="white" opacity="0.18"/>
</g>"""
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"""\
<g{tr}>
<!-- Arrow pointing UP -->
<!-- Shaft -->
<rect x="24" y="30" width="16" height="26" rx="3" fill="#444444"/>
<!-- Head -->
<polygon points="32,6 50,30 38,30 38,30 26,30 14,30" fill="#444444"/>
<!-- Highlights -->
<line x1="24" y1="30" x2="38" y2="30" stroke="white" stroke-width="1.2"
opacity="0.35" stroke-linecap="round"/>
<line x1="14" y1="30" x2="32" y2="6" stroke="white" stroke-width="1"
opacity="0.22" stroke-linecap="round"/>
</g>"""
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, """\
<!-- Title card -->
<rect x="0" y="0" width="200" height="80" fill="#FFE800"/>
<!-- Shadow text -->
<text x="103" y="61" font-family="Impact,Arial Black,sans-serif" font-size="58"
font-weight="bold" text-anchor="middle" fill="#8B0000" opacity="0.35">Rats!</text>
<!-- Main text (red) -->
<text x="100" y="58" font-family="Impact,Arial Black,sans-serif" font-size="58"
font-weight="bold" text-anchor="middle" fill="#CC1111"
stroke="#880000" stroke-width="2" paint-order="stroke">Rats!</text>""",
sw=400, sh=160))
# ─── VERMINATORS banner ───────────────────────────────────────────────────────
save("BMP_VERMINATORS", mk(200, 32, """\
<!-- Score banner -->
<rect x="0" y="0" width="200" height="32" fill="#001122"/>
<rect x="1" y="1" width="198" height="30" fill="#002244"/>
<!-- Cyan text -->
<text x="100" y="23" font-family="Impact,Arial Black,sans-serif" font-size="20"
font-weight="bold" text-anchor="middle" fill="#00EEFF"
stroke="#004466" stroke-width="1" paint-order="stroke">Top Verminators:</text>""",
sw=400, sh=64))
# ─── WEWIN screen ─────────────────────────────────────────────────────────────
# Original showed a blue/purple mouse character with wings or limbs
save("BMP_WEWIN", mk(64, 64, f"""\
<!-- WE WIN screen happy rat dancing -->
<rect x="0" y="0" width="64" height="64" fill="#1A0033"/>
<!-- Stars -->
<circle cx="10" cy="10" r="2" fill="#FFFF88" opacity="0.8"/>
<circle cx="54" cy="8" r="1.5" fill="#FFFF88" opacity="0.7"/>
<circle cx="58" cy="30" r="1.8" fill="#FFFF88" opacity="0.75"/>
<circle cx="6" cy="50" r="1.5" fill="#FFFF88" opacity="0.7"/>
<!-- Confetti -->
<rect x="12" y="18" width="4" height="4" rx="1" fill="#FF4444" transform="rotate(20 14 20)"/>
<rect x="46" y="14" width="4" height="4" rx="1" fill="#44FF44" transform="rotate(-15 48 16)"/>
<rect x="18" y="54" width="3" height="3" rx="1" fill="#FFFF44" transform="rotate(30 19 55)"/>
<rect x="50" y="50" width="3" height="3" rx="1" fill="#44AAFF" transform="rotate(-10 51 51)"/>
<!-- Happy rat (blue/purple) -->
<!-- Tail -->
<path d="M14,42 Q6,38 6,30 Q6,22 14,20" fill="none" stroke="#440088"
stroke-width="2.5" stroke-linecap="round"/>
<!-- Body -->
<ellipse cx="33" cy="42" rx="18" ry="12" fill="#6644BB"/>
<ellipse cx="33" cy="38" rx="13" ry="7" fill="#8866DD" opacity="0.35"/>
<!-- Arms raised (celebrating) - left -->
<path d="M20,38 Q10,28 14,20" fill="none" stroke="#5533AA"
stroke-width="5" stroke-linecap="round"/>
<ellipse cx="13" cy="19" rx="5" ry="4" fill="#5533AA"/>
<!-- Arms raised - right -->
<path d="M46,38 Q56,28 52,20" fill="none" stroke="#5533AA"
stroke-width="5" stroke-linecap="round"/>
<ellipse cx="53" cy="19" rx="5" ry="4" fill="#5533AA"/>
<!-- Head -->
<circle cx="46" cy="33" r="13" fill="#6644BB"/>
<ellipse cx="44" cy="28" rx="8" ry="5" fill="#8866DD" opacity="0.28"/>
<!-- Ear -->
<ellipse cx="43" cy="21" rx="7" ry="6.5" fill="#5533AA"/>
<ellipse cx="43" cy="21" rx="4.5" ry="4" fill="#FFBBCC"/>
<!-- Snout -->
<ellipse cx="58" cy="36" rx="7" ry="5.5" fill="#5533AA"/>
<ellipse cx="64" cy="36" rx="3.5" ry="2.8" fill="#CC5566"/>
<!-- Big happy eye -->
<circle cx="52" cy="29" r="4" fill="#220055"/>
<circle cx="53.5" cy="27.5" r="1.8" fill="white"/>
<!-- Smile (curved line) -->
<path d="M55,36 Q58,39 61,36" fill="none" stroke="#220055"
stroke-width="1.5" stroke-linecap="round"/>
<!-- Legs -->
<ellipse cx="50" cy="52" rx="7" ry="4" fill="#5533AA"/>
<ellipse cx="22" cy="52" rx="7" ry="4" fill="#5533AA"/>""",
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"""\
<!-- Large white flower -->
{''.join(
f'<ellipse cx="{32+14*math.cos(math.radians(a)):.1f}" '
f'cy="{32+14*math.sin(math.radians(a)):.1f}" '
f'rx="9" ry="6" fill="white" '
f'transform="rotate({a:.0f} '
f'{32+14*math.cos(math.radians(a)):.1f} '
f'{32+14*math.sin(math.radians(a)):.1f})"/>'
for a in range(0, 360, 45)
)}
<circle cx="32" cy="32" r="10" fill="#FFEE88"/>
<circle cx="30" cy="30" r="4" fill="white" opacity="0.50"/>""",
2: f"""\
<!-- Cactus -->
<!-- Main trunk -->
<rect x="27" y="20" width="10" height="30" rx="5" fill="#228822"/>
<!-- Left arm -->
<rect x="14" y="28" width="16" height="8" rx="4" fill="#228822"/>
<rect x="14" y="22" width="8" height="14" rx="4" fill="#228822"/>
<!-- Right arm -->
<rect x="34" y="32" width="16" height="8" rx="4" fill="#228822"/>
<rect x="42" y="26" width="8" height="14" rx="4" fill="#228822"/>
<!-- Spines -->
<line x1="27" y1="26" x2="22" y2="23" stroke="#AAFFAA" stroke-width="1.2" stroke-linecap="round"/>
<line x1="37" y1="30" x2="42" y2="27" stroke="#AAFFAA" stroke-width="1.2" stroke-linecap="round"/>
<!-- Flower on top -->
<circle cx="32" cy="18" r="5" fill="#FF4488"/>
<circle cx="32" cy="18" r="3" fill="#FFEE00"/>""",
3: f"""\
<!-- Penguin -->
<!-- Body -->
<ellipse cx="32" cy="42" rx="14" ry="16" fill="#111122"/>
<ellipse cx="32" cy="44" rx="9" ry="11" fill="white"/>
<!-- Head -->
<circle cx="32" cy="26" r="12" fill="#111122"/>
<!-- Eyes -->
<circle cx="27" cy="24" r="3.5" fill="white"/>
<circle cx="37" cy="24" r="3.5" fill="white"/>
<circle cx="28" cy="24" r="2" fill="#111122"/>
<circle cx="38" cy="24" r="2" fill="#111122"/>
<circle cx="28.5" cy="23.5" r="0.8" fill="white"/>
<circle cx="38.5" cy="23.5" r="0.8" fill="white"/>
<!-- Beak -->
<polygon points="32,30 29,34 35,34" fill="#FF8800"/>
<!-- Wings -->
<ellipse cx="18" cy="40" rx="7" ry="12" fill="#111122" transform="rotate(-15 18 40)"/>
<ellipse cx="46" cy="40" rx="7" ry="12" fill="#111122" transform="rotate(15 46 40)"/>
<!-- Feet -->
<ellipse cx="26" cy="58" rx="7" ry="3" fill="#FF8800"/>
<ellipse cx="38" cy="58" rx="7" ry="3" fill="#FF8800"/>""",
4: f"""\
<!-- Flame pattern -->
<path d="M32,58 Q18,44 24,28 Q28,16 32,10 Q36,16 40,28 Q46,44 32,58Z"
fill="#FF6600"/>
<path d="M32,54 Q24,42 28,28 Q30,20 32,16 Q34,20 36,28 Q40,42 32,54Z"
fill="#FFBB00"/>
<path d="M32,48 Q27,38 30,28 Q31,22 32,20 Q33,22 34,28 Q37,38 32,48Z"
fill="#FFEE55"/>
<!-- Small side flames -->
<path d="M22,50 Q14,38 20,26 Q24,18 24,26 Q22,36 28,44Z"
fill="#FF4400" opacity="0.75"/>
<path d="M42,50 Q50,38 44,26 Q40,18 40,26 Q42,36 36,44Z"
fill="#FF4400" opacity="0.75"/>
<!-- Core bright -->
<circle cx="32" cy="36" r="8" fill="#FFFFFF" opacity="0.30"/>""",
}
body = f"""\
<!-- Start tile P{player} ({state}) -->
<rect x="0" y="0" width="64" height="64" fill="{bg}" opacity="{opacity}"/>
<!-- Decorative border -->
<rect x="1" y="1" width="62" height="62" rx="3" fill="none"
stroke="{ac}" stroke-width="{border_w}" opacity="{opacity}"/>
<!-- Inner frame shadow -->
<rect x="4" y="4" width="56" height="56" rx="2" fill="none"
stroke="{dk}" stroke-width="2" opacity="{float(opacity)*0.5:.2f}"/>
{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"""\
<!-- Bonus +{val} -->
<rect x="0" y="0" width="64" height="64" rx="6" fill="#000000" opacity="0.55"/>
<!-- Shadow text -->
<text x="34" y="{ty}" font-family="Arial Black,Impact,sans-serif"
font-size="{fs}" font-weight="bold" text-anchor="middle"
fill="{sh}">{val}</text>
<!-- Main text -->
<text x="32" y="{ty}" font-family="Arial Black,Impact,sans-serif"
font-size="{fs}" font-weight="bold" text-anchor="middle"
fill="{fg}">{val}</text>"""
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}")