You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

864 lines
39 KiB

#!/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}")