diff --git a/2024/justfile b/2024/justfile new file mode 100644 index 0000000..9c044db --- /dev/null +++ b/2024/justfile @@ -0,0 +1,23 @@ +#!/usr/bin/env -S just --justfile + +DAY := `date +%d` + +setup: + python3 -m venv venv + +new: + cp python/_sample.py python/day{{ DAY }}.py + touch full/day{{ DAY }}.txt + touch samples/day{{ DAY }}.txt + +full: + /usr/bin/env python3 python/day{{ DAY }}.py + +sample: + /usr/bin/env python3 python/day{{ DAY }}.py --sample + +wsample: + watch /usr/bin/env python3 python/day{{ DAY }}.py --sample + +wfull: + watch /usr/bin/env python3 python/day{{ DAY }}.py diff --git a/2024/python/_sample.py b/2024/python/_sample.py new file mode 100644 index 0000000..f34bf85 --- /dev/null +++ b/2024/python/_sample.py @@ -0,0 +1,30 @@ +import matrix +import shared +import itertools +import functools + + +# @shared.profile +def part1(rows): + pass + + +# @shared.profile +def part2(rows): + pass + + +def main(): + rows = [row for row in shared.load_rows(15)] + with shared.elapsed_timer() as elapsed: + part1(rows) + print("🕒", elapsed()) + + rows = [row for row in shared.load_rows(1, True)] + with shared.elapsed_timer() as elapsed: + part2(rows) + print("🕒", elapsed()) + + +if __name__ == "__main__": + main() diff --git a/2024/python/anim.py b/2024/python/anim.py new file mode 100644 index 0000000..a1a1def --- /dev/null +++ b/2024/python/anim.py @@ -0,0 +1,45 @@ +from matrix import get_size, pmx +import imageio +import matplotlib.pyplot as plt +import numpy as np + + +class Animate: + def __init__(self, mx, day="CHANGEME"): + self.mx = mx + self.day = day + _size = get_size(mx) + self.height = _size[0] + self.width = _size[1] + self.f_count = -1 + + def add_frame(self, frame): + self.f_count += 1 + self.write_frame(frame) + + def write_frame(self, frame): + current = np.zeros_like(self.mx) + for y, row in enumerate(frame): + for x, col in enumerate(row): + current[y][x] = frame[y][x] + fig, ax = plt.subplots() + ax.imshow(current, cmap=plt.cm.gray) + ax.axis("off") + _figpath = f"gif-{self.day}/{self.f_count:05}.png" + plt.savefig(_figpath) + plt.close() + + def animate(self, frameskip=1): + with imageio.get_writer( + f"gif-{self.day}/day{self.day}.gif", mode="I" + ) as writer: + names = [ + f"gif-{self.day}/{x:05}.png" for x in range(0, self.f_count, frameskip) + ] + print(names) + for filename in names: + try: + image = imageio.imread(filename) + writer.append_data(image) + except FileNotFoundError: + pass diff --git a/2024/python/matrix.py b/2024/python/matrix.py new file mode 100644 index 0000000..4b5125f --- /dev/null +++ b/2024/python/matrix.py @@ -0,0 +1,472 @@ +from copy import deepcopy +from collections import defaultdict +import math + +from typing import List, Dict, Tuple + +split_word_to_chr_list = lambda y: [w for w in y] +split_word_to_int_list = lambda y: [int(w) for w in y] +split_line_to_int_list = lambda y: [int(w) for w in y.split(" ") if w] + + +def split_x_out(l): + return [x for _, x in l] + + +def split_y_out(l): + return [y for y, _ in l] + + +class colors: + # HEADER = '\033[95m' + BLUE = "\033[94m" + GREEN = "\033[92m" + YELLOW = "\033[93m" + RED = "\033[91m" + ENDC = "\033[0m" + BLINK = "\033[5m" + + +def apply_to_all(mx, func): + for row_num, row in enumerate(mx): + for col_num, val in enumerate(row): + mx[row_num][col_num] = func(val) + + +def rotate(m, right=True): # -90 + """ + Takes a matrix, and rotates all of the values 90 degrees to the left + """ + x = list(zip(*m[::-1])) + if right: + return x + return [list(reversed(y)) for y in x] + + +def load_matrix_file(name, func=None): + """ + Open a file and split all space separated word lists to integers as a matrix + """ + with open(name, "r") as f: + my_file = [] + for line in f: + my_file.append(line.rstrip()) + if func: + return [func(x) for x in my_file] + return [split_word_to_int_list(x) for x in my_file] + + +def number_starting_at(mx, x, y): + current = "" + try: + d = mx[y][x] + except IndexError: + return None + + if not d.isdigit(): + return None + + current += d + next = number_starting_at(mx, x + 1, y) + if next is None: + return current + return current + next + + +def find_in_matrix(mx, what, one=True): + coords = [] + for row_num, row in enumerate(mx): + for col_num, val in enumerate(row): + if val == what: + coord = (row_num, col_num) + if one is True: + return coord + else: + coords.append(coord) + return coords + + +def get_neighbors(matrix, x, y, _dict=False): + neighbors = [] + # left + try: + if x - 1 >= 0: + if _dict: + neighbors.append({"x": x - 1, "y": y, "value": matrix[y][x - 1]}) + else: + neighbors.append([(x - 1, y), matrix[y][x - 1]]) + except IndexError: + pass + # right + try: + if _dict: + neighbors.append({"x": x + 1, "y": y, "value": matrix[y][x + 1]}) + else: + neighbors.append([(x + 1, y), matrix[y][x + 1]]) + except IndexError: + pass + + # up + try: + if y - 1 >= 0: + if _dict: + neighbors.append({"x": x, "y": y - 1, "value": matrix[y - 1][x]}) + else: + neighbors.append([(x, y - 1), matrix[y - 1][x]]) + except IndexError: + pass + + # down + try: + if _dict: + neighbors.append({"x": x, "y": y + 1, "value": matrix[y + 1][x]}) + else: + neighbors.append([(x, y + 1), matrix[y + 1][x]]) + except IndexError: + pass + + return neighbors + + +def valid_neighbors(matrix, x, y, criteria=None): + if criteria is None: + raise Exception("Please pass in a lambda for criteria") + cur = matrix[y][x] + neighbors = get_neighbors(matrix, x, y, _dict=True) + valid = [] + for neighbor in neighbors: + if criteria(cur, neighbor["value"]): + valid.append(neighbor) + return valid + + +def sum_matrix(mtx): + total = 0 + for row in mtx: + total += sum(row) + return total + + +M_UL, M_U, M_UR = (-1, -1), (0, -1), (1, -1) +M_L, M_R = (-1, 0), (1, 0) +M_DL, M_D, M_DR = (-1, 1), (0, 1), (1, 1) + +M_NW, M_N, M_NE = (-1, -1), (0, -1), (1, -1) +M_W, M_E = (-1, 0), (1, 0) +M_SW, M_S, M_SE = (-1, 1), (0, 1), (1, 1) +CARDINALS = { + M_NW: "NW", + M_N: "N", + M_NE: "NE", + M_W: "W", + M_E: "E", + M_SW: "SW", + M_S: "S", + M_SE: "SE", +} + + +def get_neighbor_coords(matrix, c, r, diagonals=True): + height = len(matrix) + width = len(matrix[0]) + if diagonals: + coords = (M_UL, M_U, M_UR, M_L, M_R, M_DL, M_D, M_DR) + else: + coords = (M_U, M_L, M_R, M_D) + neighbors = [] + + for _c, _r in coords: + try: + value = matrix[r + _r][c + _c] # Try to get a value error + if r + _r >= 0 and c + _c >= 0: + neighbors.append( + [{"c": c + _c, "r": r + _r}, value] + ) # woo, no error, this coord is valid + except IndexError: + pass # okay we out of bounds boizzzz + return neighbors + +def get_neighbors_cardinal(matrix, c, r, diagonals=True): + height = len(matrix) + width = len(matrix[0]) + coords = [M_N, M_W, M_E, M_S] + if diagonals: + coords.extend([M_NW, M_NE, M_SW, M_SE]) + neighbors = {} + + for coord in coords: + _c, _r = coord + try: + value = matrix[r + _r][c + _c] # Try to get a value error + if r + _r >= 0 and c + _c >= 0: + neighbors[CARDINALS[coord]] = {"c": c + _c, "r": r + _r, "value":value} # woo, no error, this coord is valid + except IndexError: + pass # okay we out of bounds boizzzz + return neighbors + + + +def line_of_sight_coords( + matrix, row, col, distance=None +) -> Dict[str, List[Tuple[int, int]]]: + """ + Takes a matrix, a row, and a column + calculates the coordinates to the edge for all four cardinal directions + + returns a dict with a list of tuple coordes TRAVELING AWAY from the + requested coordinate + """ + height, width = get_size(matrix) + + col_ids = list(range(0, height)) + row_ids = list(range(0, width)) + + if distance: + up_ids, down_ids = ( + list(reversed(col_ids[:col])), + col_ids[col + 1 : col + distance + 1], + ) + left_ids, right_ids = ( + list(reversed(row_ids[:row])), + row_ids[row + 1 : row + distance + 1], + ) + else: + up_ids, down_ids = list(reversed(col_ids[:col])), col_ids[col + 1 :] + left_ids, right_ids = list(reversed(row_ids[:row])), row_ids[row + 1 :] + + left = [(r, col) for r in left_ids] + right = [(r, col) for r in right_ids] + up = [(row, c) for c in up_ids] + down = [(row, c) for c in down_ids] + + return { + "U": up, + "L": left, + "D": down, + "R": right, + } + + +def line_of_sight(mx, row, col, distance=None): + """ + renders a line of sight coord calculation, into the values + """ + coords = line_of_sight_coords(mx, row, col, distance) + los = defaultdict(list) + for k, ids in coords.items(): + for _row, _col in ids: + los[k].append(mx[_row][_col]) + return los + + +def coords_between_points(point1, point2): + y1, x1 = point1 + y2, x2 = point2 + + coords = [] + x = 0 + y = 0 + + if x2 < x1: + y = point1[0] + for _x in range(x2, x1 + 1): + coords.append((y, _x)) + elif x1 < x2: + y = point1[0] + for _x in range(x1, x2 + 1): + coords.append((y, _x)) + elif y2 < y1: + x = point1[1] + for _y in range(y2, y1 + 1): + coords.append((_y, x)) + elif y1 < y2: + x = point1[1] + for _y in range(y1, y2 + 1): + coords.append((_y, x)) + return coords + + +def get_size(matrix): + height = len(matrix) + width = len(matrix[0]) + return height, width + + +def row_col_from_int(matrix, x): + h, w = get_size(matrix) + col = x % w + row = x // h + return row, col + + +def matrix_of_size(width, height, default=0): + return [[default] * width for x in range(height)] + + +def set_matrix_dict(m): + for x in range(len(m)): + for y in range(len(m[x])): + m[x][y] = {} + return m + + +def pmx(*matrices, pad=True, space=True): + """ + print a matrix of integers, zero turns to `.` for clarity + """ + if len(matrices) > 1: + matrices = list(zip(*matrices)) + for row in matrices: + r = [] + for col in row: + r.append("".join([f"{int(x)or '.'}".rjust(3) for x in col])) + print(" ".join(r)) + else: + for row in matrices: + for c in row: + if pad: + f = lambda x: f"{int(x)or '.'}".rjust(2) + if space: + f = lambda x: f"{int(x)or '.'}".rjust(3) + else: + f = lambda x: f"{int(x)or '.'}" + if space: + f = lambda x: f"{int(x)or '.'} " + print("".join([f(x) for x in c])) + + +def ppmx(*matrices, pad=True, space=True, zero="."): + """ + print a matrix of anything, Falsy values turns to `.` for clarity + """ + out = [] + if len(matrices) > 1: + matrices = list(zip(*matrices)) + for row in matrices: + r = [] + for col in row: + r.append("".join([f"{x or zero}".rjust(3) for x in col])) + out.append(" ".join(r)) + else: + for row in matrices: + for c in row: + if pad: + f = lambda x: f"{x or zero}".rjust(2) + if space: + f = lambda x: f"{x or zero}".rjust(3) + else: + f = lambda x: f"{x or zero}" + if space: + f = lambda x: f"{x or zero} " + out.append("".join([f(x) for x in c])) + return "\n".join(out) + + +def view_matrix(matrix, y1, x1, y2, x2): + lines = ppmx(matrix, pad=0, space=0).split("\n") + for line in lines[y1 : y2 + 1]: + print(line[x1:x2]) + + +def highlight(matrix, red=[], green=[], blue=[], blink_green=[]): + """ + print a matrix of anything, Falsy values turns to `.` for clarity + """ + mx = deepcopy(matrix) + for y, x in red: + if (y, x) in blue or (y, x) in green or (y, x) in blink_green: + continue + new = f"{colors.RED}{mx[y][x]}{colors.ENDC}" + mx[y][x] = new + for y, x in green: + if (y, x) in blue or (y, x) in blink_green: + continue + new = f"{colors.GREEN}{mx[y][x]}{colors.ENDC}" + mx[y][x] = new + for y, x in blue: + if (y, x) in blink_green: + continue + new = f"{colors.BLUE}{mx[y][x]}{colors.ENDC}" + mx[y][x] = new + for y, x in blink_green: + new = f"{colors.BLINK}{colors.GREEN}{mx[y][x]}{colors.ENDC}" + mx[y][x] = new + print(ppmx(mx, pad=False, space=True, zero="0")) + + +def draw_shape_at(mx, row, col, shape=None, value=1): + if shape is None: + raise Exception("Please provide a list of coordinate offsets from Y,X to draw") + for y, x in shape: + mx[row + y][col + x] = value + + +def collision_at(mx, row, col, shape=None): + if shape is None: + raise Exception("Please provide a list of coordinate offsets from Y,X to draw") + for y, x in shape: + if mx[row + y][col + x] != 0: + return True + return False + + +def out_of_bounds(mx, row, col, shape=None): + if shape is None: + raise Exception("Please provide a list of coordinate offsets from Y,X to draw") + height, width = get_size(mx) + for y, x in shape: + if row + y > height - 1: + return True + if col + x >= width: + return True + if col + x < 0: + return True + return False + + +def spiral_generator(width, height): + k = 0 + l = 0 + m = height + n = width + + """ k - starting row index + m - ending row index + l - starting column index + n - ending column index + i - iterator """ + + while k < m and l < n: + # Print the first row from + # the remaining rows + for i in range(l, n): + yield (i, k) + # print(a[k][i], end=" ") + + k += 1 + + # Print the last column from + # the remaining columns + for i in range(k, m): + yield (n - 1, i) + # print(a[i][n - 1], end=" ") + + n -= 1 + + # Print the last row from + # the remaining rows + if k < m: + for i in range(n - 1, (l - 1), -1): + # print(a[m - 1][i], end=" ") + yield (i, m - 1) + + m -= 1 + + # Print the first column from + # the remaining columns + if l < n: + for i in range(m - 1, k - 1, -1): + # print(a[i][l], end=" ") + yield (l, i) + + l += 1 diff --git a/2024/python/shared.py b/2024/python/shared.py new file mode 100644 index 0000000..470c2a3 --- /dev/null +++ b/2024/python/shared.py @@ -0,0 +1,173 @@ +from contextlib import contextmanager +from timeit import default_timer +from pathlib import Path +import cProfile +import functools +import pstats +from itertools import groupby + +def all_equal(iterable): + g = groupby(iterable) + return next(g, True) and not next(g, False) + + +def profile(func): + @functools.wraps(func) + def inner(*args, **kwargs): + profiler = cProfile.Profile() + profiler.enable() + try: + retval = func(*args, **kwargs) + finally: + profiler.disable() + with open("profile.out", "w") as profile_file: + stats = pstats.Stats(profiler, stream=profile_file) + stats.print_stats() + return retval + + return inner + +def spl(y): + return [int(w) for w in y] + +def minmax(l): + return min(l), max(l) + + +def load_rows(day, part2=False): + return [row for row in load(day, part2)] + + +def load(day, part2=False): + if part2: + path = Path(get_fname(day) + ".part2") + try: + return path.read_text().rstrip().split("\n") + except FileNotFoundError: + # No part 2 file, use first file + pass + path = Path(get_fname(day)) + return path.read_text().rstrip().split("\n") + + +def get_fname(day: int) -> str: + import sys + + if sys.argv[-1] == "--sample": + return f"samples/day{day:02}.txt" + else: + return f"full/day{day:02}.txt" + + +############# +def load_char_matrix(f): + my_file = [] + for line in f: + my_file.append(line.rstrip()) + return [list(x) for x in my_file] + + +def load_file_char_matrix(name): + with open(name, "r") as f: + return load_char_matrix(f) + + +def load_int_matrix(f): + my_file = [] + for line in f: + my_file.append(line.rstrip()) + return [list(map(int, x)) for x in my_file] + + +def load_file_int_matrix(name): + with open(name, "r") as f: + return load_int_matrix(f) + + +def load_word_matrix(f): + my_file = [] + for line in f: + my_file.append(line.rstrip()) + return [x.split(" ") for x in my_file] + + +def load_file_word_matrix(name): + with open(name, "r") as f: + return load_word_matrix(f) + + +############# + + +def rotate(WHAT, times=1): + what = WHAT + for x in range(times): + what = list(zip(*what[::-1])) + return what + + +@contextmanager +def elapsed_timer(): + start = default_timer() + elapser = lambda: default_timer() - start + yield lambda: elapser() + end = default_timer() + elapser = lambda: end - start + + +def render_cubes(maxX, maxY, maxZ, my_cubes): + from mpl_toolkits.mplot3d import Axes3D + import numpy as np + import matplotlib.pyplot as plt + from mpl_toolkits.mplot3d.art3d import Poly3DCollection + + def cuboid_data(o, size=(1, 1, 1)): + X = [ + [[0, 1, 0], [0, 0, 0], [1, 0, 0], [1, 1, 0]], + [[0, 0, 0], [0, 0, 1], [1, 0, 1], [1, 0, 0]], + [[1, 0, 1], [1, 0, 0], [1, 1, 0], [1, 1, 1]], + [[0, 0, 1], [0, 0, 0], [0, 1, 0], [0, 1, 1]], + [[0, 1, 0], [0, 1, 1], [1, 1, 1], [1, 1, 0]], + [[0, 1, 1], [0, 0, 1], [1, 0, 1], [1, 1, 1]], + ] + X = np.array(X).astype(float) + for i in range(3): + X[:, :, i] *= size[i] + X += np.array(o) + return X + + def plotCubeAt(positions, sizes=None, colors=None, **kwargs): + if not isinstance(colors, (list, np.ndarray)): + colors = ["C0"] * len(positions) + if not isinstance(sizes, (list, np.ndarray)): + sizes = [(1, 1, 1)] * len(positions) + g = [] + for p, s, c in zip(positions, sizes, colors): + g.append(cuboid_data(p, size=s)) + return Poly3DCollection( + np.concatenate(g), facecolors=np.repeat(colors, 6, axis=0), **kwargs + ) + + N1 = maxX + N2 = maxY + N3 = maxZ + ma = np.random.choice([0, 1], size=(N1, N2, N3), p=[0.99, 0.01]) + x, y, z = np.indices((N1, N2, N3)) - 0.5 + # positions = np.c_[x[ma==1],y[ma==1],z[ma==1]] + positions = np.c_[my_cubes] + colors = np.random.rand(len(positions), 3) + + fig = plt.figure() + ax = fig.add_subplot(projection="3d") + ax.set_aspect("equal") + + pc = plotCubeAt(positions, colors=colors, edgecolor="k") + ax.add_collection3d(pc) + + ax.set_xlim([0, maxX]) + ax.set_ylim([0, maxY]) + ax.set_zlim([0, maxZ]) + # plotMatrix(ax, ma) + # ax.voxels(ma, edgecolor="k") + + plt.show()