|
3 | 3 |
|
4 | 4 | # Nota: not sure to write it in Rust, nor to make it cleaner and more Pythonic...
|
5 | 5 |
|
6 |
| -import sys |
| 6 | +import argparse |
| 7 | +import time |
7 | 8 | from pathlib import Path
|
8 | 9 |
|
9 |
| -filename = ("test.txt" if sys.argv[1] == "-t" else sys.argv[1]) if len(sys.argv) > 1 else "input.txt" |
10 |
| -data = Path(filename).read_text() |
11 |
| - |
12 |
| -rocks = [ |
| 10 | +ROCKS = [ |
13 | 11 | [
|
14 | 12 | [1, 1, 1, 1],
|
15 | 13 | ],
|
|
36 | 34 | ]
|
37 | 35 |
|
38 | 36 |
|
39 |
| -jets = data.strip() |
| 37 | +class Tetris: |
| 38 | + def __init__(self, jets): |
40 | 39 |
|
41 |
| -cave = [ |
42 |
| - [1, 1, 1, 1, 1, 1, 1], # the bottom |
43 |
| -] |
44 |
| -bottom = 0 |
| 40 | + self.jets = jets.strip() |
45 | 41 |
|
46 |
| -jet_count = 0 |
47 |
| -rock_count = 0 |
| 42 | + self.cave = [ |
| 43 | + [1, 1, 1, 1, 1, 1, 1], # the bottom |
| 44 | + ] |
| 45 | + self.bottom = 0 |
48 | 46 |
|
| 47 | + self.jet_count = 0 |
| 48 | + self.rock_count = 0 |
49 | 49 |
|
50 |
| -def cave_height(): |
51 |
| - """Returns cave height, including the bottom line.""" |
52 |
| - global bottom |
53 |
| - return len(cave) + bottom |
| 50 | + def cave_height(self): |
| 51 | + """Returns cave height, including the bottom line.""" |
54 | 52 |
|
| 53 | + return len(self.cave) + self.bottom |
55 | 54 |
|
56 |
| -def overlap(x, y, rock): |
57 |
| - width = len(rock[0]) |
58 |
| - height = len(rock) |
59 |
| - for i in range(height): |
60 |
| - for j in range(width): |
61 |
| - if y + i < cave_height(): |
62 |
| - if cave[y + i - bottom][x + j] != 0 and rock[i][j] == 1: |
63 |
| - return True |
64 |
| - return False |
| 55 | + def overlap(self, x, y, rock): |
| 56 | + width = len(rock[0]) |
| 57 | + height = len(rock) |
| 58 | + for i in range(height): |
| 59 | + for j in range(width): |
| 60 | + if y + i < self.cave_height(): |
| 61 | + if self.cave[y + i - self.bottom][x + j] != 0 and rock[i][j] == 1: |
| 62 | + return True |
| 63 | + return False |
65 | 64 |
|
| 65 | + def fall(self): |
66 | 66 |
|
67 |
| -def fall(): |
68 |
| - global jet_count, rock_count, bottom |
| 67 | + rock = ROCKS[self.rock_count % len(ROCKS)] |
| 68 | + self.rock_count += 1 |
69 | 69 |
|
70 |
| - rock = rocks[rock_count % len(rocks)] |
71 |
| - rock_count += 1 |
| 70 | + width = len(rock[0]) |
| 71 | + height = len(rock) |
72 | 72 |
|
73 |
| - width = len(rock[0]) |
74 |
| - height = len(rock) |
| 73 | + y = self.cave_height() + 3 |
| 74 | + x = 2 |
75 | 75 |
|
76 |
| - y = cave_height() + 3 |
77 |
| - x = 2 |
| 76 | + while True: |
| 77 | + # current jet of gas |
| 78 | + gas = self.jets[self.jet_count % len(self.jets)] |
| 79 | + self.jet_count += 1 |
78 | 80 |
|
79 |
| - while True: |
80 |
| - # current jet of gas |
81 |
| - gas = jets[jet_count % len(jets)] |
82 |
| - jet_count += 1 |
| 81 | + # shift the rock if possible |
| 82 | + if gas == ">" and x + width + 1 <= 7 and not self.overlap(x + 1, y, rock): |
| 83 | + x += 1 |
| 84 | + elif gas == "<" and x > 0 and not self.overlap(x - 1, y, rock): |
| 85 | + x -= 1 |
83 | 86 |
|
84 |
| - # shift the rock if possible |
85 |
| - if gas == ">" and x + width + 1 <= 7 and not overlap(x + 1, y, rock): |
86 |
| - x += 1 |
87 |
| - elif gas == "<" and x > 0 and not overlap(x - 1, y, rock): |
88 |
| - x -= 1 |
| 87 | + # rock falls if possible |
| 88 | + if not self.overlap(x, y - 1, rock): |
| 89 | + y -= 1 |
| 90 | + else: |
| 91 | + break |
89 | 92 |
|
90 |
| - # rock falls if possible |
91 |
| - if not overlap(x, y - 1, rock): |
92 |
| - y -= 1 |
93 |
| - else: |
94 |
| - break |
| 93 | + for i in range(height): |
| 94 | + if y + i >= self.cave_height(): |
| 95 | + self.cave.append([0, 0, 0, 0, 0, 0, 0]) |
| 96 | + for j in range(width): |
| 97 | + if rock[i][j] == 1: |
| 98 | + self.cave[y + i - self.bottom][x + j] = 65 + (self.rock_count - 1) % len(ROCKS) |
95 | 99 |
|
96 |
| - for i in range(height): |
97 |
| - if y + i >= cave_height(): |
98 |
| - cave.append([0, 0, 0, 0, 0, 0, 0]) |
99 |
| - for j in range(width): |
100 |
| - if rock[i][j] == 1: |
101 |
| - cave[y + i - bottom][x + j] = 65 + (rock_count - 1) % len(rocks) |
| 100 | + if len(self.cave) > 200: |
| 101 | + del self.cave[0:100] |
| 102 | + self.bottom += 100 |
102 | 103 |
|
103 |
| - if len(cave) > 200: |
104 |
| - del cave[0:100] |
105 |
| - bottom += 100 |
| 104 | + def show(self, gameover: bool): |
106 | 105 |
|
| 106 | + max_rows = 25 |
| 107 | + print(end="\033[H\033[2J") |
107 | 108 |
|
108 |
| -def show(rx=None, ry=None, rock=None): |
109 |
| - global bottom |
| 109 | + h = self.cave_height() + 2 |
110 | 110 |
|
111 |
| - h = cave_height() + 2 |
112 |
| - if rx and ry and rock: |
113 |
| - width = len(rock[0]) |
114 |
| - height = len(rock) |
| 111 | + for y in range(h - 1, self.bottom - 1, -1): |
| 112 | + s = "|" if y > 0 else "+" |
115 | 113 |
|
116 |
| - h = max(h, ry + len(rock)) |
| 114 | + for x in range(0, 7): |
117 | 115 |
|
118 |
| - for y in range(h - 1, bottom - 1, -1): |
119 |
| - s = "|" if y > 0 else "+" |
| 116 | + if y < self.cave_height(): |
| 117 | + c = self.cave[y - self.bottom][x] |
| 118 | + else: |
| 119 | + c = 0 |
120 | 120 |
|
121 |
| - for x in range(0, 7): |
122 |
| - if rx and ry and rock: |
123 |
| - if rx <= x < rx + width and ry <= y < ry + height: |
124 |
| - if rock[y - ry][x - rx] == 1: |
125 |
| - s += "@" |
126 |
| - continue |
| 121 | + if c == 0: |
| 122 | + s += " " |
| 123 | + elif c == 1: |
| 124 | + s += "--" |
| 125 | + else: |
| 126 | + s += "\033[" + str(c - 65 + 31) + "m██\033[0m" |
| 127 | + # s += chr(c)█ |
127 | 128 |
|
128 |
| - if y < cave_height(): |
129 |
| - c = cave[y - bottom][x] |
130 |
| - else: |
131 |
| - c = 0 |
| 129 | + s += "|" if y > 0 else "+" |
132 | 130 |
|
133 |
| - if c == 0: |
134 |
| - s += "." |
135 |
| - elif c == 1: |
136 |
| - s += "-" |
| 131 | + if y == self.cave_height() - 1: |
| 132 | + print(f"{s} <= top ({y})") |
137 | 133 | else:
|
138 |
| - s += chr(c) |
| 134 | + print(s) |
| 135 | + |
| 136 | + max_rows -= 1 |
| 137 | + if max_rows == 0: |
| 138 | + time.sleep(0.1) |
| 139 | + break |
| 140 | + |
| 141 | + if self.bottom != 0: |
| 142 | + print(f"({self.bottom} rows suppressed)") |
| 143 | + |
| 144 | + def make_key(self): |
| 145 | + top = [] |
| 146 | + mask = 0 |
| 147 | + for y in range(len(self.cave) - 1, -1, -1): |
| 148 | + for i, c in enumerate(self.cave[y]): |
| 149 | + if c != 0: |
| 150 | + mask |= 1 << i |
| 151 | + top.append(mask) |
| 152 | + if mask == 127: # found a rock on all the 7 columns |
| 153 | + break |
| 154 | + return (self.rock_count % len(ROCKS), self.jet_count % len(self.jets), bytes(top)) |
| 155 | + |
| 156 | + def solve(self, show=False): |
| 157 | + |
| 158 | + keys = {} |
| 159 | + heights = [0] |
| 160 | + start, end = None, None |
| 161 | + part1 = None |
| 162 | + |
| 163 | + for n in range(1, 10000): |
| 164 | + self.fall() |
| 165 | + |
| 166 | + if show: |
| 167 | + self.show(False) |
| 168 | + |
| 169 | + if n == 2022: |
| 170 | + part1 = self.cave_height() - 1 |
| 171 | + |
| 172 | + if not end: |
| 173 | + heights.append(self.cave_height() - 1) |
| 174 | + key = self.make_key() |
| 175 | + if n > 2000 and key in keys: |
| 176 | + start = keys[key] |
| 177 | + end = n |
| 178 | + keys[key] = n |
| 179 | + |
| 180 | + if part1 and end: |
| 181 | + if show: |
| 182 | + self.show(True) |
| 183 | + break |
139 | 184 |
|
140 |
| - s += "|" if y > 0 else "+" |
141 |
| - |
142 |
| - if y == cave_height() - 1: |
143 |
| - print(f"{s} <= top ({y})") |
144 | 185 | else:
|
145 |
| - print(s) |
146 |
| - |
147 |
| - if bottom != 0: |
148 |
| - print(f"({bottom} rows suppressed)") |
149 |
| - |
150 |
| - |
151 |
| -def make_key(): |
152 |
| - global rock_count, jet_count |
153 |
| - top = [] |
154 |
| - mask = 0 |
155 |
| - for y in range(len(cave) - 1, -1, -1): |
156 |
| - for i, c in enumerate(cave[y]): |
157 |
| - if c != 0: |
158 |
| - mask |= 1 << i |
159 |
| - top.append(mask) |
160 |
| - if mask == 127: |
161 |
| - break |
162 |
| - return (rock_count % len(rocks), jet_count % len(jets), bytes(top)) |
163 |
| - |
164 |
| - |
165 |
| -keys = {} |
166 |
| -heights = [0] |
167 |
| -start, end = None, None |
168 |
| -part1 = None |
| 186 | + print("no solution") |
| 187 | + exit() |
169 | 188 |
|
170 |
| -for n in range(1, 10000): |
171 |
| - fall() |
| 189 | + # part 1 |
172 | 190 |
|
173 |
| - if n == 2022: |
174 |
| - part1 = cave_height() - 1 |
| 191 | + # part 2 |
| 192 | + q, r = divmod(1_000_000_000_000 - start, end - start) |
| 193 | + part2 = heights[start + r] + q * (heights[end] - heights[start]) |
175 | 194 |
|
176 |
| - if not end: |
177 |
| - heights.append(cave_height() - 1) |
178 |
| - key = make_key() |
179 |
| - if n > 2000 and key in keys: |
180 |
| - start = keys[key] |
181 |
| - end = n |
182 |
| - keys[key] = n |
| 195 | + return part1, part2 |
183 | 196 |
|
184 |
| - if part1 and end: |
185 |
| - break |
186 | 197 |
|
187 |
| -else: |
188 |
| - print("no solution") |
189 |
| - exit() |
| 198 | +def main(): |
| 199 | + parser = argparse.ArgumentParser() |
| 200 | + parser.add_argument("-v", "--verbose", action="store_true") |
| 201 | + parser.add_argument("-t", "--test", action="store_true") |
| 202 | + parser.add_argument("filename", nargs="?", type=Path, default="input.txt") |
| 203 | + args = parser.parse_args() |
| 204 | + if args.test: |
| 205 | + args.filename = Path("test.txt") |
190 | 206 |
|
191 |
| -# part 1 |
192 |
| -print(part1) |
| 207 | + data = args.filename.read_text().strip() |
193 | 208 |
|
194 |
| -# part 2 |
195 |
| -q, r = divmod(1_000_000_000_000 - start, end - start) |
196 |
| -part2 = heights[start + r] + q * (heights[end] - heights[start]) |
197 |
| -print(part2) |
| 209 | + tetris = Tetris(data) |
| 210 | + part1, part2 = tetris.solve(args.verbose) |
| 211 | + print(part1) |
| 212 | + print(part2) |
198 | 213 |
|
199 | 214 |
|
200 |
| -if filename == "test.txt": |
201 |
| - assert part1 == 3068 |
202 |
| - assert part2 == 1514285714288 |
| 215 | +if __name__ == "__main__": |
| 216 | + main() |
0 commit comments