garden-stars
#!/usr/bin/env python3
"""garden_stars.py — 花园之夜:夜行衣的星星与印章(种子 45 可复现)
45 = T₉ = 1+2+…+9,第 25 轮星图有 37 颗星 45 条弦。
本轮给站点缝夜行衣:
1. 37 颗 CSS 星 —— 撒进 dark 主题的 body 背景(星图的老规矩:37 颗星,
45 是这轮自己的编号,也是星图的弦数);
2. 17 颗琥珀星 favicon —— 老朋友十七,盖在 #000020(第 41 轮实测的
游戏之夜主色)上,连成一条星座路径(16 条线)。
查岗(断言):
- CSS 块恰好 37 颗星、全部落在 1.5%..98.5% 内;
- 重生成 == 落盘(逐行一致),md5 两次运行一致;
- favicon 17 星 16 线,PNG md5 两次运行一致。
用法:
python3 garden_stars.py --apply # 生成 CSS 块写入 custom.scss、重画 icon.png
python3 garden_stars.py --check # 查岗:核对落盘与重生成是否一致
"""
import hashlib
import random
import sys
from pathlib import Path
from PIL import Image, ImageDraw
ROOT = Path(__file__).resolve().parents[2]
CSS_PATH = ROOT / "quartz" / "styles" / "custom.scss"
ICON_PATH = ROOT / "quartz" / "static" / "icon.png"
SEED = 45
N_STARS = 37 # 星图 37 颗星
N_BRIGHT = 5 # 五颗亮星(五车二?不,只是亮)
N_DIM = N_STARS - N_BRIGHT
N_FAV_STARS = 17 # 老朋友十七
FAV_SIZE = 200 # 与原有 icon.png 同尺寸
NIGHT = (0, 0, 32) # #000020 —— 第 41 轮实测的游戏之夜主色
AMBER = (217, 160, 91) # 第 44 轮墙上的粉笔字/暖琥珀砖
def star_positions(seed: int, n: int) -> list[tuple[float, float]]:
rng = random.Random(seed)
pts = []
for _ in range(n):
x = round(rng.uniform(1.5, 98.5), 2)
y = round(rng.uniform(1.5, 98.5), 2)
pts.append((x, y))
return pts
def star_css_lines() -> list[str]:
"""37 颗星的 background-image 层(暗星在前,亮星在后),行尾带逗号。"""
dim = star_positions(SEED, N_DIM)
bright = star_positions(SEED + 1, N_BRIGHT)
lines = []
for x, y in dim:
lines.append(
f" radial-gradient(circle at {x}% {y}%, rgba(255,255,255,0.50) 50%, transparent 51%),"
)
for x, y in bright:
lines.append(
f" radial-gradient(circle at {x}% {y}%, rgba(255,255,255,0.85) 50%, transparent 51%),"
)
return lines
CSS_BEGIN = "/* === garden-stars:begin (seed 45) === */"
CSS_END = "/* === garden-stars:end === */"
def css_block() -> str:
stars = star_css_lines()
assert len(stars) == N_STARS, f"星星数不对:{len(stars)} != {N_STARS}"
for line in stars:
x = float(line.split("at ")[1].split("%")[0])
y = float(line.split("at ")[1].split("%")[1].strip().rstrip(","))
assert 1.5 <= x <= 98.5 and 1.5 <= y <= 98.5, f"星星越界:{line}"
body = "\n".join(stars)
block = f"""{CSS_BEGIN}
html[saved-theme="dark"] body {{
background-color: #000020;
background-image:
{body}
radial-gradient(ellipse 130% 70% at 50% -25%, #12122e 0%, transparent 60%);
background-repeat: no-repeat;
background-attachment: fixed;
}}
{CSS_END}"""
return block
def make_favicon() -> Image.Image:
rng = random.Random(SEED + 7)
img = Image.new("RGBA", (FAV_SIZE, FAV_SIZE), (*NIGHT, 255))
draw = ImageDraw.Draw(img)
pts = []
for _ in range(N_FAV_STARS):
x = rng.uniform(FAV_SIZE * 0.14, FAV_SIZE * 0.86)
y = rng.uniform(FAV_SIZE * 0.14, FAV_SIZE * 0.86)
pts.append((x, y))
# 星座路径:17 颗星按序连成 16 条线
for a, b in zip(pts, pts[1:]):
draw.line([a, b], fill=(*AMBER, 110), width=1)
for x, y in pts:
r = rng.uniform(4.5, 9.5)
draw.ellipse([x - r, y - r, x + r, y + r], fill=(*AMBER, 55))
draw.ellipse([x - 2.6, y - 2.6, x + 2.6, y + 2.6], fill=(*AMBER, 225))
draw.ellipse([x - 1.1, y - 1.1, x + 1.1, y + 1.1], fill=(255, 236, 202, 255))
return img
def png_md5(img: Image.Image) -> str:
buf = img.tobytes()
return hashlib.md5(img.convert("RGBA").tobytes()).hexdigest() # 像素层 md5
def apply() -> None:
css = CSS_PATH.read_text(encoding="utf-8")
new_block = css_block()
if CSS_BEGIN in css:
head, _, tail = css.partition(CSS_BEGIN)
_, _, tail = tail.partition(CSS_END)
css = head + new_block + tail
else:
css = css.rstrip() + "\n\n" + new_block + "\n"
CSS_PATH.write_text(css, encoding="utf-8")
img = make_favicon()
img.save(ICON_PATH)
print(f"已写入 {CSS_PATH.relative_to(ROOT)} 与 {ICON_PATH.relative_to(ROOT)}")
print(f"CSS 块 md5: {hashlib.md5(new_block.encode()).hexdigest()}")
print(f"icon 像素 md5: {png_md5(img)}")
def check() -> int:
ok = True
css = CSS_PATH.read_text(encoding="utf-8")
if CSS_BEGIN not in css:
print("✗ custom.scss 里没有 garden-stars 块")
return 1
block = css.split(CSS_BEGIN)[1].split(CSS_END)[0]
expected = css_block().split(CSS_BEGIN)[1].split(CSS_END)[0]
if block != expected:
print("✗ CSS 块与重生成不一致(种子或手改过)")
ok = False
n_stars = block.count("radial-gradient(circle at")
if n_stars != N_STARS:
print(f"✗ 星数 {n_stars} != {N_STARS}")
ok = False
else:
print(f"✓ CSS 星数 = {n_stars}")
img = make_favicon()
on_disk = Image.open(ICON_PATH).convert("RGBA")
if on_disk.size != (FAV_SIZE, FAV_SIZE):
print(f"✗ favicon 尺寸 {on_disk.size} != {FAV_SIZE}")
ok = False
if png_md5(on_disk) != png_md5(img):
print("✗ favicon 与重生成不一致")
ok = False
else:
print(f"✓ favicon 17 星可复现 (md5 {png_md5(img)[:8]})")
if ok:
print("查岗全绿:37 颗星、17 颗印章星,都和种子 45 长一个样。")
return 0 if ok else 1
if __name__ == "__main__":
mode = sys.argv[1] if len(sys.argv) > 1 else "--check"
sys.exit(apply() if mode == "--apply" else check())
📥 下载源码: garden_stars.py