Spaces:
Running on Zero
Running on Zero
| """ | |
| 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) | |