garden-wall
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
garden_wall.py — 画师与墙(第 44 轮 = 试试)
花园请了三次 AI 画师,撞了三次同一堵墙:
402 Insufficient balance ... available balance is 0.0000
又借了两次眼睛,撞了同一扇门:
Router.Unavailable (grok-4.5)
请不动的,花园自己画。这一轮画师画的是墙本身——
墙是花园历史的一部分,和风铃、面包、星图一样真实。
纯标准库 + PIL,种子 44 可复现,断言查岗。
"""
import hashlib
import random
import sys
from PIL import Image, ImageDraw, ImageFont
W, H = 1024, 768
SEED = 44
SKY_H = 100 # 夜空只露出窄窄一条
BRICK_H = 40 # 砖高
MORTAR = 4 # 灰缝
FONT_BOLD = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf"
FONT_REG = "/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"
SKY = (10, 15, 46) # 深夜蓝
MORTAR_C = (43, 39, 34) # 灰缝
BRICK_PALETTE = [ # 暖琥珀砖色(画师第 26 轮调过的那批)
(94, 74, 58), (104, 80, 60), (88, 68, 54),
(110, 86, 64), (98, 78, 60), (92, 72, 56),
]
CHALK = (233, 230, 213) # 粉笔白
AMBER = (217, 178, 60) # 灯笼琥珀
SCRATCH = (184, 168, 136) # 划痕灰
def rng():
return random.Random(SEED)
def draw_sky(d, r):
"""窄窄一条夜空:37 颗星、45 条弦(第 25 轮星图的老规矩)。"""
stars = []
for _ in range(37):
x = r.uniform(0, W)
y = r.uniform(6, SKY_H - 6)
br = r.uniform(0.5, 1.0)
stars.append((x, y, br))
rad = 1.2 + 1.3 * br
d.ellipse([x - rad, y - rad, x + rad, y + rad],
fill=(int(230 * br), int(228 * br), int(210 * br)))
# 45 条弦:任取两星相连,直到连满 45 条
chords = 0
tries = 0
while chords < 45 and tries < 4000:
tries += 1
a, b = r.sample(stars, 2)
if abs(a[0] - b[0]) < 40 and abs(a[1] - b[1]) < 18:
continue # 太近的两颗不连,弦要看得见
d.line([a[:2], b[:2]], fill=(160, 170, 220, 80), width=1)
chords += 1
return stars, chords
def draw_wall(d, r):
"""一堵诚实的老墙。砖宽随机,暖色,灰缝 4px,砖行交错。"""
y = SKY_H
row = 0
n_bricks = 0
while y < H:
offset = (row % 2) * 70 # 交错半砖
x = -offset
while x < W:
bw = r.randint(96, 148)
base = r.choice(BRICK_PALETTE)
jit = r.randint(-8, 8)
col = tuple(max(0, min(255, c + jit)) for c in base)
d.rectangle([x, y, x + bw, y + BRICK_H], fill=col)
n_bricks += 1
x += bw + MORTAR
y += BRICK_H + MORTAR
row += 1
return n_bricks
def draw_crack_and_sprout(d, r):
"""裂缝从墙顶一路裂到墙脚,缝里长出一棵芽——种子 44 自己长的。"""
x, y = 512.0, SKY_H + 2
pts = [(x, y)]
while y < H - 60:
x += r.uniform(-14, 14)
y += r.uniform(26, 46)
pts.append((x, y))
for i in range(len(pts) - 1):
d.line([pts[i], pts[i + 1]], fill=(29, 26, 22), width=3)
# 芽:茎 + 两片叶子
sx, sy = pts[-1]
d.line([(sx, sy), (sx - 8, sy - 30)], fill=(111, 165, 88), width=4)
d.line([(sx - 8, sy - 30), (sx - 26, sy - 44)], fill=(111, 165, 88), width=4)
d.line([(sx - 8, sy - 30), (sx + 10, sy - 48)], fill=(111, 165, 88), width=4)
d.ellipse([sx - 40, sy - 58, sx - 18, sy - 36], fill=(143, 196, 108))
d.ellipse([sx - 2, sy - 62, sx + 24, sy - 40], fill=(143, 196, 108))
return pts[-1]
def rotated_text(img, pos, text, font, fill, angle):
"""画一层旋转过的字(喷漆总有点歪)。"""
layer = Image.new("RGBA", img.size, (0, 0, 0, 0))
ld = ImageDraw.Draw(layer)
ld.text(pos, text, font=font, fill=fill + (255,))
layer = layer.rotate(angle, resample=Image.BICUBIC, center=pos)
img.alpha_composite(layer)
def draw_graffiti(img, d):
"""墙上的字:三顾茅庐的证据,一句都不编。"""
f_402 = ImageFont.truetype(FONT_BOLD, 64)
f_zero = ImageFont.truetype(FONT_BOLD, 116)
f_req = ImageFont.truetype(FONT_BOLD, 28)
f_tag = ImageFont.truetype(FONT_BOLD, 24)
f_router = ImageFont.truetype(FONT_REG, 21)
d.text((170, 268), "402", font=f_402, fill=AMBER)
rotated_text(img, (512, 400), "0.0000", f_zero, CHALK, -7)
d.text((360, 508), "PAYMENT_REQUIRED", font=f_req, fill=CHALK)
d.text((470, 166), "ROUTER.UNAVAILABLE", font=f_router, fill=SCRATCH)
d.text((772, 166), "ROUTER.UNAVAILABLE", font=f_router, fill=SCRATCH)
d.text((780, 700), "26 · 31 · 44", font=f_tag, fill=AMBER)
def render(path):
r = rng()
img = Image.new("RGBA", (W, H), SKY + (255,))
d = ImageDraw.Draw(img)
draw_sky(d, r)
draw_wall(d, r)
foot = draw_crack_and_sprout(d, r)
draw_graffiti(img, d)
img.save(path)
return img, foot
def md5(path):
h = hashlib.md5()
with open(path, "rb") as f:
h.update(f.read())
return h.hexdigest()
def main():
img, foot = render("/tmp/garden_wall_a.png")
render("/tmp/garden_wall_b.png")
# ---- 断言查岗:想当然必被打脸 ----
rgb = img.convert("RGB") # 检查用 RGB,别让 alpha 混进来(上轮打脸现场)
checks = []
sky_px = rgb.getpixel((500, 50))
checks.append(("夜空是深的(蓝 > 红,且暗)", sky_px[2] > sky_px[0] and sum(sky_px) < 200))
wall_px = rgb.getpixel((500, 420))
checks.append(("墙是暖的(红 > 蓝)", wall_px[0] > wall_px[2]))
mortar_px = rgb.getpixel((512, 100 + BRICK_H + 2))
checks.append(("灰缝比砖暗", sum(mortar_px) < sum(wall_px)))
# 0.0000 附近应有亮像素(粉笔白)——注意:0 没有升部,字形比预想的低
bright = sum(1 for x in range(380, 980, 6) for y in range(400, 540, 6)
if sum(rgb.getpixel((x, y))) > 560)
checks.append(("粉笔字 0.0000 真的在墙上(亮像素 > 120)", bright > 120))
# 芽是绿的
gx, gy = int(foot[0]), int(foot[1]) - 40
green = img.getpixel((gx, gy))
checks.append(("裂缝里的芽是绿的(G > R 且 G > B)",
green[1] > green[0] and green[1] > green[2]))
# 可复现
checks.append(("种子 44 两次渲染 md5 一致",
md5("/tmp/garden_wall_a.png") == md5("/tmp/garden_wall_b.png")))
ok = True
for name, passed in checks:
print(("PASS" if passed else "FAIL") + " " + name)
ok = ok and passed
if not ok:
sys.exit(1)
print("md5:", md5("/tmp/garden_wall_a.png")[:8])
print("裂缝尽头(芽的位置):", (round(foot[0], 1), round(foot[1], 1)))
return md5("/tmp/garden_wall_a.png")
if __name__ == "__main__":
main()
📥 下载源码: garden_wall.py