forked from vycdev/thermal-printer-scripts
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcellular_print.py
More file actions
96 lines (80 loc) · 3.81 KB
/
Copy pathcellular_print.py
File metadata and controls
96 lines (80 loc) · 3.81 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
#!/usr/bin/env python3
"""Print a Wolfram elementary cellular automaton on the S01 thermal printer.
Each row is one generation; the pattern evolves downward. Rule 30 is famously
chaotic (it's the pattern on Conus textile shells), Rule 90 draws a Sierpinski
triangle, and Rule 110 is Turing-complete. Starts from a single live cell in the
middle unless --random is given.
Examples:
python cellular_print.py # Rule 30, single seed cell
python cellular_print.py --rule 90 # Sierpinski triangle
python cellular_print.py --rule 110 --random
python cellular_print.py --gen 220 --scale 2
python cellular_print.py --no-print
"""
from __future__ import annotations
import argparse
from pathlib import Path
import random
from PIL import Image
from print_common import ROOT, Card
def evolve(rule: int, cols: int, gens: int, start_random: bool, rng: random.Random) -> list[list[int]]:
table = [(rule >> i) & 1 for i in range(8)] # table[pattern] -> next state
if start_random:
row = [rng.randint(0, 1) for _ in range(cols)]
else:
row = [0] * cols
row[cols // 2] = 1
rows = [row]
for _ in range(gens - 1):
prev = rows[-1]
nxt = []
for x in range(cols):
left = prev[x - 1] if x > 0 else 0
mid = prev[x]
right = prev[x + 1] if x < cols - 1 else 0
nxt.append(table[(left << 2) | (mid << 1) | right])
rows.append(nxt)
return rows
def render_grid(rows: list[list[int]], scale: int) -> Image.Image:
cols = len(rows[0])
img = Image.new("1", (cols * scale, len(rows) * scale), 1) # 1 = white
px = img.load()
for y, row in enumerate(rows):
for x, cell in enumerate(row):
if cell:
for dy in range(scale):
for dx in range(scale):
px[x * scale + dx, y * scale + dy] = 0 # black
return img
def main() -> int:
parser = argparse.ArgumentParser(description="Print a Wolfram elementary cellular automaton on the S01 printer.")
parser.add_argument("--rule", type=int, default=30, help="Wolfram rule 0-255 (default: 30).")
parser.add_argument("--gen", type=int, default=180, help="Number of generations / rows (default: 180).")
parser.add_argument("--scale", type=int, default=3, help="Pixels per cell (default: 3).")
parser.add_argument("--random", action="store_true", help="Random first row instead of a single center cell.")
parser.add_argument("--seed", type=int, default=None, help="Seed for --random start.")
parser.add_argument("--out", type=Path, default=None)
parser.add_argument("--darkness", type=int, choices=range(1, 6), default=3)
parser.add_argument("--bottom-feed", type=int, default=24)
parser.add_argument("--no-print", action="store_true")
args = parser.parse_args()
if not 0 <= args.rule <= 255:
parser.error("--rule must be 0-255")
if args.scale < 1 or args.gen < 2:
parser.error("--scale >= 1 and --gen >= 2")
cols = (Card().inner_w) // args.scale
seed = args.seed if args.seed is not None else random.randrange(1, 100000)
rng = random.Random(seed)
print(f"Computing Rule {args.rule}: {cols} cells x {args.gen} generations ...")
rows = evolve(args.rule, cols, args.gen, args.random, rng)
grid = render_grid(rows, args.scale)
out = args.out or ROOT / f"cellular_rule{args.rule}.png"
card = Card()
card.title(f"RULE {args.rule}")
start = f"random seed {seed}" if args.random else "single seed cell"
card.line(start, size=12, bold=False, center=True).gap(4)
card.image(grid, border=False)
card.footer("elementary cellular automaton", f"{cols}x{args.gen}")
return card.finish(out, args.bottom_feed, args.darkness, do_print=not args.no_print)
if __name__ == "__main__":
raise SystemExit(main())