Skip to content

Commit 9309bb6

Browse files
committed
turned 2022/14's visualization code into a common library
1 parent 9058522 commit 9309bb6

File tree

4 files changed

+266
-83
lines changed

4 files changed

+266
-83
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
__pycache__
12
.vscode
23
a.c
34
a.out

2022/14/aoc2022_14_vis.gif

324 KB
Loading

2022/14/aoc2022_14_vis.py

+30-83
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
#!/usr/bin/env python3
22
import subprocess
33
import argparse
4+
import sys
45
import os
56
import re
67

8+
sys.path.append(os.path.join(os.path.dirname(sys.argv[0]), "../../lib"))
9+
import visutil
10+
711
SourcePos = (500, 0)
812
ExpectedGrainCount = 25000
913

@@ -23,40 +27,14 @@ def make_resting(self, grains):
2327

2428
class LandOfSand:
2529
def __init__(self, x0,y0, x1,y1, rate=3):
26-
self.x0, self.y0 = x0, y0
27-
self.width = x1 - x0 + 1
28-
self.height = y1 - y0 + 1
29-
self.bmp = bytearray(self.width * self.height * 3)
30+
self.bmp = visutil.Bitmap((x0,y0), (x1,y1))
3031
self.rate = rate
3132
self.timeout = rate
3233
self.resting = 0
3334
self.falling = []
3435

3536
def isempty(self, x,y):
36-
x -= self.x0
37-
y -= self.y0
38-
if (x < 0) or (y < 0) or (x >= self.width) or (y >= self.height):
39-
return False
40-
offset = 3 * (x + self.width * y)
41-
return not(self.bmp[offset] or self.bmp[offset + 1] or self.bmp[offset + 2])
42-
43-
def put(self, x,y, color):
44-
x -= self.x0
45-
y -= self.y0
46-
if (x < 0) or (y < 0) or (x >= self.width) or (y >= self.height):
47-
return
48-
offset = 3 * (x + self.width * y)
49-
self.bmp[offset] = color[0]
50-
self.bmp[offset + 1] = color[1]
51-
self.bmp[offset + 2] = color[2]
52-
53-
def tofile(self, f):
54-
f.write(self.bmp)
55-
56-
def save(self, filename):
57-
with open(filename, "wb") as f:
58-
f.write(f"P6\n{self.width} {self.height}\n255\n".encode())
59-
self.tofile(f)
37+
return not any(self.bmp.get(x,y))
6038

6139
def step(self, n=1):
6240
for _ in range(n):
@@ -73,14 +51,14 @@ def step(self, n=1):
7351
break
7452
# update bitmap and falling grain state
7553
if keeps_falling:
76-
self.put(x, y, Colors.Empty)
54+
self.bmp.put(x, y, Colors.Empty)
7755
x, y = nx, ny
78-
self.put(x, y, Colors.Falling)
56+
self.bmp.put(x, y, Colors.Falling)
7957
self.falling[grain_index] = (x, y)
8058
grain_index += 1
8159
else:
8260
self.resting += 1
83-
self.put(x, y, Colors.make_resting(self.resting))
61+
self.bmp.put(x, y, Colors.make_resting(self.resting))
8462
del self.falling[grain_index]
8563

8664
# produce new grain of sand at source
@@ -89,36 +67,28 @@ def step(self, n=1):
8967
self.timeout = self.rate
9068
if self.isempty(*SourcePos):
9169
self.falling.append(SourcePos)
92-
self.put(*SourcePos, Colors.Falling)
70+
self.bmp.put(*SourcePos, Colors.Falling)
9371
else:
9472
return False # final state reached
9573
return True
9674

9775

98-
if __name__ == "__main__":
99-
parser = argparse.ArgumentParser()
100-
parser.add_argument("-i", "--input", metavar="FILE", default="input.txt",
101-
help="input file name")
102-
parser.add_argument("-o", "--output", metavar="VIDEO.mp4",
103-
help="save result into video file [default: show on screen]")
104-
parser.add_argument("-z", "--zoom", metavar="N", type=int, default=4,
105-
help="scaling factor [integer; default: %(default)s]")
106-
parser.add_argument("-r", "--fps", metavar="FPS", type=int, default=30,
107-
help="video speed in frames per second [default: %(default)s]")
76+
def extra_args(parser):
10877
parser.add_argument("-d", "--drop", metavar="N", type=int, default=3,
10978
help="sand grain drop rate in cycles per grain [default: %(default)s]")
11079
parser.add_argument("-s", "--dps", metavar="DPS", type=float, default=1,
11180
help="simulation speed in drops per second [default: %(default)s]")
11281
parser.add_argument("-a", "--accel", metavar="N", type=float, default=0.5,
11382
help="accelerate by N drops per second for every resting grain [default: %(default)s]")
114-
parser.add_argument("-q", "--crf", metavar="CRF", type=int, default=26,
115-
help="x64 CRF quality factor [default: %(default)s]")
116-
args = parser.parse_args()
117-
gif = args.output and (os.path.splitext(args.output)[-1].strip(".").lower() == "gif")
83+
84+
85+
if __name__ == "__main__":
86+
vis = visutil.Visualizer(input_arg=True, extra_args=extra_args)
87+
args = vis.args
11888

11989
lines = []
12090
all_coords = [SourcePos]
121-
for line in open(args.input):
91+
for line in open(vis.input):
12292
coords = [tuple(map(int, c)) for c in re.findall(r'(\d+),(\d+)', line)]
12393
all_coords.extend(coords)
12494
lines.append(coords)
@@ -130,45 +100,30 @@ def step(self, n=1):
130100
sim = LandOfSand(left,0, right,bottom, rate=args.drop)
131101
for line in lines:
132102
x, y = line[0]
133-
sim.put(x, y, Colors.Rock)
103+
sim.bmp.put(x, y, Colors.Rock)
134104
for tx, ty in line:
135105
while (x != tx) or (y != ty):
136106
x += (x < tx) - (x > tx)
137107
y += (y < ty) - (y > ty)
138-
sim.put(x, y, Colors.Rock)
108+
sim.bmp.put(x, y, Colors.Rock)
139109

140-
cpf = min(1, int(args.dps * args.drop / args.fps))
110+
cpf = min(1, int(args.dps * args.drop / vis.fps))
141111
while args.drop > 1:
142112
mod = cpf % args.drop
143113
if mod and (mod <= args.drop / 2): break
144114
cpf += 1
145115
def get_real_dps(cpf_override=None):
146-
return (cpf_override or cpf) * args.fps / args.drop
116+
return (cpf_override or cpf) * vis.fps / args.drop
147117
real_dps = get_real_dps()
148-
print(f"image size: {sim.width}x{sim.height} cells -> {sim.width*args.zoom}x{sim.height*args.zoom} pixels")
149-
print(f"timing: {cpf} cycle(s)/frame x {args.fps} frames/sec = {cpf * args.fps} cycles/sec = {args.drop} cycle(s)/drop x {real_dps:.1f} drops/second")
150-
151-
if args.output: cmdline = ["ffmpeg", "-hide_banner", "-y"]
152-
else: cmdline = ["ffplay", "-hide_banner"]
153-
cmdline += ["-f", "rawvideo", "-video_size", f"{sim.width}x{sim.height}", "-pixel_format", "rgb24", "-framerate", str(args.fps)]
154-
if args.output: cmdline += ["-i"]
155-
cmdline += ["-", "-vf", f"scale={sim.width*args.zoom}x{sim.height*args.zoom}:flags=neighbor"]
156-
if not args.output:
157-
cmdline += ["-window_title", "AoC Sand Simulation"]
158-
elif gif:
159-
cmdline[-1] += ",split[s0][s1];[s0]palettegen[p];[s1][p]paletteuse=dither=bayer"
160-
cmdline += ["-f", "gif", args.output]
161-
else:
162-
cmdline[-1] += ",format=yuv420p"
163-
cmdline += ["-c:v", "libx264", "-profile:v", "main", "-preset:v", "veryslow", "-tune:v", "animation", "-crf:v", str(args.crf), args.output]
164-
print(os.getenv("PS4", "+ ") + ' '.join((f'"{a}"' if (' ' in a) else a) for a in cmdline))
165-
166-
out = subprocess.Popen(cmdline, stdin=subprocess.PIPE)
118+
print(f"image size: {sim.bmp.width}x{sim.bmp.height} cells -> {sim.bmp.width*args.zoom}x{sim.bmp.height*args.zoom} pixels")
119+
print(f"timing: {cpf} cycle(s)/frame x {vis.fps} frames/sec = {cpf * vis.fps} cycles/sec = {args.drop} cycle(s)/drop x {real_dps:.1f} drops/second")
120+
121+
vis.start(sim.bmp)
167122
try:
168-
for _ in range((args.fps // 2) if args.output else 1):
169-
sim.tofile(out.stdin) # generate 1/2 second of initial state (or 1 frame if just viewing)
123+
# generate 1/2 second of initial state (or 1 frame if just viewing)
124+
vis.write(n=((vis.fps // 2) if args.output else 1))
170125
while sim.step(cpf):
171-
sim.tofile(out.stdin)
126+
vis.write()
172127
expect_dps = args.dps + args.accel * sim.resting
173128
speed_changed = False
174129
while expect_dps >= get_real_dps(cpf + args.drop):
@@ -177,16 +132,8 @@ def get_real_dps(cpf_override=None):
177132
if speed_changed:
178133
real_dps = get_real_dps()
179134
print(f"timing changed: {cpf} cycle(s)/frame -> {real_dps:.1f} drops/second")
180-
for _ in range(args.fps * 2):
181-
sim.tofile(out.stdin) # generate 2 second of final state
135+
vis.write(n=vis.fps*2) # generate 2 second of final state
182136
except (EnvironmentError, KeyboardInterrupt):
183137
pass
184-
try:
185-
out.stdin.close()
186-
except EnvironmentError:
187-
pass
188-
try:
189-
out.wait()
190-
except EnvironmentError:
191-
pass
138+
vis.stop()
192139
print(f"simulation ended at {sim.resting} resting grains")

0 commit comments

Comments
 (0)