ai-puzzle-maker / tools /make_fallbacks.py
munish0838's picture
AI Puzzle Maker — FLUX jigsaws + MiniCPM mascot commentary
019630f verified
Raw
History Blame Contribute Delete
10.2 kB
"""
Generate the bundled fallback content: three puzzle artworks + a mascot sprite.
These ship in static/assets/ so the game is fully playable on machines where
FLUX.2-klein isn't live (local dev, CPU Spaces). All procedural PIL drawing —
no model calls, deterministic seeds, safe to re-run.
Run: python3 tools/make_fallbacks.py
"""
import math
import os
import random
from PIL import Image, ImageDraw, ImageFilter
W, H = 1024, 768
OUT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "..", "static", "assets")
os.makedirs(OUT, exist_ok=True)
def vgrad(stops):
"""Vertical gradient through (t, (r,g,b)) stops, t in 0..1."""
img = Image.new("RGB", (W, H))
px = img.load()
for y in range(H):
t = y / (H - 1)
for i in range(len(stops) - 1):
t0, c0 = stops[i]
t1, c1 = stops[i + 1]
if t0 <= t <= t1:
f = (t - t0) / max(t1 - t0, 1e-6)
c = tuple(round(c0[k] + (c1[k] - c0[k]) * f) for k in range(3))
break
else:
c = stops[-1][1]
for x in range(W):
px[x, y] = c
return img
def glow_disc(img, xy, r, color, blur=40):
layer = Image.new("RGB", img.size, (0, 0, 0))
d = ImageDraw.Draw(layer)
d.ellipse([xy[0] - r, xy[1] - r, xy[0] + r, xy[1] + r], fill=color)
layer = layer.filter(ImageFilter.GaussianBlur(blur))
return Image.blend(img, Image.new("RGB", img.size, (0, 0, 0)), 0) if False else \
Image.fromarray(__import__("numpy").clip(
__import__("numpy").asarray(img, int) + __import__("numpy").asarray(layer, int), 0, 255
).astype("uint8"))
def ridge(draw, base_y, amp, color, seed, rough=4):
rnd = random.Random(seed)
pts = [(0, base_y)]
y = base_y
n = 24
for i in range(1, n + 1):
x = W * i / n
y = base_y - amp * (0.4 + 0.6 * rnd.random()) * math.sin(i * 0.9 + rnd.random())
y = min(base_y + 10, base_y - abs(base_y - y))
pts.append((x, y + rnd.uniform(-rough, rough) * 6))
pts += [(W, H), (0, H)]
draw.polygon(pts, fill=color)
def sunset_peaks():
img = vgrad([(0, (24, 16, 48)), (0.35, (104, 40, 96)), (0.6, (228, 100, 70)), (0.78, (255, 176, 96)), (1, (255, 208, 130))])
# stars
rnd = random.Random(7)
d = ImageDraw.Draw(img)
for _ in range(140):
x, y = rnd.uniform(0, W), rnd.uniform(0, H * 0.4)
s = rnd.uniform(0.6, 2.2)
d.ellipse([x - s, y - s, x + s, y + s], fill=(255, 244, 220))
img = glow_disc(img, (W * 0.62, H * 0.55), 70, (255, 140, 60), blur=60)
d = ImageDraw.Draw(img)
d.ellipse([W * 0.62 - 58, H * 0.55 - 58, W * 0.62 + 58, H * 0.55 + 58], fill=(255, 224, 150))
# mountain layers, far to near
ridge(d, H * 0.62, 150, (96, 56, 110), seed=11)
ridge(d, H * 0.72, 130, (66, 38, 88), seed=22)
ridge(d, H * 0.84, 120, (40, 24, 60), seed=33)
# foreground pines
rnd = random.Random(5)
for x in range(-20, W + 40, 36):
h = rnd.uniform(60, 130)
bx = x + rnd.uniform(-10, 10)
by = H - rnd.uniform(-8, 30)
for k in range(3):
w = h * (0.5 - k * 0.12)
ty = by - h * (0.45 + k * 0.28)
d.polygon([(bx - w, by - k * h * 0.22), (bx + w, by - k * h * 0.22), (bx, ty)], fill=(18, 14, 34))
# birds
for i in range(7):
x, y = rnd.uniform(W * 0.1, W * 0.9), rnd.uniform(H * 0.12, H * 0.34)
s = rnd.uniform(6, 13)
d.arc([x - s, y - s / 2, x, y + s / 2], 200, 340, fill=(30, 18, 40), width=3)
d.arc([x, y - s / 2, x + s, y + s / 2], 200, 340, fill=(30, 18, 40), width=3)
return img
def neon_tides():
img = vgrad([(0, (8, 8, 26)), (0.5, (16, 12, 44)), (1, (10, 6, 30))])
img = glow_disc(img, (W * 0.28, H * 0.26), 60, (120, 200, 255), blur=70)
d = ImageDraw.Draw(img)
d.ellipse([W * 0.28 - 48, H * 0.26 - 48, W * 0.28 + 48, H * 0.26 + 48], fill=(225, 245, 255))
d.ellipse([W * 0.28 - 20, H * 0.26 - 34, W * 0.28 + 10, H * 0.26 - 4], fill=(190, 220, 245))
rnd = random.Random(9)
for _ in range(120):
x, y = rnd.uniform(0, W), rnd.uniform(0, H * 0.45)
s = rnd.uniform(0.6, 2.0)
d.ellipse([x - s, y - s, x + s, y + s], fill=(180, 220, 255))
# neon wave bands
colors = [(255, 70, 160), (90, 220, 255), (160, 90, 255), (70, 255, 190), (255, 170, 60),
(255, 70, 160), (90, 220, 255), (160, 90, 255)]
base = H * 0.46
for i, col in enumerate(colors):
layer = Image.new("RGB", (W, H), (0, 0, 0))
ld = ImageDraw.Draw(layer)
pts = []
for x in range(0, W + 8, 8):
y = base + i * 34 + 18 * math.sin(x / 90 + i * 1.7) + 9 * math.sin(x / 37 + i * 0.8)
pts.append((x, y))
ld.line(pts, fill=col, width=6)
glow = layer.filter(ImageFilter.GaussianBlur(8))
np = __import__("numpy")
img = Image.fromarray(np.clip(np.asarray(img, int) + np.asarray(glow, int) + np.asarray(layer, int) // 2, 0, 255).astype("uint8"))
d = ImageDraw.Draw(img)
# city silhouette strip between moon and waves
rnd = random.Random(4)
x = 0
while x < W:
bw = rnd.uniform(28, 70)
bh = rnd.uniform(40, 150)
d.rectangle([x, H * 0.46 - bh, x + bw, H * 0.47], fill=(12, 8, 30))
for wy in range(int(H * 0.46 - bh + 8), int(H * 0.45), 14):
for wx in range(int(x + 6), int(x + bw - 6), 12):
if rnd.random() < 0.5:
d.rectangle([wx, wy, wx + 4, wy + 6], fill=rnd.choice([(255, 90, 170), (90, 220, 255), (255, 200, 90)]))
x += bw + rnd.uniform(6, 18)
return img
def bloom_meadow():
img = vgrad([(0, (110, 190, 235)), (0.45, (185, 228, 240)), (0.62, (250, 240, 200)), (1, (255, 250, 230))])
d = ImageDraw.Draw(img)
rnd = random.Random(3)
# clouds
for _ in range(8):
cx, cy = rnd.uniform(0, W), rnd.uniform(H * 0.06, H * 0.3)
for k in range(5):
r = rnd.uniform(18, 44)
d.ellipse([cx + k * 26 - r, cy + rnd.uniform(-8, 8) - r * 0.6, cx + k * 26 + r, cy + r * 0.6], fill=(255, 255, 255))
img = glow_disc(img, (W * 0.82, H * 0.18), 55, (255, 230, 120), blur=50)
d = ImageDraw.Draw(img)
d.ellipse([W * 0.82 - 44, H * 0.18 - 44, W * 0.82 + 44, H * 0.18 + 44], fill=(255, 246, 180))
# rolling hills
for base, col in [(0.58, (118, 188, 96)), (0.68, (96, 168, 80)), (0.8, (74, 146, 66))]:
pts = [(x, H * base + 26 * math.sin(x / 160 + base * 9)) for x in range(0, W + 16, 16)]
d.polygon(pts + [(W, H), (0, H)], fill=col)
# flowers — denser toward the bottom
petals = [(255, 120, 150), (255, 200, 80), (170, 130, 255), (255, 140, 90), (240, 240, 255)]
for _ in range(170):
fy = H * (0.6 + 0.4 * rnd.random() ** 0.7)
fx = rnd.uniform(0, W)
s = 4 + (fy - H * 0.6) / (H * 0.4) * 14
col = rnd.choice(petals)
d.line([fx, fy, fx, fy + s * 2.2], fill=(46, 110, 50), width=max(1, int(s / 4)))
for a in range(6):
ang = a * math.pi / 3 + rnd.uniform(-0.2, 0.2)
px_, py_ = fx + math.cos(ang) * s, fy + math.sin(ang) * s
d.ellipse([px_ - s * 0.55, py_ - s * 0.55, px_ + s * 0.55, py_ + s * 0.55], fill=col)
d.ellipse([fx - s * 0.45, fy - s * 0.45, fx + s * 0.45, fy + s * 0.45], fill=(255, 235, 140))
# butterflies
for _ in range(6):
bx, by = rnd.uniform(W * 0.1, W * 0.9), rnd.uniform(H * 0.35, H * 0.6)
s = rnd.uniform(7, 12)
col = rnd.choice(petals)
d.ellipse([bx - s, by - s * 0.7, bx, by + s * 0.7], fill=col)
d.ellipse([bx, by - s * 0.7, bx + s, by + s * 0.7], fill=col)
d.line([bx, by - s * 0.7, bx, by + s * 0.7], fill=(60, 40, 40), width=2)
return img
def mascot():
"""Fallback companion: a round amber critter with big eyes and a star antenna."""
S = 4 # supersample
w, h = 130 * S, 160 * S
img = Image.new("RGBA", (w, h), (0, 0, 0, 0))
d = ImageDraw.Draw(img)
cx, cy, r = w // 2, int(h * 0.6), int(w * 0.36)
# feet
for sx in (-0.45, 0.45):
d.ellipse([cx + sx * r - 14 * S, cy + r - 10 * S, cx + sx * r + 14 * S, cy + r + 8 * S],
fill=(214, 120, 50, 255), outline=(40, 24, 18, 255), width=2 * S)
# body
d.ellipse([cx - r, cy - r, cx + r, cy + r], fill=(255, 176, 70, 255), outline=(40, 24, 18, 255), width=3 * S)
d.ellipse([cx - r * 0.62, cy - r * 0.1, cx + r * 0.62, cy + r * 0.86], fill=(255, 222, 150, 255))
# antenna + star
ax, ay = cx, cy - r
d.line([ax, ay, ax + 6 * S, ay - 26 * S], fill=(40, 24, 18, 255), width=3 * S)
sx, sy, sr = ax + 8 * S, ay - 34 * S, 11 * S
star = [(sx + sr * math.cos(a - math.pi / 2), sy + sr * math.sin(a - math.pi / 2)) if i % 2 == 0 else
(sx + sr * 0.45 * math.cos(a - math.pi / 2), sy + sr * 0.45 * math.sin(a - math.pi / 2))
for i, a in enumerate(math.pi / 5 * k for k in range(10))]
d.polygon(star, fill=(255, 232, 90, 255), outline=(40, 24, 18, 255))
# eyes
for sx_ in (-0.32, 0.32):
ex = cx + sx_ * r
ey = cy - r * 0.22
d.ellipse([ex - 13 * S, ey - 16 * S, ex + 13 * S, ey + 16 * S], fill=(255, 255, 255, 255), outline=(40, 24, 18, 255), width=2 * S)
d.ellipse([ex - 6 * S, ey - 6 * S, ex + 6 * S, ey + 8 * S], fill=(40, 24, 18, 255))
d.ellipse([ex - 1 * S, ey - 4 * S, ex + 4 * S, ey + 1 * S], fill=(255, 255, 255, 255))
# smile + cheeks
d.arc([cx - 16 * S, cy + 2 * S, cx + 16 * S, cy + 24 * S], 20, 160, fill=(40, 24, 18, 255), width=3 * S)
for sx_ in (-0.52, 0.52):
d.ellipse([cx + sx_ * r - 8 * S, cy + 2 * S, cx + sx_ * r + 8 * S, cy + 14 * S], fill=(255, 130, 90, 160))
return img.resize((130, 160), Image.LANCZOS)
if __name__ == "__main__":
jobs = [("fallback1.jpg", sunset_peaks), ("fallback2.jpg", neon_tides), ("fallback3.jpg", bloom_meadow)]
for name, fn in jobs:
p = os.path.join(OUT, name)
fn().save(p, "JPEG", quality=88, optimize=True)
print("wrote", p)
mp = os.path.join(OUT, "mascot.png")
mascot().save(mp)
print("wrote", mp)