From 6b938861a212342929b6d9b8470af2a5ddb5aa10 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 21 May 2020 17:56:37 +0200 Subject: [PATCH 001/143] Added days 2017-01 to 2017-04 --- 2017/01-Inverse Captcha.py | 65 ++++ 2017/02-Corruption Checksum.py | 70 +++++ 2017/03-Spiral Memory.py | 103 +++++++ 2017/04-High-Entropy Passphrases.py | 71 +++++ 2017/pathfinding.py | 457 ++++++++++++++++++++++++++++ 5 files changed, 766 insertions(+) create mode 100644 2017/01-Inverse Captcha.py create mode 100644 2017/02-Corruption Checksum.py create mode 100644 2017/03-Spiral Memory.py create mode 100644 2017/04-High-Entropy Passphrases.py create mode 100644 2017/pathfinding.py diff --git a/2017/01-Inverse Captcha.py b/2017/01-Inverse Captcha.py new file mode 100644 index 0000000..5efa306 --- /dev/null +++ b/2017/01-Inverse Captcha.py @@ -0,0 +1,65 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """1212""", + "expected": ['3', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['Unknown', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +captcha = 0 +if part_to_test == 1: + puzzle_input += puzzle_input[0] + for i in range (len(puzzle_input)-1): + if puzzle_input[i] == puzzle_input[i+1]: + captcha += int(puzzle_input[i]) + + puzzle_actual_result = captcha + + +else: + for i in range (len(puzzle_input)-1): + if puzzle_input[i] == puzzle_input[(i+len(puzzle_input)//2)%len(puzzle_input)]: + captcha += int(puzzle_input[i]) + + puzzle_actual_result = captcha + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/02-Corruption Checksum.py b/2017/02-Corruption Checksum.py new file mode 100644 index 0000000..8cf5e2a --- /dev/null +++ b/2017/02-Corruption Checksum.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data -------------------------------- # +import os, itertools + +test_data = {} + +test = 1 +test_data[test] = {"input": """5 1 9 5 +7 5 3 +2 4 6 8""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """5 9 2 8 +9 4 7 3 +3 8 6 5""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['Unknown', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +checksum = 0 +puzzle_input = puzzle_input.replace('\t', ' ') +if part_to_test == 1: + for string in puzzle_input.split('\n'): + digits = list(map(int, string.split(' '))) + checksum += max (digits) + checksum -= min (digits) + puzzle_actual_result = checksum + +else: + for string in puzzle_input.split('\n'): + digits = list(map(int, string.split(' '))) + for val in itertools.permutations(digits, 2): + if val[1] % val[0] == 0: + checksum += val[1] // val[0] + break + puzzle_actual_result = checksum + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/03-Spiral Memory.py b/2017/03-Spiral Memory.py new file mode 100644 index 0000000..2d8dca7 --- /dev/null +++ b/2017/03-Spiral Memory.py @@ -0,0 +1,103 @@ +# -------------------------------- Input data -------------------------------- # +import os, math + +test_data = {} + +test = 1 +test_data[test] = {"input": 17, + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +test_data[test] = {"input": 312051, + "expected": ['430', '312453'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + square_size = int(math.sqrt(puzzle_input)) + if square_size % 2 == 0: + square_size += 1 + else: + square_size += 2 + + + distance_from_square = (square_size ** 2 - puzzle_input) % (square_size-1) + + if distance_from_square <= square_size // 2: + distance_from_square = square_size // 2 - distance_from_square + else: + distance_from_square -= square_size // 2 + + puzzle_actual_result = (square_size - 1) // 2 + distance_from_square + + + +else: + vals = {} + direction = (1, 0) + current = (1,0) + vals[(0,0)] = 1 + + max_square = 1000 + + corner_SE = {x**2+1: (0, -1) for x in range(1, max_square) if x % 2 == 1} + corner_SW = {x**2 - (x-1): (1, 0) for x in range(1, max_square) if x % 2 == 1} + corner_NW = {x**2 - (x-1)*2: (0, 1) for x in range(2, max_square) if x % 2 == 1} + corner_NE = {x**2 - (x-1)*3: (-1, 0) for x in range(2, max_square) if x % 2 == 1} + corners = corner_SE.copy() + corners.update(corner_SW) + corners.update(corner_NW) + corners.update(corner_NE) + + for i in range (2, max_square): + value = 0 + + for neighbor in [(x, y) for x in (-1, 0, 1) for y in (-1, 0, 1) if not((x, y) == (0,0))]: + x, y = (current[0] + neighbor[0], current[1] + neighbor[1]) + if (x, y) in vals: + value += vals[(x, y)] + + vals[current] = value + + # In which direction are we going? + if i in corners: + direction = corners[i] + + current = (current[0] + direction[0], current[1] + direction[1]) + + if value > puzzle_input: + puzzle_actual_result = value + break + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/04-High-Entropy Passphrases.py b/2017/04-High-Entropy Passphrases.py new file mode 100644 index 0000000..d5a9ca6 --- /dev/null +++ b/2017/04-High-Entropy Passphrases.py @@ -0,0 +1,71 @@ +# -------------------------------- Input data -------------------------------- # +import os, itertools + +test_data = {} + +test = 1 +test_data[test] = {"input": """aa bb cc dd aaa""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """abcde fghij""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['455', '186'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + valid = 0 + for string in puzzle_input.split('\n'): + vals = string.split(' ') + duplicates = [vals.count(a) for a in vals if vals.count(a) != 1] + if not duplicates: + valid += 1 + puzzle_actual_result = valid + + +else: + valid = 0 + for string in puzzle_input.split('\n'): + vals = string.split(' ') + duplicates = [vals.count(a) for a in vals if vals.count(a) != 1] + + for val in vals: + anagram = [vals.count(''.join(permut)) for x in vals for permut in itertools.permutations(x) if x != ''.join(permut)] + + if not duplicates and not any(anagram): + valid += 1 + puzzle_actual_result = valid + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/pathfinding.py b/2017/pathfinding.py new file mode 100644 index 0000000..059b16f --- /dev/null +++ b/2017/pathfinding.py @@ -0,0 +1,457 @@ +import heapq + + +class TargetFound(Exception): + pass + +class NegativeWeightCycle(Exception): + pass + + + +class Graph: + vertices = [] + edges = {} + distance_from_start = {} + came_from = {} + + def __init__ (self, vertices = [], edges = {}): + self.vertices = vertices + self.edges = edges + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def is_valid (self, vertex): + return vertex in self.vertices + + def estimate_to_complete (self, source_vertex, target_vertex): + return 0 + + def reset_search (self): + self.distance_from_start = {} + self.came_from = {} + + def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have (x, y) as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + y += 1 + + directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] + if diagonals_allowed: + directions += [(1, 1), (1, -1), (-1, 1), (-1, -1)] + + for coords in self.vertices: + for direction in directions: + x, y = coords[0] + direction[0], coords[1] + direction[1] + if (x, y) in self.vertices: + if coords in self.edges: + self.edges[(coords)].append((x, y)) + else: + self.edges[(coords)] = [(x, y)] + + return True + + def vertices_to_grid (self, mark_coords = [], wall = '#'): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param list mark_coords: List of coordonates to mark + :param string wall: Which character to use as walls + :return: True if the grid was converted + """ + x, y = (0, 0) + grid = '' + + all_x = [i[0] for i in self.vertices] + all_y = [i[1] for i in self.vertices] + min_x, max_x = min(all_x), max(all_x) + min_y, max_y = min(all_y), max(all_y) + + if isinstance(next(iter(self.vertices)), dict): + vertices = self.vertices.keys() + else: + vertices = self.vertices + + for y in range(min_y, max_y+1): + for x in range(min_x, max_x+1): + if (x, y) in mark_coords: + grid += 'X' + elif (x, y) in vertices: + grid += '.' + else: + grid += wall + grid += '\n' + + return grid + + def depth_first_search (self, start, end = None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion (self, current_distance, vertex, end = None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def breadth_first_search (self, start, end = None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return False + return True + + def greedy_best_first_search (self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush(frontier, (self.estimate_to_complete(neighbor, end), neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path (self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#', cost_straight = 1, cost_diagonal = 2): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have (x, y) as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param boolean diagonals_allowed: Whether diagonal movement is allowed + :param float cost_straight: The cost of horizontal and vertical movements + :param float cost_diagonal: The cost of diagonal movements + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + y += 1 + + directions_straight = [(1, 0), (-1, 0), (0, 1), (0, -1)] + directions_diagonal = [(1, 1), (1, -1), (-1, 1), (-1, -1)] + + directions = directions_straight[:] + if diagonals_allowed: + directions += directions_diagonal + + for coords in self.vertices: + for direction in directions: + cost = cost_straight if direction in directions_straight \ + else cost_diagonal + x, y = coords[0] + direction[0], coords[1] + direction[1] + if (x, y) in self.vertices: + if coords in self.edges: + self.edges[(coords)][(x, y)] = cost + else: + self.edges[(coords)] = {(x, y): cost} + + return True + + def dijkstra (self, start, end = None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + return end is None or end in self.distance_from_start + + def a_star_search (self, start, end = None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush(frontier, (priority, neighbor, current_distance + weight)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford (self, start, end = None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range (len(self.vertices)-1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start + From 72b7d8987fcc5198f4c7ab8997e8db7d0a88d16e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 23 May 2020 22:11:53 +0200 Subject: [PATCH 002/143] Added days 2017-05 to 2017-08 --- ...A Maze of Twisty Trampolines, All Alike.py | 92 ++++++++++++++ 2017/06-Memory Reallocation.py | 66 ++++++++++ 2017/07-Recursive Circus.py | 119 ++++++++++++++++++ 2017/08-I Heard You Like Registers.py | 97 ++++++++++++++ 4 files changed, 374 insertions(+) create mode 100644 2017/05-A Maze of Twisty Trampolines, All Alike.py create mode 100644 2017/06-Memory Reallocation.py create mode 100644 2017/07-Recursive Circus.py create mode 100644 2017/08-I Heard You Like Registers.py diff --git a/2017/05-A Maze of Twisty Trampolines, All Alike.py b/2017/05-A Maze of Twisty Trampolines, All Alike.py new file mode 100644 index 0000000..ec1da2a --- /dev/null +++ b/2017/05-A Maze of Twisty Trampolines, All Alike.py @@ -0,0 +1,92 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0 +3 +0 +1 +-3""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['339351', '24315397'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + instructions = list(map(int, puzzle_input.split('\n'))) + i = 0 + step = 0 + while True: + try: + instruction = instructions[i] + instructions[i] += 1 + except IndexError: + break + + step += 1 + + i = i + instruction + + puzzle_actual_result = step + + + + +else: + instructions = list(map(int, puzzle_input.split('\n'))) + i = 0 + step = 0 + while True: + try: + instruction = instructions[i] + if instructions[i] >= 3: + instructions[i] -= 1 + else: + instructions[i] += 1 + except IndexError: + break + + step += 1 + + i = i + instruction + + puzzle_actual_result = step + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/06-Memory Reallocation.py b/2017/06-Memory Reallocation.py new file mode 100644 index 0000000..4cdcf6c --- /dev/null +++ b/2017/06-Memory Reallocation.py @@ -0,0 +1,66 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0 2 7 0""", + "expected": ['5', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +test_data[test] = {"input": '14 0 15 12 11 11 3 5 1 6 8 4 9 1 8 4', + "expected": ['11137', '1037'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +banks_history = [list(map(int, puzzle_input.split(' ')))] +steps = 0 +while True: + banks = banks_history[steps].copy() + bank_id = min([x for x in range(len(banks)) if banks[x] == max(banks)]) + redistribute = banks[bank_id] + banks[bank_id] = 0 + for i in range(1, redistribute + 1): + banks[(bank_id + i) % len(banks)] += 1 + + steps += 1 + if banks in banks_history: + if part_to_test == 1: + puzzle_actual_result = steps + else: + puzzle_actual_result = steps - min([x for x in range(len(banks_history)) if banks_history[x] == banks]) + break + + banks_history.append(banks) + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/07-Recursive Circus.py b/2017/07-Recursive Circus.py new file mode 100644 index 0000000..3a98451 --- /dev/null +++ b/2017/07-Recursive Circus.py @@ -0,0 +1,119 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """pbga (66) +xhth (57) +ebii (61) +havc (66) +ktlj (57) +fwft (72) -> ktlj, cntj, xhth +qoyq (66) +padx (45) -> pbga, havc, qoyq +tknk (41) -> ugml, padx, fwft +jptl (61) +ugml (68) -> gyxo, ebii, jptl +gyxo (61) +cntj (57)""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['vtzay', '910'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +all_held = [] +holders = [] +individual_weights = {} +branches = {} +for string in puzzle_input.split('\n'): + if string == '': + continue + + individual_weights[string.split(' ')[0]] = int(string.split(' ')[1][1:-1]) + + is_holder = string.split('->') + if len(is_holder) == 1: + all_held.append(string.split(' ')[0]) + continue + + holder, weight, _, *held = string.split(' ') + all_held += [x.replace(',', '') for x in held] + holders.append(holder) + + branches[holder] = [x.replace(',', '') for x in held] + +for holder in holders: + if holder not in all_held: + puzzle_actual_result = holder + break + + +if part_to_test == 2: + unknown_weights = holders.copy() + held_weight = {} + total_weight = {x:individual_weights[x] for x in individual_weights if x not in holders} + mismatch = {} + while len(unknown_weights): + for holder in unknown_weights: + if all([x in total_weight for x in branches[holder]]): + # We know the weights of all leaves, including sub-towers + + held_weight[holder] = [total_weight[x] for x in branches[holder]] + if any([x != held_weight[holder][0] for x in held_weight[holder]]): + mismatch.update({holder: held_weight[holder]}) + total_weight[holder] = sum(held_weight[holder]) + individual_weights[holder] + unknown_weights.remove(holder) + + # This is very ugly code + # First, determine which mismatch disk has the minimum weight (because that's the closest to the problem) + min_weight = min([y for x in mismatch for y in mismatch[x]]) + min_holder = [x for x in mismatch if min_weight in mismatch[x]][0] + + # Then, determine what are the correct and incorrect weights + count_weights = {mismatch[min_holder].count(x):x for x in mismatch[min_holder]} + wrong_weight = count_weights[1] + correct_weight = count_weights[len(mismatch[min_holder])-1] + delta = correct_weight - wrong_weight + + # Find which tower has the wrong individual weight, then calculate its new weight + wrong_holder = [x for x in branches[min_holder] if total_weight[x] == wrong_weight][0] + new_weight = individual_weights[wrong_holder] + delta + + puzzle_actual_result = new_weight + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/08-I Heard You Like Registers.py b/2017/08-I Heard You Like Registers.py new file mode 100644 index 0000000..703ef9f --- /dev/null +++ b/2017/08-I Heard You Like Registers.py @@ -0,0 +1,97 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """b inc 5 if a > 1 +a inc 1 if b < 5 +c dec -10 if a >= 1 +c inc -20 if c == 10""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['4416', '5199'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + +def is_integer (value): + try: + val = int(value) + return True + except ValueError: + return False + +def apply_operation (val1, action, val2): + if action == 'dec': + return val1 - int(val2) + else: + return val1 + int(val2) + +# -------------------------------- Actual code execution -------------------------------- # +registers = {} +max_value = 0 +for string in puzzle_input.split('\n'): + target, action, value, _, source, condition, operand = string.split(' ') + if not target in registers: + registers[target] = 0 + if not source in registers: + registers[source] = 0 + + if condition == '==': + if registers[source] == int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '!=': + if registers[source] != int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '>=': + if registers[source] >= int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '<=': + if registers[source] <= int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '>': + if registers[source] > int(operand): + registers[target] = apply_operation (registers[target], action, value) + elif condition == '<': + if registers[source] < int(operand): + registers[target] = apply_operation (registers[target], action, value) + + max_value = max(max_value, max(registers.values())) + +if part_to_test == 1: + puzzle_actual_result = max(registers.values()) +else: + puzzle_actual_result = max_value + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 96bc519d584fff559ca0f72bac6eac1636cf8802 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 24 May 2020 19:06:00 +0200 Subject: [PATCH 003/143] Added days 2017-09, 2017-10, 2017-11 --- 2017/09-Stream Processing.py | 90 +++++++++++++++++++++ 2017/10-Knot Hash.py | 105 +++++++++++++++++++++++++ 2017/11-Hex Ed.py | 147 +++++++++++++++++++++++++++++++++++ 3 files changed, 342 insertions(+) create mode 100644 2017/09-Stream Processing.py create mode 100644 2017/10-Knot Hash.py create mode 100644 2017/11-Hex Ed.py diff --git a/2017/09-Stream Processing.py b/2017/09-Stream Processing.py new file mode 100644 index 0000000..efb15e9 --- /dev/null +++ b/2017/09-Stream Processing.py @@ -0,0 +1,90 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """<{o"i!a,<{i""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['9251', '4322'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +for string in puzzle_input.split('\n'): + old_string = string + new_string = '' + skip = False + for index in range(len(old_string)): + if skip: + skip = False + continue + elif old_string[index] == '!': + skip = True + else: + new_string += old_string[index] + + garbage = False + total_garbage = 0 + old_string = new_string + new_string = '' + for index in range(len(old_string)): + if old_string[index] == '<' and not garbage: + garbage = True + elif old_string[index] == '>': + garbage = False + elif garbage: + total_garbage += 1 + else: + new_string += old_string[index] + + old_string = new_string + new_string = '' + total_score = 0 + local_score = 0 + for index in range(len(old_string)): + if old_string[index] == '{': + local_score += 1 + elif old_string[index] == '}': + total_score += local_score + local_score -= 1 + + if part_to_test == 1: + puzzle_actual_result = total_score + else: + puzzle_actual_result = total_garbage + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/10-Knot Hash.py b/2017/10-Knot Hash.py new file mode 100644 index 0000000..ae4890e --- /dev/null +++ b/2017/10-Knot Hash.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os +from functools import reduce + +test_data = {} + +test = 1 +test_data[test] = {"input": (range(0, 5), '3,4,1,5'), + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": (range(0, 256), open(input_file, "r+").read().strip()), + "expected": ['19591', '62e2204d2ca4f4924f6e7a80f1288786'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + current_position = 0 + skip_len = 0 + rope = list(puzzle_input[0]) + + for reverse_length in puzzle_input[1].split(','): + reverse_length = int(reverse_length) + + if current_position+reverse_length > len(rope): + new_rope = rope[current_position:] + rope[:(current_position+reverse_length) % len(rope)] + new_rope = new_rope[::-1] + rope[current_position:] = new_rope[:len(rope)-current_position] + rope[:(current_position+reverse_length) % len(rope)] = new_rope[len(rope)-current_position:] + else: + new_rope = rope[current_position:current_position+reverse_length] + new_rope = new_rope[::-1] + rope[current_position:current_position+reverse_length] = new_rope + + current_position += reverse_length + skip_len + current_position = current_position % len(rope) + skip_len += 1 + + puzzle_actual_result = rope[0] * rope[1] + + +else: + current_position = 0 + skip_len = 0 + rope = list(puzzle_input[0]) + + for i in range (64): + + lengths_list = [ord(x) for x in puzzle_input[1]] + [17, 31, 73, 47, 23] + + for reverse_length in lengths_list: + if current_position+reverse_length > len(rope): + new_rope = rope[current_position:] + rope[:(current_position+reverse_length) % len(rope)] + new_rope = new_rope[::-1] + rope[current_position:] = new_rope[:len(rope)-current_position] + rope[:(current_position+reverse_length) % len(rope)] = new_rope[len(rope)-current_position:] + else: + new_rope = rope[current_position:current_position+reverse_length] + new_rope = new_rope[::-1] + rope[current_position:current_position+reverse_length] = new_rope + + current_position += reverse_length + skip_len + current_position = current_position % len(rope) + skip_len += 1 + + dense_hash = '' + for i in range (16): + xor_value = reduce(lambda a, b: a^b, rope[i*16:i*16+16]) + dense_hash += '%02x'%xor_value + + puzzle_actual_result = dense_hash + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/11-Hex Ed.py b/2017/11-Hex Ed.py new file mode 100644 index 0000000..107e34e --- /dev/null +++ b/2017/11-Hex Ed.py @@ -0,0 +1,147 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """ne,ne,ne""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['877', '1622'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + for string in puzzle_input.split('\n'): + # Simplifies counting + string = string.split(',') + nb_se = string.count('se') + nb_sw = string.count('sw') + nb_s = string.count('s') + nb_ne = string.count('ne') + nb_nw = string.count('nw') + nb_n = string.count('n') + + # This just makes sure all conversions are done twice (as they influence each other) + for i in range (2): + + # Convert se+sw in s + nb_s += min(nb_se, nb_sw) + nb_se, nb_sw = nb_se - min(nb_se, nb_sw), nb_sw - min(nb_se, nb_sw) + + # Convert ne+nw in n + nb_n += min(nb_ne, nb_nw) + nb_ne, nb_nw = nb_ne - min(nb_ne, nb_nw), nb_nw - min(nb_ne, nb_nw) + + # Convert sw+n in nw + nb_nw += min(nb_sw, nb_n) + nb_sw, nb_n = nb_sw - min(nb_sw, nb_n), nb_n - min(nb_sw, nb_n) + + # Convert nw+s in sw + nb_sw += min(nb_nw, nb_s) + nb_nw, nb_s = nb_nw - min(nb_nw, nb_s), nb_s - min(nb_nw, nb_s) + + # Convert se+n in ne + nb_ne += min(nb_se, nb_n) + nb_se, nb_n = nb_se - min(nb_se, nb_n), nb_n - min(nb_se, nb_n) + + # Convert ne+s in se + nb_se += min(nb_ne, nb_s) + nb_ne, nb_s = nb_ne - min(nb_ne, nb_s), nb_s - min(nb_ne, nb_s) + + # Cancel ne and sw + nb_ne, nb_sw = nb_ne - min(nb_ne, nb_sw), nb_sw - min(nb_ne, nb_sw) + + # Cancel nw and se + nb_nw, nb_se = nb_nw - min(nb_nw, nb_se), nb_se - min(nb_ne, nb_se) + + # Cancel n and s + nb_n, nb_s = nb_n - min(nb_n, nb_s), nb_s - min(nb_n, nb_s) + + puzzle_actual_result = sum([nb_se, nb_sw, nb_s, nb_ne, nb_nw, nb_n]) + +else: + max_distance = 0 + + all_steps = puzzle_input.split(',') + + for i in range (len(all_steps)): + steps = all_steps[0:i+1] + + nb_se = steps.count('se') + nb_sw = steps.count('sw') + nb_s = steps.count('s') + nb_ne = steps.count('ne') + nb_nw = steps.count('nw') + nb_n = steps.count('n') + + # This just makes sure all conversions are done twice (as they influence each other) + for i in range (2): + + # Convert se+sw in s + nb_s += min(nb_se, nb_sw) + nb_se, nb_sw = nb_se - min(nb_se, nb_sw), nb_sw - min(nb_se, nb_sw) + + # Convert ne+nw in n + nb_n += min(nb_ne, nb_nw) + nb_ne, nb_nw = nb_ne - min(nb_ne, nb_nw), nb_nw - min(nb_ne, nb_nw) + + # Convert sw+n in nw + nb_nw += min(nb_sw, nb_n) + nb_sw, nb_n = nb_sw - min(nb_sw, nb_n), nb_n - min(nb_sw, nb_n) + + # Convert nw+s in sw + nb_sw += min(nb_nw, nb_s) + nb_nw, nb_s = nb_nw - min(nb_nw, nb_s), nb_s - min(nb_nw, nb_s) + + # Convert se+n in ne + nb_ne += min(nb_se, nb_n) + nb_se, nb_n = nb_se - min(nb_se, nb_n), nb_n - min(nb_se, nb_n) + + # Convert ne+s in se + nb_se += min(nb_ne, nb_s) + nb_ne, nb_s = nb_ne - min(nb_ne, nb_s), nb_s - min(nb_ne, nb_s) + + # Cancel ne and sw + nb_ne, nb_sw = nb_ne - min(nb_ne, nb_sw), nb_sw - min(nb_ne, nb_sw) + + # Cancel nw and se + nb_nw, nb_se = nb_nw - min(nb_nw, nb_se), nb_se - min(nb_ne, nb_se) + + # Cancel n and s + nb_n, nb_s = nb_n - min(nb_n, nb_s), nb_s - min(nb_n, nb_s) + + max_distance = max(max_distance, sum([nb_se, nb_sw, nb_s, nb_ne, nb_nw, nb_n])) + + puzzle_actual_result = max_distance + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From b22e8e778bc84f17c7b607c6136cfefd5b01c754 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 25 May 2020 22:17:25 +0200 Subject: [PATCH 004/143] Added days 2017-12 and 2017-13 --- 2017/12-Digital Plumber.py | 96 ++++++++++++++++++++++++++++++++++++++ 2017/13-Packet Scanners.py | 87 ++++++++++++++++++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 2017/12-Digital Plumber.py create mode 100644 2017/13-Packet Scanners.py diff --git a/2017/12-Digital Plumber.py b/2017/12-Digital Plumber.py new file mode 100644 index 0000000..597b0e8 --- /dev/null +++ b/2017/12-Digital Plumber.py @@ -0,0 +1,96 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = {"input": """0 <-> 2 +1 <-> 1 +2 <-> 0, 3, 4 +3 <-> 2, 4 +4 <-> 2, 3, 6 +5 <-> 6 +6 <-> 4, 5""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['378', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +pipes = {} +programs = [] +for string in puzzle_input.split('\n'): + source, _, *targets = string.split(' ') + targets = [x.replace(',', '') for x in targets] + + if not source in pipes: + pipes[source] = [] + + pipes[source] += targets + + for target in targets: + if not target in pipes: + pipes[target] = [] + else: + pipes[target].append(source) + +programs = pipes.keys() + +village = pathfinding.Graph(programs, pipes) +village.breadth_first_search('0') + +if part_to_test == 1: + puzzle_actual_result = len(village.distance_from_start) + +else: + nb_groups = 1 + programs_in_groups = list(village.distance_from_start.keys()) + for program in programs: + if program in programs_in_groups: + continue + + nb_groups += 1 + + village.reset_search() + village.breadth_first_search(program) + programs_in_groups += list(village.distance_from_start.keys()) + + puzzle_actual_result = nb_groups + + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/13-Packet Scanners.py b/2017/13-Packet Scanners.py new file mode 100644 index 0000000..b223da6 --- /dev/null +++ b/2017/13-Packet Scanners.py @@ -0,0 +1,87 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0: 3 +1: 2 +4: 4 +6: 4""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['3184', '3878062'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +levels = {} +for string in puzzle_input.split('\n'): + depth, size = string.split(': ') + depth, size = int(depth), int(size) + levels[depth] = size + + +if part_to_test == 1: + scanners = {x:0 for x in levels} + severity = 0 + for position in range(max(levels.keys())+1): + # Move packet + if position in scanners: + if scanners[position] == 0: + severity += position * levels[position] + if part_to_test == 2: + severity = 1 + break + + # Move scanners + scanners = {x:min(position+1, 2*(levels[x]-1) - position-1) % (2*levels[x]-2) for x in scanners} + + puzzle_actual_result = severity + +else: + for delay in range (10**15): + caught = False + for depth, size in levels.items(): + if ((delay + depth) / (2*(size-1))).is_integer(): + caught = True + break + + if not caught: + puzzle_actual_result = delay + break + +# Fails for 0-1999 + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From d15829e936541746bfcad636b85ddabe0b4713b9 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 26 May 2020 22:24:33 +0200 Subject: [PATCH 005/143] Added day 2017-14 --- 2017/14-Disk Defragmentation.py | 100 ++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 2017/14-Disk Defragmentation.py diff --git a/2017/14-Disk Defragmentation.py b/2017/14-Disk Defragmentation.py new file mode 100644 index 0000000..b447ed9 --- /dev/null +++ b/2017/14-Disk Defragmentation.py @@ -0,0 +1,100 @@ +# -------------------------------- Input data -------------------------------- # +import os +from functools import reduce +import pathfinding + +test_data = {} + +test = 1 +test_data[test] = {"input": """flqrgnkx""", + "expected": ['8108', '1242'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": 'wenycdww', + "expected": ['8226', '1128'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + + +count_used = 0 +dense_hash = '' +for row in range(128): + current_position = 0 + skip_len = 0 + rope = list(range(256)) + + lengths_list = [ord(x) for x in (puzzle_input + '-' + str(row))] + [17, 31, 73, 47, 23] + for i in range (64): + for reverse_length in lengths_list: + if current_position+reverse_length > len(rope): + new_rope = rope[current_position:] + rope[:(current_position+reverse_length) % len(rope)] + new_rope = new_rope[::-1] + rope[current_position:] = new_rope[:len(rope)-current_position] + rope[:(current_position+reverse_length) % len(rope)] = new_rope[len(rope)-current_position:] + else: + new_rope = rope[current_position:current_position+reverse_length] + new_rope = new_rope[::-1] + rope[current_position:current_position+reverse_length] = new_rope + + current_position += reverse_length + skip_len + current_position = current_position % len(rope) + skip_len += 1 + + + for i in range (16): + xor_value = reduce(lambda a, b: a^b, rope[i*16:(i+1)*16]) + dense_hash += '{0:08b}'.format(xor_value) + + dense_hash += '\n' + +if part_to_test == 1: + puzzle_actual_result = dense_hash.count('1') + +else: + dense_hash = dense_hash.replace('1', '.').replace('0', '#') + + graph = pathfinding.Graph() + graph.grid_to_vertices(dense_hash) + + nb_groups = 0 + cells_in_groups = [] + for vertex in graph.vertices: + if vertex in cells_in_groups: + continue + + nb_groups += 1 + + graph.reset_search() + graph.breadth_first_search(vertex) + cells_in_groups += list(graph.distance_from_start.keys()) + + + puzzle_actual_result = nb_groups + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 08aa8350c84bccfe286a97f0feb63f7ccf048665 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 27 May 2020 21:34:51 +0200 Subject: [PATCH 006/143] Added day 2017-15 --- 2017/15-Dueling Generators.py | 93 +++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 2017/15-Dueling Generators.py diff --git a/2017/15-Dueling Generators.py b/2017/15-Dueling Generators.py new file mode 100644 index 0000000..92f21c8 --- /dev/null +++ b/2017/15-Dueling Generators.py @@ -0,0 +1,93 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """Generator A starts with 65 +Generator B starts with 8921""", + "expected": ['588', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['597', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +divisor = 2147483647 +factors = {'A': 16807, 'B': 48271} +value = {'A': 0, 'B': 0} + + +def gen_a (): + while True: + value['A'] *= factors['A'] + value['A'] %= divisor + if value['A'] % 4 == 0: + yield value['A'] + +def gen_b (): + while True: + value['B'] *= factors['B'] + value['B'] %= divisor + if value['B'] % 8 == 0: + yield value['B'] + +if part_to_test == 1: + for string in puzzle_input.split('\n'): + _, generator, _, _, start_value = string.split() + value[generator] = int(start_value) + + nb_matches = 0 + for i in range (40 * 10 ** 6): + value = {gen: value[gen] * factors[gen] % divisor for gen in value} + if '{0:b}'.format(value['A'])[-16:] == '{0:b}'.format(value['B'])[-16:]: + nb_matches += 1 + + puzzle_actual_result = nb_matches + + +else: + for string in puzzle_input.split('\n'): + _, generator, _, _, start_value = string.split() + value[generator] = int(start_value) + + nb_matches = 0 + A = gen_a() + B = gen_b() + for count_pairs in range (5 * 10**6): + a, b = next(A), next(B) + if '{0:b}'.format(a)[-16:] == '{0:b}'.format(b)[-16:]: + nb_matches += 1 + + + puzzle_actual_result = nb_matches + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 452c69b6f16e68976473b050b38b5283d0bd4129 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 30 May 2020 22:20:26 +0200 Subject: [PATCH 007/143] Added day 2017-16 --- 2017/16-Permutation Promenade.py | 89 ++++++++++++++++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 2017/16-Permutation Promenade.py diff --git a/2017/16-Permutation Promenade.py b/2017/16-Permutation Promenade.py new file mode 100644 index 0000000..f999cc1 --- /dev/null +++ b/2017/16-Permutation Promenade.py @@ -0,0 +1,89 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": ('abcde', """s1,x3/4,pe/b"""), + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": ('abcdefghijklmnop', open(input_file, "r+").read().strip()), + "expected": ['ceijbfoamgkdnlph', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + + + +if part_to_test == 1: + programs = puzzle_input[0] + for string in puzzle_input[1].split(','): + if string[0] == 's': + shift = int(string[1:]) + programs = programs[-shift:] + programs[:-shift] + elif string[0] == 'x': + a, b = int(string[1:].split('/')[0]), int(string[1:].split('/')[1]) + a, b = min(a, b), max(a, b) + programs = programs[:a] + programs[b] + programs[a+1:b] + programs[a] + programs[b+1:] + elif string[0] == 'p': + a, b = string[1:].split('/')[0], string[1:].split('/')[1] + programs = programs.replace(a, '#').replace(b, a).replace('#', b) + + puzzle_actual_result = programs + + + +else: + programs = puzzle_input[0] + positions = [programs] + i = 0 + while i < 10**9: + i += 1 + for string in puzzle_input[1].split(','): + if string[0] == 's': + shift = int(string[1:]) + programs = programs[-shift:] + programs[:-shift] + elif string[0] == 'x': + a, b = int(string[1:].split('/')[0]), int(string[1:].split('/')[1]) + a, b = min(a, b), max(a, b) + programs = programs[:a] + programs[b] + programs[a+1:b] + programs[a] + programs[b+1:] + elif string[0] == 'p': + a, b = string[1:].split('/')[0], string[1:].split('/')[1] + programs = programs.replace(a, '#').replace(b, a).replace('#', b) + + if programs in positions: + cycle_length = i - positions.index(programs) + i += (10**9 // cycle_length - 1) * cycle_length + print ('cycle length', cycle_length) + + + puzzle_actual_result = programs + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 088c54aced5f16cc2c8e9c6169408b2d5990ad0e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 31 May 2020 16:18:17 +0200 Subject: [PATCH 008/143] Removed a lot of print statements --- 2015/08-Matchsticks.py | 8 +--- 2015/22-Wizard Simulator 20XX.py | 42 ++++++++++++------- 2015/24-It Hangs in the Balance.py | 2 - 2016/01-No Time for a Taxicab.py | 1 - 2016/03-Squares With Three Sides.py | 4 -- 2016/05-How About a Nice Game of Chess.py | 2 - 2016/09-Explosives in Cyberspace.py | 1 - ...-Radioisotope Thermoelectric Generators.py | 2 - 2016/14-One-Time Pad.py | 10 ----- 2016/15-Timing is Everything.py | 2 - 2016/17-Two Steps Forward.py | 2 - 2016/20-Firewall Rules.py | 6 --- 2016/21-Scrambled Letters and Hash.py | 2 - 2016/23-Safe Cracking.py | 2 - 2016/25-Clock Signal.py | 6 +-- 15 files changed, 30 insertions(+), 62 deletions(-) diff --git a/2015/08-Matchsticks.py b/2015/08-Matchsticks.py index 7c6304f..d79a41d 100644 --- a/2015/08-Matchsticks.py +++ b/2015/08-Matchsticks.py @@ -4,7 +4,7 @@ test_data = {} test = 1 -test_data[test] = {"input": open('test.txt', "r+").read(), +test_data[test] = {"input": '', "expected": ['12', '19'], } @@ -35,7 +35,6 @@ len_literals = 0 len_memory = 0 for string in puzzle_input.split('\n'): - print (string) len_literals += len(string) string = string.replace('\\\\', '\\').replace('\\"', '"') @@ -44,8 +43,6 @@ len_memory += len(string) - print (string, len_literals, len_memory) - puzzle_actual_result = len_literals - len_memory @@ -53,7 +50,6 @@ len_literals = 0 len_escaped = 0 for string in puzzle_input.split('\n'): - print (string) len_literals += len(string) string = string.replace('\\', '\\\\').replace('"', '\\"') @@ -61,8 +57,6 @@ len_escaped += len(string) - print (string, len_literals, len_escaped) - puzzle_actual_result = len_escaped - len_literals # -------------------------------- Outputs / results -------------------------------- # diff --git a/2015/22-Wizard Simulator 20XX.py b/2015/22-Wizard Simulator 20XX.py index d922174..a7bdc0c 100644 --- a/2015/22-Wizard Simulator 20XX.py +++ b/2015/22-Wizard Simulator 20XX.py @@ -91,7 +91,8 @@ def apply_effects (counters, player_stats, boss_stats): if part_to_test == 2: player_stats[1] -= 1 if player_stats[1] <= 0: - print ('Boss wins') + if verbose_level >=2: + print ('Boss wins') break # Apply effects @@ -102,14 +103,16 @@ def apply_effects (counters, player_stats, boss_stats): # Apply player move if spells[player_action][0] > player_stats[0]: - print ('Aborting: not enough mana') + if verbose_level >=2: + print ('Aborting: not enough mana') break if spells[player_action][1] == 1: player_stats[1] += spells[player_action][3] boss_stats[0] -= spells[player_action][2] else: if counters[player_action] != 0: - print ('Aborting: reused ' + player_action) + if verbose_level >=2: + print ('Aborting: reused ' + player_action) break else: counters[player_action] = spells[player_action][1] @@ -120,7 +123,8 @@ def apply_effects (counters, player_stats, boss_stats): print (counters, player_stats, boss_stats) if boss_stats[0] <= 0: - print ('Player wins with', mana_used, 'mana used') + if verbose_level >=2: + print ('Player wins with', mana_used, 'mana used') min_mana_used = min (min_mana_used, mana_used) break if mana_used > min_mana_used: @@ -140,7 +144,8 @@ def apply_effects (counters, player_stats, boss_stats): print (counters, player_stats, boss_stats) if player_stats[1] <= 0: - print ('Boss wins') + if verbose_level >=2: + print ('Boss wins') break else: max_moves = 15 @@ -152,7 +157,8 @@ def apply_effects (counters, player_stats, boss_stats): for strategy in itertools.product(spells.keys(), repeat=max_moves): count_strategies -= 1 if 'S' not in strategy[0:4] or 'R' not in strategy[0:5]: - print (' Missing Shield or Recharge') + if verbose_level >=2: + print (' Missing Shield or Recharge') continue if any ([True for i in range(1, max_moves) if strategy[0:i] in pruned_strategies]): print (' Pruned') @@ -175,7 +181,8 @@ def apply_effects (counters, player_stats, boss_stats): # Player turn player_hp -= 1 if player_hp <= 0: - print ('Boss wins') + if verbose_level >=2: + print ('Boss wins') # pruned_strategies.append(tuple(actions_done)) break @@ -198,7 +205,8 @@ def apply_effects (counters, player_stats, boss_stats): # Apply player move if spells[player_action][0] > player_mana: - print ('Aborting: not enough mana') + if verbose_level >=2: + print ('Aborting: not enough mana') # pruned_strategies.append(actions_done) break # Missile @@ -215,7 +223,8 @@ def apply_effects (counters, player_stats, boss_stats): # Shield elif player_action == 'S': if shield_left != 0: - print ('Aborting: reused ' + player_action) + if verbose_level >=2: + print ('Aborting: reused ' + player_action) # pruned_strategies.append(actions_done) break else: @@ -223,7 +232,8 @@ def apply_effects (counters, player_stats, boss_stats): # Poison elif player_action == 'P': if poison_left != 0: - print ('Aborting: reused ' + player_action) + if verbose_level >=2: + print ('Aborting: reused ' + player_action) # pruned_strategies.append(actions_done) break else: @@ -231,18 +241,21 @@ def apply_effects (counters, player_stats, boss_stats): # Recharge elif player_action == 'R': if recharge_left != 0: - print ('Aborting: reused ' + player_action) + if verbose_level >=2: + print ('Aborting: reused ' + player_action) # pruned_strategies.append(actions_done) break else: shield_left = 5 if boss_hp <= 0: - print ('Player wins with', mana_used, 'mana used') + if verbose_level >=2: + print ('Player wins with', mana_used, 'mana used') min_mana_used = min (min_mana_used, mana_used) break if mana_used > min_mana_used: - print ('Aborting: too much mana used') + if verbose_level >=2: + print ('Aborting: too much mana used') break @@ -263,7 +276,8 @@ def apply_effects (counters, player_stats, boss_stats): player_hp -= boss_dmg - player_armor if player_hp <= 0: - print ('Boss wins') + if verbose_level >=2: + print ('Boss wins') # pruned_strategies.append(actions_done) break else: diff --git a/2015/24-It Hangs in the Balance.py b/2015/24-It Hangs in the Balance.py index 95247a8..5201414 100644 --- a/2015/24-It Hangs in the Balance.py +++ b/2015/24-It Hangs in the Balance.py @@ -50,7 +50,6 @@ group_weight = total_weight // 3 if part_to_test == 1 else total_weight // 4 for group1_size in range (1, len(list_packages) - 2): - print('Testing with group 1 of size', group1_size) for group1 in itertools.combinations(list_packages, group1_size): if sum(group1) != group_weight: continue @@ -60,7 +59,6 @@ remaining_packages = [x for x in list_packages if x not in group1] for group2_size in range (1, len(remaining_packages) - 2): - print('Testing with group 2 of size', group2_size) for group2 in itertools.combinations(remaining_packages, group2_size): if sum(group2) == group_weight: mini_quantum_entanglement = min(mini_quantum_entanglement, reduce(mul, group1, 1)) diff --git a/2016/01-No Time for a Taxicab.py b/2016/01-No Time for a Taxicab.py index 787664d..badb468 100644 --- a/2016/01-No Time for a Taxicab.py +++ b/2016/01-No Time for a Taxicab.py @@ -89,7 +89,6 @@ if (x1, y1) == (x, y): continue if (x1, y1) in locations_visited and puzzle_actual_result == 'Unknown': - print (x1, y1) puzzle_actual_result = abs(x1) + abs(y1) break locations_visited.append((x1, y1)) diff --git a/2016/03-Squares With Three Sides.py b/2016/03-Squares With Three Sides.py index c974c21..bcfcd83 100644 --- a/2016/03-Squares With Three Sides.py +++ b/2016/03-Squares With Three Sides.py @@ -42,8 +42,6 @@ sides.sort() a, b, c = sides - print (string, a, b, c, a+b) - if c < (a + b): possible_triangles += 1 @@ -57,8 +55,6 @@ for n in range(len(lines)//3): for i in range (3): sides = [int(lines[n*3+y][i]) for y in range (3)] - print (lines[n*3:n*3+3]) - print(sides) sides.sort() a, b, c = sides diff --git a/2016/05-How About a Nice Game of Chess.py b/2016/05-How About a Nice Game of Chess.py index 7612b04..ed6bc4a 100644 --- a/2016/05-How About a Nice Game of Chess.py +++ b/2016/05-How About a Nice Game of Chess.py @@ -43,7 +43,6 @@ encoded = hashlib.md5(coded_value).hexdigest() if encoded[0:5] == '00000': password += encoded[5] - print (i, password, coded_value, encoded) if len(password) == 8: puzzle_actual_result = password break @@ -61,7 +60,6 @@ continue if password[int(encoded[5])] == '_': password[int(encoded[5])] = encoded[6] - print (i, ''.join(password), coded_value, encoded) if '_' not in password: puzzle_actual_result = ''.join(password) break diff --git a/2016/09-Explosives in Cyberspace.py b/2016/09-Explosives in Cyberspace.py index 61be23f..7f01104 100644 --- a/2016/09-Explosives in Cyberspace.py +++ b/2016/09-Explosives in Cyberspace.py @@ -87,7 +87,6 @@ continue def decompress(string): - print (string) total_length = 0 if '(' in string: diff --git a/2016/11-Radioisotope Thermoelectric Generators.py b/2016/11-Radioisotope Thermoelectric Generators.py index 1281b4b..52648c2 100644 --- a/2016/11-Radioisotope Thermoelectric Generators.py +++ b/2016/11-Radioisotope Thermoelectric Generators.py @@ -110,8 +110,6 @@ def cost(self, current_node, next_node): end = '4' * 11 - print ('number of states', len(states)) - graph = pathfinding.WeightedGraph() came_from, total_cost = graph.a_star_search(puzzle_input, end) diff --git a/2016/14-One-Time Pad.py b/2016/14-One-Time Pad.py index cc400d4..c09aa98 100644 --- a/2016/14-One-Time Pad.py +++ b/2016/14-One-Time Pad.py @@ -49,7 +49,6 @@ new_hash = hashlib.md5((puzzle_input + str(index + i)).encode('utf-8')).hexdigest() if triplet * 5 in new_hash: found_keys += 1 - print (init_hash, triplet, index, index + i, found_keys) break if found_keys == 64: @@ -81,21 +80,12 @@ if quintuplets: hashes_quintuplets[i] = quintuplets - if i % 100 == 0: - print ('calculated', i) - - print ('Calculated hashes') - - - print (hashes_first_triplet) - print (hashes_quintuplets) for index, triplet in hashes_first_triplet.items(): for i in range (1, 1000): if index + i in hashes_quintuplets: if triplet in hashes_quintuplets[index + i]: found_keys += 1 - print (hashes[index], triplet, index, index + i, found_keys) break if found_keys == 64: diff --git a/2016/15-Timing is Everything.py b/2016/15-Timing is Everything.py index b20dafc..d03a494 100644 --- a/2016/15-Timing is Everything.py +++ b/2016/15-Timing is Everything.py @@ -43,8 +43,6 @@ if part_to_test == 2: disks.append((len(disks)+1, 11, 0)) -print (disks) - time = 0 while True: disk_ok = 0 diff --git a/2016/17-Two Steps Forward.py b/2016/17-Two Steps Forward.py index 47edc44..b3cfcd1 100644 --- a/2016/17-Two Steps Forward.py +++ b/2016/17-Two Steps Forward.py @@ -51,7 +51,6 @@ def neighbors (self, vertex): directions = ((0, 'U', (0, -1)), (1, 'D', (0, 1)), (2, 'L', (-1, 0)), (3, 'R', (1, 0))) if coords == (3, 3): - print ('found path of length', len(path)+1) return [] neighbors = [] @@ -76,7 +75,6 @@ def neighbors (self, vertex): if part_to_test == 1: for vertex in vault.vertices: - print (vertex) if vertex[0] == (3, 3): puzzle_actual_result = vertex[1] break diff --git a/2016/20-Firewall Rules.py b/2016/20-Firewall Rules.py index f3d21d1..0e5dcfe 100644 --- a/2016/20-Firewall Rules.py +++ b/2016/20-Firewall Rules.py @@ -51,7 +51,6 @@ max_blocked = blocked[0][1] for block in blocked: - print (block, max_blocked, 'start') if max_blocked + 1 >= block[0]: max_blocked = max(max_blocked, block[1]) else: @@ -60,21 +59,16 @@ break else: puzzle_actual_result += block[0] - max_blocked - 1 - print ('Reset', puzzle_actual_result) max_blocked = block[1] reset_max_blocked = True - print (block, max_blocked, 'end') -print (reset_max_blocked, max_blocked) if part_to_test == 2: if reset_max_blocked: max_blocked = max([block[1] for block in blocked]) if max_blocked != max_IP: puzzle_actual_result += max_IP - max_blocked - 1 -# 544541374 too high -# 544541246 too high # -------------------------------- Outputs / results -------------------------------- # diff --git a/2016/21-Scrambled Letters and Hash.py b/2016/21-Scrambled Letters and Hash.py index ab401a2..9ec11ef 100644 --- a/2016/21-Scrambled Letters and Hash.py +++ b/2016/21-Scrambled Letters and Hash.py @@ -80,7 +80,6 @@ def scramble_password (puzzle_input): else: new_password = password[len(password)-x:] + password[0:len(password)-x] password = new_password -# print (string, password) return password if part_to_test == 1: @@ -91,7 +90,6 @@ def scramble_password (puzzle_input): for combination in itertools.permutations('abcdefgh'): password = ''.join(combination) scrambled = scramble_password((password, puzzle_input[1])) - print (password, scrambled) if scrambled == 'fbgdceah': puzzle_actual_result = password break diff --git a/2016/23-Safe Cracking.py b/2016/23-Safe Cracking.py index 024445b..214562e 100644 --- a/2016/23-Safe Cracking.py +++ b/2016/23-Safe Cracking.py @@ -61,8 +61,6 @@ def RepresentsInt(s): instruction = instructions[i] i += 1 -# print (i, instruction, registers) - if instruction[0:3] == 'cpy': _, val, target = instruction.split(' ') try: diff --git a/2016/25-Clock Signal.py b/2016/25-Clock Signal.py index 09e95cf..85e6b7e 100644 --- a/2016/25-Clock Signal.py +++ b/2016/25-Clock Signal.py @@ -64,9 +64,6 @@ def RepresentsInt(s): x = '' instructions = puzzle_input.split('\n') - print ('testing', init_a) - - while True: instruction = instructions[i] i += 1 @@ -158,13 +155,12 @@ def RepresentsInt(s): if i >= len(instructions): break if x != '' and len (x) % 4 == 0: - print (x) if not (x == '01'*(len(x) // 2) or x == '10'*(len(x) // 2)): break if len (x) == 20: puzzle_actual_result = init_a break - print (x) + if puzzle_actual_result != 'Unknown': break From 18cc7a2c1db2e4162f5c712cd8428aa6dc6e2b86 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 31 May 2020 16:18:35 +0200 Subject: [PATCH 009/143] Added day 2017-17 --- 2017/17-Spinlock.py | 72 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 2017/17-Spinlock.py diff --git a/2017/17-Spinlock.py b/2017/17-Spinlock.py new file mode 100644 index 0000000..0dd3786 --- /dev/null +++ b/2017/17-Spinlock.py @@ -0,0 +1,72 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": 3, + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": 344, + "expected": ['996', '1898341'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +spinlock = [0] + +if part_to_test == 1: + position = 0 + for i in range (1, 2017+1): + position += puzzle_input + position %= len(spinlock) + spinlock = spinlock[:position+1] + [i] + spinlock[position+1:] + position += 1 + position %= len(spinlock) + + puzzle_actual_result = spinlock[(position + 1) % len(spinlock)] + + +else: + position = 0 + number_after_zero = 0 + spinlock_length = 1 + for i in range (1, 50000000+1): + position += puzzle_input + position %= spinlock_length + spinlock_length += 1 + if position == 0: + number_after_zero = i + position += 1 + position %= spinlock_length + + puzzle_actual_result = number_after_zero + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From f8bb0c2a815015f9357013c962cd474e200eeb5c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 31 May 2020 23:25:23 +0200 Subject: [PATCH 010/143] Corrected file name for 2015-13 --- 2015/{13-JSAbacusFramework.io.py => 12-JSAbacusFramework.io.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 2015/{13-JSAbacusFramework.io.py => 12-JSAbacusFramework.io.py} (100%) diff --git a/2015/13-JSAbacusFramework.io.py b/2015/12-JSAbacusFramework.io.py similarity index 100% rename from 2015/13-JSAbacusFramework.io.py rename to 2015/12-JSAbacusFramework.io.py From eadcc9d96096027d3dfc0a1e207831f051e38dec Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 31 May 2020 23:25:35 +0200 Subject: [PATCH 011/143] Removed some prints --- 2015/22-Wizard Simulator 20XX.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/2015/22-Wizard Simulator 20XX.py b/2015/22-Wizard Simulator 20XX.py index a7bdc0c..52f16cb 100644 --- a/2015/22-Wizard Simulator 20XX.py +++ b/2015/22-Wizard Simulator 20XX.py @@ -164,7 +164,8 @@ def apply_effects (counters, player_stats, boss_stats): print (' Pruned') continue - print ('Min mana :', min_mana_used, '###### Strategy #', count_strategies,'- pruned: ', len(pruned_strategies), '-', strategy) + if verbose_level >=2: + print ('Min mana :', min_mana_used, '###### Strategy #', count_strategies,'- pruned: ', len(pruned_strategies), '-', strategy) shield_left = 0 poison_left = 0 recharge_left = 0 From e56725c18dc452039a0da5e79964e5472a62a217 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 1 Jun 2020 19:41:12 +0200 Subject: [PATCH 012/143] Added day 2017-18 --- 2017/18-Duet.py | 191 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 191 insertions(+) create mode 100644 2017/18-Duet.py diff --git a/2017/18-Duet.py b/2017/18-Duet.py new file mode 100644 index 0000000..05e50b7 --- /dev/null +++ b/2017/18-Duet.py @@ -0,0 +1,191 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """set a 1 +add a 2 +mul a a +mod a 5 +snd a +set a 0 +rcv a +jgz a -1 +set a 1 +jgz a -2""", + "expected": ['4', 'Unknown'], + } +test += 1 +test_data[test] = {"input": """snd 1 +snd 2 +snd p +rcv a +rcv b +rcv c +rcv d""", + "expected": ['4', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['7071', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def val_get (registers, value): + try: + return int(value) + except ValueError: + return registers[value] + + +def computer (instructions, program_id): + global verbose_level, puzzle_actual_result + i = 0 + registers = {'p': program_id} + while i < len(instructions): + instr = instructions[i] + + if verbose_level == 3: + print (program_id, instr) + + if instr[0] == 'snd': + if verbose_level == 2: + print (program_id, i, instr, 'sending', val_get(registers, instr[1]), registers) + + if program_id == 1: + puzzle_actual_result += 1 + yield val_get(registers, instr[1]) + elif instr[0] == 'set': + registers.update({instr[1]: val_get(registers, instr[2])}) + elif instr[0] == 'add': + registers.setdefault(instr[1], 0) + registers[instr[1]] += val_get(registers, instr[2]) + elif instr[0] == 'mul': + registers.setdefault(instr[1], 0) + registers[instr[1]] *= val_get(registers, instr[2]) + elif instr[0] == 'mod': + registers.setdefault(instr[1], 0) + registers[instr[1]] %= val_get(registers, instr[2]) + elif instr[0] == 'rcv': + registers.setdefault(instr[1], 0) + registers[instr[1]] = yield None + if verbose_level == 2: + print (program_id, i, instr, 'received', registers[instr[1]], registers) + elif instr[0] == 'jgz': + if val_get(registers, instr[1]) > 0: + i += val_get(registers, instr[2]) - 1 + + i += 1 + + + +if part_to_test == 1: + instructions = [(string.split(' ')) for string in puzzle_input.split('\n')] + + i = 0 + registers = {} + playing = 0 + while i < len(instructions): + instr = instructions[i] + + if instr[0] == 'snd': + playing = val_get(registers, instr[1]) + elif instr[0] == 'set': + registers.update({instr[1]: val_get(registers, instr[2])}) + elif instr[0] == 'add': + registers.setdefault(instr[1], 0) + registers[instr[1]] += val_get(registers, instr[2]) + elif instr[0] == 'mul': + registers.setdefault(instr[1], 0) + registers[instr[1]] *= val_get(registers, instr[2]) + elif instr[0] == 'mod': + registers.setdefault(instr[1], 0) + registers[instr[1]] %= val_get(registers, instr[2]) + elif instr[0] == 'rcv': + if val_get(registers, instr[1]): + puzzle_actual_result = playing + break + elif instr[0] == 'jgz': + if val_get(registers, instr[1]): + i += val_get(registers, instr[2]) - 1 + + i += 1 + + +else: + instructions = [(string.split(' ')) for string in puzzle_input.split('\n')] + + i = 0 + registers = {} + playing = 0 + program = {x: computer(instructions, x) for x in range(2)} + reception = {x: [] for x in range(2)} + start = True + stalled = {x:False for x in range(2)} + prog = 0 + puzzle_actual_result = 0 + + while (len(reception[0]) + len(reception[1])) > 0 or start: + start = False + if stalled[prog] and len(reception[prog]): + result = program[prog].send(reception[prog].pop(0)) + stalled[prog] = False + elif not stalled[prog]: + result = next(program[prog]) + else: + break + + if verbose_level == 2: + print ('main received', result, 'from', prog) + + while result is not None or len(reception[prog]) > 0: + if result is None: + if verbose_level == 2: + print ('main sends', reception[prog][0], 'to', prog) + result = program[prog].send(reception[prog].pop(0)) + else: + reception[1-prog].append(result) + result = next(program[prog]) + + if verbose_level == 2: + print ('main received', result, 'from', prog) + + stalled[prog] = True + + if verbose_level == 3: + print (reception) + elif verbose_level == 2: + print (len(reception[0]), len(reception[1]), puzzle_actual_result) + + prog = 1 - prog + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 297495a50b4690cfd1d07f90eac2d493083fc9fa Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 2 Jun 2020 22:23:14 +0200 Subject: [PATCH 013/143] Added days 2017-19 and 2017-20 --- 2017/19-A Series of Tubes.py | 88 +++++++++++++++++++++++++++ 2017/20-Particle Swarm.py | 111 +++++++++++++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 2017/19-A Series of Tubes.py create mode 100644 2017/20-Particle Swarm.py diff --git a/2017/19-A Series of Tubes.py b/2017/19-A Series of Tubes.py new file mode 100644 index 0000000..49fe8fe --- /dev/null +++ b/2017/19-A Series of Tubes.py @@ -0,0 +1,88 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, string + +test_data = {} + +test = 1 +test_data[test] = {"input": """.....|.......... +.....|..+--+.... +.....A..|..C.... +.F---|----E|--+. +.....|..|..|..D. +.....+B-+..+--+.""", + "expected": ['ABCDEF', '38'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read(), + "expected": ['UICRNSDOK', '16064'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + +# -------------------------------- Actual code execution -------------------------------- # + + +lines = puzzle_input.splitlines() +if lines[len(lines)-1] == '': + del lines[len(lines)-1] + +width = max(len(line) for line in lines) +grid = {(x, y): lines[y][x].replace('.', ' ') for x in range(width) for y in range(len(lines))} + +direction = (0, 1) +x, y = lines[0].index('|'), 0 +letters_seen = '' +steps_taken = 1 + +cross_directions = {(0, 1): [(1, 0), (-1, 0)], (0, -1): [(1, 0), (-1, 0)], (1, 0): [(0, 1), (0, -1)], (-1, 0): [(0, 1), (0, -1)]} + +while (x, y) in grid and grid[(x, y)] != ' ': + new_cell = grid[(x, y)] + + if new_cell in string.ascii_uppercase: + letters_seen += new_cell + elif new_cell == '+': + new_direction = cross_directions[direction][0] + new_x, new_y = x + new_direction[0], y + new_direction[1] + + if (new_x, new_y) in grid: + if grid[(new_x, new_y)] == ' ': + direction = cross_directions[direction][1] + else: + direction = new_direction + else: + direction = cross_directions[direction][1] + + x, y = x + direction[0], y + direction[1] + steps_taken += 1 + +if part_to_test == 1: + puzzle_actual_result = letters_seen +else: + puzzle_actual_result = steps_taken - 1 + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/20-Particle Swarm.py b/2017/20-Particle Swarm.py new file mode 100644 index 0000000..4bd0d65 --- /dev/null +++ b/2017/20-Particle Swarm.py @@ -0,0 +1,111 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """p=<3,0,0>, v=<2,0,0>, a=<-1,0,0> +p=<4,0,0>, v=<0,0,0>, a=<-2,0,0>""", + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """p=<-6,0,0>, v=<3,0,0>, a=<0,0,0> +p=<-4,0,0>, v=<2,0,0>, a=<0,0,0> +p=<-2,0,0>, v=<1,0,0>, a=<0,0,0> +p=<3,0,0>, v=<-1,0,0>, a=<0,0,0>""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['Unknown', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +max_accel = 10**6 + +if part_to_test == 1: + part_nr = 0 + for string in puzzle_input.split('\n'): + _, _, acceleration = string.split(' ') + acceleration = list(map(int, acceleration[3:-1].split(','))) + + if max_accel > sum(map(abs, acceleration)): + max_accel = sum(map(abs, acceleration)) + closest_part = part_nr + + part_nr += 1 + + puzzle_actual_result = closest_part + + + +else: + particles = {} + collisions = [] + part_nr = 0 + saved_len = 0 + for string in puzzle_input.split('\n'): + position, speed, acceleration = string.split(' ') + position = list(map(int, position[3:-2].split(','))) + speed = list(map(int, speed[3:-2].split(','))) + acceleration = list(map(int, acceleration[3:-1].split(','))) + + particles[part_nr] = [position, speed, acceleration] + + part_nr += 1 + + for i in range(10**4): + collisions = [] + for part_nr in particles: + position, speed, acceleration = particles[part_nr] + speed = [speed[x] + acceleration[x] for x in range (3)] + position = [position[x] + speed[x] for x in range (3)] + particles[part_nr] = [position, speed, acceleration] + collisions.append(position) + + coordinates = [','.join(map(str, collision)) for collision in collisions] + + list_particles = list(particles.keys()) + for part_nr in list_particles: + if collisions.count(particles[part_nr][0]) > 1: + del particles[part_nr] + + if i % 10 == 0 and len(particles) == saved_len: + break + elif i % 10 == 0: + saved_len = len(particles) + + print(i, len(particles)) + + puzzle_actual_result = len(particles) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 60f7527f6291e43f99ed9f5955cb3583a950ccaa Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:59:27 +0200 Subject: [PATCH 014/143] Added expected results --- 2017/01-Inverse Captcha.py | 2 +- 2017/02-Corruption Checksum.py | 2 +- 2017/12-Digital Plumber.py | 2 +- 2017/15-Dueling Generators.py | 2 +- 2017/16-Permutation Promenade.py | 2 +- 2017/18-Duet.py | 2 +- 2017/20-Particle Swarm.py | 4 +--- 7 files changed, 7 insertions(+), 9 deletions(-) diff --git a/2017/01-Inverse Captcha.py b/2017/01-Inverse Captcha.py index 5efa306..aac9552 100644 --- a/2017/01-Inverse Captcha.py +++ b/2017/01-Inverse Captcha.py @@ -16,7 +16,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['Unknown', 'Unknown'], + "expected": ['1069', '1268'], } # -------------------------------- Control program execution -------------------------------- # diff --git a/2017/02-Corruption Checksum.py b/2017/02-Corruption Checksum.py index 8cf5e2a..6ac4575 100644 --- a/2017/02-Corruption Checksum.py +++ b/2017/02-Corruption Checksum.py @@ -20,7 +20,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['Unknown', 'Unknown'], + "expected": ['46402', '265'], } # -------------------------------- Control program execution -------------------------------- # diff --git a/2017/12-Digital Plumber.py b/2017/12-Digital Plumber.py index 597b0e8..ad39266 100644 --- a/2017/12-Digital Plumber.py +++ b/2017/12-Digital Plumber.py @@ -22,7 +22,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['378', 'Unknown'], + "expected": ['378', '204'], } # -------------------------------- Control program execution -------------------------------- # diff --git a/2017/15-Dueling Generators.py b/2017/15-Dueling Generators.py index 92f21c8..9fdebc8 100644 --- a/2017/15-Dueling Generators.py +++ b/2017/15-Dueling Generators.py @@ -12,7 +12,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['597', 'Unknown'], + "expected": ['597', '303'], } # -------------------------------- Control program execution -------------------------------- # diff --git a/2017/16-Permutation Promenade.py b/2017/16-Permutation Promenade.py index f999cc1..47ce0f5 100644 --- a/2017/16-Permutation Promenade.py +++ b/2017/16-Permutation Promenade.py @@ -11,7 +11,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": ('abcdefghijklmnop', open(input_file, "r+").read().strip()), - "expected": ['ceijbfoamgkdnlph', 'Unknown'], + "expected": ['ceijbfoamgkdnlph', 'pnhajoekigcbflmd'], } # -------------------------------- Control program execution -------------------------------- # diff --git a/2017/18-Duet.py b/2017/18-Duet.py index 05e50b7..a4ac448 100644 --- a/2017/18-Duet.py +++ b/2017/18-Duet.py @@ -30,7 +30,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['7071', 'Unknown'], + "expected": ['7071', '8001'], } # -------------------------------- Control program execution -------------------------------- # diff --git a/2017/20-Particle Swarm.py b/2017/20-Particle Swarm.py index 4bd0d65..5d0b094 100644 --- a/2017/20-Particle Swarm.py +++ b/2017/20-Particle Swarm.py @@ -20,7 +20,7 @@ test = 'real' input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['Unknown', 'Unknown'], + "expected": ['125', '461'], } # -------------------------------- Control program execution -------------------------------- # @@ -92,8 +92,6 @@ elif i % 10 == 0: saved_len = len(particles) - print(i, len(particles)) - puzzle_actual_result = len(particles) From 6c08d212a21dca79f956d4d42adb9d8479fd8f38 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 4 Jun 2020 08:59:58 +0200 Subject: [PATCH 015/143] Added day 2017-21 and a drawing library (not very efficient...) --- 2017/21-Fractal Art.py | 107 +++++++++++++++++++++++++++++ 2017/drawing.py | 149 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 2017/21-Fractal Art.py create mode 100644 2017/drawing.py diff --git a/2017/21-Fractal Art.py b/2017/21-Fractal Art.py new file mode 100644 index 0000000..8c4cee1 --- /dev/null +++ b/2017/21-Fractal Art.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing, itertools, math + +test_data = {} + +test = 1 +test_data[test] = {"input": """../.# => ##./#../... +.#./..#/### => #..#/..../..../#..#""", + "expected": ['12', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['139', '1857134'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +pattern = '''.#. +..# +###''' + +grid = drawing.text_to_grid(pattern) +parts = drawing.split_in_parts(grid, 2, 2) +merged_grid = drawing.merge_parts(parts, 2, 2) + + +if case_to_test == 1: + iterations = 2 +elif part_to_test == 1: + iterations = 5 +else: + iterations = 18 + + +enhancements = {} +for string in puzzle_input.split('\n'): + if string == '': + continue + + source, _, target = string.split(' ') + source = source.replace('/', '\n') + target = target.replace('/', '\n') + + source_grid = drawing.text_to_grid(source) + enhancements[source] = target + + for rotated_source in drawing.rotate(source_grid): + rotated_source_text = drawing.grid_to_text(rotated_source) + enhancements[rotated_source_text] = target + + for flipped_source in drawing.flip(rotated_source): + flipped_source_text = drawing.grid_to_text(flipped_source) + enhancements[flipped_source_text] = target + +pattern_grid = drawing.text_to_grid(pattern) +for i in range(iterations): + + grid_x, grid_y = zip(*pattern_grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + + if grid_width % 2 == 0: + parts = drawing.split_in_parts(pattern_grid, 2, 2) + else: + parts = drawing.split_in_parts(pattern_grid, 3, 3) + + grid_size = int(math.sqrt(len(parts))) + + new_parts = [] + for part in parts: + part_text = drawing.grid_to_text(part) + new_parts.append(drawing.text_to_grid(enhancements[part_text])) + + new_grid = drawing.merge_parts(new_parts, grid_size, grid_size) + + pattern_grid = new_grid + +grid_text = drawing.grid_to_text(pattern_grid) + +puzzle_actual_result = grid_text.count('#') + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/drawing.py b/2017/drawing.py new file mode 100644 index 0000000..d7ef23e --- /dev/null +++ b/2017/drawing.py @@ -0,0 +1,149 @@ +import math, os + + +def text_to_grid (text): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + Each character will have its coordinates as keys + + :param string text: The text to convert + :return: The converted grid, its height and width + """ + grid = {} + lines = text.splitlines() + height = len(lines) + width = 0 + for y in range(len(lines)): + width = max(width, len(lines[y])) + for x in range(len(lines[y])): + grid[(x, y)] = lines[y][x] + + return grid + +def grid_to_text (grid, blank_character = ' '): + """ + Converts the grid to a text format + + :param dict grid: The grid to convert, in format (x, y): value + :param string blank_character: What to use for cells with unknown value + :return: The grid in text format + """ + + text = '' + + grid_x, grid_y = zip(*grid.keys()) + + for y in range (min(grid_y), max(grid_y)+1): + for x in range (min(grid_x), max(grid_x)+1): + if (x, y) in grid: + text += grid[(x, y)] + else: + text += blank_character + text += os.linesep + text = text[:-len(os.linesep)] + + return text + +def split_in_parts (grid, width, height): + """ + Splits a grid in parts of width*height size + + :param dict grid: The grid to convert, in format (x, y): value + :param integer width: The width of parts to use + :param integer height: The height of parts to use + :return: The different parts + """ + + if not isinstance(width, int) or not isinstance(height, int): + return False + if width <= 0 or height <= 0: + return False + + grid_x, grid_y = zip(*grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + grid_height = max(grid_y) - min(grid_y) + 1 + + parts = [] + + for part_y in range(math.ceil(grid_height / height)): + for part_x in range (math.ceil(grid_width / width)): + parts.append({(x, y):grid[(x, y)] \ + for x in range(part_x*width, min((part_x + 1)*width, grid_width)) \ + for y in range(part_y*height, min((part_y + 1)*height, grid_height))}) + + return parts + +def merge_parts (parts, width, height): + """ + Merges different parts in a single grid + + :param dict parts: The parts to merge, in format (x, y): value + :return: The merged grid + """ + + grid = {} + + part_x, part_y = zip(*parts[0].keys()) + part_width = max(part_x) - min(part_x) + 1 + part_height = max(part_y) - min(part_y) + 1 + + part_nr = 0 + for part_y in range(height): + for part_x in range(width): + grid.update({(x + part_x*part_width, y + part_y*part_height): parts[part_nr][(x, y)] for (x, y) in parts[part_nr]}) + part_nr += 1 + + return grid + +def rotate (grid, rotations = (0, 90, 180, 270)): + """ + Rotates a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple rotations: Which angles to use for rotation + :return: The parts in text format + """ + + rotated_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for angle in rotations: + if angle == 0: + rotated_grid.append(grid) + elif angle == 90: + rotated_grid.append({(height-y, x): grid[(x, y)] for (x, y) in grid}) + elif angle == 180: + rotated_grid.append({(width-x, height-y): grid[(x, y)] for (x, y) in grid}) + elif angle == 270: + rotated_grid.append({(y, width-x): grid[(x, y)] for (x, y) in grid}) + + return rotated_grid + +def flip (grid, flips = ('V', 'H')): + """ + Flips a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple flips: Which flips (horizontal, vertical) to use for flip + :return: The parts in text format + """ + + flipped_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for flip in flips: + if flip == 'H': + flipped_grid.append({(x, height-y): grid[(x, y)] for (x, y) in grid}) + elif flip == 'V': + flipped_grid.append({(width-x, y): grid[(x, y)] for (x, y) in grid}) + + return flipped_grid + From 11ca74b9885845ad615e069dc3f58ae60a119ca4 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 4 Jun 2020 21:15:50 +0200 Subject: [PATCH 016/143] Added day 2017-22 --- 2017/22-Soporifica Virus.py | 106 ++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 2017/22-Soporifica Virus.py diff --git a/2017/22-Soporifica Virus.py b/2017/22-Soporifica Virus.py new file mode 100644 index 0000000..306a6b7 --- /dev/null +++ b/2017/22-Soporifica Virus.py @@ -0,0 +1,106 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing + +test_data = {} + +test = 1 +test_data[test] = {"input": """..# +#.. +...""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['5182', '2512008'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def turn_left (direction): + return (direction[1], -direction[0]) + +def turn_right (direction): + return (-direction[1], direction[0]) + +if part_to_test == 1: + grid = drawing.text_to_grid (puzzle_input) + position = (len(puzzle_input.split('\n'))//2, len(puzzle_input.split('\n'))//2) + direction = (0, -1) + new_infections = 0 + + for i in range (10**4): + if position in grid: + if grid[position] == '.': + direction = turn_left(direction) + grid[position] = '#' + new_infections += 1 + else: + direction = turn_right(direction) + grid[position] = '.' + else: + direction = turn_left(direction) + grid[position] = '#' + new_infections += 1 + + position = (position[0] + direction[0], position[1] + direction[1]) + + puzzle_actual_result = new_infections + + + +else: + grid = drawing.text_to_grid (puzzle_input) + position = (len(puzzle_input.split('\n'))//2, len(puzzle_input.split('\n'))//2) + direction = (0, -1) + new_infections = 0 + + for i in range (10**7): + if position in grid: + if grid[position] == '.': + direction = turn_left(direction) + grid[position] = 'W' + elif grid[position] == 'W': + grid[position] = '#' + new_infections += 1 + elif grid[position] == '#': + direction = turn_right(direction) + grid[position] = 'F' + else: + direction = turn_right(turn_right(direction)) + grid[position] = '.' + else: + direction = turn_left(direction) + grid[position] = 'W' + + position = (position[0] + direction[0], position[1] + direction[1]) + + puzzle_actual_result = new_infections + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From f99302dccc857cba258438797351f89307140a1e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 5 Jun 2020 21:43:17 +0200 Subject: [PATCH 017/143] Added 2017-23, 2017-24 and 2017-25 --- 2017/23-Coprocessor Conflagration.py | 105 ++++++++++++++++++++++++++ 2017/24-Electromagnetic Moat.py | 92 +++++++++++++++++++++++ 2017/25-The Halting Problem.py | 107 +++++++++++++++++++++++++++ 3 files changed, 304 insertions(+) create mode 100644 2017/23-Coprocessor Conflagration.py create mode 100644 2017/24-Electromagnetic Moat.py create mode 100644 2017/25-The Halting Problem.py diff --git a/2017/23-Coprocessor Conflagration.py b/2017/23-Coprocessor Conflagration.py new file mode 100644 index 0000000..698822d --- /dev/null +++ b/2017/23-Coprocessor Conflagration.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os, math + +test_data = {} + +test = 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['6724', '903'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def val_get (registers, value): + try: + return int(value) + except ValueError: + return registers[value] + +def get_divisors (value): + small_divisors = [d for d in range (1, int(math.sqrt(value))+1) if value % d == 0 ] + big_divisors = [value // d for d in small_divisors if not d**2 == value] + return set(small_divisors + big_divisors) + + + +instructions = [(string.split(' ')) for string in puzzle_input.split('\n')] + +i = 0 +registers = {x:0 for x in 'abcdefgh'} +registers['a'] = part_to_test - 1 +count_mul = 0 +val_h = 1 +nb_instructions = 0 + +if part_to_test == 1: + while i < len(instructions): + instr = instructions[i] + + if instr[0] == 'set': + registers.update({instr[1]: val_get(registers, instr[2])}) + elif instr[0] == 'sub': + registers.setdefault(instr[1], 0) + registers[instr[1]] -= val_get(registers, instr[2]) + elif instr[0] == 'mul': + registers.setdefault(instr[1], 0) + registers[instr[1]] *= val_get(registers, instr[2]) + count_mul += 1 + elif instr[0] == 'mod': + registers.setdefault(instr[1], 0) + registers[instr[1]] %= val_get(registers, instr[2]) + elif instr[0] == 'jnz': + if val_get(registers, instr[1]) != 0: + i += val_get(registers, instr[2]) - 1 + + i += 1 + nb_instructions += 1 + + if nb_instructions == 10 ** 7: + break + + puzzle_actual_result = count_mul + + +else: + count_composite = 0 + for i in range (84*100+100000, 84*100+100000+17000+1, 17): + if len(get_divisors(i)) != 2: + print (i, get_divisors(i)) + count_composite += 1 + + puzzle_actual_result = count_composite + +# 116206 too high +# 500 too low +# 10477 is wrong + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/24-Electromagnetic Moat.py b/2017/24-Electromagnetic Moat.py new file mode 100644 index 0000000..f08406f --- /dev/null +++ b/2017/24-Electromagnetic Moat.py @@ -0,0 +1,92 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """0/2 +2/2 +2/3 +3/4 +3/5 +0/1 +10/1 +9/10""", + "expected": ['31', '19'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['1940', '1928'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def build_bridge (bridge, last, available_pieces): + global bridges + next_pieces = [x for x in available_pieces if last in x] + + for next_piece in next_pieces: + new_bridge = bridge + [next_piece] + new_available_pieces = available_pieces.copy() + new_available_pieces.remove(next_piece) + if next_piece[0] == next_piece[1]: + new_last = next_piece[0] + else: + new_last = [x for x in next_piece if x != last][0] + build_bridge (new_bridge, new_last, new_available_pieces) + + bridges.append(bridge) + + +pieces = [] +bridges = [] +for string in puzzle_input.split('\n'): + if string == '': + continue + + a, b = map(int, string.split('/')) + pieces.append((a, b)) + +build_bridge([], 0, pieces) + +max_strength = 0 +if part_to_test == 1: + for bridge in bridges: + max_strength = max (max_strength, sum(map(sum, bridge))) + puzzle_actual_result = max_strength +else: + max_length = max(map(len, bridges)) + for bridge in bridges: + if len(bridge) != max_length: + continue + max_strength = max (max_strength, sum(map(sum, bridge))) + puzzle_actual_result = max_strength + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2017/25-The Halting Problem.py b/2017/25-The Halting Problem.py new file mode 100644 index 0000000..17e00c7 --- /dev/null +++ b/2017/25-The Halting Problem.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """Begin in state A. +Perform a diagnostic checksum after 6 steps. + +In state A: + If the current value is 0: + - Write the value 1. + - Move one slot to the right. + - Continue with state B. + If the current value is 1: + - Write the value 0. + - Move one slot to the left. + - Continue with state B. + +In state B: + If the current value is 0: + - Write the value 1. + - Move one slot to the left. + - Continue with state A. + If the current value is 1: + - Write the value 1. + - Move one slot to the right. + - Continue with state A.""", + "expected": ['3', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['2794', 'Unknown'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + states = {} + for string in puzzle_input.split('\n'): + if string == '': + continue + + if string.startswith('Begin in state '): + start_state = string[-2:-1] + elif string.startswith('Perform a diagnostic checksum after '): + _,_,_,_,_, steps, _ = string.split(' ') + steps = int(steps) + elif string.startswith('In state '): + state = string[-2:-1] + elif string.startswith(' If the current value is'): + current_value = int(string[-2:-1]) + elif string.startswith(' - Write the value'): + target_value = int(string[-2:-1]) + elif string.startswith(' - Move one slot to the'): + direction = string.split(' ')[-1] + elif string.startswith(' - Continue with state'): + next_state = string[-2:-1] + if state not in states: + states[state] = {} + states[state].update({current_value: (target_value, direction, next_state)}) + + state = start_state + tape = {0:0} + position = 0 + + for _ in range (steps): + value = tape[position] if position in tape else 0 + tape[position] = states[state][value][0] + position += 1 if states[state][value][1] == 'right.' else -1 + state = states[state][value][2] + + puzzle_actual_result = sum(tape[x] for x in tape) + + + +else: + pass + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 040c095208e1f81daab146e72c22d96598f71b34 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 6 Jun 2020 21:07:22 +0200 Subject: [PATCH 018/143] Added days 2018-01 and 2018-02 --- 2018/01-Chronal Calibration.py | 61 ++++++++++++++++++ 2018/02-Inventory Management System.py | 86 ++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 2018/01-Chronal Calibration.py create mode 100644 2018/02-Inventory Management System.py diff --git a/2018/01-Chronal Calibration.py b/2018/01-Chronal Calibration.py new file mode 100644 index 0000000..b200ddf --- /dev/null +++ b/2018/01-Chronal Calibration.py @@ -0,0 +1,61 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['585', '83173'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + puzzle_actual_result = sum(map(int, puzzle_input.splitlines())) + + +else: + used_frequencies = [0] + frequency = 0 + while True: + for string in puzzle_input.split('\n'): + frequency += int(string) + if frequency in used_frequencies: + puzzle_actual_result = frequency + break + used_frequencies.append(frequency) + + if puzzle_actual_result != 'Unknown': + break + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/02-Inventory Management System.py b/2018/02-Inventory Management System.py new file mode 100644 index 0000000..2b78134 --- /dev/null +++ b/2018/02-Inventory Management System.py @@ -0,0 +1,86 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """abcdef +bababc +abbcde +abcccd +aabcdd +abcdee +ababab""", + "expected": ['Unknown', 'Unknown'], + } + +test = 2 +test_data[test] = {"input": """abcde +fghij +klmno +pqrst +fguij +axcye +wvxyz""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['7688', 'lsrivmotzbdxpkxnaqmuwcchj'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + count_2_letters = 0 + count_3_letters = 0 + + for string in puzzle_input.split('\n'): + if any(string.count(x) == 2 for x in string): + count_2_letters += 1 + if any(string.count(x) == 3 for x in string): + count_3_letters += 1 + + puzzle_actual_result = count_2_letters*count_3_letters + + +else: + list_strings = puzzle_input.split('\n') + for string in list_strings: + for i in range(len(string)): + new_strings = [string[:i] + x + string[i+1:] for x in 'azertyuiopqsdfghjklmwxcvbn'] + new_strings.remove(string) + if any(x in list_strings for x in new_strings): + puzzle_actual_result = string[:i] + string[i+1:] + break + if puzzle_actual_result != 'Unknown': + break + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 688e701fb4a324a4bd84cad3434257a2755af006 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 7 Jun 2020 19:12:25 +0200 Subject: [PATCH 019/143] Added parse module in gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 8f12661..ac5e756 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ Inputs/ template.py __pycache__ +parse/ From 6d33d35f439f4c1a66bdcf1cd39ddf8573b02cc0 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 7 Jun 2020 19:16:43 +0200 Subject: [PATCH 020/143] Added days 2018-03, 2018-04 --- 2018/03-No Matter How You Slice It.py | 87 +++++++++++++++ 2018/04-Repose Record.py | 105 ++++++++++++++++++ 2018/drawing.py | 149 ++++++++++++++++++++++++++ 3 files changed, 341 insertions(+) create mode 100644 2018/03-No Matter How You Slice It.py create mode 100644 2018/04-Repose Record.py create mode 100644 2018/drawing.py diff --git a/2018/03-No Matter How You Slice It.py b/2018/03-No Matter How You Slice It.py new file mode 100644 index 0000000..57e2d2b --- /dev/null +++ b/2018/03-No Matter How You Slice It.py @@ -0,0 +1,87 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing + +test_data = {} + +test = 1 +test_data[test] = {"input": """#1 @ 1,3: 4x4 +#2 @ 3,1: 4x4 +#3 @ 5,5: 2x2""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['110546', '819'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + fabric = {} + for string in puzzle_input.split('\n'): + if string == '': + continue + _, _, start, size = string.split(' ') + cut_x, cut_y = int(start.split(',')[0]), int(start.split(',')[1][:-1]) + size_x, size_y = int(size.split('x')[0]), int(size.split('x')[1]) + + fabric.update({(x, y): fabric.get((x, y), 0) + 1 + for x in range (cut_x, cut_x + size_x) + for y in range (cut_y, cut_y + size_y)}) + + puzzle_actual_result = len([fabric[coord] for coord in fabric if fabric[coord] > 1]) + + + +else: + fabric = {} + cuts = [] + for string in puzzle_input.split('\n'): + if string == '': + continue + _, _, start, size = string.split(' ') + cut_x, cut_y = int(start.split(',')[0]), int(start.split(',')[1][:-1]) + size_x, size_y = int(size.split('x')[0]), int(size.split('x')[1]) + + cuts.append((cut_x, cut_y, size_x, size_y)) + + fabric.update({(x, y): fabric[(x, y)] + 1 if (x, y) in fabric else 1 + for x in range (cut_x, cut_x + size_x) + for y in range (cut_y, cut_y + size_y)}) + + cut_id = 0 + for cut in cuts: + cut_id += 1 + if all(fabric[(x, y)] == 1 + for x in range (cut[0], cut[0] + cut[2]) + for y in range (cut[1], cut[1] + cut[3])): + puzzle_actual_result = cut_id + break + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/04-Repose Record.py b/2018/04-Repose Record.py new file mode 100644 index 0000000..02a8012 --- /dev/null +++ b/2018/04-Repose Record.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os, parse, numpy as np + +test_data = {} + +test = 1 +test_data[test] = {"input": """[1518-11-01 00:00] Guard #10 begins shift +[1518-11-01 00:05] falls asleep +[1518-11-01 00:25] wakes up +[1518-11-01 00:30] falls asleep +[1518-11-01 00:55] wakes up +[1518-11-01 23:58] Guard #99 begins shift +[1518-11-02 00:40] falls asleep +[1518-11-02 00:50] wakes up +[1518-11-03 00:05] Guard #10 begins shift +[1518-11-03 00:24] falls asleep +[1518-11-03 00:29] wakes up +[1518-11-04 00:02] Guard #99 begins shift +[1518-11-04 00:36] falls asleep +[1518-11-04 00:46] wakes up +[1518-11-05 00:03] Guard #99 begins shift +[1518-11-05 00:45] falls asleep +[1518-11-05 00:55] wakes up""", + "expected": ['240', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['30630', '136571'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 1 +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +parse_format1 = '''[{date:ti}] Guard #{guard:d} begins shift''' +parse_format2 = '''[{date:ti}] falls asleep''' +parse_format3 = '''[{date:ti}] wakes up''' +all_notes = sorted(puzzle_input.split('\n')) + +sleep_pattern = {} +for string in all_notes: + r = parse.parse(parse_format1, string) + if r is not None: + guard = r['guard'] + if guard not in sleep_pattern: + sleep_pattern[guard] = np.zeros(60) + continue + + r = parse.parse(parse_format2, string) + if r is not None: + asleep = r['date'].minute if r['date'].hour == 0 else 0 + continue + + r = parse.parse(parse_format3, string) + if r is not None: + sleep_pattern[guard][asleep:r['date'].minute] += 1 + continue + +if part_to_test == 1: + sleep_duration = {x:sum(sleep_pattern[x]) for x in sleep_pattern} + + most_sleepy = [x for x,v in sleep_duration.items() if v == max(sleep_duration.values())][0] + + puzzle_actual_result = most_sleepy * np.argpartition(-sleep_pattern[most_sleepy], 1)[0] + + +else: + most_slept = 0 + most_slept_guard = 0 + most_slept_minute = 0 + for guard in sleep_pattern: + if most_slept < max(sleep_pattern[guard]): + most_slept = max(sleep_pattern[guard]) + most_slept_guard = guard + most_slept_minute = np.argpartition(-sleep_pattern[guard], 1)[0] + + puzzle_actual_result = most_slept_guard * most_slept_minute + + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/drawing.py b/2018/drawing.py new file mode 100644 index 0000000..0e807b5 --- /dev/null +++ b/2018/drawing.py @@ -0,0 +1,149 @@ +import math, os + + +def text_to_grid (text): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + Each character will have its coordinates as keys + + :param string text: The text to convert + :return: The converted grid, its height and width + """ + grid = {} + lines = text.splitlines() + height = len(lines) + width = 0 + for y in range(len(lines)): + width = max(width, len(lines[y])) + for x in range(len(lines[y])): + grid[(x, y)] = lines[y][x] + + return grid + +def grid_to_text (grid, blank_character = ' '): + """ + Converts the grid to a text format + + :param dict grid: The grid to convert, in format (x, y): value + :param string blank_character: What to use for cells with unknown value + :return: The grid in text format + """ + + text = '' + + grid_x, grid_y = zip(*grid.keys()) + + for y in range (min(grid_y), max(grid_y)+1): + for x in range (min(grid_x), max(grid_x)+1): + if (x, y) in grid: + text += str(grid[(x, y)]) + else: + text += blank_character + text += os.linesep + text = text[:-len(os.linesep)] + + return text + +def split_in_parts (grid, width, height): + """ + Splits a grid in parts of width*height size + + :param dict grid: The grid to convert, in format (x, y): value + :param integer width: The width of parts to use + :param integer height: The height of parts to use + :return: The different parts + """ + + if not isinstance(width, int) or not isinstance(height, int): + return False + if width <= 0 or height <= 0: + return False + + grid_x, grid_y = zip(*grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + grid_height = max(grid_y) - min(grid_y) + 1 + + parts = [] + + for part_y in range(math.ceil(grid_height / height)): + for part_x in range (math.ceil(grid_width / width)): + parts.append({(x, y):grid[(x, y)] \ + for x in range(part_x*width, min((part_x + 1)*width, grid_width)) \ + for y in range(part_y*height, min((part_y + 1)*height, grid_height))}) + + return parts + +def merge_parts (parts, width, height): + """ + Merges different parts in a single grid + + :param dict parts: The parts to merge, in format (x, y): value + :return: The merged grid + """ + + grid = {} + + part_x, part_y = zip(*parts[0].keys()) + part_width = max(part_x) - min(part_x) + 1 + part_height = max(part_y) - min(part_y) + 1 + + part_nr = 0 + for part_y in range(height): + for part_x in range(width): + grid.update({(x + part_x*part_width, y + part_y*part_height): parts[part_nr][(x, y)] for (x, y) in parts[part_nr]}) + part_nr += 1 + + return grid + +def rotate (grid, rotations = (0, 90, 180, 270)): + """ + Rotates a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple rotations: Which angles to use for rotation + :return: The parts in text format + """ + + rotated_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for angle in rotations: + if angle == 0: + rotated_grid.append(grid) + elif angle == 90: + rotated_grid.append({(height-y, x): grid[(x, y)] for (x, y) in grid}) + elif angle == 180: + rotated_grid.append({(width-x, height-y): grid[(x, y)] for (x, y) in grid}) + elif angle == 270: + rotated_grid.append({(y, width-x): grid[(x, y)] for (x, y) in grid}) + + return rotated_grid + +def flip (grid, flips = ('V', 'H')): + """ + Flips a grid and returns the result + + :param dict grid: The grid to rotate, in format (x, y): value + :param tuple flips: Which flips (horizontal, vertical) to use for flip + :return: The parts in text format + """ + + flipped_grid = [] + + grid_x, grid_y = zip(*grid.keys()) + width = max(grid_x) - min(grid_x) + 1 + height = max(grid_y) - min(grid_y) + 1 + + for flip in flips: + if flip == 'H': + flipped_grid.append({(x, height-y): grid[(x, y)] for (x, y) in grid}) + elif flip == 'V': + flipped_grid.append({(width-x, y): grid[(x, y)] for (x, y) in grid}) + + return flipped_grid + From 6fa48bf5957f4d6a8aa7035355cfc2fe4360e9d9 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 9 Jun 2020 21:08:16 +0200 Subject: [PATCH 021/143] Added days 2018-05, 2018-06, 2018-07 and update on pathfinding lib --- 2018/05-Alchemical Reduction.py | 70 +++++ 2018/06-Chronal Coordinates.py | 103 +++++++ 2018/07-The Sum of Its Parts.py | 123 ++++++++ 2018/pathfinding.py | 518 ++++++++++++++++++++++++++++++++ 4 files changed, 814 insertions(+) create mode 100644 2018/05-Alchemical Reduction.py create mode 100644 2018/06-Chronal Coordinates.py create mode 100644 2018/07-The Sum of Its Parts.py create mode 100644 2018/pathfinding.py diff --git a/2018/05-Alchemical Reduction.py b/2018/05-Alchemical Reduction.py new file mode 100644 index 0000000..a38c27f --- /dev/null +++ b/2018/05-Alchemical Reduction.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """dabAcCaCBAcCcaDA""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['9390', '5898'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + string = puzzle_input + prev_len = 0 + while prev_len != len(string): + prev_len = len(string) + for letter in 'azertyuiopmlkjhgfdsqwxcvbn': + string = string.replace(letter + letter.upper(), '') + string = string.replace(letter.upper() + letter, '') + + puzzle_actual_result = len(string) + + +else: + shortest_len = 10**6 + for letter in 'azertyuiopmlkjhgfdsqwxcvbn': + + string = puzzle_input.replace(letter, '').replace(letter.upper(), '') + prev_len = 0 + while prev_len != len(string): + prev_len = len(string) + for letter in 'azertyuiopmlkjhgfdsqwxcvbn': + string = string.replace(letter + letter.upper(), '') + string = string.replace(letter.upper() + letter, '') + + shortest_len = min(shortest_len, len(string)) + + puzzle_actual_result = shortest_len + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/06-Chronal Coordinates.py b/2018/06-Chronal Coordinates.py new file mode 100644 index 0000000..cd19f79 --- /dev/null +++ b/2018/06-Chronal Coordinates.py @@ -0,0 +1,103 @@ +# -------------------------------- Input data -------------------------------- # +import os, numpy as np +from collections import Counter + +test_data = {} + +test = 1 +test_data[test] = {"input": """1, 1 +1, 6 +8, 3 +3, 4 +5, 5 +8, 9""", + "expected": ['17', '16'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['4060', '36136'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + dots = [] + for string in puzzle_input.split('\n'): + if string == '': + continue + + x, y = map(int, string.split(', ')) + + dots.append((x, y)) + + grid = {} + min_x, max_x = min(dots)[0], max(dots)[0] + min_y, max_y = min(dots, key=lambda d: d[1])[1], max(dots, key=lambda d: d[1])[1] + for x in range (min_x - 1, max_x + 1): + for y in range (min_y - 1, max_y + 1): + min_distance = min([abs(x-dot[0])+abs(y-dot[1]) for dot in dots]) + for i, dot in enumerate(dots): + if abs(x-dot[0])+abs(y-dot[1]) == min_distance: + if grid.get((x, y), -1) != -1: + grid[(x, y)] = -1 + break + grid[(x, y)] = i + + corners = set([-1]) + corners = corners.union(grid[x, min_y] for x in range(min_x - 1, max_x + 1)) + corners = corners.union(grid[x, max_y] for x in range(min_x - 1, max_x + 1)) + corners = corners.union(grid[min_x, y] for y in range(min_y - 1, max_y + 1)) + corners = corners.union(grid[max_x, y] for y in range(min_y - 1, max_y + 1)) + + puzzle_actual_result = next(x[1] for x in Counter(grid.values()).most_common() if x[0] not in corners) + + + + +else: + dots = [] + for string in puzzle_input.split('\n'): + if string == '': + continue + + x, y = map(int, string.split(', ')) + + dots.append((x, y)) + + grid = {} + min_x, max_x = min(dots)[0], max(dots)[0] + min_y, max_y = min(dots, key=lambda d: d[1])[1], max(dots, key=lambda d: d[1])[1] + for x in range (min_x - 1, max_x + 1): + for y in range (min_y - 1, max_y + 1): + for dot in dots: + grid[(x, y)] = grid.get((x, y), 0) + abs(x-dot[0])+abs(y-dot[1]) + + puzzle_actual_result = sum(1 for x in grid if grid[x] < 10000) + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/07-The Sum of Its Parts.py b/2018/07-The Sum of Its Parts.py new file mode 100644 index 0000000..ce49cfd --- /dev/null +++ b/2018/07-The Sum of Its Parts.py @@ -0,0 +1,123 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """Step C must be finished before step A can begin. +Step C must be finished before step F can begin. +Step A must be finished before step B can begin. +Step A must be finished before step D can begin. +Step B must be finished before step E can begin. +Step D must be finished before step E can begin. +Step F must be finished before step E can begin.""", + "expected": ['CABDFE', 'CABFDE'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['OVXCKZBDEHINPFSTJLUYRWGAMQ', '955'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 1 +part_to_test = 1 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +def list_remove (remove_list, element): + try: + remove_list.remove(element) + return remove_list + except ValueError: + return remove_list + +if part_to_test == 1: + predecessors = {} + dots = [] + for string in puzzle_input.split('\n'): + _, source, _, _, _, _, _, target, *_ = string.split(' ') + if not target in predecessors: + predecessors[target] = [source] + else: + predecessors[target].append(source) + + dots.append(target) + dots.append(source) + + dots = set(dots) + + path = '' + while len(path) != len(dots): + next_dot = sorted(x for x in dots if x not in predecessors and x not in path)[0] + path += next_dot + predecessors = {x:list_remove(predecessors[x], next_dot) for x in predecessors} + predecessors = {x:predecessors[x] for x in predecessors if len(predecessors[x])} + + puzzle_actual_result = path + + + + +else: + predecessors = {} + dots = [] + for string in puzzle_input.split('\n'): + _, source, _, _, _, _, _, target, *_ = string.split(' ') + if not target in predecessors: + predecessors[target] = [source] + else: + predecessors[target].append(source) + + dots.append(target) + dots.append(source) + + dots = set(dots) + + + path = '' + construction = [] + tick = 0 + while len(path) != len(dots): + tick = 0 if len(construction) == 0 else min(x[2] for x in construction) + finished = [x for x in construction if x[2] == tick] + path += ''.join(x[0] for x in sorted(finished)) + predecessors = {x:list(set(predecessors[x]) - set(path)) for x in predecessors} + predecessors = {x:predecessors[x] for x in predecessors if len(predecessors[x])} + + construction = list(set(construction) - set(finished)) + in_construction = [x[0] for x in construction] + + next_dots = sorted(x for x in dots if x not in predecessors and x not in path and x not in in_construction) + workers_busy = sum(1 for worker in construction if worker[1] <= tick and worker[2] >= tick) + + if len(next_dots) and workers_busy < 5: + next_dots = sorted(next_dots)[:5-workers_busy] + construction += [(next_dot, tick, tick + ord(next_dot) - ord('A') + 60 + 1) for next_dot in next_dots] + + + + puzzle_actual_result = tick + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/pathfinding.py b/2018/pathfinding.py new file mode 100644 index 0000000..f299c6d --- /dev/null +++ b/2018/pathfinding.py @@ -0,0 +1,518 @@ +import heapq + + +class TargetFound(Exception): + pass + +class NegativeWeightCycle(Exception): + pass + + + +class Graph: + vertices = [] + edges = {} + distance_from_start = {} + came_from = {} + + def __init__ (self, vertices = [], edges = {}): + self.vertices = vertices + self.edges = edges + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def is_valid (self, vertex): + return vertex in self.vertices + + def estimate_to_complete (self, source_vertex, target_vertex): + return 0 + + def reset_search (self): + self.distance_from_start = {} + self.came_from = {} + + def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have (x, y) as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + y += 1 + + directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] + if diagonals_allowed: + directions += [(1, 1), (1, -1), (-1, 1), (-1, -1)] + + for coords in self.vertices: + for direction in directions: + x, y = coords[0] + direction[0], coords[1] + direction[1] + if (x, y) in self.vertices: + if coords in self.edges: + self.edges[(coords)].append((x, y)) + else: + self.edges[(coords)] = [(x, y)] + + return True + + def vertices_to_grid (self, mark_coords = [], wall = '#'): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param list mark_coords: List of coordonates to mark + :param string wall: Which character to use as walls + :return: True if the grid was converted + """ + x, y = (0, 0) + grid = '' + + all_x = [i[0] for i in self.vertices] + all_y = [i[1] for i in self.vertices] + min_x, max_x = min(all_x), max(all_x) + min_y, max_y = min(all_y), max(all_y) + + if isinstance(next(iter(self.vertices)), dict): + vertices = self.vertices.keys() + else: + vertices = self.vertices + + for y in range(min_y, max_y+1): + for x in range(min_x, max_x+1): + if (x, y) in mark_coords: + grid += 'X' + elif (x, y) in vertices: + grid += '.' + else: + grid += wall + grid += '\n' + + return grid + + def depth_first_search (self, start, end = None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion (self, current_distance, vertex, end = None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort (self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x:edges[x] for x in edges if x in not_visited} + next_nodes = sorted(x for x in not_visited if not x in sum(edges.values(), [])) + + return True + + def topological_sort_alphabetical (self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[0] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x:edges[x] for x in edges if x in not_visited} + print (not_visited, edges) + next_node = sorted(x for x in not_visited if not x in sum(edges.values(), [])) + print (len(next_node), next_node) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search (self, start, end = None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + def greedy_best_first_search (self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush(frontier, (self.estimate_to_complete(neighbor, end), neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path (self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#', cost_straight = 1, cost_diagonal = 2): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have (x, y) as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param boolean diagonals_allowed: Whether diagonal movement is allowed + :param float cost_straight: The cost of horizontal and vertical movements + :param float cost_diagonal: The cost of diagonal movements + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + y += 1 + + directions_straight = [(1, 0), (-1, 0), (0, 1), (0, -1)] + directions_diagonal = [(1, 1), (1, -1), (-1, 1), (-1, -1)] + + directions = directions_straight[:] + if diagonals_allowed: + directions += directions_diagonal + + for coords in self.vertices: + for direction in directions: + cost = cost_straight if direction in directions_straight \ + else cost_diagonal + x, y = coords[0] + direction[0], coords[1] + direction[1] + if (x, y) in self.vertices: + if coords in self.edges: + self.edges[(coords)][(x, y)] = cost + else: + self.edges[(coords)] = {(x, y): cost} + + return True + + def dijkstra (self, start, end = None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + return end is None or end in self.distance_from_start + + def a_star_search (self, start, end = None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush(frontier, (priority, neighbor, current_distance + weight)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford (self, start, end = None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range (len(self.vertices)-1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start \ + and self.distance_from_start[neighbor] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start + From 10ce0f498f8e5846db89db207a78d9fad0a16913 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 9 Jun 2020 21:53:23 +0200 Subject: [PATCH 022/143] Added day 2018-08 --- 2018/08-Memory Maneuver.py | 92 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 92 insertions(+) create mode 100644 2018/08-Memory Maneuver.py diff --git a/2018/08-Memory Maneuver.py b/2018/08-Memory Maneuver.py new file mode 100644 index 0000000..c7f0234 --- /dev/null +++ b/2018/08-Memory Maneuver.py @@ -0,0 +1,92 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = {"input": """2 3 0 3 10 11 12 1 1 0 1 99 2 1 1 2""", + "expected": ['138', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['41849', '32487'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +nodes = {} +node_hierarchy = {} + +def process_node (data, i): + global nodes, node_hierarchy + parent = i + subnodes, metadata = data[i:i+2] + if subnodes == 0: + nodes.update({i:data[i+2:i+2+metadata]}) + i += 2+metadata + return i + else: + i += 2 + node_hierarchy[parent] = list() + for j in range (subnodes): + node_hierarchy[parent].append(i) + i = process_node (data, i) + nodes.update({parent:data[i:i+metadata]}) + i += metadata + return i + +def node_value (node, node_values): + global nodes, node_hierarchy + if node in node_values: + return node_values[node] + elif node not in node_hierarchy: + return sum(nodes[node]) + else: + children = [node_hierarchy[node][child-1] for child in nodes[node] if child <= len(node_hierarchy[node])] + unknown_child_value = set(child for child in children if child not in node_values) + if unknown_child_value: + for child in unknown_child_value: + node_values[child] = node_value(child, node_values) + return sum(node_values[child] for child in children) + + return node_values[node] + +header = True + +data = list(map(int, puzzle_input.split(' '))) +process_node(data, 0) + +if part_to_test == 1: + puzzle_actual_result = sum(sum(nodes.values(), [])) + +else: + puzzle_actual_result = node_value(0, {}) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print ('Input : ' + puzzle_input) +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 153aa771e6782607ba8db9362003cd4e9c72983e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 11 Jun 2020 21:45:33 +0200 Subject: [PATCH 023/143] Added day 2018-09 --- 2018/09-Marble Mania.py | 106 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 2018/09-Marble Mania.py diff --git a/2018/09-Marble Mania.py b/2018/09-Marble Mania.py new file mode 100644 index 0000000..a0df1a4 --- /dev/null +++ b/2018/09-Marble Mania.py @@ -0,0 +1,106 @@ +# -------------------------------- Input data -------------------------------- # +import os, collections + +test_data = {} + +test = 1 +test_data[test] = {"input": """9 players; last marble is worth 25 points""", + "expected": ['32', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """10 players; last marble is worth 1618 points""", + "expected": ['8317', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """13 players; last marble is worth 7999 points""", + "expected": ['146373', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """17 players; last marble is worth 1104 points""", + "expected": ['2764', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """21 players; last marble is worth 6111 points""", + "expected": ['54718', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": """30 players; last marble is worth 5807 points""", + "expected": ['37305', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": '404 players; last marble is worth 71852 points', + "expected": ['434674', '3653994575'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +for string in puzzle_input.split('\n'): + nb_players, _, _, _, _, _, points, _ = string.split(' ') + nb_players, points = map(int, (nb_players, points)) + + +if part_to_test == 2: + points *= 100 + +position = 0 +scores = [0] * nb_players +if part_to_test == 1: + marbles = [0, 1] + for new_marble in range(2, points + 1): + if new_marble % 23 == 0: + scores[new_marble % nb_players] += new_marble + position = (position-7) % len(marbles) + scores[new_marble % nb_players] += marbles[position-1] + del marbles[position-1] + else: + marbles.insert(position+1, new_marble) + position = ((position + 2) % len(marbles)) + + if new_marble % 10000 == 0: + print (new_marble) + + +else: + marbles = collections.deque([0, 1]) + for new_marble in range(2, points + 1): + if new_marble % 23 == 0: + scores[new_marble % nb_players] += new_marble + marbles.rotate(7) + scores[new_marble % nb_players] += marbles.pop() + marbles.rotate(-1) + else: + marbles.rotate(-1) + marbles.append(new_marble) + +puzzle_actual_result = max(scores) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 7e8c5f4039c6d5ea33db022510ea067e5be749ce Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 12 Jun 2020 22:38:55 +0200 Subject: [PATCH 024/143] Added day 2018-10 --- 2018/10-The Stars Align.py | 97 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 2018/10-The Stars Align.py diff --git a/2018/10-The Stars Align.py b/2018/10-The Stars Align.py new file mode 100644 index 0000000..bd435b7 --- /dev/null +++ b/2018/10-The Stars Align.py @@ -0,0 +1,97 @@ +# -------------------------------- Input data -------------------------------- # +import os, parse, pathfinding + +test_data = {} + +test = 1 +test_data[test] = {"input": """position=< 9, 1> velocity=< 0, 2> +position=< 7, 0> velocity=<-1, 0> +position=< 3, -2> velocity=<-1, 1> +position=< 6, 10> velocity=<-2, -1> +position=< 2, -4> velocity=< 2, 2> +position=<-6, 10> velocity=< 2, -2> +position=< 1, 8> velocity=< 1, -1> +position=< 1, 7> velocity=< 1, 0> +position=<-3, 11> velocity=< 1, -2> +position=< 7, 6> velocity=<-1, -1> +position=<-2, 3> velocity=< 1, 0> +position=<-4, 3> velocity=< 2, 0> +position=<10, -3> velocity=<-1, 1> +position=< 5, 11> velocity=< 1, -2> +position=< 4, 7> velocity=< 0, -1> +position=< 8, -2> velocity=< 0, 1> +position=<15, 0> velocity=<-2, 0> +position=< 1, 6> velocity=< 1, 0> +position=< 8, 9> velocity=< 0, -1> +position=< 3, 3> velocity=<-1, 1> +position=< 0, 5> velocity=< 0, -1> +position=<-2, 2> velocity=< 2, 0> +position=< 5, -2> velocity=< 1, 2> +position=< 1, 4> velocity=< 2, 1> +position=<-2, 7> velocity=< 2, -2> +position=< 3, 6> velocity=<-1, -1> +position=< 5, 0> velocity=< 1, 0> +position=<-6, 0> velocity=< 2, 0> +position=< 5, 9> velocity=< 1, -2> +position=<14, 7> velocity=<-2, 0> +position=<-3, 6> velocity=< 2, -1>""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['RLEZNRAN', '10240'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # +stars = [] +for string in puzzle_input.split('\n'): + if string == '': + continue + r = parse.parse('position=<{:>d},{:>d}> velocity=<{:>d},{:>d}>', string) + stars.append(list(map(int, r))) + +star_map = pathfinding.Graph() +for i in range (2*10**4): + stars = [(x+vx,y+vy,vx,vy) for x, y, vx, vy in stars] + vertices = [(x, y) for x, y, vx, vy in stars] + + + # This was solved a bit manually + # I noticed all coordinates would converge around 0 at some point + # That point was around 10300 seconds + # Then made a limit: all coordinates should be within 300 from zero + # (my first test was actually 200, but that was gave no result) + # This gave ~ 20 seconds of interesting time + # At the end it was trial and error to find 10 240 + coords = [v[0] in range(-300, 300) for v in vertices] + [v[1] in range(-300, 300) for v in vertices] + + if all(coords) and i == 10239: + star_map.vertices = vertices + print (i+1) + print (star_map.vertices_to_grid(wall=' ')) + + + + +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From bbedcfdd1804f5cbc6455ba176500fe28a05f01b Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 13 Jun 2020 21:22:46 +0200 Subject: [PATCH 025/143] Added day 2018-11 --- 2018/11-Chronal Charge.py | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 2018/11-Chronal Charge.py diff --git a/2018/11-Chronal Charge.py b/2018/11-Chronal Charge.py new file mode 100644 index 0000000..ade9209 --- /dev/null +++ b/2018/11-Chronal Charge.py @@ -0,0 +1,71 @@ +# -------------------------------- Input data -------------------------------- # +import os, numpy as np + +test_data = {} + +test = 1 +test_data[test] = {"input": 18, + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": 7165, + "expected": ['(235, 20) with 31', '(237, 223, 14) with 83'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + + +if part_to_test == 1: + grid_power = {(x, y): int(((((10+x)*y + puzzle_input) * (10+x)) // 100) % 10)-5 for x in range (1, 301) for y in range (1, 301)} + + sum_power = {(x, y): sum(grid_power[x1, y1] for x1 in range (x, x+3) for y1 in range (y, y+3)) for x in range (1, 299) for y in range (1, 299)} + + max_power = max(sum_power.values()) + + puzzle_actual_result = list(coord for coord in sum_power if sum_power[coord] == max_power) + + +else: + grid_power = {(x, y): int(((((10+x)*y + puzzle_input) * (10+x)) // 100) % 10)-5 for x in range (1, 301) for y in range (1, 301)} + + max_power = 31 + sum_power = grid_power.copy() + for size in range (2, 300): + sum_power = {(x, y, size): sum(grid_power[x1, y1] + for x1 in range (x, x+size) + for y1 in range (y, y+size)) + for x in range (1, 301-size+1) + for y in range (1, 301-size+1)} + + new_max = max(sum_power.values()) + if new_max > max_power: + max_power = new_max + puzzle_actual_result = list(coord + (size,) for coord in sum_power if sum_power[coord] == max_power) + + # Basically, let it run until it decreases multiple times + print (size, new_max, list(coord for coord in sum_power if sum_power[coord] == new_max)) + + + +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 29f2dd63608e22bf16255febbec9de3597b58276 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 16 Jun 2020 11:07:01 +0200 Subject: [PATCH 026/143] Added day 2018-12 --- 2018/12-Subterranean Sustainability.py | 98 ++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 2018/12-Subterranean Sustainability.py diff --git a/2018/12-Subterranean Sustainability.py b/2018/12-Subterranean Sustainability.py new file mode 100644 index 0000000..9b9943d --- /dev/null +++ b/2018/12-Subterranean Sustainability.py @@ -0,0 +1,98 @@ +# -------------------------------- Input data -------------------------------- # +import os, numpy as np + +test_data = {} + +test = 1 +test_data[test] = {"input": '''initial state: #..#.#..##......###...### + +...## => # +..#.. => # +.#... => # +.#.#. => # +.#.## => # +.##.. => # +.#### => # +#.#.# => # +#.### => # +##.#. => # +##.## => # +###.. => # +###.# => # +####. => #''', + "expected": ['325', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read().strip(), + "expected": ['3890', '23743'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +# Note: numpy was used to practice. Clearly not the best choice here. + +if part_to_test == 1: + generations = 20 +else: + generations = 50000000000 + + +initial_state = puzzle_input.splitlines()[0][15:] + +pots = np.full((len(initial_state) + 10**6), '.') +pots[5*10**5:5*10**5+len(initial_state)] = np.fromiter(initial_state, dtype='S1', count=len(initial_state)) + +rules = {} +for string in puzzle_input.splitlines()[2:]: + source, target = string.split(' => ') + rules[source] = target + +prev_sum = sum(np.where(pots == '#')[0]) - 5*10**5 * len(np.where(pots == '#')[0]) +for i in range (1, generations): + + if case_to_test == 1: + for i in range (2, len(pots)-3): + if ''.join(pots[i-2:i+3]) not in rules: + rules[''.join(pots[i-2:i+3])] = '.' + + min_x, max_x = min(np.where(pots == '#')[0]), max(np.where(pots == '#')[0]) + + new_pots = np.full((len(initial_state) + 10**6), '.') + new_pots[min_x-2:max_x+2] = [rules[''.join(pots[i-2:i+3])] for i in range(min_x-2, max_x+2)] + pots = new_pots.copy() + + sum_pots = sum(np.where(new_pots == '#')[0]) - 5*10**5 * len(np.where(new_pots == '#')[0]) + + print (i, sum_pots, sum_pots - prev_sum) + prev_sum = sum_pots + + if i == 200: + puzzle_actual_result = sum_pots + 96 * (generations-200) + break + +if part_to_test == 1: + puzzle_actual_result = sum_pots + + +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From d4348280f80c27039ee423b91480d732dffb8b5a Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:00:51 +0200 Subject: [PATCH 027/143] Added search to pathfinding lib --- 2018/pathfinding.py | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/2018/pathfinding.py b/2018/pathfinding.py index f299c6d..6cf05d1 100644 --- a/2018/pathfinding.py +++ b/2018/pathfinding.py @@ -51,6 +51,7 @@ def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): :param string grid: The grid to convert :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :param str wall: What is considered as a wall :return: True if the grid was converted """ self.vertices = [] @@ -77,6 +78,28 @@ def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): return True + def grid_search (self, grid, items): + """ + Searches the grid for some items + + :param string grid: The grid to convert + :param Boolean items: Whether diagonal movement is allowed + :return: True if the grid was converted + """ + items_found = {} + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] in items: + if line[x] in items_found: + items_found[line[x]].append((x, y)) + else: + items_found[line[x]] = [(x, y)] + y += 1 + + return items_found + def vertices_to_grid (self, mark_coords = [], wall = '#'): """ Converts a set of coordinates to a text @@ -221,9 +244,7 @@ def topological_sort_alphabetical (self): not_visited.remove(next_node) current_distance += 1 edges = {x:edges[x] for x in edges if x in not_visited} - print (not_visited, edges) next_node = sorted(x for x in not_visited if not x in sum(edges.values(), [])) - print (len(next_node), next_node) if len(next_node): next_node = next_node[0] From c53a5c7dd466ddd501412d7dee6d22edd5b3c09a Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:01:18 +0200 Subject: [PATCH 028/143] Added day 2018-13 (buggy v1 + version with works) --- 2018/13-Mine Cart Madness.py | 136 ++++++++++++++++++++ 2018/13-Mine Cart Madness.v1.py | 211 ++++++++++++++++++++++++++++++++ 2 files changed, 347 insertions(+) create mode 100644 2018/13-Mine Cart Madness.py create mode 100644 2018/13-Mine Cart Madness.v1.py diff --git a/2018/13-Mine Cart Madness.py b/2018/13-Mine Cart Madness.py new file mode 100644 index 0000000..35f01ca --- /dev/null +++ b/2018/13-Mine Cart Madness.py @@ -0,0 +1,136 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, re + +test_data = {} + +test = 1 +test_data[test] = {"input": """/->-\\ +| | /----\\ +| /-+--+-\ | +| | | | v | +\-+-/ \-+--/ + \------/ """, + "expected": ['7,3', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": r"""/>-<\ +| | +| /<+-\ +| | | v +\>+/""", + "expected": ['Unknown', '6,4'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read(), + "expected": ['124,130', '143, 123'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 +verbose = 3 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + +cart_to_track = {'^': '|', '>': '-', '<': '-', 'v': '|'} +up, right, left, down = ((0, -1), (1, 0), (-1, 0), (0, 1)) +directions = {'^': up, '>': right, '<': left, 'v': down} +new_dirs = { + '^':['<', '^', '>'], + '>':['^', '>', 'v'], + '<':['v', '<', '^'], + 'v':['>', 'v', '<'], + '/': {'^': '>', '>': '^', '<': 'v', 'v': '<'}, + '\\':{'^': '<', '>': 'v', '<': '^', 'v': '>'}, +} + + +def move_cart (track, cart): + (x, y), dir, choice = cart + + x += directions[dir][0] + y += directions[dir][1] + + if track[y][x] == '+': + dir = new_dirs[dir][choice] + choice += 1 + choice %= 3 + elif track[y][x] in ('\\', '/'): + dir = new_dirs[track[y][x]][dir] + + return ((x, y), dir, choice) + +# Setting up the track +track = [] +cart_positions = [] +carts = [] +for y, line in enumerate(puzzle_input.split('\n')): + track.append([]) + for x, letter in enumerate(line): + if letter in cart_to_track: + track[y].append(cart_to_track[letter]) + carts.append(((x, y), letter, 0)) + cart_positions.append((x, y)) + else: + track[y].append(letter) + +# Run them! +tick = 0 + +carts.append('new') +while len(carts) > 0: + cart = carts.pop(0) + if cart == 'new': + if len(carts) == 1: + break + tick += 1 +# print ('tick', tick, 'completed - Remaining', len(carts)) + carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) + cart_positions = [c[0] for c in carts] + cart = carts.pop(0) + carts.append('new') + cart_positions.pop(0) + + + + cart = move_cart(track, cart) + + # Check collisions + if cart[0] in cart_positions: + if part_to_test == 1: + puzzle_actual_result = cart[0] + break + else: + print ('collision', cart[0]) + carts = [c for c in carts if c[0] != cart[0]] + cart_positions = [c[0] for c in carts] + else: + carts.append(cart) + cart_positions.append(cart[0]) + +if part_to_test == 2: + puzzle_actual_result = carts[0][0] + + + +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + diff --git a/2018/13-Mine Cart Madness.v1.py b/2018/13-Mine Cart Madness.v1.py new file mode 100644 index 0000000..53f5414 --- /dev/null +++ b/2018/13-Mine Cart Madness.v1.py @@ -0,0 +1,211 @@ + + +# This v1 works for part 1, not part 2 +# Since it's also quite slow, I've done a v2 that should be better + + + + + + +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, re + +test_data = {} + +test = 1 +test_data[test] = {"input": """/->-\\ +| | /----\\ +| /-+--+-\ | +| | | | v | +\-+-/ \-+--/ + \------/ """, + "expected": ['Unknown', 'Unknown'], + } + +test += 1 +test_data[test] = {"input": r"""/>-<\ +| | +| /<+-\ +| | | v +\>+/""", + "expected": ['Unknown', 'Unknown'], + } + +test = 'real' +input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) +test_data[test] = {"input": open(input_file, "r+").read(), + "expected": ['124,130', '99, 96'], + } + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 'real' +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]['input'] +puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] +puzzle_actual_result = 'Unknown' + + +# -------------------------------- Actual code execution -------------------------------- # + + +def grid_to_vertices (self, grid, wall = '#'): + self.vertices = [] + track = {} + y = 0 + + for line in grid.splitlines(): + line = line.replace('^', '|').replace('v', '|').replace('>', '-').replace('<', '-') + for x in range(len(line)): + if line[x] != wall: + self.vertices.append((x, y)) + track[(x, y)] = line[x] + + y += 1 + + directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] + right, left, down, up = directions + + for coords in self.vertices: + for direction in directions: + x, y = coords[0] + direction[0], coords[1] + direction[1] + + if track[coords] == '-' and direction in [up, down]: + continue + if track[coords] == '|' and direction in [left, right]: + continue + + if (x, y) in self.vertices: + if track[coords] in ('\\', '/'): + if track[(x, y)] in ('\\', '/'): + continue + if track[(x, y)] == '-' and direction in [up, down]: + continue + elif track[(x, y)] == '|' and direction in [left, right]: + continue + if coords in self.edges: + self.edges[(coords)].append((x, y)) + else: + self.edges[(coords)] = [(x, y)] + + return True + +pathfinding.Graph.grid_to_vertices = grid_to_vertices + +def turn_left (direction): + return (direction[1], -direction[0]) + +def turn_right (direction): + return (-direction[1], direction[0]) + + + + + +# Analyze grid +grid = puzzle_input +graph = pathfinding.Graph() +graph.grid_to_vertices(puzzle_input, ' ') + +intersections = graph.grid_search(grid, '+')['+'] + +directions = {'^': (0, -1), '>': (1, 0), '<': (-1, 0), 'v': (0, 1)} +dirs = {(0, -1): '^', (1, 0): '>', (-1, 0): '<', (0, 1): 'v'} + + +# Find carts +list_carts = graph.grid_search(grid, ('^', '<', '>', 'v')) +carts = [] +cart_positions = [] +for direction in list_carts: + dir = directions[direction] + for cart in list_carts[direction]: + carts.append((cart, dir, 0)) + cart_positions.append(list_carts[direction]) +del list_carts +carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) + +# Run them! +subtick = 0 +tick = 0 + +nb_carts = len(carts) +collision = 0 +while True: + cart = carts.pop(0) + cart_positions.pop(0) + pos, dir, choice = cart + new_pos = (pos[0] + dir[0], pos[1] + dir[1]) + + print (pos, choice, dirs[dir]) + + + # We need to turn + if new_pos not in graph.edges[pos]: + options = [((pos[0] + x[0], pos[1] + x[1]), x) + for x in directions.values() + if x != (-dir[0], -dir[1]) + and (pos[0] + x[0], pos[1] + x[1]) in graph.edges[pos]] + new_pos, dir = options[0] + + # Intersection + if new_pos in intersections: + if choice % 3 == 0: + dir = turn_left(dir) + elif choice % 3 == 2: + dir = turn_right(dir) + choice += 1 + choice %= 3 + + new_cart = (new_pos, dir, choice) + + + + + + # Check collisions + if new_cart[0] in cart_positions: + if part_to_test == 1: + puzzle_actual_result = new_cart[0] + break + else: + print ('collision', new_cart[0]) + collision += 1 + carts = [c for c in carts if c[0] != new_cart[0]] + cart_positions = [c[0] for c in carts] + else: + carts.append(new_cart) + cart_positions.append(new_cart[0]) + + + # Count ticks + sort carts + subtick += 1 + if subtick == nb_carts - collision: + tick += 1 + subtick = 0 + collision = 0 + nb_carts = len(carts) + carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) + cart_positions = [c[0] for c in carts] + + print ('End of tick', tick, ' - Remaining', len(carts)) + if len(carts) == 1: + break + +if part_to_test == 2: + puzzle_actual_result = carts +#99, 96 +# -------------------------------- Outputs / results -------------------------------- # + +print ('Expected result : ' + str(puzzle_expected_result)) +print ('Actual result : ' + str(puzzle_actual_result)) + + + + From 72174b0cab5def2adf643ef82ec64ee6b00d3f61 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 21 Jun 2020 14:29:21 +0200 Subject: [PATCH 029/143] Added day 2018-14 --- 2018/14-Chocolate Charts.py | 110 ++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 2018/14-Chocolate Charts.py diff --git a/2018/14-Chocolate Charts.py b/2018/14-Chocolate Charts.py new file mode 100644 index 0000000..176d9bf --- /dev/null +++ b/2018/14-Chocolate Charts.py @@ -0,0 +1,110 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": 9, + "expected": ["5158916779", "Unknown"], +} +test += 1 +test_data[test] = { + "input": 5, + "expected": ["0124515891", "Unknown"], +} +test += 1 +test_data[test] = { + "input": 18, + "expected": ["9251071085", "Unknown"], +} +test += 1 +test_data[test] = { + "input": 2018, + "expected": ["5941429882", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": "51589", + "expected": ["Unknown", "9"], +} +test += 1 +test_data[test] = { + "input": "01245", + "expected": ["Unknown", "5"], +} +test += 1 +test_data[test] = { + "input": "92510", + "expected": ["Unknown", "18"], +} +test += 1 +test_data[test] = { + "input": "59414", + "expected": ["Unknown", "2018"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": "633601", + "expected": ["5115114101", "20310465"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +elf1, elf2 = 0, 1 +recipes = [3, 7] + +if part_to_test == 1: + while len(recipes) < int(puzzle_input) + 10: + new_score = recipes[elf1] + recipes[elf2] + if new_score >= 10: + recipes.append(new_score // 10) + recipes.append(new_score % 10) + elf1 += 1 + recipes[elf1] + elf2 += 1 + recipes[elf2] + elf1 %= len(recipes) + elf2 %= len(recipes) + + puzzle_actual_result = "".join(map(str, recipes[puzzle_input : puzzle_input + 10])) + + +else: + recipes = "37" + puzzle_input = str(puzzle_input) + while puzzle_input not in recipes[-10:]: + e1, e2 = int(recipes[elf1]), int(recipes[elf2]) + new_score = e1 + e2 + if new_score >= 10: + recipes += str(new_score // 10) + recipes += str(new_score % 10) + elf1 += 1 + e1 + elf2 += 1 + e2 + elf1 %= len(recipes) + elf2 %= len(recipes) + + puzzle_actual_result = recipes.find(puzzle_input) + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 0e9bd66c7cf3480c4f456d4cc0fda4adeefa1775 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:02:12 +0200 Subject: [PATCH 030/143] Updated pathfinding to use complex numbers rather than coordinates --- 2018/10-The Stars Align.py | 63 ++++----- 2018/13-Mine Cart Madness.v1.py | 135 ++++++++++--------- 2018/pathfinding.py | 224 +++++++++++++++++++++----------- 3 files changed, 247 insertions(+), 175 deletions(-) diff --git a/2018/10-The Stars Align.py b/2018/10-The Stars Align.py index bd435b7..3ec5dcc 100644 --- a/2018/10-The Stars Align.py +++ b/2018/10-The Stars Align.py @@ -4,7 +4,8 @@ test_data = {} test = 1 -test_data[test] = {"input": """position=< 9, 1> velocity=< 0, 2> +test_data[test] = { + "input": """position=< 9, 1> velocity=< 0, 2> position=< 7, 0> velocity=<-1, 0> position=< 3, -2> velocity=<-1, 1> position=< 6, 10> velocity=<-2, -1> @@ -35,40 +36,44 @@ position=< 5, 9> velocity=< 1, -2> position=<14, 7> velocity=<-2, 0> position=<-3, 6> velocity=< 2, -1>""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['RLEZNRAN', '10240'], - } + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["RLEZNRAN", "10240"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # stars = [] -for string in puzzle_input.split('\n'): - if string == '': +for string in puzzle_input.split("\n"): + if string == "": continue - r = parse.parse('position=<{:>d},{:>d}> velocity=<{:>d},{:>d}>', string) + r = parse.parse("position=<{:>d},{:>d}> velocity=<{:>d},{:>d}>", string) stars.append(list(map(int, r))) star_map = pathfinding.Graph() -for i in range (2*10**4): - stars = [(x+vx,y+vy,vx,vy) for x, y, vx, vy in stars] - vertices = [(x, y) for x, y, vx, vy in stars] - +for i in range(2 * 10 ** 4): + stars = [(x + vx, y + vy, vx, vy) for x, y, vx, vy in stars] + vertices = [x - y * 1j for x, y, vx, vy in stars] # This was solved a bit manually # I noticed all coordinates would converge around 0 at some point @@ -77,21 +82,17 @@ # (my first test was actually 200, but that was gave no result) # This gave ~ 20 seconds of interesting time # At the end it was trial and error to find 10 240 - coords = [v[0] in range(-300, 300) for v in vertices] + [v[1] in range(-300, 300) for v in vertices] + coords = [v.real in range(-300, 300) for v in vertices] + [ + v.imag in range(-300, 300) for v in vertices + ] if all(coords) and i == 10239: star_map.vertices = vertices - print (i+1) - print (star_map.vertices_to_grid(wall=' ')) - - + print(i + 1) + print(star_map.vertices_to_grid(wall=" ")) # -------------------------------- Outputs / results -------------------------------- # -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/13-Mine Cart Madness.v1.py b/2018/13-Mine Cart Madness.v1.py index 53f5414..a7aab63 100644 --- a/2018/13-Mine Cart Madness.v1.py +++ b/2018/13-Mine Cart Madness.v1.py @@ -1,67 +1,70 @@ - - # This v1 works for part 1, not part 2 # Since it's also quite slow, I've done a v2 that should be better - - - - # -------------------------------- Input data -------------------------------- # import os, pathfinding, re test_data = {} test = 1 -test_data[test] = {"input": """/->-\\ +test_data[test] = { + "input": """/->-\\ | | /----\\ | /-+--+-\ | | | | | v | \-+-/ \-+--/ \------/ """, - "expected": ['Unknown', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": r"""/>-<\ +test_data[test] = { + "input": r"""/>-<\ | | | /<+-\ | | | v \>+/""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read(), - "expected": ['124,130', '99, 96'], - } + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["124,130", "99, 96"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 2 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -def grid_to_vertices (self, grid, wall = '#'): +def grid_to_vertices(self, grid, wall="#"): self.vertices = [] track = {} y = 0 for line in grid.splitlines(): - line = line.replace('^', '|').replace('v', '|').replace('>', '-').replace('<', '-') + line = ( + line.replace("^", "|").replace("v", "|").replace(">", "-").replace("<", "-") + ) for x in range(len(line)): if line[x] != wall: self.vertices.append((x, y)) @@ -69,58 +72,62 @@ def grid_to_vertices (self, grid, wall = '#'): y += 1 - directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] - right, left, down, up = directions + north = 1j + south = -1j + west = -1 + east = 1 + + directions = [north, south, west, east] - for coords in self.vertices: + for source in self.vertices: for direction in directions: - x, y = coords[0] + direction[0], coords[1] + direction[1] + target = source + direction - if track[coords] == '-' and direction in [up, down]: + if track[source] == "-" and direction in [north, south]: continue - if track[coords] == '|' and direction in [left, right]: + if track[source] == "|" and direction in [west, east]: continue - if (x, y) in self.vertices: - if track[coords] in ('\\', '/'): - if track[(x, y)] in ('\\', '/'): + if target in self.vertices: + if track[source] in ("\\", "/"): + if track[target] in ("\\", "/"): continue - if track[(x, y)] == '-' and direction in [up, down]: + if track[target] == "-" and direction in [north, south]: continue - elif track[(x, y)] == '|' and direction in [left, right]: + elif track[target] == "|" and direction in [west, east]: continue - if coords in self.edges: - self.edges[(coords)].append((x, y)) + if source in self.edges: + self.edges[(source)].append(target) else: - self.edges[(coords)] = [(x, y)] + self.edges[(source)] = [target] return True -pathfinding.Graph.grid_to_vertices = grid_to_vertices -def turn_left (direction): - return (direction[1], -direction[0]) +pathfinding.Graph.grid_to_vertices = grid_to_vertices -def turn_right (direction): - return (-direction[1], direction[0]) +def turn_left(direction): + return (direction[1], -direction[0]) +def turn_right(direction): + return (-direction[1], direction[0]) # Analyze grid grid = puzzle_input graph = pathfinding.Graph() -graph.grid_to_vertices(puzzle_input, ' ') +graph.grid_to_vertices(puzzle_input, " ") -intersections = graph.grid_search(grid, '+')['+'] +intersections = graph.grid_search(grid, "+")["+"] -directions = {'^': (0, -1), '>': (1, 0), '<': (-1, 0), 'v': (0, 1)} -dirs = {(0, -1): '^', (1, 0): '>', (-1, 0): '<', (0, 1): 'v'} +directions = {"^": (0, -1), ">": (1, 0), "<": (-1, 0), "v": (0, 1)} +dirs = {(0, -1): "^", (1, 0): ">", (-1, 0): "<", (0, 1): "v"} # Find carts -list_carts = graph.grid_search(grid, ('^', '<', '>', 'v')) +list_carts = graph.grid_search(grid, ("^", "<", ">", "v")) carts = [] cart_positions = [] for direction in list_carts: @@ -143,15 +150,16 @@ def turn_right (direction): pos, dir, choice = cart new_pos = (pos[0] + dir[0], pos[1] + dir[1]) - print (pos, choice, dirs[dir]) - + print(pos, choice, dirs[dir]) # We need to turn if new_pos not in graph.edges[pos]: - options = [((pos[0] + x[0], pos[1] + x[1]), x) - for x in directions.values() - if x != (-dir[0], -dir[1]) - and (pos[0] + x[0], pos[1] + x[1]) in graph.edges[pos]] + options = [ + ((pos[0] + x[0], pos[1] + x[1]), x) + for x in directions.values() + if x != (-dir[0], -dir[1]) + and (pos[0] + x[0], pos[1] + x[1]) in graph.edges[pos] + ] new_pos, dir = options[0] # Intersection @@ -165,17 +173,13 @@ def turn_right (direction): new_cart = (new_pos, dir, choice) - - - - # Check collisions if new_cart[0] in cart_positions: if part_to_test == 1: puzzle_actual_result = new_cart[0] break else: - print ('collision', new_cart[0]) + print("collision", new_cart[0]) collision += 1 carts = [c for c in carts if c[0] != new_cart[0]] cart_positions = [c[0] for c in carts] @@ -183,7 +187,6 @@ def turn_right (direction): carts.append(new_cart) cart_positions.append(new_cart[0]) - # Count ticks + sort carts subtick += 1 if subtick == nb_carts - collision: @@ -194,18 +197,14 @@ def turn_right (direction): carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) cart_positions = [c[0] for c in carts] - print ('End of tick', tick, ' - Remaining', len(carts)) + print("End of tick", tick, " - Remaining", len(carts)) if len(carts) == 1: break if part_to_test == 2: puzzle_actual_result = carts -#99, 96 +# 99, 96 # -------------------------------- Outputs / results -------------------------------- # -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/pathfinding.py b/2018/pathfinding.py index 6cf05d1..0127f4e 100644 --- a/2018/pathfinding.py +++ b/2018/pathfinding.py @@ -1,21 +1,59 @@ import heapq +# Cardinal directions +north = 1j +south = -1j +west = -1 +east = 1 +northeast = 1 + 1j +northwest = -1 + 1j +southeast = 1 - 1j +southwest = -1 - 1j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + + +def min_real(complexes): + real_values = [x.real for x in complexes] + return min(real_values) + + +def min_imag(complexes): + real_values = [x.imag for x in complexes] + return min(real_values) + + +def max_real(complexes): + real_values = [x.real for x in complexes] + return max(real_values) + + +def max_imag(complexes): + real_values = [x.imag for x in complexes] + return max(real_values) + class TargetFound(Exception): pass + class NegativeWeightCycle(Exception): pass - class Graph: vertices = [] edges = {} distance_from_start = {} came_from = {} - def __init__ (self, vertices = [], edges = {}): + def __init__(self, vertices=[], edges={}): self.vertices = vertices self.edges = edges @@ -31,22 +69,22 @@ def neighbors(self, vertex): else: return False - def is_valid (self, vertex): + def is_valid(self, vertex): return vertex in self.vertices - def estimate_to_complete (self, source_vertex, target_vertex): + def estimate_to_complete(self, source_vertex, target_vertex): return 0 - def reset_search (self): + def reset_search(self): self.distance_from_start = {} self.came_from = {} - def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): + def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): """ Converts a text to a set of coordinates The text is expected to be separated by newline characters - The vertices will have (x, y) as coordinates + The vertices will have x - y * 1j as coordinates Edges will be calculated as well :param string grid: The grid to convert @@ -60,25 +98,26 @@ def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#'): for line in grid.splitlines(): for x in range(len(line)): if line[x] != wall: - self.vertices.append((x, y)) + self.vertices.append(x - y * 1j) y += 1 - directions = [(1, 0), (-1, 0), (0, 1), (0, -1)] if diagonals_allowed: - directions += [(1, 1), (1, -1), (-1, 1), (-1, -1)] + directions = directions_diagonals + else: + directions = directions_straight - for coords in self.vertices: + for source in self.vertices: for direction in directions: - x, y = coords[0] + direction[0], coords[1] + direction[1] - if (x, y) in self.vertices: - if coords in self.edges: - self.edges[(coords)].append((x, y)) + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[(source)].append(target) else: - self.edges[(coords)] = [(x, y)] + self.edges[(source)] = [target] return True - def grid_search (self, grid, items): + def grid_search(self, grid, items): """ Searches the grid for some items @@ -93,14 +132,14 @@ def grid_search (self, grid, items): for x in range(len(line)): if line[x] in items: if line[x] in items_found: - items_found[line[x]].append((x, y)) + items_found[line[x]].append(x - y * 1j) else: - items_found[line[x]] = [(x, y)] + items_found[line[x]] = [x - y * 1j] y += 1 return items_found - def vertices_to_grid (self, mark_coords = [], wall = '#'): + def vertices_to_grid(self, mark_coords=[], wall="#"): """ Converts a set of coordinates to a text @@ -110,32 +149,42 @@ def vertices_to_grid (self, mark_coords = [], wall = '#'): :param string wall: Which character to use as walls :return: True if the grid was converted """ - x, y = (0, 0) - grid = '' + grid = "" - all_x = [i[0] for i in self.vertices] - all_y = [i[1] for i in self.vertices] - min_x, max_x = min(all_x), max(all_x) - min_y, max_y = min(all_y), max(all_y) + min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) + min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) if isinstance(next(iter(self.vertices)), dict): vertices = self.vertices.keys() else: vertices = self.vertices - for y in range(min_y, max_y+1): - for x in range(min_x, max_x+1): - if (x, y) in mark_coords: - grid += 'X' - elif (x, y) in vertices: - grid += '.' + for y in range(min_y, max_y - 1, -1): + for x in range(min_x, max_x + 1): + if x + y * 1j in mark_coords: + grid += "X" + elif x + y * 1j in vertices: + grid += "." else: grid += wall - grid += '\n' + grid += "\n" return grid - def depth_first_search (self, start, end = None): + def add_traps(self, vertex): + """ + Creates traps: places that can be reached, but not exited + + :param Any vertex: The vertex to consider + :return: True if successful, False if no vertex found + """ + if vertex in self.edges: + del self.edges[vertex] + return True + else: + return False + + def depth_first_search(self, start, end=None): """ Performs a depth-first search based on a start node @@ -159,7 +208,7 @@ def depth_first_search (self, start, end = None): return False return False - def depth_first_search_recursion (self, current_distance, vertex, end = None): + def depth_first_search_recursion(self, current_distance, vertex, end=None): """ Recurrence function for depth-first search @@ -190,7 +239,7 @@ def depth_first_search_recursion (self, current_distance, vertex, end = None): if neighbor == end: raise TargetFound - def topological_sort (self): + def topological_sort(self): """ Performs a topological sort @@ -214,12 +263,14 @@ def topological_sort (self): not_visited -= set(next_nodes) current_distance += 1 - edges = {x:edges[x] for x in edges if x in not_visited} - next_nodes = sorted(x for x in not_visited if not x in sum(edges.values(), [])) + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) return True - def topological_sort_alphabetical (self): + def topological_sort_alphabetical(self): """ Performs a topological sort with alphabetical sort @@ -235,7 +286,9 @@ def topological_sort_alphabetical (self): not_visited = set(self.vertices) edges = self.edges.copy() - next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[0] + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] current_distance = 0 while not_visited: @@ -243,14 +296,16 @@ def topological_sort_alphabetical (self): not_visited.remove(next_node) current_distance += 1 - edges = {x:edges[x] for x in edges if x in not_visited} - next_node = sorted(x for x in not_visited if not x in sum(edges.values(), [])) + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) if len(next_node): next_node = next_node[0] return True - def breadth_first_search (self, start, end = None): + def breadth_first_search(self, start, end=None): """ Performs a breath-first search based on a start node @@ -293,7 +348,7 @@ def breadth_first_search (self, start, end = None): return True return False - def greedy_best_first_search (self, start, end): + def greedy_best_first_search(self, start, end): """ Performs a greedy best-first search based on a start node @@ -326,7 +381,14 @@ def greedy_best_first_search (self, start, end): continue # Adding for future examination - heapq.heappush(frontier, (self.estimate_to_complete(neighbor, end), neighbor, current_distance)) + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) # Adding for final search self.distance_from_start[neighbor] = current_distance @@ -337,7 +399,7 @@ def greedy_best_first_search (self, start, end): return False - def path (self, target_vertex): + def path(self, target_vertex): """ Reconstructs the path followed to reach a given vertex @@ -355,12 +417,14 @@ def path (self, target_vertex): class WeightedGraph(Graph): - def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#', cost_straight = 1, cost_diagonal = 2): + def grid_to_vertices( + self, grid, diagonals_allowed=False, wall="#", cost_straight=1, cost_diagonal=2 + ): """ Converts a text to a set of coordinates The text is expected to be separated by newline characters - The vertices will have (x, y) as coordinates + The vertices will have x - y * 1j as coordinates Edges will be calculated as well :param string grid: The grid to convert @@ -375,30 +439,31 @@ def grid_to_vertices (self, grid, diagonals_allowed = False, wall = '#', cost_st for line in grid.splitlines(): for x in range(len(line)): if line[x] != wall: - self.vertices.append((x, y)) + self.vertices.append(x - y * 1j) y += 1 - directions_straight = [(1, 0), (-1, 0), (0, 1), (0, -1)] - directions_diagonal = [(1, 1), (1, -1), (-1, 1), (-1, -1)] - - directions = directions_straight[:] if diagonals_allowed: - directions += directions_diagonal + directions = directions_diagonals + else: + directions = directions_straight - for coords in self.vertices: + for source in self.vertices: for direction in directions: - cost = cost_straight if direction in directions_straight \ - else cost_diagonal - x, y = coords[0] + direction[0], coords[1] + direction[1] - if (x, y) in self.vertices: - if coords in self.edges: - self.edges[(coords)][(x, y)] = cost + cost = ( + cost_straight if direction in directions_straight else cost_diagonal + ) + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[(source)][target] = cost else: - self.edges[(coords)] = {(x, y): cost} + self.edges[(source)] = {target: cost} return True - def dijkstra (self, start, end = None): + return True + + def dijkstra(self, start, end=None): """ Applies the Dijkstra algorithm to a given search @@ -429,8 +494,9 @@ def dijkstra (self, start, end = None): for neighbor, weight in neighbors.items(): # We've already checked that node, and it's not better now - if neighbor in self.distance_from_start \ - and self.distance_from_start[neighbor] <= (current_distance + weight): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): continue # Adding for future examination @@ -442,7 +508,7 @@ def dijkstra (self, start, end = None): return end is None or end in self.distance_from_start - def a_star_search (self, start, end = None): + def a_star_search(self, start, end=None): """ Performs a A* search @@ -479,13 +545,16 @@ def a_star_search (self, start, end = None): for neighbor, weight in neighbors.items(): # We've already checked that node, and it's not better now - if neighbor in self.distance_from_start \ - and self.distance_from_start[neighbor] <= (current_distance + weight): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): continue # Adding for future examination priority = current_distance + self.estimate_to_complete(neighbor, end) - heapq.heappush(frontier, (priority, neighbor, current_distance + weight)) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) # Adding for final search self.distance_from_start[neighbor] = current_distance + weight @@ -496,7 +565,7 @@ def a_star_search (self, start, end = None): return end in self.distance_from_start - def bellman_ford (self, start, end = None): + def bellman_ford(self, start, end=None): """ Applies the Bellman–Ford algorithm to a given search @@ -515,13 +584,16 @@ def bellman_ford (self, start, end = None): self.distance_from_start = {start: 0} self.came_from = {start: None} - for i in range (len(self.vertices)-1): + for i in range(len(self.vertices) - 1): for vertex in self.vertices: current_distance = self.distance_from_start[vertex] for neighbor, weight in self.neighbors(vertex).items(): # We've already checked that node, and it's not better now - if neighbor in self.distance_from_start \ - and self.distance_from_start[neighbor] <= (current_distance + weight): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= ( + current_distance + weight + ): continue # Adding for final search @@ -531,9 +603,9 @@ def bellman_ford (self, start, end = None): # Check for cycles for vertex in self.vertices: for neighbor, weight in self.neighbors(vertex).items(): - if neighbor in self.distance_from_start \ - and self.distance_from_start[neighbor] <= (current_distance + weight): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): raise NegativeWeightCycle return end is None or end in self.distance_from_start - From 3d6a72fa9c30ca1f901cb523279fff6a0866dfb4 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:02:52 +0200 Subject: [PATCH 031/143] Added a library for track-like games --- 2018/racetrack.py | 288 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 288 insertions(+) create mode 100644 2018/racetrack.py diff --git a/2018/racetrack.py b/2018/racetrack.py new file mode 100644 index 0000000..75f82fb --- /dev/null +++ b/2018/racetrack.py @@ -0,0 +1,288 @@ +from math import sqrt + +# Cardinal directions +north = 1j +south = -1j +west = -1 +east = 1 +directions_all = [north, south, west, east] +directions_horizontal = [west, east] +directions_vertical = [north, south] + +# To be multiplied by the current cartinal direction +relative_directions = { + "left": 1j, + "right": -1j, + "ahead": 1, + "back": -1, +} + + +class PlayerBlocked(Exception): + pass + + +def min_real(complexes): + real_values = [x.real for x in complexes] + return min(real_values) + + +def min_imag(complexes): + real_values = [x.imag for x in complexes] + return min(real_values) + + +def max_real(complexes): + real_values = [x.real for x in complexes] + return max(real_values) + + +def max_imag(complexes): + real_values = [x.imag for x in complexes] + return max(real_values) + + +def complex_sort(complexes, mode=""): + # Sorts by real, then by imaginary component (x then y) + if mode == "xy": + complexes.sort(key=lambda a: (a.real, a.imag)) + # Sorts by imaginary, then by real component (y then x) + elif mode == "yx": + complexes.sort(key=lambda a: (a.imag, a.real)) + # Sorts by distance from 0,0 (kind of polar coordinates) + else: + complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) + return complexes + + +def collisions(players): + positions = [x.position for x in players] + if positions == set(positions): + return None + else: + return [x for x in set(positions) if positions.count(x) > 1] + + +class RaceTrack: + vertices = {} + edges = {} + """ + Represents which directions are allowed based on the track piece + + Structure: + track_piece: allowed directions + """ + allowed_directions = { + "/": directions_all, + "\\": directions_all, + "+": directions_all, + "|": directions_vertical, + "-": directions_horizontal, + "^": directions_vertical, + "v": directions_vertical, + ">": directions_horizontal, + "<": directions_horizontal, + } + + # Usual replacements + player_replace = { + ">": "-", + "<": "-", + "^": "|", + "v": "|", + } + + def __init__(self, vertices=[], edges={}): + self.vertices = vertices + self.edges = edges + + def text_to_track(self, text, allowed_directions={}): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x-y*j as coordinates (so y axis points south) + Edges will be calculated as well + + :param string text: The text to convert + :param str elements: How to interpret the track + :return: True if the text was converted + """ + self.vertices = {} + self.allowed_directions.update(allowed_directions) + + for y, line in enumerate(text.splitlines()): + for x in range(len(line)): + if line[x] in self.allowed_directions: + self.vertices[x - y * 1j] = line[x] + + for source, track in self.vertices.items(): + for direction in self.allowed_directions[track]: + target = source + direction + if not target in self.vertices: + continue + + target_dirs = self.allowed_directions[self.vertices[target]] + if -direction not in target_dirs: + continue + + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + def track_to_text(self, mark_coords={}, wall=" "): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string wall: Which character to use as walls + :return: the converted text + """ + + min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) + min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) + + text = "" + + for y in range(min_y, max_y - 1, -1): + for x in range(min_x, max_x + 1): + if x + y * 1j in mark_coords: + text += mark_coords[x + y * 1j] + else: + text += self.vertices.get(x + y * 1j, wall) + text += "\n" + + return text + + def replace_elements(self, replace_map=None): + """ + Replaces elements in the track (useful to remove players) + + :param dict replace_map: Replacement map + :return: True + """ + + if replace_map is None: + replace_map = self.player_replace + self.vertices = {x: replace_map.get(y, y) for x, y in self.vertices.items()} + return True + + def find_elements(self, elements): + """ + Finds elements in the track + + :param dict elements: elements to find + :return: True + """ + + found = {x: y for x, y in self.vertices.items() if y in elements} + return found + + +class Player: + """ + Represents which directions are allowed based on the track piece + + Structure: + track_piece: source direction: allowed target direction + """ + + allowed_directions = { + "/": {north: [east], south: [west], east: [north], west: [south],}, + "\\": {north: [west], south: [east], east: [south], west: [north],}, + "+": { + north: directions_all, + south: directions_all, + east: directions_all, + west: directions_all, + }, + "|": { + north: directions_vertical, + south: directions_vertical, + east: None, + west: None, + }, + "-": { + north: None, + south: None, + east: directions_horizontal, + west: directions_horizontal, + }, + } + + initial_directions = { + ">": east, + "<": west, + "^": north, + "v": south, + } + + position = 0 + direction = 0 + + def __init__(self, racetrack, position=0, direction=None): + self.position = position + if direction is None: + self.direction = self.initial_directions[racetrack.vertices[position]] + else: + self.direction = direction + + def move(self, racetrack, steps=1): + """ + Moves the player in the direction provided + + :param RaceTrack racetrack: The track to use + :param int steps: The number of steps to take + :return: nothing + """ + for step in range(steps): + # First, let's move the player + self.before_move() + + self.position += self.direction + + if self.position not in racetrack.vertices: + raise PlayerBlocked + + self.after_move() + + # Then, let's make him turn + self.before_rotation() + + track = racetrack.vertices[self.position] + possible_directions = self.allowed_directions[track][self.direction] + + if possible_directions is None: + raise PlayerBlocked + elif len(possible_directions) == 1: + self.direction = possible_directions[0] + else: + self.choose_direction(possible_directions) + + self.after_rotation() + + def before_move(self): + pass + + def after_move(self): + pass + + def before_rotation(self): + pass + + def after_rotation(self): + pass + + def choose_direction(self, possible_directions): + self.direction = possible_directions[0] + + def turn_left(self): + self.direction *= 1j + + def turn_right(self): + self.direction *= -1j From 1eecc0b426fe92a8c0edf8b731d5ae07e77a23d9 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:25:57 +0200 Subject: [PATCH 032/143] Pathfinding: added ability to add walls + various fixes --- 2018/pathfinding.py | 82 +++++++++++++++++++-------------------------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/2018/pathfinding.py b/2018/pathfinding.py index 0127f4e..a1dac44 100644 --- a/2018/pathfinding.py +++ b/2018/pathfinding.py @@ -1,42 +1,6 @@ import heapq -# Cardinal directions -north = 1j -south = -1j -west = -1 -east = 1 -northeast = 1 + 1j -northwest = -1 + 1j -southeast = 1 - 1j -southwest = -1 - 1j - -directions_straight = [north, south, west, east] -directions_diagonals = directions_straight + [ - northeast, - northwest, - southeast, - southwest, -] - - -def min_real(complexes): - real_values = [x.real for x in complexes] - return min(real_values) - - -def min_imag(complexes): - real_values = [x.imag for x in complexes] - return min(real_values) - - -def max_real(complexes): - real_values = [x.real for x in complexes] - return max(real_values) - - -def max_imag(complexes): - real_values = [x.imag for x in complexes] - return max(real_values) +from complex_utils import * class TargetFound(Exception): @@ -121,21 +85,20 @@ def grid_search(self, grid, items): """ Searches the grid for some items - :param string grid: The grid to convert - :param Boolean items: Whether diagonal movement is allowed + :param string grid: The grid in which to search + :param Boolean items: The items to search :return: True if the grid was converted """ items_found = {} y = 0 - for line in grid.splitlines(): + for y, line in enumerate(grid.splitlines()): for x in range(len(line)): if line[x] in items: if line[x] in items_found: items_found[line[x]].append(x - y * 1j) else: items_found[line[x]] = [x - y * 1j] - y += 1 return items_found @@ -171,18 +134,41 @@ def vertices_to_grid(self, mark_coords=[], wall="#"): return grid - def add_traps(self, vertex): + def add_traps(self, vertices): """ Creates traps: places that can be reached, but not exited - :param Any vertex: The vertex to consider + :param Any vertex: The vertices to consider :return: True if successful, False if no vertex found """ - if vertex in self.edges: - del self.edges[vertex] - return True - else: - return False + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + changed = True + + return changed + + def add_walls(self, vertices): + """ + Adds walls - useful for modification of map + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + del self.vertices[vertex] + changed = True + + self.edges = { + source: [target for target in self.edges[source] if target not in vertices] + for source in self.edges + } + + return changed def depth_first_search(self, start, end=None): """ From 085b375c30268fb1cdc9012157aa405689b5e67b Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:26:16 +0200 Subject: [PATCH 033/143] Racetrack: moved some elements to complex utils library --- 2018/racetrack.py | 50 ----------------------------------------------- 1 file changed, 50 deletions(-) diff --git a/2018/racetrack.py b/2018/racetrack.py index 75f82fb..c89bbf6 100644 --- a/2018/racetrack.py +++ b/2018/racetrack.py @@ -1,60 +1,10 @@ from math import sqrt -# Cardinal directions -north = 1j -south = -1j -west = -1 -east = 1 -directions_all = [north, south, west, east] -directions_horizontal = [west, east] -directions_vertical = [north, south] - -# To be multiplied by the current cartinal direction -relative_directions = { - "left": 1j, - "right": -1j, - "ahead": 1, - "back": -1, -} - class PlayerBlocked(Exception): pass -def min_real(complexes): - real_values = [x.real for x in complexes] - return min(real_values) - - -def min_imag(complexes): - real_values = [x.imag for x in complexes] - return min(real_values) - - -def max_real(complexes): - real_values = [x.real for x in complexes] - return max(real_values) - - -def max_imag(complexes): - real_values = [x.imag for x in complexes] - return max(real_values) - - -def complex_sort(complexes, mode=""): - # Sorts by real, then by imaginary component (x then y) - if mode == "xy": - complexes.sort(key=lambda a: (a.real, a.imag)) - # Sorts by imaginary, then by real component (y then x) - elif mode == "yx": - complexes.sort(key=lambda a: (a.imag, a.real)) - # Sorts by distance from 0,0 (kind of polar coordinates) - else: - complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) - return complexes - - def collisions(players): positions = [x.position for x in players] if positions == set(positions): From 2d328390e97b8c5be745c28216aa71f1a890980b Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 22 Jun 2020 21:26:36 +0200 Subject: [PATCH 034/143] Added a complex_utils lib for complex number manipulations --- 2018/complex_utils.py | 67 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 2018/complex_utils.py diff --git a/2018/complex_utils.py b/2018/complex_utils.py new file mode 100644 index 0000000..183e56c --- /dev/null +++ b/2018/complex_utils.py @@ -0,0 +1,67 @@ +""" +Small library for complex numbers +""" + + +# Cardinal directions +north = 1j +south = -1j +west = -1 +east = 1 +northeast = 1 + 1j +northwest = -1 + 1j +southeast = 1 - 1j +southwest = -1 - 1j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + +# To be multiplied by the current cartinal direction +relative_directions = { + "left": 1j, + "right": -1j, + "ahead": 1, + "back": -1, +} + + +def min_real(complexes): + real_values = [x.real for x in complexes] + return min(real_values) + + +def min_imag(complexes): + real_values = [x.imag for x in complexes] + return min(real_values) + + +def max_real(complexes): + real_values = [x.real for x in complexes] + return max(real_values) + + +def max_imag(complexes): + real_values = [x.imag for x in complexes] + return max(real_values) + + +def manhattan_distance(a, b): + return abs(b.imag - a.imag) + abs(b.real - a.real) + + +def complex_sort(complexes, mode=""): + # Sorts by real, then by imaginary component (x then y) + if mode == "xy": + complexes.sort(key=lambda a: (a.real, a.imag)) + # Sorts by imaginary, then by real component (y then x) + elif mode == "yx": + complexes.sort(key=lambda a: (a.imag, a.real)) + # Sorts by distance from 0,0 (kind of polar coordinates) + else: + complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) + return complexes From 0e1b91f2cd7160f6bb46db21bba25efe42df1c50 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:34:17 +0200 Subject: [PATCH 035/143] Added reading sort for complex utilities --- 2018/complex_utils.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/2018/complex_utils.py b/2018/complex_utils.py index 183e56c..decd16d 100644 --- a/2018/complex_utils.py +++ b/2018/complex_utils.py @@ -1,7 +1,7 @@ """ Small library for complex numbers """ - +from math import sqrt # Cardinal directions north = 1j @@ -61,6 +61,9 @@ def complex_sort(complexes, mode=""): # Sorts by imaginary, then by real component (y then x) elif mode == "yx": complexes.sort(key=lambda a: (a.imag, a.real)) + # Sorts by negative imaginary, then by real component (-y then x) - 'Reading" order + elif mode == "reading": + complexes.sort(key=lambda a: (-a.imag, a.real)) # Sorts by distance from 0,0 (kind of polar coordinates) else: complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) From ed10edcb344da9f6bfe3b9777ff122e9e88113c4 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:34:43 +0200 Subject: [PATCH 036/143] Fixes on pathfinding lib --- 2018/pathfinding.py | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/2018/pathfinding.py b/2018/pathfinding.py index a1dac44..12798e3 100644 --- a/2018/pathfinding.py +++ b/2018/pathfinding.py @@ -87,7 +87,7 @@ def grid_search(self, grid, items): :param string grid: The grid in which to search :param Boolean items: The items to search - :return: True if the grid was converted + :return: A dictionnary of the items found """ items_found = {} y = 0 @@ -102,13 +102,13 @@ def grid_search(self, grid, items): return items_found - def vertices_to_grid(self, mark_coords=[], wall="#"): + def vertices_to_grid(self, mark_coords={}, wall="#"): """ Converts a set of coordinates to a text The text will be separated by newline characters - :param list mark_coords: List of coordonates to mark + :param dict mark_coords: List of coordinates to mark, with letter to use :param string wall: Which character to use as walls :return: True if the grid was converted """ @@ -117,19 +117,21 @@ def vertices_to_grid(self, mark_coords=[], wall="#"): min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) - if isinstance(next(iter(self.vertices)), dict): - vertices = self.vertices.keys() - else: - vertices = self.vertices - for y in range(min_y, max_y - 1, -1): for x in range(min_x, max_x + 1): - if x + y * 1j in mark_coords: - grid += "X" - elif x + y * 1j in vertices: - grid += "." - else: - grid += wall + try: + grid += mark_coords[x + y * 1j] + except KeyError: + if x + y * 1j in mark_coords: + grid += "X" + else: + try: + grid += self.vertices.get(x + y * 1j, wall) + except AttributeError: + if x + y * 1j in self.vertices: + grid += "." + else: + grid += wall grid += "\n" return grid @@ -160,7 +162,7 @@ def add_walls(self, vertices): for vertex in vertices: if vertex in self.edges: del self.edges[vertex] - del self.vertices[vertex] + self.vertices.remove(vertex) changed = True self.edges = { From 8ae0a424b8c4867a04ae3c8aa46b7f006b81a7b0 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 24 Jun 2020 21:34:56 +0200 Subject: [PATCH 037/143] Added day 2018-15 --- 2018/15-Beverage Bandits.py | 322 ++++++++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) create mode 100644 2018/15-Beverage Bandits.py diff --git a/2018/15-Beverage Bandits.py b/2018/15-Beverage Bandits.py new file mode 100644 index 0000000..75fe434 --- /dev/null +++ b/2018/15-Beverage Bandits.py @@ -0,0 +1,322 @@ +# -------------------------------- Input data -------------------------------- # +import os, pathfinding, complex_utils, copy + +test_data = {} + +test = 1 +test_data[test] = { + "input": """####### +#G..#E# +#E#E.E# +#G.##.# +#...#E# +#...E.# +#######""", + "expected": ["36334 (37, 982, E)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#E..EG# +#.#G.E# +#E.##E# +#G..#.# +#..E#.# +#######""", + "expected": ["39514 (46 rounds, 859 HP, E)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#E.G#.# +#.#G..# +#G.#.G# +#G..#.# +#...E.# +#######""", + "expected": ["27755 (35 rounds, 793 HP, G)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#.G...# +#...EG# +#.#.#G# +#..G#E# +#.....# +#######""", + "expected": ["Unknown", "15 attack power, 4988 (29 rounds, 172 HP)"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["207542", "64688"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 4 +part_to_test = 2 +verbose_level = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Player class definition --------------------------- # + + +class Player: + position = 0 + type = "" + HP = 200 + graph = "" + alive = True + attack_power = 3 + + def __init__(self, type, position, attack_power=3): + self.position = position + self.type = type + if self.type == "E": + self.attack_power = attack_power + + def __lt__(self, other): + if self.position.imag < other.position.imag: + return True + else: + return self.position.real < other.position.real + + def move(self, graph, creatures): + """ + Searches for the closest ennemy + + :param Graph graph: The game map + :param list creatures: A list of creatures + :return: The target position + """ + + # Modify graph so that allies are walls, ennemies are traps + self.graph = copy.deepcopy(graph) + verbose = False + if False: + verbose = True + allies = [ + c.position + for c in creatures + if c.type == self.type and c != self and c.alive + ] + ennemies = [c.position for c in creatures if c.type != self.type and c.alive] + self.graph.add_traps(ennemies) + self.graph.add_walls(allies) + + # Run BFS from my position to determine closest target + self.graph.breadth_first_search(self.position) + + # Determine all target positions (= cells next to the ennemies), then choose closest + target_positions = [ + (self.graph.distance_from_start[e + dir], e + dir) + for e in ennemies + for dir in complex_utils.directions_straight + if e + dir in self.graph.distance_from_start + ] + if not target_positions: + return + + min_distance = min([pos[0] for pos in target_positions]) + closest_targets = [pos[1] for pos in target_positions if pos[0] == min_distance] + target = complex_utils.complex_sort(closest_targets, "reading")[0] + + if min_distance == 0: + return + + if verbose: + print("before", self.position, target_positions, closest_targets, target) + + # Then we do the opposite, to know in which direction to go + # Run BFS from the target + self.graph.breadth_first_search(target) + # Determine which direction to go to is best + next_positions = [ + (self.graph.distance_from_start[self.position + dir], self.position + dir,) + for dir in complex_utils.directions_straight + if self.position + dir in self.graph.vertices + ] + min_distance = min([pos[0] for pos in next_positions]) + closest_positions = [pos[1] for pos in next_positions if pos[0] == min_distance] + target = complex_utils.complex_sort(closest_positions, "reading")[0] + if verbose: + print( + "after", self.position, next_positions, closest_positions, target, self + ) + + self.position = target + + def attack(self, creatures): + """ + Attacks an ennemy in range + + :param Graph graph: The game map + :param list creatures: A list of creatures + :return: Nothing + """ + + # Find who to attack + ennemies = [ + c + for c in creatures + for dir in complex_utils.directions_straight + if self.position + dir == c.position and c.type != self.type and c.alive + ] + if not ennemies: + return + + min_HP_ennemies = player_sort( + [e for e in ennemies if e.HP == min([e.HP for e in ennemies])] + ) + ennemy = player_sort(min_HP_ennemies)[0] + + ennemy.lose_HP(self.attack_power) + + def lose_HP(self, HP): + """ + Loses HP following an attack + + :param int HP: How many HP to lose + :return: Nothing + """ + self.HP -= HP + self.alive = self.HP > 0 + + +def player_sort(players): + players.sort(key=lambda a: (-a.position.imag, a.position.real)) + return players + + +# -------------------------------- Actual code execution ----------------------------- # + + +if part_to_test == 1: + grid = puzzle_input + + # Initial grid with everything + graph = pathfinding.Graph() + graph.grid_to_vertices(grid) + + # Identify all creatures + creatures = graph.grid_search(grid, ("E", "G")) + + creatures = [ + Player(type, position) for type in creatures for position in creatures[type] + ] + factions = set(c.type for c in creatures) + + round = 0 + if verbose_level >= 2: + print("Start") + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print([(c.type, c.position, c.HP) for c in player_sort(creatures)]) + while True: + player_sort(creatures) + for i, creature in enumerate(creatures): + if not creature.alive: + continue + creature.move(graph, creatures) + creature.attack(creatures) + + creatures = [c for c in creatures if c.alive] + factions = set(c.type for c in creatures) + if len(factions) == 1: + break + + round += 1 + if verbose_level >= 3: + print("round", round) + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print([(c.type, c.position, c.HP, c.alive) for c in player_sort(creatures)]) + + if verbose_level >= 2: + print("End of combat") + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print([(c.type, c.position, c.HP) for c in player_sort(creatures)]) + print( + "Reached round:", + round, + "- Remaining HP:", + sum(c.HP for c in creatures), + "- Winner:", + factions, + ) + puzzle_actual_result = sum(c.HP for c in creatures if c.alive) * round + + +else: + grid = puzzle_input + + # Initial grid with everything + graph = pathfinding.Graph() + graph.grid_to_vertices(grid) + + # Identify all creatures + creatures_positions = graph.grid_search(grid, ("E", "G")) + + for attack in range(3, 100): + creatures = [ + Player(type, position, attack) + for type in creatures_positions + for position in creatures_positions[type] + ] + factions = set(c.type for c in creatures) + dead_elves = 0 + + round = 0 + while dead_elves == 0: + player_sort(creatures) + for i, creature in enumerate(creatures): + if not creature.alive: + continue + creature.move(graph, creatures) + creature.attack(creatures) + + dead_elves = len([c for c in creatures if c.type == "E" and not c.alive]) + creatures = [c for c in creatures if c.alive] + factions = set(c.type for c in creatures) + if len(factions) == 1: + break + + round += 1 + + if verbose_level >= 2: + print("End of combat with attack", attack) + if verbose_level >= 3: + print(graph.vertices_to_grid({c.position: c.type for c in creatures})) + print( + "Reached round:", + round, + "- Remaining HP:", + sum(c.HP for c in creatures), + "- Winner:", + factions, + ) + print("Dead elves:", dead_elves) + + if factions == set("E",): + puzzle_actual_result = sum(c.HP for c in creatures if c.alive) * round + break + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 7b91f98f8f3a97c005abf533e25854bd67ab1d64 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 7 Jul 2020 20:42:58 +0200 Subject: [PATCH 038/143] Added day 2018-16 --- 2018/16-Chronal Classification.py | 207 ++++++++++++++++++++++++++++++ 1 file changed, 207 insertions(+) create mode 100644 2018/16-Chronal Classification.py diff --git a/2018/16-Chronal Classification.py b/2018/16-Chronal Classification.py new file mode 100644 index 0000000..e675501 --- /dev/null +++ b/2018/16-Chronal Classification.py @@ -0,0 +1,207 @@ +# -------------------------------- Input data ---------------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Before: [3, 2, 1, 1] +9 2 1 2 +After: [3, 2, 2, 1]""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["Unknown", "Unknown"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +registers = [0] * 4 + +i = 0 +file_contents = puzzle_input.splitlines() +nb_lines = len(file_contents) + +more_than_3_opcodes = 0 +opcodes_mapping = {i: [] for i in range(16)} +while i < nb_lines: + if file_contents[i] == "": + i += 1 + continue + elif file_contents[i][:4] != "Befo": + i += 1 + continue + + test_program = file_contents[i + 6 :] + + before, operation, after = file_contents[i : i + 3] + + numeric_opcode = int(operation.split(" ")[0]) + a, b, c = map(int, operation.split(" ")[1:]) + + init = before[9:-1].split(", ") + init = [int(x) for x in init] + + final = after[9:-1].split(", ") + final = [int(x) for x in final] + + matching_opcodes = [] + + for opcode in [ + "addr", + "addi", + "mulr", + "muli", + "banr", + "bani", + "borr", + "bori", + "setr", + "seti", + "gtir", + "gtri", + "gtrr", + "eqir", + "eqri", + "eqrr", + ]: + registers = init.copy() + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + if registers == final: + opcodes_mapping[numeric_opcode].append(opcode) + matching_opcodes.append(opcode) + + if len(matching_opcodes) >= 3: + more_than_3_opcodes += 1 + + i += 3 + +if part_to_test == 1: + puzzle_actual_result = more_than_3_opcodes + +else: + opcodes_mapping = {i: set(opcodes_mapping[i]) for i in opcodes_mapping} + + final_mapping = [0] * 16 + + while 0 in final_mapping: + new_match = [i for i in opcodes_mapping if len(opcodes_mapping[i]) == 1] + numeric, alpha = new_match[0], opcodes_mapping[new_match[0]].pop() + + final_mapping[numeric] = alpha + + for i in opcodes_mapping: + if alpha in opcodes_mapping[i]: + opcodes_mapping[i].remove(alpha) + + registers = [0] * 4 + for operation in test_program: + opcode = final_mapping[int(operation.split(" ")[0])] + a, b, c = map(int, operation.split(" ")[1:]) + + print(operation, opcode, a, b, c) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + puzzle_actual_result = registers[0] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 804e989f95b427c8609d24659c41c8fbedcbe197 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 19 Jul 2020 18:47:18 +0200 Subject: [PATCH 039/143] Added days 2018-17, 2018-18 and 2018-19 --- 2018/17-Reservoir Research.py | 199 ++++++++++++++++++++++++++ 2018/18-Settlers of The North Pole.py | 169 ++++++++++++++++++++++ 2018/19-Go With The Flow.py | 184 ++++++++++++++++++++++++ 3 files changed, 552 insertions(+) create mode 100644 2018/17-Reservoir Research.py create mode 100644 2018/18-Settlers of The North Pole.py create mode 100644 2018/19-Go With The Flow.py diff --git a/2018/17-Reservoir Research.py b/2018/17-Reservoir Research.py new file mode 100644 index 0000000..a4750c2 --- /dev/null +++ b/2018/17-Reservoir Research.py @@ -0,0 +1,199 @@ +# -------------------------------- Input data ---------------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """x=495, y=2..7 +y=7, x=495..501 +x=501, y=3..7 +x=498, y=2..4 +x=506, y=1..2 +x=498, y=10..13 +x=504, y=10..13 +y=13, x=498..504""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """x=496, y=2..8 +x=499, y=3..5 +x=501, y=3..5 +y=5, x=499..501 +x=505, y=2..8""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """x=491, y=2..8 +x=497, y=4..8 +x=504, y=3..8 +y=8, x=497..504 +x=508, y=2..8""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["39877", "33291"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +walls = [] +min_y, max_y = 0, -(10 ** 6) +min_x, max_x = 500, 500 + +for string in puzzle_input.split("\n"): + a, b = string.split(", ") + dim1, val1 = a.split("=") + val1 = int(val1) + + dim2, val2 = b.split("=") + val2 = val2.split("..") + val2, val3 = int(val2[0]), int(val2[1]) + + if dim1 == "x": + min_y = min(min_y, -val3) + max_y = max(max_y, -val2) + min_x = min(min_x, val1) + max_x = max(max_x, val1) + else: + min_y = min(min_y, -val1) + max_y = max(max_y, -val1) + min_x = min(min_x, val2) + max_x = max(max_x, val3) + + for spot in range(val2, val3 + 1): + if dim1 == "x": + dot = val1 - spot * 1j + else: + dot = spot - val1 * 1j + walls.append(dot) + +walls = set(walls) + +current_position = 500 +wet_positions = set() +pools = set() +flowing = [current_position] +settled = set() + +i = 0 +while flowing: + current_position = flowing.pop() + # print ('--------------') + # print ('now', current_position, current_position - 1j not in walls, current_position - 1j not in pools) + # print ('pools', pools) + + if current_position.imag <= min_y: + settled.add(current_position) + position = current_position + 1j + + while position in flowing: + settled.add(position) + flowing.remove(position) + position += 1j + continue + + if current_position - 1j in settled: + settled.add(current_position) + continue + if current_position - 1j not in walls and current_position - 1j not in pools: + flowing.append(current_position) + flowing.append(current_position - 1j) + current_position -= 1j + if current_position.imag >= min_y and current_position.imag <= max_y: + wet_positions.add(current_position) + else: + + pooling = True + settling = False + pool = set([current_position]) + # fill horizontally + + for direction in [-1, 1]: + position = current_position + while True: + # Extend to the right + position += direction + if position in walls: + break + elif position in settled: + settling = True + break + else: + wet_positions.add(position) + pool.add(position) + if position - 1j not in walls and position - 1j not in pools: + pooling = False + flowing.append(position) + break + + if settling: + settled = settled.union(pool) + elif pooling: + pools = pools.union(pool) + + # print ('pools', pools) + # print ('flowing', flowing) + + # This limit is totally arbitrary + if i == 10 ** 4: + print("stop") + break + i += 1 + +print("step", i) +for y in range(max_y + 1, min_y - 1, -1): + for x in range(min_x - 2, max_x + 3): + if x + y * 1j in pools: + print("~", end="") + elif x + y * 1j in settled: + print("S", end="") + elif x + y * 1j in flowing: + print("F", end="") + elif x + y * 1j in pools: + print("~", end="") + elif x + y * 1j in wet_positions: + print("|", end="") + elif x + y * 1j in walls: + print("#", end="") + else: + print(".", end="") + print("") + + +if part_to_test == 1: + puzzle_actual_result = len(wet_positions) +else: + puzzle_actual_result = len(pools) +# 33556 too high + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/18-Settlers of The North Pole.py b/2018/18-Settlers of The North Pole.py new file mode 100644 index 0000000..bd101d7 --- /dev/null +++ b/2018/18-Settlers of The North Pole.py @@ -0,0 +1,169 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, drawing +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#.#...|#. +.....#|##| +.|..|...#. +..|#.....# +#.#|||#|#| +...#.||... +.|....|... +||...#|.#| +|.||||..|. +...#.|..|.""", + "expected": ["1147", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["483840", "219919"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def text_to_grid(text): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + Each character will have its coordinates as keys + + :param string text: The text to convert + :return: The converted grid, its height and width + """ + grid = {} + lines = text.splitlines() + height = len(lines) + width = 0 + for y in range(len(lines)): + width = max(width, len(lines[y])) + for x in range(len(lines[y])): + grid[x - 1j * y] = lines[y][x] + + return grid + + +def grid_to_text(grid, blank_character=" "): + """ + Converts the grid to a text format + + :param dict grid: The grid to convert, in format (x, y): value + :param string blank_character: What to use for cells with unknown value + :return: The grid in text format + """ + + text = "" + + min_y, max_y = int(max_imag(grid.keys())), int(min_imag(grid.keys())) + min_x, max_x = int(min_real(grid.keys())), int(max_real(grid.keys())) + + for y in range(min_y, max_y + 1, -1): + for x in range(min_x, max_x + 1): + if x + 1j * y in grid: + text += str(grid[x + 1j * y]) + else: + text += blank_character + text += os.linesep + text = text[: -len(os.linesep)] + + return text + + +if part_to_test == 1: + end = 10 +else: + end = 1000000000 + + +graph = text_to_grid(puzzle_input) + +if verbose_level == 3: + print("Initial state") + print(grid_to_text(graph)) + +i = 1 +scores = [] +while i <= end: + new_graph = graph.copy() + + for space in graph: + neighbors = [ + graph[space + direction] + for direction in directions_diagonals + if space + direction in graph + ] + if graph[space] == ".": + if len([x for x in neighbors if x == "|"]) >= 3: + new_graph[space] = "|" + elif graph[space] == "|": + if len([x for x in neighbors if x == "#"]) >= 3: + new_graph[space] = "#" + elif graph[space] == "#": + if ( + len([x for x in neighbors if x == "#"]) >= 1 + and len([x for x in neighbors if x == "|"]) >= 1 + ): + new_graph[space] = "#" + else: + new_graph[space] = "." + + graph = new_graph.copy() + if verbose_level == 3: + print("step", i) + print(grid_to_text(new_graph)) + + score = len([1 for x in graph if graph[x] == "#"]) * len( + [1 for x in graph if graph[x] == "|"] + ) + if i > 800 and i < 10 ** 8 and score in scores: + repeats_every = i - scores.index(score) - 1 - 800 + i += (end - i) // repeats_every * repeats_every + print( + "repeats_every", + repeats_every, + "score", + score, + "index", + scores.index(score), + i, + ) + + if i > 800: + scores.append(score) + print(i, score) + + i += 1 + +puzzle_actual_result = len([1 for x in graph if graph[x] == "#"]) * len( + [1 for x in graph if graph[x] == "|"] +) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/19-Go With The Flow.py b/2018/19-Go With The Flow.py new file mode 100644 index 0000000..9f95c0b --- /dev/null +++ b/2018/19-Go With The Flow.py @@ -0,0 +1,184 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, math + +test_data = {} + +test = 1 +test_data[test] = { + "input": """#ip 0 +seti 5 0 1 +seti 6 0 2 +addi 0 1 0 +addr 1 2 3 +setr 1 0 0 +seti 8 0 4 +seti 9 0 5""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["2240", "26671554"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +registers = [0] * 6 + +init = puzzle_input.splitlines()[0] +program = puzzle_input.splitlines()[1:] +nb_lines = len(program) + +pointer = int(init[4:]) + + +if part_to_test == 1: + i = 0 + while registers[pointer] < nb_lines: + + operation = program[registers[pointer]] + # print (pointer, operation, registers) + + opcode = operation.split(" ")[0] + a, b, c = map(int, operation.split(" ")[1:]) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + # print (operation, registers) + registers[pointer] += 1 + + print(i, pointer, operation, registers) + + if i == 150: + break + if i % 10000 == 0: + print(i, pointer, operation, registers) + # if registers[2] < registers[3]-1250: + # registers[2] += 1250 * ((registers[3] - registers[2]) // 1250-1) + # print (i, pointer, operation, registers, 'after') + + i += 1 + + puzzle_actual_result = registers[0] + +else: + + def get_divisors(value): + small_divisors = [ + d for d in range(1, int(math.sqrt(value)) + 1) if value % d == 0 + ] + big_divisors = [value // d for d in small_divisors if not d ** 2 == value] + return set(small_divisors + big_divisors) + + registers[0] = 1 + for i in range(0, 200): + + operation = program[registers[pointer]] + print(i, pointer, operation, registers) + + opcode = operation.split(" ")[0] + a, b, c = map(int, operation.split(" ")[1:]) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + registers[pointer] += 1 + + number = registers[3] + + puzzle_actual_result = sum(get_divisors(number)) +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From ea5df0ae4bd83e915447216d118f2c29ce6177fc Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 19 Jul 2020 18:48:20 +0200 Subject: [PATCH 040/143] Cleanup on pathfinding --- 2018/pathfinding.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/2018/pathfinding.py b/2018/pathfinding.py index 12798e3..2cad5b8 100644 --- a/2018/pathfinding.py +++ b/2018/pathfinding.py @@ -449,8 +449,6 @@ def grid_to_vertices( return True - return True - def dijkstra(self, start, end=None): """ Applies the Dijkstra algorithm to a given search From 57bcc01c51a0fca46d10f0e680deb92f3e0aedeb Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 24 Jul 2020 22:02:51 +0200 Subject: [PATCH 041/143] Added day 2018-20 --- 2018/20-A Regular Map.py | 124 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 2018/20-A Regular Map.py diff --git a/2018/20-A Regular Map.py b/2018/20-A Regular Map.py new file mode 100644 index 0000000..b875a88 --- /dev/null +++ b/2018/20-A Regular Map.py @@ -0,0 +1,124 @@ +# -------------------------------- Input data ---------------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """^WNE$""", + "expected": ["3", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^W(N|W)E$""", + "expected": ["3", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^ENWWW(NEEE|SSE(EE|N))$""", + "expected": ["10", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^ENNWSWW(NEWS|)SSSEEN(WNSE|)EE(SWEN|)NNN$""", + "expected": ["18", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^ESSWWN(E|NNENN(EESS(WNSE|)SSS|WWWSSSSE(SW|NNNE)))$""", + "expected": ["23", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """^WSSEESWWWNW(S|NENNEEEENN(ESSSSW(NWSW|SSEN)|WSWWN(E|WWS(E|SS))))$""", + "expected": ["31", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3207", "8361"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +forks = [{0: 0}] + +positions = {0: 0} +dir = {"N": -1j, "S": 1j, "E": 1, "W": -1} +movement = 0 +length = 0 + +all_positions = tuple() +positions_below_1000 = tuple() +for letter in puzzle_input[1:-1]: + if letter in "NSEW": + # Move ! + positions = {pos + dir[letter]: positions[pos] + 1 for pos in positions} + positions_below_1000 += tuple(x for x in positions if positions[x] < 1000) + all_positions += tuple(x for x in positions) + elif letter == "(": + # Put current positions in the queue (= start of fork) + forks.append(positions) + # Initiate the "last fork targets" that'll get updated later + forks.append({}) + elif letter == "|": + # Update the "last fork targets" (forks[-1]), then reset to forks[-2] + forks[-1] = { + pos: min(forks[-1][pos], positions.get(pos, 10 ** 6)) for pos in forks[-1] + } + forks[-1].update( + {pos: positions[pos] for pos in positions if pos not in forks[-1]} + ) + positions = forks[-2] + elif letter == ")": + # Merge the current positions, the last fork targets (forks[-1]) and the positions before forking (forks[-2]) + positions.update( + {pos: min(forks[-1][pos], positions.get(pos, 10 ** 6)) for pos in forks[-1]} + ) + positions.update( + {pos: min(forks[-2][pos], positions.get(pos, 10 ** 6)) for pos in forks[-2]} + ) + # Then go back to before the forking + forks.pop() + forks.pop() + +# Merge all forks with the most recent positions +for fork in forks: + positions.update({pos: min(fork[pos], positions.get(pos, 10 ** 6)) for pos in fork}) + + +if part_to_test == 1: + puzzle_actual_result = max(positions.values()) + +else: + puzzle_actual_result = len(set(all_positions)) - len(set(positions_below_1000)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From f6229161eda6771849db489f5ae078b4df233587 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 25 Jul 2020 22:01:32 +0200 Subject: [PATCH 042/143] Added day 2018-21 --- 2018/21-Chronal Conversion.py | 125 ++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 2018/21-Chronal Conversion.py diff --git a/2018/21-Chronal Conversion.py b/2018/21-Chronal Conversion.py new file mode 100644 index 0000000..8a7b17d --- /dev/null +++ b/2018/21-Chronal Conversion.py @@ -0,0 +1,125 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, math + +test_data = {} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["15615244", "12963935"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +registers = [0] * 6 + +init = puzzle_input.splitlines()[0] +program = puzzle_input.splitlines()[1:] +nb_lines = len(program) + +pointer = int(init[4:]) + + +i = 0 + +if part_to_test == 1: + while registers[pointer] < nb_lines: + + operation = program[registers[pointer]] + + opcode = operation.split(" ")[0] + a, b, c = map(int, operation.split(" ")[1:]) + + if opcode == "addr": + registers[c] = registers[a] + registers[b] + elif opcode == "addi": + registers[c] = registers[a] + b + + elif opcode == "mulr": + registers[c] = registers[a] * registers[b] + elif opcode == "muli": + registers[c] = registers[a] * b + + elif opcode == "banr": + registers[c] = registers[a] & registers[b] + elif opcode == "bani": + registers[c] = registers[a] & b + + elif opcode == "borr": + registers[c] = registers[a] | registers[b] + elif opcode == "bori": + registers[c] = registers[a] | b + + elif opcode == "setr": + registers[c] = registers[a] + elif opcode == "seti": + registers[c] = a + + elif opcode == "gtir": + registers[c] = 1 if a > registers[b] else 0 + elif opcode == "gtri": + registers[c] = 1 if registers[a] > b else 0 + elif opcode == "gtrr": + registers[c] = 1 if registers[a] > registers[b] else 0 + + elif opcode == "eqir": + registers[c] = 1 if a == registers[b] else 0 + elif opcode == "eqri": + registers[c] = 1 if registers[a] == b else 0 + elif opcode == "eqrr": + registers[c] = 1 if registers[a] == registers[b] else 0 + + # The program stops if r0 = r5 on line 28 + if registers[pointer] == 28: + puzzle_actual_result = registers[5] + break + + registers[pointer] += 1 + + +else: + r5 = 0 + r4 = r5 | 65536 + r5 = 15466939 + r5 = (((r5 + (r4 & 255)) & 16777215) * 65899) & 16777215 + + list_values = [] + compared = [] + i = 0 + while (r4 if r4 > 256 else False, r5) not in list_values: + list_values.append((r4 if r4 > 256 else False, r5)) + if r4 < 256: + compared.append(r5) + r4 = r5 | 65536 + r5 = 15466939 + else: + r4 = r4 // 256 + r5 = (((r5 + (r4 & 255)) & 16777215) * 65899) & 16777215 + + i += 1 + if i == 10 ** 6: + break + + puzzle_actual_result = compared[-1] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From c21d7eb425d8827a0b1d6398c255530ce4747617 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 26 Jul 2020 12:36:52 +0200 Subject: [PATCH 043/143] Added day 2018-22 --- 2018/22-Mode Maze.py | 137 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 2018/22-Mode Maze.py diff --git a/2018/22-Mode Maze.py b/2018/22-Mode Maze.py new file mode 100644 index 0000000..7f1df10 --- /dev/null +++ b/2018/22-Mode Maze.py @@ -0,0 +1,137 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """depth: 510 +target: 10,10""", + "expected": ["114", "45"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6256", "Unknown"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +_, depth = puzzle_input.splitlines()[0].split(" ") +_, target = puzzle_input.splitlines()[1].split(" ") + +depth = int(depth) +max_x, max_y = map(int, target.split(",")) +target = max_x - 1j * max_y + +geological = {0: 0} +erosion = {0: 0} +for x in range(max_x + 1): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 +for y in range(max_y + 1): + geological[-1j * y] = y * 48271 + erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + +for x in range(1, max_x + 1): + for y in range(1, max_y + 1): + geological[x - 1j * y] = ( + erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + ) % 20183 + erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + +geological[target] = 0 +erosion[target] = 0 + +terrain = {x: erosion[x] % 3 for x in erosion} + +if part_to_test == 1: + puzzle_actual_result = sum(terrain.values()) + +else: + neither, climbing, torch = 0, 1, 2 + rocky, wet, narrow = 0, 1, 2 + + # Override the neighbors function + def neighbors(self, vertex): + north = (0, 1) + south = (0, -1) + west = (-1, 0) + east = (1, 0) + directions_straight = [north, south, west, east] + + neighbors = {} + for dir in directions_straight: + target = (vertex[0] + dir[0], vertex[1] + dir[1], vertex[2]) + if target in self.vertices: + neighbors[target] = 1 + for tool in (neither, climbing, torch): + target = (vertex[0], vertex[1], tool) + if target in self.vertices and tool != vertex[1]: + neighbors[target] = 7 + + return neighbors + + # Add some coordinates around the target + padding = 10 if case_to_test == 1 else 50 + for x in range(max_x, max_x + padding): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 + for y in range(max_y, max_y + padding): + geological[-1j * y] = y * 48271 + erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + for x in range(1, max_x + padding): + for y in range(1, max_y + padding): + if x - 1j * y in geological: + continue + geological[x - 1j * y] = ( + erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + ) % 20183 + erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + + terrain = {x: erosion[x] % 3 for x in erosion} + del erosion + del geological + + # Then run pathfinding algo + pathfinding.WeightedGraph.neighbors = neighbors + vertices = [ + (x.real, x.imag, neither) for x in terrain if terrain[x] in (wet, narrow) + ] + vertices += [ + (x.real, x.imag, climbing) for x in terrain if terrain[x] in (rocky, wet) + ] + vertices += [ + (x.real, x.imag, torch) for x in terrain if terrain[x] in (rocky, narrow) + ] + graph = pathfinding.WeightedGraph(vertices) + + graph.dijkstra((0, 0, torch), (max_x, -max_y, torch)) + + puzzle_actual_result = graph.distance_from_start[(max_x, -max_y, torch)] + +# 979 is too high + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From eff324ab893c77822e4da7d3e5f7371925f35ff8 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 26 Jul 2020 20:39:00 +0200 Subject: [PATCH 044/143] Added day 2018-23 --- ...23-Experimental Emergency Teleportation.py | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 2018/23-Experimental Emergency Teleportation.py diff --git a/2018/23-Experimental Emergency Teleportation.py b/2018/23-Experimental Emergency Teleportation.py new file mode 100644 index 0000000..5796a74 --- /dev/null +++ b/2018/23-Experimental Emergency Teleportation.py @@ -0,0 +1,221 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, heapq + +test_data = {} + +test = 1 +test_data[test] = { + "input": """pos=<0,0,0>, r=4 +pos=<1,0,0>, r=1 +pos=<4,0,0>, r=3 +pos=<0,2,0>, r=1 +pos=<0,5,0>, r=3 +pos=<0,0,3>, r=1 +pos=<1,1,1>, r=1 +pos=<1,1,2>, r=1 +pos=<1,3,1>, r=1""", + "expected": ["7", "Unknown"], +} +test += 1 +test_data[test] = { + "input": """pos=<10,12,12>, r=2 +pos=<12,14,12>, r=2 +pos=<16,12,12>, r=4 +pos=<14,14,14>, r=6 +pos=<50,50,50>, r=200 +pos=<10,10,10>, r=5""", + "expected": ["Unknown", "Position 12, 12, 12 => 36"], +} +test += 1 +test_data[test] = { + "input": """pos=<20,0,0>, r=15 +pos=<0,0,0>, r=6""", + "expected": ["Unknown", "5"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["761", "89915526"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Various functions ----------------------------- # + + +def manhattan_distance(source, target): + dist = 0 + for i in range(len(source)): + dist += abs(target[i] - source[i]) + return dist + + +def in_range_cube(corners): + nb = 0 + for bot in bots: + xb, yb, zb = bot + radius = bots[bot] + + # bot is outside the cube extended by radius in a cubic manner + # said differently: bot is outside cube of size initial_size+radius*2 + if xb < corners[0][0] - radius or xb > corners[1][0] + radius: + continue + if yb < corners[0][1] - radius or yb > corners[1][1] + radius: + continue + if zb < corners[0][2] - radius or zb > corners[1][2] + radius: + continue + + # bot is inside the cube + if xb >= corners[0][0] and xb <= corners[1][0]: + if yb >= corners[0][1] and yb <= corners[1][1]: + if zb >= corners[0][2] and zb <= corners[1][2]: + nb += 1 + continue + + # bot is too far from the cube's center + cube_size = ( + corners[1][0] - corners[0][0] + 4 + ) # 4 added for margin of error & rounding + center = [(corners[0][i] + corners[1][i]) // 2 for i in (0, 1, 2)] + # The center is at cube_size // 2 * 3 distance from each corner + max_distance = cube_size // 2 * 3 + radius + if manhattan_distance(center, bot) <= max_distance: + nb += 1 + + return nb + + +def all_corners(cube): + coords = list(zip(*cube)) + corners = [[x, y, z] for x in coords[0] for y in coords[1] for z in coords[2]] + return corners + + +def in_range_spot(spot): + nb = 0 + for bot in bots: + if manhattan_distance(spot, bot) <= bots[bot]: + nb += 1 + + return nb + + +def add_each(a, b): + cpy = a.copy() + for i in range(len(cpy)): + cpy[i] += b[i] + return cpy + + +# -------------------------------- Actual code execution ----------------------------- # + + +bots = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + pos, rad = string.split(", ") + pos = tuple(map(int, pos[5:-1].split(","))) + bots[pos] = int(rad[2:]) + +max_strength = max(bots.values()) +max_strength_bots = [x for x in bots if bots[x] == max(bots.values())] + + +if part_to_test == 1: + in_range = {} + for bot in max_strength_bots: + in_range[bot] = 0 + for target in bots: + if manhattan_distance(bot, target) <= max_strength: + in_range[bot] += 1 + puzzle_actual_result = max(in_range.values()) + +else: + x, y, z = zip(*bots) + corners = [[min(x), min(y), min(z)], [max(x), max(y), max(z)]] + cube_size = max(max(x) - min(x), max(y) - min(y), max(z) - min(z)) + count_bots = in_range_cube(corners) + + cubes = [(-count_bots, cube_size, corners)] + heapq.heapify(cubes) + + all_cubes = [(count_bots, cube_size, corners)] + + # First, octree algorithm: the best candidates are split in 8 and analyzed + min_bots = 1 + best_dot = [10 ** 9, 10 ** 9, 10 ** 9] + while cubes: + nb, cube_size, cube = heapq.heappop(cubes) + + if -nb < min_bots: + # Not enough bots in range + continue + if -nb == min_bots: + if manhattan_distance((0, 0, 0), cube[0]) > sum(map(abs, best_dot)): + # Cube is too far away from source + continue + + # print (-nb, len(cubes), min_bots, cube_size, cube, best_dot, sum(map(abs, best_dot))) + + # Analyze all corners in all cases, it helps reduce the volume in the end + corners = all_corners(cube) + for dot in corners: + nb_spot = in_range_spot(dot) + if nb_spot > min_bots: + min_bots = nb_spot + best_dot = dot + print("Min bots updated to ", nb_spot, "for dot", dot) + elif nb_spot == min_bots: + if manhattan_distance((0, 0, 0), best_dot) > manhattan_distance( + (0, 0, 0), dot + ): + best_dot = dot + print("Best dot set to ", dot) + + if cube_size == 1: + # We can't divide it any further + continue + + cube_size = (cube_size // 2) if cube_size % 2 == 0 else (cube_size // 2 + 1) + + new_cubes = [ + [ + add_each(cube[0], [x, y, z]), + add_each(cube[0], [x + cube_size, y + cube_size, z + cube_size]), + ] + for x in (0, cube_size) + for y in (0, cube_size) + for z in (0, cube_size) + ] + + for new_cube in new_cubes: + count_bots = in_range_cube(new_cube) + if count_bots >= min_bots: + heapq.heappush(cubes, (-count_bots, cube_size, new_cube)) + all_cubes.append((count_bots, cube_size, new_cube)) + + print("max power", min_bots) + puzzle_actual_result = manhattan_distance((0, 0, 0), best_dot) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 53e3385d7d91ca1a2ff2e92affcfd1940dde8109 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 28 Jul 2020 22:21:11 +0200 Subject: [PATCH 045/143] Updated pathfinding to calculate groups --- 2018/pathfinding.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/2018/pathfinding.py b/2018/pathfinding.py index 2cad5b8..5a280cc 100644 --- a/2018/pathfinding.py +++ b/2018/pathfinding.py @@ -172,6 +172,25 @@ def add_walls(self, vertices): return changed + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = self.vertices.copy() + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited = [x for x in unvisited if x not in newly_visited] + groups.append(newly_visited) + + return groups + def depth_first_search(self, start, end=None): """ Performs a depth-first search based on a start node From 5c462d13a67a31bf638a70a5d15e1dd2fc3a1689 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 28 Jul 2020 22:21:31 +0200 Subject: [PATCH 046/143] Added day 2018-24 and 2018-25 --- 2018/24-Immune System Simulator 20XX.py | 242 ++++++++++++++++++++++++ 2018/25-Four-Dimensional Adventure.py | 104 ++++++++++ 2 files changed, 346 insertions(+) create mode 100644 2018/24-Immune System Simulator 20XX.py create mode 100644 2018/25-Four-Dimensional Adventure.py diff --git a/2018/24-Immune System Simulator 20XX.py b/2018/24-Immune System Simulator 20XX.py new file mode 100644 index 0000000..b80dee0 --- /dev/null +++ b/2018/24-Immune System Simulator 20XX.py @@ -0,0 +1,242 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, re + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Immune System: +17 units each with 5390 hit points (weak to radiation, bludgeoning) with an attack that does 4507 fire damage at initiative 2 +989 units each with 1274 hit points (immune to fire; weak to bludgeoning, slashing) with an attack that does 25 slashing damage at initiative 3 + +Infection: +801 units each with 4706 hit points (weak to radiation) with an attack that does 116 bludgeoning damage at initiative 1 +4485 units each with 2961 hit points (immune to radiation; weak to fire, cold) with an attack that does 12 slashing damage at initiative 4""", + "expected": ["5216", "Unknown"], +} + + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["22676", "4510"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose = False + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def choose_target(opponents, unit, ignore_targets): + targets = [] + for opponent in opponents: + # Same team + if opponent[-2] == unit[-2]: + continue + # target is already targetted + if opponent[-2:] in ignore_targets: + continue + + # Determine multipliers + if unit[3] in opponent[5]: + multiplier = 0 + elif unit[3] in opponent[6]: + multiplier = 2 + else: + multiplier = 1 + + # Order: damage, effective power, initiative + target = ( + unit[0] * unit[2] * multiplier, + opponent[0] * opponent[2], + opponent[4], + opponent, + ) + targets.append(target) + + targets.sort(reverse=True) + + if len(targets) > 0: + return targets[0] + + +def determine_damage(attacker, defender): + # Determine multipliers + if attacker[3] in defender[5]: + multiplier = 0 + elif attacker[3] in defender[6]: + multiplier = 2 + else: + multiplier = 1 + + return attacker[0] * attacker[2] * multiplier + + +def attack_order(units): + # Decreasing order of initiative + units.sort(key=lambda unit: unit[4], reverse=True) + return units + + +def target_selection_order(units): + # Decreasing order of effective power then initiative + units.sort(key=lambda unit: (unit[0] * unit[2], unit[4]), reverse=True) + return units + + +def teams(units): + teams = set([unit[-2] for unit in units]) + return teams + + +def team_size(units): + teams = { + team: len([unit for unit in units if unit[-2] == team]) + for team in ("Immune System:", "Infection:") + } + return teams + + +regex = "([0-9]*) units each with ([0-9]*) hit points (?:\((immune|weak) to ([a-z]*)(?:, ([a-z]*))*(?:; (immune|weak) to ([a-z]*)(?:, ([a-z]*))*)?\))? ?with an attack that does ([0-9]*) ([a-z]*) damage at initiative ([0-9]*)" +units = [] +for string in puzzle_input.split("\n"): + if string == "": + continue + + if string == "Immune System:" or string == "Infection:": + team = string + continue + + matches = re.match(regex, string) + if matches is None: + print(string) + items = matches.groups() + + # nb_units, hitpoints, damage, damage type, initative, immune, weak, team, number + unit = [ + int(items[0]), + int(items[1]), + int(items[-3]), + items[-2], + int(items[-1]), + [], + [], + team, + team_size(units)[team] + 1, + ] + for item in items[2:-3]: + if item is None: + continue + if item in ("immune", "weak"): + attack_type = item + else: + if attack_type == "immune": + unit[-4].append(item) + else: + unit[-3].append(item) + + units.append(unit) + + +boost = 0 +min_boost = 0 +max_boost = 10 ** 9 +winner = "Infection:" +base_units = [unit.copy() for unit in units] +while True: + if part_to_test == 2: + # Update boost for part 2 + if winner == "Infection:" or winner == "None": + min_boost = boost + if max_boost == 10 ** 9: + boost += 20 + else: + boost = (min_boost + max_boost) // 2 + else: + max_boost = boost + boost = (min_boost + max_boost) // 2 + if min_boost == max_boost - 1: + break + + units = [unit.copy() for unit in base_units] + for uid in range(len(units)): + if units[uid][-2] == "Immune System:": + units[uid][2] += boost + print("Applying boost", boost) + + while len(teams(units)) > 1: + units_killed = 0 + if verbose: + print() + print("New Round") + print([(x[-2:], x[0], "units") for x in units]) + order = target_selection_order(units) + targets = {} + for unit in order: + target = choose_target(units, unit, [x[3][-2:] for x in targets.values()]) + if target: + if target[0] != 0: + targets[unit[-2] + str(unit[-1])] = target + + order = attack_order(units) + for unit in order: + if unit[-2] + str(unit[-1]) not in targets: + continue + target = targets[unit[-2] + str(unit[-1])] + position = units.index(target[3]) + damage = determine_damage(unit, target[3]) + kills = determine_damage(unit, target[3]) // target[3][1] + units_killed += kills + target[3][0] -= kills + if target[3][0] > 0: + units[position] = target[3] + else: + del units[position] + + if verbose: + print( + unit[-2:], + "attacked", + target[3][-2:], + "dealt", + damage, + "damage and killed", + kills, + ) + + if units_killed == 0: + break + + puzzle_actual_result = sum([x[0] for x in units]) + if part_to_test == 1: + break + else: + if units_killed == 0: + winner = "None" + else: + winner = units[0][-2] + print("Boost", boost, " - Winner:", winner) + if verbose: + print([unit[0] for unit in units]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/25-Four-Dimensional Adventure.py b/2018/25-Four-Dimensional Adventure.py new file mode 100644 index 0000000..843b659 --- /dev/null +++ b/2018/25-Four-Dimensional Adventure.py @@ -0,0 +1,104 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0,0,0,0 +3,0,0,0 +0,3,0,0 +0,0,3,0 +0,0,0,3 +0,0,0,6 +9,0,0,0 +12,0,0,0""", + "expected": ["2", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """-1,2,2,0 +0,0,2,-2 +0,0,0,-2 +-1,2,0,0 +-2,-2,-2,2 +3,0,2,-1 +-1,3,2,2 +-1,0,-1,0 +0,2,1,-2 +3,0,0,0""", + "expected": ["4", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["420", "Unknown"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def manhattan_distance(source, target): + dist = 0 + for i in range(len(source)): + dist += abs(target[i] - source[i]) + return dist + + +if part_to_test == 1: + + distances = {} + stars = [] + for string in puzzle_input.split("\n"): + if string == "": + continue + stars.append(tuple(map(int, string.split(",")))) + + graph = pathfinding.Graph(list(range(len(stars)))) + + merges = [] + for star_id in range(len(stars)): + for star2_id in range(len(stars)): + if star_id == star2_id: + continue + if manhattan_distance(stars[star_id], stars[star2_id]) <= 3: + if star_id in graph.edges: + graph.edges[star_id].append(star2_id) + else: + graph.edges[star_id] = [star2_id] + + groups = graph.dfs_groups() + + print(groups) + puzzle_actual_result = len(groups) + + +else: + for string in puzzle_input.split("\n"): + if string == "": + continue + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 89a79470e0c652c1412e6a2a6d0a19d3829f769b Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 29 Jul 2020 20:54:00 +0200 Subject: [PATCH 047/143] Cleanup of prints & useless racetrack module --- 2018/11-Chronal Charge.py | 96 ++++--- 2018/12-Subterranean Sustainability.py | 76 +++--- 2018/13-Mine Cart Madness.py | 83 +++--- 2018/15-Beverage Bandits.py | 4 +- 2018/16-Chronal Classification.py | 4 +- 2018/17-Reservoir Research.py | 36 +-- 2018/18-Settlers of The North Pole.py | 20 +- 2018/19-Go With The Flow.py | 10 +- ...23-Experimental Emergency Teleportation.py | 6 +- 2018/24-Immune System Simulator 20XX.py | 6 +- 2018/25-Four-Dimensional Adventure.py | 2 +- 2018/racetrack.py | 238 ------------------ 12 files changed, 189 insertions(+), 392 deletions(-) delete mode 100644 2018/racetrack.py diff --git a/2018/11-Chronal Charge.py b/2018/11-Chronal Charge.py index ade9209..b88ce32 100644 --- a/2018/11-Chronal Charge.py +++ b/2018/11-Chronal Charge.py @@ -4,68 +4,100 @@ test_data = {} test = 1 -test_data[test] = {"input": 18, - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": 7165, - "expected": ['(235, 20) with 31', '(237, 223, 14) with 83'], - } +test_data[test] = { + "input": 18, + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": 7165, + "expected": ["(235, 20) with 31", "(237, 223, 14) with 83"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 2 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # if part_to_test == 1: - grid_power = {(x, y): int(((((10+x)*y + puzzle_input) * (10+x)) // 100) % 10)-5 for x in range (1, 301) for y in range (1, 301)} - - sum_power = {(x, y): sum(grid_power[x1, y1] for x1 in range (x, x+3) for y1 in range (y, y+3)) for x in range (1, 299) for y in range (1, 299)} + grid_power = { + (x, y): int(((((10 + x) * y + puzzle_input) * (10 + x)) // 100) % 10) - 5 + for x in range(1, 301) + for y in range(1, 301) + } + + sum_power = { + (x, y): sum( + grid_power[x1, y1] for x1 in range(x, x + 3) for y1 in range(y, y + 3) + ) + for x in range(1, 299) + for y in range(1, 299) + } max_power = max(sum_power.values()) - puzzle_actual_result = list(coord for coord in sum_power if sum_power[coord] == max_power) + puzzle_actual_result = list( + coord for coord in sum_power if sum_power[coord] == max_power + ) else: - grid_power = {(x, y): int(((((10+x)*y + puzzle_input) * (10+x)) // 100) % 10)-5 for x in range (1, 301) for y in range (1, 301)} + grid_power = { + (x, y): int(((((10 + x) * y + puzzle_input) * (10 + x)) // 100) % 10) - 5 + for x in range(1, 301) + for y in range(1, 301) + } max_power = 31 sum_power = grid_power.copy() - for size in range (2, 300): - sum_power = {(x, y, size): sum(grid_power[x1, y1] - for x1 in range (x, x+size) - for y1 in range (y, y+size)) - for x in range (1, 301-size+1) - for y in range (1, 301-size+1)} + decreasing = False + last_power = 0 + for size in range(2, 300): + sum_power = { + (x, y, size): sum( + grid_power[x1, y1] + for x1 in range(x, x + size) + for y1 in range(y, y + size) + ) + for x in range(1, 301 - size + 1) + for y in range(1, 301 - size + 1) + } new_max = max(sum_power.values()) if new_max > max_power: + decreasing = False max_power = new_max - puzzle_actual_result = list(coord + (size,) for coord in sum_power if sum_power[coord] == max_power) + puzzle_actual_result = list( + coord for coord in sum_power if sum_power[coord] == max_power + ) # Basically, let it run until it decreases multiple times - print (size, new_max, list(coord for coord in sum_power if sum_power[coord] == new_max)) + # print (size, new_max, list(coord for coord in sum_power if sum_power[coord] == new_max)) + if not decreasing and new_max < last_power: + decreasing = True + elif decreasing and new_max < last_power: + break + last_power = new_max # -------------------------------- Outputs / results -------------------------------- # -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/12-Subterranean Sustainability.py b/2018/12-Subterranean Sustainability.py index 9b9943d..4ec5577 100644 --- a/2018/12-Subterranean Sustainability.py +++ b/2018/12-Subterranean Sustainability.py @@ -4,7 +4,8 @@ test_data = {} test = 1 -test_data[test] = {"input": '''initial state: #..#.#..##......###...### +test_data[test] = { + "input": """initial state: #..#.#..##......###...### ...## => # ..#.. => # @@ -19,26 +20,31 @@ ##.## => # ###.. => # ###.# => # -####. => #''', - "expected": ['325', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['3890', '23743'], - } +####. => #""", + "expected": ["325", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3890", "23743"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 2 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # @@ -53,35 +59,41 @@ initial_state = puzzle_input.splitlines()[0][15:] -pots = np.full((len(initial_state) + 10**6), '.') -pots[5*10**5:5*10**5+len(initial_state)] = np.fromiter(initial_state, dtype='S1', count=len(initial_state)) +pots = np.full((len(initial_state) + 10 ** 6), ".") +pots[5 * 10 ** 5 : 5 * 10 ** 5 + len(initial_state)] = np.fromiter( + initial_state, dtype="S1", count=len(initial_state) +) rules = {} for string in puzzle_input.splitlines()[2:]: - source, target = string.split(' => ') + source, target = string.split(" => ") rules[source] = target -prev_sum = sum(np.where(pots == '#')[0]) - 5*10**5 * len(np.where(pots == '#')[0]) -for i in range (1, generations): +prev_sum = sum(np.where(pots == "#")[0]) - 5 * 10 ** 5 * len(np.where(pots == "#")[0]) +for i in range(1, generations): if case_to_test == 1: - for i in range (2, len(pots)-3): - if ''.join(pots[i-2:i+3]) not in rules: - rules[''.join(pots[i-2:i+3])] = '.' + for i in range(2, len(pots) - 3): + if "".join(pots[i - 2 : i + 3]) not in rules: + rules["".join(pots[i - 2 : i + 3])] = "." - min_x, max_x = min(np.where(pots == '#')[0]), max(np.where(pots == '#')[0]) + min_x, max_x = min(np.where(pots == "#")[0]), max(np.where(pots == "#")[0]) - new_pots = np.full((len(initial_state) + 10**6), '.') - new_pots[min_x-2:max_x+2] = [rules[''.join(pots[i-2:i+3])] for i in range(min_x-2, max_x+2)] + new_pots = np.full((len(initial_state) + 10 ** 6), ".") + new_pots[min_x - 2 : max_x + 2] = [ + rules["".join(pots[i - 2 : i + 3])] for i in range(min_x - 2, max_x + 2) + ] pots = new_pots.copy() - sum_pots = sum(np.where(new_pots == '#')[0]) - 5*10**5 * len(np.where(new_pots == '#')[0]) + sum_pots = sum(np.where(new_pots == "#")[0]) - 5 * 10 ** 5 * len( + np.where(new_pots == "#")[0] + ) - print (i, sum_pots, sum_pots - prev_sum) + # print (i, sum_pots, sum_pots - prev_sum) prev_sum = sum_pots if i == 200: - puzzle_actual_result = sum_pots + 96 * (generations-200) + puzzle_actual_result = sum_pots + 96 * (generations - 200) break if part_to_test == 1: @@ -90,9 +102,5 @@ # -------------------------------- Outputs / results -------------------------------- # -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/13-Mine Cart Madness.py b/2018/13-Mine Cart Madness.py index 35f01ca..8622581 100644 --- a/2018/13-Mine Cart Madness.py +++ b/2018/13-Mine Cart Madness.py @@ -4,80 +4,88 @@ test_data = {} test = 1 -test_data[test] = {"input": """/->-\\ +test_data[test] = { + "input": """/->-\\ | | /----\\ | /-+--+-\ | | | | | v | \-+-/ \-+--/ \------/ """, - "expected": ['7,3', 'Unknown'], - } + "expected": ["7,3", "Unknown"], +} test += 1 -test_data[test] = {"input": r"""/>-<\ +test_data[test] = { + "input": r"""/>-<\ | | | /<+-\ | | | v \>+/""", - "expected": ['Unknown', '6,4'], - } + "expected": ["Unknown", "6,4"], +} -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read(), - "expected": ['124,130', '143, 123'], - } +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["124,130", "143, 123"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 2 verbose = 3 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -cart_to_track = {'^': '|', '>': '-', '<': '-', 'v': '|'} +cart_to_track = {"^": "|", ">": "-", "<": "-", "v": "|"} up, right, left, down = ((0, -1), (1, 0), (-1, 0), (0, 1)) -directions = {'^': up, '>': right, '<': left, 'v': down} +directions = {"^": up, ">": right, "<": left, "v": down} new_dirs = { - '^':['<', '^', '>'], - '>':['^', '>', 'v'], - '<':['v', '<', '^'], - 'v':['>', 'v', '<'], - '/': {'^': '>', '>': '^', '<': 'v', 'v': '<'}, - '\\':{'^': '<', '>': 'v', '<': '^', 'v': '>'}, + "^": ["<", "^", ">"], + ">": ["^", ">", "v"], + "<": ["v", "<", "^"], + "v": [">", "v", "<"], + "/": {"^": ">", ">": "^", "<": "v", "v": "<"}, + "\\": {"^": "<", ">": "v", "<": "^", "v": ">"}, } -def move_cart (track, cart): +def move_cart(track, cart): (x, y), dir, choice = cart x += directions[dir][0] y += directions[dir][1] - if track[y][x] == '+': + if track[y][x] == "+": dir = new_dirs[dir][choice] choice += 1 choice %= 3 - elif track[y][x] in ('\\', '/'): + elif track[y][x] in ("\\", "/"): dir = new_dirs[track[y][x]][dir] return ((x, y), dir, choice) + # Setting up the track track = [] cart_positions = [] carts = [] -for y, line in enumerate(puzzle_input.split('\n')): +for y, line in enumerate(puzzle_input.split("\n")): track.append([]) for x, letter in enumerate(line): if letter in cart_to_track: @@ -90,22 +98,20 @@ def move_cart (track, cart): # Run them! tick = 0 -carts.append('new') +carts.append("new") while len(carts) > 0: cart = carts.pop(0) - if cart == 'new': + if cart == "new": if len(carts) == 1: break tick += 1 -# print ('tick', tick, 'completed - Remaining', len(carts)) + # print ('tick', tick, 'completed - Remaining', len(carts)) carts = sorted(carts, key=lambda x: (x[0][1], x[0][0])) cart_positions = [c[0] for c in carts] cart = carts.pop(0) - carts.append('new') + carts.append("new") cart_positions.pop(0) - - cart = move_cart(track, cart) # Check collisions @@ -114,7 +120,7 @@ def move_cart (track, cart): puzzle_actual_result = cart[0] break else: - print ('collision', cart[0]) + # print ('collision', cart[0]) carts = [c for c in carts if c[0] != cart[0]] cart_positions = [c[0] for c in carts] else: @@ -125,12 +131,7 @@ def move_cart (track, cart): puzzle_actual_result = carts[0][0] - # -------------------------------- Outputs / results -------------------------------- # -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/15-Beverage Bandits.py b/2018/15-Beverage Bandits.py index 75fe434..0027d9a 100644 --- a/2018/15-Beverage Bandits.py +++ b/2018/15-Beverage Bandits.py @@ -64,9 +64,9 @@ # -------------------------------- Control program execution ------------------------- # -case_to_test = 4 +case_to_test = "real" part_to_test = 2 -verbose_level = 2 +verbose_level = 1 # -------------------------------- Initialize some variables ------------------------- # diff --git a/2018/16-Chronal Classification.py b/2018/16-Chronal Classification.py index e675501..b92517b 100644 --- a/2018/16-Chronal Classification.py +++ b/2018/16-Chronal Classification.py @@ -19,7 +19,7 @@ ) test_data[test] = { "input": open(input_file, "r+").read().strip(), - "expected": ["Unknown", "Unknown"], + "expected": ["612", "485"], } # -------------------------------- Control program execution ------------------------- # @@ -158,7 +158,7 @@ opcode = final_mapping[int(operation.split(" ")[0])] a, b, c = map(int, operation.split(" ")[1:]) - print(operation, opcode, a, b, c) + # print(operation, opcode, a, b, c) if opcode == "addr": registers[c] = registers[a] + registers[b] diff --git a/2018/17-Reservoir Research.py b/2018/17-Reservoir Research.py index a4750c2..947192a 100644 --- a/2018/17-Reservoir Research.py +++ b/2018/17-Reservoir Research.py @@ -166,24 +166,24 @@ break i += 1 -print("step", i) -for y in range(max_y + 1, min_y - 1, -1): - for x in range(min_x - 2, max_x + 3): - if x + y * 1j in pools: - print("~", end="") - elif x + y * 1j in settled: - print("S", end="") - elif x + y * 1j in flowing: - print("F", end="") - elif x + y * 1j in pools: - print("~", end="") - elif x + y * 1j in wet_positions: - print("|", end="") - elif x + y * 1j in walls: - print("#", end="") - else: - print(".", end="") - print("") +# print("step", i) +# for y in range(max_y + 1, min_y - 1, -1): +# for x in range(min_x - 2, max_x + 3): +# if x + y * 1j in pools: +# print("~", end="") +# elif x + y * 1j in settled: +# print("S", end="") +# elif x + y * 1j in flowing: +# print("F", end="") +# elif x + y * 1j in pools: +# print("~", end="") +# elif x + y * 1j in wet_positions: +# print("|", end="") +# elif x + y * 1j in walls: +# print("#", end="") +# else: +# print(".", end="") +# print("") if part_to_test == 1: diff --git a/2018/18-Settlers of The North Pole.py b/2018/18-Settlers of The North Pole.py index bd101d7..6ab48e8 100644 --- a/2018/18-Settlers of The North Pole.py +++ b/2018/18-Settlers of The North Pole.py @@ -143,19 +143,19 @@ def grid_to_text(grid, blank_character=" "): if i > 800 and i < 10 ** 8 and score in scores: repeats_every = i - scores.index(score) - 1 - 800 i += (end - i) // repeats_every * repeats_every - print( - "repeats_every", - repeats_every, - "score", - score, - "index", - scores.index(score), - i, - ) + # print( + # "repeats_every", + # repeats_every, + # "score", + # score, + # "index", + # scores.index(score), + # i, + # ) if i > 800: scores.append(score) - print(i, score) + # print(i, score) i += 1 diff --git a/2018/19-Go With The Flow.py b/2018/19-Go With The Flow.py index 9f95c0b..7d55794 100644 --- a/2018/19-Go With The Flow.py +++ b/2018/19-Go With The Flow.py @@ -104,14 +104,6 @@ print(i, pointer, operation, registers) - if i == 150: - break - if i % 10000 == 0: - print(i, pointer, operation, registers) - # if registers[2] < registers[3]-1250: - # registers[2] += 1250 * ((registers[3] - registers[2]) // 1250-1) - # print (i, pointer, operation, registers, 'after') - i += 1 puzzle_actual_result = registers[0] @@ -129,7 +121,7 @@ def get_divisors(value): for i in range(0, 200): operation = program[registers[pointer]] - print(i, pointer, operation, registers) + # print(i, pointer, operation, registers) opcode = operation.split(" ")[0] a, b, c = map(int, operation.split(" ")[1:]) diff --git a/2018/23-Experimental Emergency Teleportation.py b/2018/23-Experimental Emergency Teleportation.py index 5796a74..057c976 100644 --- a/2018/23-Experimental Emergency Teleportation.py +++ b/2018/23-Experimental Emergency Teleportation.py @@ -181,13 +181,13 @@ def add_each(a, b): if nb_spot > min_bots: min_bots = nb_spot best_dot = dot - print("Min bots updated to ", nb_spot, "for dot", dot) + # print("Min bots updated to ", nb_spot, "for dot", dot) elif nb_spot == min_bots: if manhattan_distance((0, 0, 0), best_dot) > manhattan_distance( (0, 0, 0), dot ): best_dot = dot - print("Best dot set to ", dot) + # print("Best dot set to ", dot) if cube_size == 1: # We can't divide it any further @@ -211,7 +211,7 @@ def add_each(a, b): heapq.heappush(cubes, (-count_bots, cube_size, new_cube)) all_cubes.append((count_bots, cube_size, new_cube)) - print("max power", min_bots) + # print("max power", min_bots) puzzle_actual_result = manhattan_distance((0, 0, 0), best_dot) diff --git a/2018/24-Immune System Simulator 20XX.py b/2018/24-Immune System Simulator 20XX.py index b80dee0..99f5bfc 100644 --- a/2018/24-Immune System Simulator 20XX.py +++ b/2018/24-Immune System Simulator 20XX.py @@ -178,7 +178,8 @@ def team_size(units): for uid in range(len(units)): if units[uid][-2] == "Immune System:": units[uid][2] += boost - print("Applying boost", boost) + if verbose: + print("Applying boost", boost) while len(teams(units)) > 1: units_killed = 0 @@ -231,7 +232,8 @@ def team_size(units): winner = "None" else: winner = units[0][-2] - print("Boost", boost, " - Winner:", winner) + if verbose: + print("Boost", boost, " - Winner:", winner) if verbose: print([unit[0] for unit in units]) diff --git a/2018/25-Four-Dimensional Adventure.py b/2018/25-Four-Dimensional Adventure.py index 843b659..518e224 100644 --- a/2018/25-Four-Dimensional Adventure.py +++ b/2018/25-Four-Dimensional Adventure.py @@ -88,7 +88,7 @@ def manhattan_distance(source, target): groups = graph.dfs_groups() - print(groups) + # print(groups) puzzle_actual_result = len(groups) diff --git a/2018/racetrack.py b/2018/racetrack.py deleted file mode 100644 index c89bbf6..0000000 --- a/2018/racetrack.py +++ /dev/null @@ -1,238 +0,0 @@ -from math import sqrt - - -class PlayerBlocked(Exception): - pass - - -def collisions(players): - positions = [x.position for x in players] - if positions == set(positions): - return None - else: - return [x for x in set(positions) if positions.count(x) > 1] - - -class RaceTrack: - vertices = {} - edges = {} - """ - Represents which directions are allowed based on the track piece - - Structure: - track_piece: allowed directions - """ - allowed_directions = { - "/": directions_all, - "\\": directions_all, - "+": directions_all, - "|": directions_vertical, - "-": directions_horizontal, - "^": directions_vertical, - "v": directions_vertical, - ">": directions_horizontal, - "<": directions_horizontal, - } - - # Usual replacements - player_replace = { - ">": "-", - "<": "-", - "^": "|", - "v": "|", - } - - def __init__(self, vertices=[], edges={}): - self.vertices = vertices - self.edges = edges - - def text_to_track(self, text, allowed_directions={}): - """ - Converts a text to a set of coordinates - - The text is expected to be separated by newline characters - The vertices will have x-y*j as coordinates (so y axis points south) - Edges will be calculated as well - - :param string text: The text to convert - :param str elements: How to interpret the track - :return: True if the text was converted - """ - self.vertices = {} - self.allowed_directions.update(allowed_directions) - - for y, line in enumerate(text.splitlines()): - for x in range(len(line)): - if line[x] in self.allowed_directions: - self.vertices[x - y * 1j] = line[x] - - for source, track in self.vertices.items(): - for direction in self.allowed_directions[track]: - target = source + direction - if not target in self.vertices: - continue - - target_dirs = self.allowed_directions[self.vertices[target]] - if -direction not in target_dirs: - continue - - if source in self.edges: - self.edges[source].append(target) - else: - self.edges[source] = [target] - - return True - - def track_to_text(self, mark_coords={}, wall=" "): - """ - Converts a set of coordinates to a text - - The text will be separated by newline characters - - :param dict mark_coords: List of coordinates to mark, with letter to use - :param string wall: Which character to use as walls - :return: the converted text - """ - - min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) - min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) - - text = "" - - for y in range(min_y, max_y - 1, -1): - for x in range(min_x, max_x + 1): - if x + y * 1j in mark_coords: - text += mark_coords[x + y * 1j] - else: - text += self.vertices.get(x + y * 1j, wall) - text += "\n" - - return text - - def replace_elements(self, replace_map=None): - """ - Replaces elements in the track (useful to remove players) - - :param dict replace_map: Replacement map - :return: True - """ - - if replace_map is None: - replace_map = self.player_replace - self.vertices = {x: replace_map.get(y, y) for x, y in self.vertices.items()} - return True - - def find_elements(self, elements): - """ - Finds elements in the track - - :param dict elements: elements to find - :return: True - """ - - found = {x: y for x, y in self.vertices.items() if y in elements} - return found - - -class Player: - """ - Represents which directions are allowed based on the track piece - - Structure: - track_piece: source direction: allowed target direction - """ - - allowed_directions = { - "/": {north: [east], south: [west], east: [north], west: [south],}, - "\\": {north: [west], south: [east], east: [south], west: [north],}, - "+": { - north: directions_all, - south: directions_all, - east: directions_all, - west: directions_all, - }, - "|": { - north: directions_vertical, - south: directions_vertical, - east: None, - west: None, - }, - "-": { - north: None, - south: None, - east: directions_horizontal, - west: directions_horizontal, - }, - } - - initial_directions = { - ">": east, - "<": west, - "^": north, - "v": south, - } - - position = 0 - direction = 0 - - def __init__(self, racetrack, position=0, direction=None): - self.position = position - if direction is None: - self.direction = self.initial_directions[racetrack.vertices[position]] - else: - self.direction = direction - - def move(self, racetrack, steps=1): - """ - Moves the player in the direction provided - - :param RaceTrack racetrack: The track to use - :param int steps: The number of steps to take - :return: nothing - """ - for step in range(steps): - # First, let's move the player - self.before_move() - - self.position += self.direction - - if self.position not in racetrack.vertices: - raise PlayerBlocked - - self.after_move() - - # Then, let's make him turn - self.before_rotation() - - track = racetrack.vertices[self.position] - possible_directions = self.allowed_directions[track][self.direction] - - if possible_directions is None: - raise PlayerBlocked - elif len(possible_directions) == 1: - self.direction = possible_directions[0] - else: - self.choose_direction(possible_directions) - - self.after_rotation() - - def before_move(self): - pass - - def after_move(self): - pass - - def before_rotation(self): - pass - - def after_rotation(self): - pass - - def choose_direction(self, possible_directions): - self.direction = possible_directions[0] - - def turn_left(self): - self.direction *= 1j - - def turn_right(self): - self.direction *= -1j From 34dbe358c1897a691a6b5d575fe230c3c2f4e89f Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 29 Jul 2020 20:54:33 +0200 Subject: [PATCH 048/143] Improved performance for days 2018-10 and 2018-22 --- 2018/10-The Stars Align.py | 38 ++++++---- 2018/10-The Stars Align.v1.py | 98 ++++++++++++++++++++++++ 2018/22-Mode Maze.py | 125 +++++++++++++++++++------------ 2018/22-Mode Maze.v1.py | 135 ++++++++++++++++++++++++++++++++++ 2018/complex_utils.py | 67 +++++++++++++++-- 5 files changed, 390 insertions(+), 73 deletions(-) create mode 100644 2018/10-The Stars Align.v1.py create mode 100644 2018/22-Mode Maze.v1.py diff --git a/2018/10-The Stars Align.py b/2018/10-The Stars Align.py index 3ec5dcc..9c89f1b 100644 --- a/2018/10-The Stars Align.py +++ b/2018/10-The Stars Align.py @@ -71,25 +71,31 @@ stars.append(list(map(int, r))) star_map = pathfinding.Graph() +stars_init = [star.copy() for star in stars] +min_galaxy_size = 10 ** 15 +min_i_galaxy_size = 0 for i in range(2 * 10 ** 4): - stars = [(x + vx, y + vy, vx, vy) for x, y, vx, vy in stars] - vertices = [x - y * 1j for x, y, vx, vy in stars] - - # This was solved a bit manually - # I noticed all coordinates would converge around 0 at some point - # That point was around 10300 seconds - # Then made a limit: all coordinates should be within 300 from zero - # (my first test was actually 200, but that was gave no result) - # This gave ~ 20 seconds of interesting time - # At the end it was trial and error to find 10 240 - coords = [v.real in range(-300, 300) for v in vertices] + [ - v.imag in range(-300, 300) for v in vertices - ] - - if all(coords) and i == 10239: + stars = [(x + i * vx, y + i * vy, vx, i * vy) for x, y, vx, vy in stars_init] + + # This gives a very rough idea of the galaxy's size + coords = list(zip(*stars)) + galaxy_size = max(coords[0]) - min(coords[0]) + max(coords[1]) - max(coords[1]) + + if i == 0: + min_galaxy_size = galaxy_size + + if galaxy_size < min_galaxy_size: + min_i_galaxy_size = i + min_galaxy_size = galaxy_size + elif galaxy_size > min_galaxy_size: + vertices = [ + x + vx * min_i_galaxy_size - (y + vy * min_i_galaxy_size) * 1j + for x, y, vx, vy in stars_init + ] star_map.vertices = vertices - print(i + 1) + puzzle_actual_result = min_i_galaxy_size print(star_map.vertices_to_grid(wall=" ")) + break # -------------------------------- Outputs / results -------------------------------- # diff --git a/2018/10-The Stars Align.v1.py b/2018/10-The Stars Align.v1.py new file mode 100644 index 0000000..3ec5dcc --- /dev/null +++ b/2018/10-The Stars Align.v1.py @@ -0,0 +1,98 @@ +# -------------------------------- Input data -------------------------------- # +import os, parse, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """position=< 9, 1> velocity=< 0, 2> +position=< 7, 0> velocity=<-1, 0> +position=< 3, -2> velocity=<-1, 1> +position=< 6, 10> velocity=<-2, -1> +position=< 2, -4> velocity=< 2, 2> +position=<-6, 10> velocity=< 2, -2> +position=< 1, 8> velocity=< 1, -1> +position=< 1, 7> velocity=< 1, 0> +position=<-3, 11> velocity=< 1, -2> +position=< 7, 6> velocity=<-1, -1> +position=<-2, 3> velocity=< 1, 0> +position=<-4, 3> velocity=< 2, 0> +position=<10, -3> velocity=<-1, 1> +position=< 5, 11> velocity=< 1, -2> +position=< 4, 7> velocity=< 0, -1> +position=< 8, -2> velocity=< 0, 1> +position=<15, 0> velocity=<-2, 0> +position=< 1, 6> velocity=< 1, 0> +position=< 8, 9> velocity=< 0, -1> +position=< 3, 3> velocity=<-1, 1> +position=< 0, 5> velocity=< 0, -1> +position=<-2, 2> velocity=< 2, 0> +position=< 5, -2> velocity=< 1, 2> +position=< 1, 4> velocity=< 2, 1> +position=<-2, 7> velocity=< 2, -2> +position=< 3, 6> velocity=<-1, -1> +position=< 5, 0> velocity=< 1, 0> +position=<-6, 0> velocity=< 2, 0> +position=< 5, 9> velocity=< 1, -2> +position=<14, 7> velocity=<-2, 0> +position=<-3, 6> velocity=< 2, -1>""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["RLEZNRAN", "10240"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # +stars = [] +for string in puzzle_input.split("\n"): + if string == "": + continue + r = parse.parse("position=<{:>d},{:>d}> velocity=<{:>d},{:>d}>", string) + stars.append(list(map(int, r))) + +star_map = pathfinding.Graph() +for i in range(2 * 10 ** 4): + stars = [(x + vx, y + vy, vx, vy) for x, y, vx, vy in stars] + vertices = [x - y * 1j for x, y, vx, vy in stars] + + # This was solved a bit manually + # I noticed all coordinates would converge around 0 at some point + # That point was around 10300 seconds + # Then made a limit: all coordinates should be within 300 from zero + # (my first test was actually 200, but that was gave no result) + # This gave ~ 20 seconds of interesting time + # At the end it was trial and error to find 10 240 + coords = [v.real in range(-300, 300) for v in vertices] + [ + v.imag in range(-300, 300) for v in vertices + ] + + if all(coords) and i == 10239: + star_map.vertices = vertices + print(i + 1) + print(star_map.vertices_to_grid(wall=" ")) + + +# -------------------------------- Outputs / results -------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/22-Mode Maze.py b/2018/22-Mode Maze.py index 7f1df10..9e8dc21 100644 --- a/2018/22-Mode Maze.py +++ b/2018/22-Mode Maze.py @@ -1,6 +1,12 @@ # -------------------------------- Input data ---------------------------------------- # import os, pathfinding +from complex_utils import * + + +j = SuperComplex(1j) + + test_data = {} test = 1 @@ -18,7 +24,7 @@ ) test_data[test] = { "input": open(input_file, "r+").read().strip(), - "expected": ["6256", "Unknown"], + "expected": ["6256", "973"], } # -------------------------------- Control program execution ------------------------- # @@ -40,7 +46,7 @@ depth = int(depth) max_x, max_y = map(int, target.split(",")) -target = max_x - 1j * max_y +target = max_x - j * max_y geological = {0: 0} erosion = {0: 0} @@ -48,15 +54,15 @@ geological[x] = x * 16807 erosion[x] = (geological[x] + depth) % 20183 for y in range(max_y + 1): - geological[-1j * y] = y * 48271 - erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + geological[-j * y] = y * 48271 + erosion[-j * y] = (geological[-j * y] + depth) % 20183 for x in range(1, max_x + 1): for y in range(1, max_y + 1): - geological[x - 1j * y] = ( - erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + geological[x - j * y] = ( + erosion[x - 1 - j * y] * erosion[x - j * (y - 1)] ) % 20183 - erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + erosion[x - j * y] = (geological[x - j * y] + depth) % 20183 geological[target] = 0 erosion[target] = 0 @@ -70,25 +76,11 @@ neither, climbing, torch = 0, 1, 2 rocky, wet, narrow = 0, 1, 2 - # Override the neighbors function - def neighbors(self, vertex): - north = (0, 1) - south = (0, -1) - west = (-1, 0) - east = (1, 0) - directions_straight = [north, south, west, east] - - neighbors = {} - for dir in directions_straight: - target = (vertex[0] + dir[0], vertex[1] + dir[1], vertex[2]) - if target in self.vertices: - neighbors[target] = 1 - for tool in (neither, climbing, torch): - target = (vertex[0], vertex[1], tool) - if target in self.vertices and tool != vertex[1]: - neighbors[target] = 7 - - return neighbors + allowed = { + rocky: [torch, climbing], + wet: [neither, climbing], + narrow: [torch, neither], + } # Add some coordinates around the target padding = 10 if case_to_test == 1 else 50 @@ -96,39 +88,74 @@ def neighbors(self, vertex): geological[x] = x * 16807 erosion[x] = (geological[x] + depth) % 20183 for y in range(max_y, max_y + padding): - geological[-1j * y] = y * 48271 - erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + geological[-j * y] = y * 48271 + erosion[-j * y] = (geological[-j * y] + depth) % 20183 for x in range(1, max_x + padding): for y in range(1, max_y + padding): - if x - 1j * y in geological: + if x - j * y in geological: continue - geological[x - 1j * y] = ( - erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + geological[x - j * y] = ( + erosion[x - 1 - j * y] * erosion[x - j * (y - 1)] ) % 20183 - erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + erosion[x - j * y] = (geological[x - j * y] + depth) % 20183 terrain = {x: erosion[x] % 3 for x in erosion} + del erosion del geological - # Then run pathfinding algo + # Prepare pathfinding algorithm + + # Override the neighbors function + def neighbors(self, vertex): + north = j + south = -j + west = -1 + east = 1 + directions_straight = [north, south, west, east] + + neighbors = {} + for dir in directions_straight: + target = (vertex[0] + dir, vertex[1]) + if self.is_valid(target): + neighbors[target] = 1 + for tool in (neither, climbing, torch): + target = (vertex[0], tool) + if self.is_valid(target): + neighbors[target] = 7 + + return neighbors + + # Define what is a valid spot + def is_valid(self, vertex): + if vertex[0].real < 0 or vertex[0].imag > 0: + return False + if vertex[0].real >= max_x + padding or vertex[0].imag <= -(max_y + padding): + return False + if vertex[1] in allowed[terrain[vertex[0]]]: + return True + return False + + # Heuristics function for A* search + def estimate_to_complete(self, start, target): + distance = 0 + for i in range(len(start) - 1): + distance += abs(start[i] - target[i]) + distance += 7 if start[-1] != target[-1] else 0 + return distance + + # Run pathfinding algorithm pathfinding.WeightedGraph.neighbors = neighbors - vertices = [ - (x.real, x.imag, neither) for x in terrain if terrain[x] in (wet, narrow) - ] - vertices += [ - (x.real, x.imag, climbing) for x in terrain if terrain[x] in (rocky, wet) - ] - vertices += [ - (x.real, x.imag, torch) for x in terrain if terrain[x] in (rocky, narrow) - ] - graph = pathfinding.WeightedGraph(vertices) - - graph.dijkstra((0, 0, torch), (max_x, -max_y, torch)) - - puzzle_actual_result = graph.distance_from_start[(max_x, -max_y, torch)] - -# 979 is too high + pathfinding.WeightedGraph.is_valid = is_valid + pathfinding.Graph.estimate_to_complete = estimate_to_complete + + graph = pathfinding.WeightedGraph() + + graph.a_star_search( + (SuperComplex(0), torch), (SuperComplex(max_x - j * max_y), torch) + ) + + puzzle_actual_result = graph.distance_from_start[(max_x - j * max_y, torch)] # -------------------------------- Outputs / results --------------------------------- # diff --git a/2018/22-Mode Maze.v1.py b/2018/22-Mode Maze.v1.py new file mode 100644 index 0000000..a5a6f82 --- /dev/null +++ b/2018/22-Mode Maze.v1.py @@ -0,0 +1,135 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +test_data = {} + +test = 1 +test_data[test] = { + "input": """depth: 510 +target: 10,10""", + "expected": ["114", "45"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6256", "973"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +_, depth = puzzle_input.splitlines()[0].split(" ") +_, target = puzzle_input.splitlines()[1].split(" ") + +depth = int(depth) +max_x, max_y = map(int, target.split(",")) +target = max_x - 1j * max_y + +geological = {0: 0} +erosion = {0: 0} +for x in range(max_x + 1): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 +for y in range(max_y + 1): + geological[-1j * y] = y * 48271 + erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + +for x in range(1, max_x + 1): + for y in range(1, max_y + 1): + geological[x - 1j * y] = ( + erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + ) % 20183 + erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + +geological[target] = 0 +erosion[target] = 0 + +terrain = {x: erosion[x] % 3 for x in erosion} + +if part_to_test == 1: + puzzle_actual_result = sum(terrain.values()) + +else: + neither, climbing, torch = 0, 1, 2 + rocky, wet, narrow = 0, 1, 2 + + # Override the neighbors function + def neighbors(self, vertex): + north = (0, 1) + south = (0, -1) + west = (-1, 0) + east = (1, 0) + directions_straight = [north, south, west, east] + + neighbors = {} + for dir in directions_straight: + target = (vertex[0] + dir[0], vertex[1] + dir[1], vertex[2]) + if target in self.vertices: + neighbors[target] = 1 + for tool in (neither, climbing, torch): + target = (vertex[0], vertex[1], tool) + if target in self.vertices and tool != vertex[1]: + neighbors[target] = 7 + + return neighbors + + # Add some coordinates around the target + padding = 10 if case_to_test == 1 else 50 + for x in range(max_x, max_x + padding): + geological[x] = x * 16807 + erosion[x] = (geological[x] + depth) % 20183 + for y in range(max_y, max_y + padding): + geological[-1j * y] = y * 48271 + erosion[-1j * y] = (geological[-1j * y] + depth) % 20183 + for x in range(1, max_x + padding): + for y in range(1, max_y + padding): + if x - 1j * y in geological: + continue + geological[x - 1j * y] = ( + erosion[x - 1 - 1j * y] * erosion[x - 1j * (y - 1)] + ) % 20183 + erosion[x - 1j * y] = (geological[x - 1j * y] + depth) % 20183 + + terrain = {x: erosion[x] % 3 for x in erosion} + del erosion + del geological + + # Then run pathfinding algo + pathfinding.WeightedGraph.neighbors = neighbors + vertices = [ + (x.real, x.imag, neither) for x in terrain if terrain[x] in (wet, narrow) + ] + vertices += [ + (x.real, x.imag, climbing) for x in terrain if terrain[x] in (rocky, wet) + ] + vertices += [ + (x.real, x.imag, torch) for x in terrain if terrain[x] in (rocky, narrow) + ] + graph = pathfinding.WeightedGraph(vertices) + + graph.dijkstra((0, 0, torch), (max_x, -max_y, torch)) + + puzzle_actual_result = graph.distance_from_start[(max_x, -max_y, torch)] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/complex_utils.py b/2018/complex_utils.py index decd16d..51c4cb0 100644 --- a/2018/complex_utils.py +++ b/2018/complex_utils.py @@ -3,15 +3,66 @@ """ from math import sqrt + +class ReturnTypeWrapper(type): + def __new__(mcs, name, bases, dct): + cls = type.__new__(mcs, name, bases, dct) + for attr, obj in cls.wrapped_base.__dict__.items(): + # skip 'member descriptor's and overridden methods + if type(obj) == type(complex.real) or attr in dct: + continue + if getattr(obj, "__objclass__", None) is cls.wrapped_base: + setattr(cls, attr, cls.return_wrapper(obj)) + return cls + + def return_wrapper(cls, obj): + def convert(value): + return cls(value) if type(value) is cls.wrapped_base else value + + def wrapper(*args, **kwargs): + return convert(obj(*args, **kwargs)) + + wrapper.__name__ = obj.__name__ + return wrapper + + +class SuperComplex(complex): + __metaclass__ = ReturnTypeWrapper + wrapped_base = complex + + def __lt__(self, other): + return abs(other - self) < 0 + + def __le__(self, other): + return abs(other - self) <= 0 + + def __gt__(self, other): + return abs(other - self) > 0 + + def __ge__(self, other): + return abs(other - self) >= 0 + + def __str__(self): + return "(" + str(self.real) + "," + str(self.imag) + ")" + + def __add__(self, no): + return SuperComplex(self.real + no.real, self.imag + no.imag) + + def __sub__(self, no): + return SuperComplex(self.real - no.real, self.imag - no.imag) + + +j = SuperComplex(1j) + # Cardinal directions -north = 1j -south = -1j +north = j +south = -j west = -1 east = 1 -northeast = 1 + 1j -northwest = -1 + 1j -southeast = 1 - 1j -southwest = -1 - 1j +northeast = 1 + j +northwest = -1 + j +southeast = 1 - j +southwest = -1 - j directions_straight = [north, south, west, east] directions_diagonals = directions_straight + [ @@ -23,8 +74,8 @@ # To be multiplied by the current cartinal direction relative_directions = { - "left": 1j, - "right": -1j, + "left": j, + "right": -j, "ahead": 1, "back": -1, } From 0b9e5d027cba56fc7698d817e97ce8581dd6c03e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 29 Jul 2020 21:10:32 +0200 Subject: [PATCH 049/143] Improved performance for day 2018-01 --- 2018/01-Chronal Calibration.py | 63 ++++++++++++++++--------------- 2018/01-Chronal Calibration.v1.py | 62 ++++++++++++++++++++++++++++++ 2 files changed, 94 insertions(+), 31 deletions(-) create mode 100644 2018/01-Chronal Calibration.v1.py diff --git a/2018/01-Chronal Calibration.py b/2018/01-Chronal Calibration.py index b200ddf..8f08cf9 100644 --- a/2018/01-Chronal Calibration.py +++ b/2018/01-Chronal Calibration.py @@ -4,27 +4,33 @@ test_data = {} test = 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['585', '83173'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["585", "83173"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # @@ -34,28 +40,23 @@ else: - used_frequencies = [0] + data = list(map(int, puzzle_input.splitlines())) + used_frequencies = [sum(data[0 : i + 1]) for i in range(len(data))] + delta = sum(map(int, puzzle_input.splitlines())) frequency = 0 + i = 0 while True: - for string in puzzle_input.split('\n'): - frequency += int(string) - if frequency in used_frequencies: - puzzle_actual_result = frequency - break - used_frequencies.append(frequency) - - if puzzle_actual_result != 'Unknown': + i += 1 + new_freq = [x + i * delta for x in used_frequencies] + reuse = [freq for freq in new_freq if freq in used_frequencies] + if reuse: + puzzle_actual_result = reuse[0] break - # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/01-Chronal Calibration.v1.py b/2018/01-Chronal Calibration.v1.py new file mode 100644 index 0000000..a7d90cd --- /dev/null +++ b/2018/01-Chronal Calibration.v1.py @@ -0,0 +1,62 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["585", "83173"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + puzzle_actual_result = sum(map(int, puzzle_input.splitlines())) + + +else: + used_frequencies = [0] + frequency = 0 + while True: + for string in puzzle_input.split("\n"): + frequency += int(string) + if frequency in used_frequencies: + puzzle_actual_result = frequency + break + used_frequencies.append(frequency) + + if puzzle_actual_result != "Unknown": + break + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From b28bff884f61caafb6834471561e515e3c1fc10e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 9 Aug 2020 18:37:06 +0200 Subject: [PATCH 050/143] Improved 2018-15 --- 2018/15-Beverage Bandits.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/2018/15-Beverage Bandits.py b/2018/15-Beverage Bandits.py index 0027d9a..470e0a1 100644 --- a/2018/15-Beverage Bandits.py +++ b/2018/15-Beverage Bandits.py @@ -118,6 +118,16 @@ def move(self, graph, creatures): if c.type == self.type and c != self and c.alive ] ennemies = [c.position for c in creatures if c.type != self.type and c.alive] + + # Check if there is an ennemy next to me => no movement in this case + ennemy_next_to_me = [ + self.position + for dir in complex_utils.directions_straight + if self.position + dir in ennemies + ] + if ennemy_next_to_me: + return + self.graph.add_traps(ennemies) self.graph.add_walls(allies) From 3c83d1f6139f8459e39832f328d8510014591825 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 9 Aug 2020 19:11:46 +0200 Subject: [PATCH 051/143] Added days 2019-01 and 2019-02 --- 2019/01-The Tyranny of the Rocket Equation.py | 64 ++ 2019/02-1202 Program Alarm.py | 130 ++++ 2019/complex_utils.py | 121 ++++ 2019/pathfinding.py | 616 ++++++++++++++++++ 4 files changed, 931 insertions(+) create mode 100644 2019/01-The Tyranny of the Rocket Equation.py create mode 100644 2019/02-1202 Program Alarm.py create mode 100644 2019/complex_utils.py create mode 100644 2019/pathfinding.py diff --git a/2019/01-The Tyranny of the Rocket Equation.py b/2019/01-The Tyranny of the Rocket Equation.py new file mode 100644 index 0000000..4308288 --- /dev/null +++ b/2019/01-The Tyranny of the Rocket Equation.py @@ -0,0 +1,64 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1969""", + "expected": ["2", "966"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3360301", "5037595"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + total = 0 + for string in puzzle_input.split("\n"): + val = int(string) + val = val // 3 - 2 + total += val + + puzzle_actual_result = total + + +else: + total = 0 + for string in puzzle_input.split("\n"): + val = int(string) + val = val // 3 - 2 + while val > 0: + total += val + val = val // 3 - 2 + + puzzle_actual_result = total + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/02-1202 Program Alarm.py b/2019/02-1202 Program Alarm.py new file mode 100644 index 0000000..2cf4ba0 --- /dev/null +++ b/2019/02-1202 Program Alarm.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1,9,10,3,2,3,11,0,99,30,40,50""", + "expected": ["3500,9,10,70,2,3,11,0,99,30,40,50", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """1,0,0,0,99""", + "expected": ["2,0,0,0,99", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """2,4,4,5,99,0""", + "expected": ["2,4,4,5,99,9801", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6327510", "4112"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class IntCode: + instructions = [] + pointer = 0 + state = "Running" + + def __init__(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + + def reset(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + self.pointer = 0 + self.state = "Running" + + def get_instruction(self): + if self.instructions[self.pointer] in [1, 2]: + return self.instructions[self.pointer : self.pointer + 4] + else: + return [self.instructions[self.pointer]] + + def op_1(self, instr): + self.instructions[instr[3]] = ( + self.instructions[instr[1]] + self.instructions[instr[2]] + ) + self.pointer += 4 + self.state = "Running" + + def op_2(self, instr): + self.instructions[instr[3]] = ( + self.instructions[instr[1]] * self.instructions[instr[2]] + ) + self.pointer += 4 + self.state = "Running" + + def op_99(self, instr): + self.pointer += 1 + self.state = "Stopped" + + def run(self): + while self.state == "Running": + current_instruction = self.get_instruction() + getattr(self, "op_" + str(current_instruction[0]))(current_instruction) + if verbose_level >= 3: + print("Pointer after execution:", self.pointer) + print("Instructions:", self.export()) + + def export(self): + return ",".join(map(str, self.instructions)) + + +if part_to_test == 1: + computer = IntCode(puzzle_input) + if case_to_test == "real": + computer.instructions[1] = 12 + computer.instructions[2] = 2 + computer.run() + puzzle_actual_result = computer.instructions[0] + + +else: + computer = IntCode(puzzle_input) + for noon in range(100): + for verb in range(100): + computer.reset(puzzle_input) + computer.instructions[1] = noon + computer.instructions[2] = verb + computer.run() + if computer.instructions[0] == 19690720: + puzzle_actual_result = 100 * noon + verb + break + + if puzzle_actual_result != "Unknown": + break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/complex_utils.py b/2019/complex_utils.py new file mode 100644 index 0000000..51c4cb0 --- /dev/null +++ b/2019/complex_utils.py @@ -0,0 +1,121 @@ +""" +Small library for complex numbers +""" +from math import sqrt + + +class ReturnTypeWrapper(type): + def __new__(mcs, name, bases, dct): + cls = type.__new__(mcs, name, bases, dct) + for attr, obj in cls.wrapped_base.__dict__.items(): + # skip 'member descriptor's and overridden methods + if type(obj) == type(complex.real) or attr in dct: + continue + if getattr(obj, "__objclass__", None) is cls.wrapped_base: + setattr(cls, attr, cls.return_wrapper(obj)) + return cls + + def return_wrapper(cls, obj): + def convert(value): + return cls(value) if type(value) is cls.wrapped_base else value + + def wrapper(*args, **kwargs): + return convert(obj(*args, **kwargs)) + + wrapper.__name__ = obj.__name__ + return wrapper + + +class SuperComplex(complex): + __metaclass__ = ReturnTypeWrapper + wrapped_base = complex + + def __lt__(self, other): + return abs(other - self) < 0 + + def __le__(self, other): + return abs(other - self) <= 0 + + def __gt__(self, other): + return abs(other - self) > 0 + + def __ge__(self, other): + return abs(other - self) >= 0 + + def __str__(self): + return "(" + str(self.real) + "," + str(self.imag) + ")" + + def __add__(self, no): + return SuperComplex(self.real + no.real, self.imag + no.imag) + + def __sub__(self, no): + return SuperComplex(self.real - no.real, self.imag - no.imag) + + +j = SuperComplex(1j) + +# Cardinal directions +north = j +south = -j +west = -1 +east = 1 +northeast = 1 + j +northwest = -1 + j +southeast = 1 - j +southwest = -1 - j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + +# To be multiplied by the current cartinal direction +relative_directions = { + "left": j, + "right": -j, + "ahead": 1, + "back": -1, +} + + +def min_real(complexes): + real_values = [x.real for x in complexes] + return min(real_values) + + +def min_imag(complexes): + real_values = [x.imag for x in complexes] + return min(real_values) + + +def max_real(complexes): + real_values = [x.real for x in complexes] + return max(real_values) + + +def max_imag(complexes): + real_values = [x.imag for x in complexes] + return max(real_values) + + +def manhattan_distance(a, b): + return abs(b.imag - a.imag) + abs(b.real - a.real) + + +def complex_sort(complexes, mode=""): + # Sorts by real, then by imaginary component (x then y) + if mode == "xy": + complexes.sort(key=lambda a: (a.real, a.imag)) + # Sorts by imaginary, then by real component (y then x) + elif mode == "yx": + complexes.sort(key=lambda a: (a.imag, a.real)) + # Sorts by negative imaginary, then by real component (-y then x) - 'Reading" order + elif mode == "reading": + complexes.sort(key=lambda a: (-a.imag, a.real)) + # Sorts by distance from 0,0 (kind of polar coordinates) + else: + complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) + return complexes diff --git a/2019/pathfinding.py b/2019/pathfinding.py new file mode 100644 index 0000000..2a4572a --- /dev/null +++ b/2019/pathfinding.py @@ -0,0 +1,616 @@ +import heapq + +from complex_utils import * + + +class TargetFound(Exception): + pass + + +class NegativeWeightCycle(Exception): + pass + + +class Graph: + vertices = [] + edges = {} + distance_from_start = {} + came_from = {} + + def __init__(self, vertices=[], edges={}): + self.vertices = vertices + self.edges = edges + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def is_valid(self, vertex): + return vertex in self.vertices + + def estimate_to_complete(self, source_vertex, target_vertex): + return 0 + + def reset_search(self): + self.distance_from_start = {} + self.came_from = {} + + def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x - y * 1j as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param Boolean diagonals_allowed: Whether diagonal movement is allowed + :param str wall: What is considered as a wall + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append(x - y * j) + y += 1 + + if diagonals_allowed: + directions = directions_diagonals + else: + directions = directions_straight + + for source in self.vertices: + for direction in directions: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + def grid_search(self, grid, items): + """ + Searches the grid for some items + + :param string grid: The grid in which to search + :param Boolean items: The items to search + :return: A dictionnary of the items found + """ + items_found = {} + y = 0 + + for y, line in enumerate(grid.splitlines()): + for x in range(len(line)): + if line[x] in items: + if line[x] in items_found: + items_found[line[x]].append(x - y * j) + else: + items_found[line[x]] = [x - y * j] + + return items_found + + def vertices_to_grid(self, mark_coords={}, wall="#"): + """ + Converts a set of coordinates to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string wall: Which character to use as walls + :return: True if the grid was converted + """ + grid = "" + + min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices)) + min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices)) + + for y in range(min_y, max_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + grid += mark_coords[x + y * j] + except KeyError: + if x + y * j in mark_coords: + grid += "X" + else: + try: + grid += self.vertices.get(x + y * j, wall) + except AttributeError: + if x + y * j in self.vertices: + grid += "." + else: + grid += wall + grid += "\n" + + return grid + + def add_traps(self, vertices): + """ + Creates traps: places that can be reached, but not exited + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + changed = True + + return changed + + def add_walls(self, vertices): + """ + Adds walls - useful for modification of map + + :param Any vertex: The vertices to consider + :return: True if successful, False if no vertex found + """ + changed = False + for vertex in vertices: + if vertex in self.edges: + del self.edges[vertex] + self.vertices.remove(vertex) + changed = True + + self.edges = { + source: [target for target in self.edges[source] if target not in vertices] + for source in self.edges + } + + return changed + + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = self.vertices.copy() + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited = [x for x in unvisited if x not in newly_visited] + groups.append(newly_visited) + + return groups + + def depth_first_search(self, start, end=None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion(self, current_distance, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort(self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + + return True + + def topological_sort_alphabetical(self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search(self, start, end=None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + def greedy_best_first_search(self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def grid_to_vertices( + self, grid, diagonals_allowed=False, wall="#", cost_straight=1, cost_diagonal=2 + ): + """ + Converts a text to a set of coordinates + + The text is expected to be separated by newline characters + The vertices will have x - y * 1j as coordinates + Edges will be calculated as well + + :param string grid: The grid to convert + :param boolean diagonals_allowed: Whether diagonal movement is allowed + :param float cost_straight: The cost of horizontal and vertical movements + :param float cost_diagonal: The cost of diagonal movements + :return: True if the grid was converted + """ + self.vertices = [] + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices.append(x - y * j) + y += 1 + + if diagonals_allowed: + directions = directions_diagonals + else: + directions = directions_straight + + for source in self.vertices: + for direction in directions: + cost = ( + cost_straight if direction in directions_straight else cost_diagonal + ) + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[(source)][target] = cost + else: + self.edges[(source)] = {target: cost} + + return True + + def dijkstra(self, start, end=None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + return end is None or end in self.distance_from_start + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford(self, start, end=None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range(len(self.vertices) - 1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= ( + current_distance + weight + ): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start From 7c0fd36dc4c018b55897a9272a4d3284d86a02c2 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 10:41:39 +0200 Subject: [PATCH 052/143] Complex utils: added manhattan distance sorting --- 2019/complex_utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/2019/complex_utils.py b/2019/complex_utils.py index 51c4cb0..40d19f5 100644 --- a/2019/complex_utils.py +++ b/2019/complex_utils.py @@ -116,6 +116,8 @@ def complex_sort(complexes, mode=""): elif mode == "reading": complexes.sort(key=lambda a: (-a.imag, a.real)) # Sorts by distance from 0,0 (kind of polar coordinates) + elif mode == "manhattan": + complexes.sort(key=lambda a: manhattan_distance(0, a)) else: complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2)) return complexes From 712a7759c38c6fdebdf7fd0fab14b3bdc776c152 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 11:48:16 +0200 Subject: [PATCH 053/143] IntCode class added (Day 5) --- 2019/IntCode.py | 119 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 2019/IntCode.py diff --git a/2019/IntCode.py b/2019/IntCode.py new file mode 100644 index 0000000..f56ae4e --- /dev/null +++ b/2019/IntCode.py @@ -0,0 +1,119 @@ +class IntCode: + instructions = [] + pointer = 0 + state = "Running" + modes = "000" + inputs = [] + outputs = [] + verbose_level = 0 + instr_length = { + "01": 4, + "02": 4, + "03": 2, + "04": 2, + "05": 3, + "06": 3, + "07": 4, + "08": 4, + "99": 1, + } + + def __init__(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + + def reset(self, instructions): + self.instructions = list(map(int, instructions.split(","))) + self.pointer = 0 + self.state = "Running" + + def get_opcode(self): + instr = self.instructions[self.pointer] + opcode_full = "0" * (5 - len(str(instr))) + str(instr) + return opcode_full + + def get_instruction(self, opcode): + return self.instructions[ + self.pointer : self.pointer + self.instr_length[opcode] + ] + + def get_value(self, param_position): + if self.modes[2 - (param_position - 1)] == "0": + return self.instructions[self.instructions[self.pointer + param_position]] + else: + return self.instructions[self.pointer + param_position] + + def op_01(self, instr): + self.instructions[instr[3]] = self.get_value(1) + self.get_value(2) + self.pointer += self.instr_length["01"] + self.state = "Running" + + def op_02(self, instr): + self.instructions[instr[3]] = self.get_value(1) * self.get_value(2) + self.pointer += self.instr_length["02"] + self.state = "Running" + + def op_03(self, instr): + self.instructions[instr[1]] = self.inputs.pop(0) + self.pointer += self.instr_length["03"] + self.state = "Running" + + def op_04(self, instr): + self.outputs.append(self.get_value(1)) + self.pointer += self.instr_length["04"] + self.state = "Running" + + def op_05(self, instr): + if self.get_value(1) != 0: + self.pointer = self.get_value(2) + else: + self.pointer += self.instr_length["05"] + self.state = "Running" + + def op_06(self, instr): + if self.get_value(1) == 0: + self.pointer = self.get_value(2) + else: + self.pointer += self.instr_length["06"] + self.state = "Running" + + def op_07(self, instr): + if self.get_value(1) < self.get_value(2): + self.instructions[instr[3]] = 1 + else: + self.instructions[instr[3]] = 0 + self.pointer += self.instr_length["07"] + self.state = "Running" + + def op_08(self, instr): + if self.get_value(1) == self.get_value(2): + self.instructions[instr[3]] = 1 + else: + self.instructions[instr[3]] = 0 + self.pointer += self.instr_length["08"] + self.state = "Running" + + def op_99(self, instr): + self.pointer += self.instr_length["99"] + self.state = "Stopped" + + def run(self): + while self.state == "Running": + opcode_full = self.get_opcode() + opcode = opcode_full[-2:] + self.modes = opcode_full[:-2] + current_instr = self.get_instruction(opcode) + if self.verbose_level >= 3: + print("Executing", current_instr) + print("Found opcode", opcode_full, opcode, self.modes) + getattr(self, "op_" + opcode)(current_instr) + if self.verbose_level >= 2: + print("Pointer after execution:", self.pointer) + print("Instructions:", ",".join(map(str, self.instructions))) + + def export(self): + instr = ",".join(map(str, self.instructions)) + inputs = ",".join(map(str, self.inputs)) + outputs = ",".join(map(str, self.outputs)) + return ( + "Instructions: " + instr + "\nInputs: " + inputs + "\nOutputs: " + outputs + ) From 4efbb88134e465f6381a2d30ffdc3699aadb0ab3 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 11:48:34 +0200 Subject: [PATCH 054/143] Added days 2019-03, 2019-04, 2019-05 --- 2019/03-Crossed Wires.py | 73 +++++++++++++++++++ 2019/04-Secure Container.py | 77 +++++++++++++++++++++ 2019/05-Sunny with a Chance of Asteroids.py | 70 +++++++++++++++++++ 3 files changed, 220 insertions(+) create mode 100644 2019/03-Crossed Wires.py create mode 100644 2019/04-Secure Container.py create mode 100644 2019/05-Sunny with a Chance of Asteroids.py diff --git a/2019/03-Crossed Wires.py b/2019/03-Crossed Wires.py new file mode 100644 index 0000000..3aae9b1 --- /dev/null +++ b/2019/03-Crossed Wires.py @@ -0,0 +1,73 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """R75,D30,R83,U83,L12,D49,R71,U7,L72 +U62,R66,U55,R34,D71,R55,D58,R83""", + "expected": ["159", "610"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["308", "12934"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +wires = [] +for i in range(len(puzzle_input.split("\n"))): + wire = puzzle_input.split("\n")[i] + position = 0 + wires.append(list()) + for line in wire.split(","): + direction = {"U": north, "D": south, "L": west, "R": east}[line[0]] + for step in range(int(line[1:])): + position += direction + wires[i].append(position) + +common = list(set(wires[0]).intersection(set(wires[1]))) + + +if part_to_test == 1: + common = complex_sort(common, "manhattan") + puzzle_actual_result = int(manhattan_distance(0, common[0])) + + +else: + min_distance = 10 ** 20 + for spot in common: + distance = ( + wires[0].index(spot) + wires[1].index(spot) + 2 + ) # 2 because start is not included + min_distance = min(min_distance, distance) + + puzzle_actual_result = min_distance + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/04-Secure Container.py b/2019/04-Secure Container.py new file mode 100644 index 0000000..662698c --- /dev/null +++ b/2019/04-Secure Container.py @@ -0,0 +1,77 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """112233-112233""", + "expected": ["1", "Unknown"], +} + +test = "real" +test_data[test] = { + "input": "273025-767253", + "expected": ["910", "598"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def has_double(password): + password = str(password) + return any([True for x in "0123456789" if x + x in password]) + + +def numbers_increase(password): + password = str(password) + return all([password[i + 1] >= password[i] for i in range(len(password) - 1)]) + + +def larger_group_test(password): + password = str(password) + doubles = [x for x in "0123456789" if x * 2 in password] + if not doubles: + return True + larger_group = [x for x in doubles for n in range(3, 7) if x * n in password] + return any([x not in larger_group for x in doubles]) + + +if part_to_test == 1: + start, end = map(int, puzzle_input.split("-")) + matches = 0 + for i in range(start, end + 1): + if has_double(i) and numbers_increase(i): + matches += 1 + + puzzle_actual_result = matches + + +else: + start, end = map(int, puzzle_input.split("-")) + matches = 0 + for i in range(start, end + 1): + if has_double(i) and numbers_increase(i) and larger_group_test(i): + matches += 1 + + puzzle_actual_result = matches + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/05-Sunny with a Chance of Asteroids.py b/2019/05-Sunny with a Chance of Asteroids.py new file mode 100644 index 0000000..e72c03a --- /dev/null +++ b/2019/05-Sunny with a Chance of Asteroids.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from IntCode import IntCode + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1101,100,-1,4,0""", + "expected": ["Unknown", "Unknown"], +} +test += 1 +test_data[test] = { + "input": """3,21,1008,21,8,20,1005,20,22,107,8,21,20,1006,20,31,1106,0,36,98,0,0,1002,21,125,20,4,20,1105,1,46,104,999,1105,1,46,1101,1000,1,20,4,20,1105,1,46,98,99""", + "expected": [ + "Unknown", + "output 999 if the input value is below 8, output 1000 if the input value is equal to 8, or output 1001 if the input value is greater than 8", + ], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["15097178", "1558663"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +IntCode.verbose_level = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + computer = IntCode(puzzle_input) + computer.inputs.append(1) + computer.run() + + if computer.state == "Stopped": + puzzle_actual_result = computer.outputs[-1] + + +else: + computer = IntCode(puzzle_input) + computer.inputs.append(5) + computer.run() + + if computer.state == "Stopped": + puzzle_actual_result = computer.outputs[-1] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 0f35a022e1e9b5fb78e6aef247d160ecb268ccb9 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 16:33:07 +0200 Subject: [PATCH 055/143] Added Tree library --- 2019/tree.py | 49 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) create mode 100644 2019/tree.py diff --git a/2019/tree.py b/2019/tree.py new file mode 100644 index 0000000..153514d --- /dev/null +++ b/2019/tree.py @@ -0,0 +1,49 @@ +class Tree: + parent = "" + children = [] + name = "" + + def __init__(self, name, parent="", children=[]): + self.name = name + self.children = [child for child in children if isinstance(child, Tree)] + self.parent = parent + + def __repr__(self): + return self.name + + def add_child(self, child): + if isinstance(child, Tree): + self.children.append(child) + + def count_children(self): + return len(self.children) + + def count_descendants(self): + return len(self.children) + sum( + [child.count_descendants() for child in self.children] + ) + + def get_descendants(self): + return self.children + [child.get_descendants() for child in self.children] + + def get_ancestors(self): + if self.parent == "": + return [] + else: + result = self.parent.get_ancestors() + result.insert(0, self.parent) + return result + + def get_common_ancestor(self, other): + my_parents = [self] + self.get_ancestors() + his_parents = [other] + other.get_ancestors() + common = [x for x in my_parents if x in his_parents] + if not common: + return None + return common[0] + + def get_degree_of_separation(self, other): + my_parents = [self] + self.get_ancestors() + his_parents = [other] + other.get_ancestors() + common = self.get_common_ancestor(other) + return my_parents.index(common) + his_parents.index(common) From d1bc9e6245b26ca3a3296b8040106403cab5c52c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 17:19:51 +0200 Subject: [PATCH 056/143] IntCode class updated (Day 7-compatible) --- 2019/IntCode.py | 59 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 13 deletions(-) diff --git a/2019/IntCode.py b/2019/IntCode.py index f56ae4e..549848c 100644 --- a/2019/IntCode.py +++ b/2019/IntCode.py @@ -1,11 +1,8 @@ class IntCode: - instructions = [] - pointer = 0 - state = "Running" - modes = "000" - inputs = [] - outputs = [] + # Verbosity verbose_level = 0 + + # Count of parameters per opcode instr_length = { "01": 4, "02": 4, @@ -18,14 +15,38 @@ class IntCode: "99": 1, } - def __init__(self, instructions): + def __init__(self, instructions, reference=""): self.instructions = list(map(int, instructions.split(","))) + self.reference = reference + + # Current state + self.pointer = 0 + self.state = "Running" + + # Current instruction modes + self.modes = "000" + + # Inputs and outputs + self.inputs = [] + self.all_inputs = [] + self.outputs = [] def reset(self, instructions): self.instructions = list(map(int, instructions.split(","))) self.pointer = 0 self.state = "Running" + def restart(self): + self.state = "Running" + + def add_input(self, value): + try: + self.inputs += value + self.all_inputs += value + except: + self.inputs.append(value) + self.all_inputs.append(value) + def get_opcode(self): instr = self.instructions[self.pointer] opcode_full = "0" * (5 - len(str(instr))) + str(instr) @@ -53,6 +74,9 @@ def op_02(self, instr): self.state = "Running" def op_03(self, instr): + if len(self.inputs) == 0: + self.state = "Paused" + return self.instructions[instr[1]] = self.inputs.pop(0) self.pointer += self.instr_length["03"] self.state = "Running" @@ -111,9 +135,18 @@ def run(self): print("Instructions:", ",".join(map(str, self.instructions))) def export(self): - instr = ",".join(map(str, self.instructions)) - inputs = ",".join(map(str, self.inputs)) - outputs = ",".join(map(str, self.outputs)) - return ( - "Instructions: " + instr + "\nInputs: " + inputs + "\nOutputs: " + outputs - ) + output = "" + if self.reference != "": + output += "Computer # " + str(self.reference) + output += "\n" + "Instructions: " + ",".join(map(str, self.instructions)) + output += "\n" + "Inputs: " + ",".join(map(str, self.all_inputs)) + output += "\n" + "Outputs: " + ",".join(map(str, self.outputs)) + return output + + def export_io(self): + output = "" + if self.reference != "": + output += "Computer # " + str(self.reference) + output += "\n" + "Inputs: " + ",".join(map(str, self.all_inputs)) + output += "\n" + "Outputs: " + ",".join(map(str, self.outputs)) + return output From 2ec91144939a2a5d37161c77d9501d22ffef140c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 17:20:07 +0200 Subject: [PATCH 057/143] Added days 2019-06 and 2019-07 --- 2019/06-Universal Orbit Map.py | 80 ++++++++++++++++++++++++++ 2019/07-Amplification Circuit.py | 96 ++++++++++++++++++++++++++++++++ 2 files changed, 176 insertions(+) create mode 100644 2019/06-Universal Orbit Map.py create mode 100644 2019/07-Amplification Circuit.py diff --git a/2019/06-Universal Orbit Map.py b/2019/06-Universal Orbit Map.py new file mode 100644 index 0000000..7cdc84f --- /dev/null +++ b/2019/06-Universal Orbit Map.py @@ -0,0 +1,80 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from tree import Tree + +test_data = {} + +test = 1 +test_data[test] = { + "input": """COM)B +B)C +C)D +D)E +E)F +B)G +G)H +D)I +E)J +J)K +K)L +K)YOU +I)SAN""", + "expected": ["42 (without SAN and YOU), 54 (with)", "4"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["151345", "391"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +all_nodes = {"COM": Tree("COM")} +for string in puzzle_input.split("\n"): + orbitee, orbiter = string.split(")") + if orbitee not in all_nodes: + all_nodes[orbitee] = Tree(orbitee) + if orbiter not in all_nodes: + all_nodes[orbiter] = Tree(orbiter) + + all_nodes[orbitee].add_child(all_nodes[orbiter]) + all_nodes[orbiter].parent = all_nodes[orbitee] + +if part_to_test == 1: + nb_orbits = 0 + for node in all_nodes.values(): + nb_orbits += node.count_descendants() + + puzzle_actual_result = nb_orbits + + +else: + puzzle_actual_result = ( + all_nodes["SAN"].get_degree_of_separation(all_nodes["YOU"]) - 2 + ) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/07-Amplification Circuit.py b/2019/07-Amplification Circuit.py new file mode 100644 index 0000000..663f31b --- /dev/null +++ b/2019/07-Amplification Circuit.py @@ -0,0 +1,96 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, itertools + +from complex_utils import * +from IntCode import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """3,15,3,16,1002,16,10,16,1,16,15,15,4,15,99,0,0""", + "expected": ["43210 (from phase setting sequence 4,3,2,1,0)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """3,23,3,24,1002,24,10,24,1002,23,-1,23,101,5,23,23,1,24,23,23,4,23,99,0,0""", + "expected": ["54321 (from phase setting sequence 0,1,2,3,4)", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """3,52,1001,52,-5,52,3,53,1,52,56,54,1007,54,5,55,1005,55,26,1001,54, +-5,54,1105,1,12,1,53,54,53,1008,54,0,55,1001,55,1,55,2,53,55,53,4, +53,1001,56,-1,56,1005,56,6,99,0,0,0,0,10""", + "expected": ["Unknown", "18216 (from phase setting sequence 9,7,8,5,6)"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["929800", "15432220"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + max_signal = 0 + for settings in itertools.permutations("01234"): + amplifiers = [IntCode(puzzle_input, i) for i in range(5)] + for i in range(5): + amplifiers[i].add_input(int(settings[i])) + amplifiers[0].add_input(0) + + amplifiers[0].run() + for i in range(1, 5): + amplifiers[i].add_input(amplifiers[i - 1].outputs[-1]) + amplifiers[i].run() + + max_signal = max(max_signal, amplifiers[4].outputs[-1]) + + puzzle_actual_result = max_signal + + +else: + max_signal = 0 + for settings in itertools.permutations("56789"): + amplifiers = [IntCode(puzzle_input, i) for i in range(5)] + for i in range(5): + amplifiers[i].add_input(int(settings[i])) + amplifiers[0].add_input(0) + + while not all([x.state == "Stopped" for x in amplifiers]): + for i in range(0, 5): + if len(amplifiers[i - 1].outputs) > 0: + amplifiers[i].add_input(amplifiers[i - 1].outputs) + amplifiers[i - 1].outputs = [] + amplifiers[i].restart() + amplifiers[i].run() + + max_signal = max(max_signal, amplifiers[4].outputs[-1]) + + puzzle_actual_result = max_signal + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 15f4d2bf8d04e668e2b33c8f23cc0fb77b029662 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 19:10:39 +0200 Subject: [PATCH 058/143] Added days 2019-08 and 2019-09 --- 2019/08-Space Image Format.py | 70 +++++++++++++++++++++++++++++++++++ 2019/09-Sensor Boost.py | 55 +++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 2019/08-Space Image Format.py create mode 100644 2019/09-Sensor Boost.py diff --git a/2019/08-Space Image Format.py b/2019/08-Space Image Format.py new file mode 100644 index 0000000..5e11755 --- /dev/null +++ b/2019/08-Space Image Format.py @@ -0,0 +1,70 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["2480", "ZYBLH"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +layers = [] +width = 25 +height = 6 +size = width * height +layers = [ + puzzle_input[i * size : i * size + size] for i in range(len(puzzle_input) // size) +] + +if part_to_test == 1: + layers.sort(key=lambda a: a.count("0")) + fewest_zero = layers[0] + puzzle_actual_result = fewest_zero.count("1") * fewest_zero.count("2") + + +else: + image = ["2"] * size + for layer in layers: + image = [image[i] if image[i] != "2" else layer[i] for i in range(len(image))] + + output = "" + for row in range(height): + output += "".join(image[row * width : (row + 1) * width]) + output += "\n" + + output = "\n" + output.replace("2", "x").replace("1", "#").replace("0", " ") + puzzle_actual_result = output + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/09-Sensor Boost.py b/2019/09-Sensor Boost.py new file mode 100644 index 0000000..a002337 --- /dev/null +++ b/2019/09-Sensor Boost.py @@ -0,0 +1,55 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from IntCode import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """109,1,204,-1,1001,100,1,100,1008,100,16,101,1006,101,0,99""", + "expected": [ + "109,1,204,-1,1001,100,1,100,1008,100,16,101,1006,101,0,99", + "Unknown", + ], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["3380552333", "78831"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +computer = IntCode(puzzle_input) +computer.add_input(part_to_test) +computer.run() +if len(computer.outputs) == 1: + puzzle_actual_result = computer.outputs[0] +else: + puzzle_actual_result = "Errors on opcodes : " + ",".join(map(str, computer.outputs)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From da7e4f80e6158960fde13ff07c8bb7cd1e6e3309 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 16 Aug 2020 19:10:50 +0200 Subject: [PATCH 059/143] IntCode class updated (Day 9-compatible) --- 2019/IntCode.py | 71 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 62 insertions(+), 9 deletions(-) diff --git a/2019/IntCode.py b/2019/IntCode.py index 549848c..a0714ee 100644 --- a/2019/IntCode.py +++ b/2019/IntCode.py @@ -12,6 +12,7 @@ class IntCode: "06": 3, "07": 4, "08": 4, + "09": 2, "99": 1, } @@ -22,6 +23,7 @@ def __init__(self, instructions, reference=""): # Current state self.pointer = 0 self.state = "Running" + self.relative_base = 0 # Current instruction modes self.modes = "000" @@ -58,18 +60,63 @@ def get_instruction(self, opcode): ] def get_value(self, param_position): + assert self.modes[2 - (param_position - 1)] in "012" + try: + if self.modes[2 - (param_position - 1)] == "0": + return self.instructions[ + self.instructions[self.pointer + param_position] + ] + elif self.modes[2 - (param_position - 1)] == "1": + return self.instructions[self.pointer + param_position] + else: + return self.instructions[ + self.relative_base + + self.instructions[self.pointer + param_position] + ] + except: + return 0 + + def set_value(self, param_position, value): + assert self.modes[2 - (param_position - 1)] in "02" if self.modes[2 - (param_position - 1)] == "0": - return self.instructions[self.instructions[self.pointer + param_position]] + try: + self.instructions[ + self.instructions[self.pointer + param_position] + ] = value + except: + self.instructions += [0] * ( + self.instructions[self.pointer + param_position] + - len(self.instructions) + + 1 + ) + self.instructions[ + self.instructions[self.pointer + param_position] + ] = value else: - return self.instructions[self.pointer + param_position] + try: + self.instructions[ + self.relative_base + + self.instructions[self.pointer + param_position] + ] = value + except: + self.instructions += [0] * ( + self.relative_base + + self.instructions[self.pointer + param_position] + - len(self.instructions) + + 1 + ) + self.instructions[ + self.relative_base + + self.instructions[self.pointer + param_position] + ] = value def op_01(self, instr): - self.instructions[instr[3]] = self.get_value(1) + self.get_value(2) + self.set_value(3, self.get_value(1) + self.get_value(2)) self.pointer += self.instr_length["01"] self.state = "Running" def op_02(self, instr): - self.instructions[instr[3]] = self.get_value(1) * self.get_value(2) + self.set_value(3, self.get_value(1) * self.get_value(2)) self.pointer += self.instr_length["02"] self.state = "Running" @@ -77,7 +124,7 @@ def op_03(self, instr): if len(self.inputs) == 0: self.state = "Paused" return - self.instructions[instr[1]] = self.inputs.pop(0) + self.set_value(1, self.inputs.pop(0)) self.pointer += self.instr_length["03"] self.state = "Running" @@ -102,20 +149,25 @@ def op_06(self, instr): def op_07(self, instr): if self.get_value(1) < self.get_value(2): - self.instructions[instr[3]] = 1 + self.set_value(3, 1) else: - self.instructions[instr[3]] = 0 + self.set_value(3, 0) self.pointer += self.instr_length["07"] self.state = "Running" def op_08(self, instr): if self.get_value(1) == self.get_value(2): - self.instructions[instr[3]] = 1 + self.set_value(3, 1) else: - self.instructions[instr[3]] = 0 + self.set_value(3, 0) self.pointer += self.instr_length["08"] self.state = "Running" + def op_09(self, instr): + self.relative_base += self.get_value(1) + self.pointer += self.instr_length["09"] + self.state = "Running" + def op_99(self, instr): self.pointer += self.instr_length["99"] self.state = "Stopped" @@ -139,6 +191,7 @@ def export(self): if self.reference != "": output += "Computer # " + str(self.reference) output += "\n" + "Instructions: " + ",".join(map(str, self.instructions)) + output += "\n" + "Relative base: " + str(self.relative_base) output += "\n" + "Inputs: " + ",".join(map(str, self.all_inputs)) output += "\n" + "Outputs: " + ",".join(map(str, self.outputs)) return output From 172914cfa1319957c84940f012779e3fb13d7a6a Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 17 Aug 2020 09:59:53 +0200 Subject: [PATCH 060/143] Complex: Added phase and amplitude methods --- 2019/complex_utils.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/2019/complex_utils.py b/2019/complex_utils.py index 40d19f5..5120b12 100644 --- a/2019/complex_utils.py +++ b/2019/complex_utils.py @@ -1,7 +1,7 @@ """ Small library for complex numbers """ -from math import sqrt +from math import sqrt, atan2 class ReturnTypeWrapper(type): @@ -51,6 +51,12 @@ def __add__(self, no): def __sub__(self, no): return SuperComplex(self.real - no.real, self.imag - no.imag) + def phase(self): + return atan2(self.imag, self.real) + + def amplitude(self): + return sqrt(self.imag ** 2 + self.real ** 2) + j = SuperComplex(1j) From b7e8156a9f44bfd2a1be5299df2d71ff276e1d57 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 17 Aug 2020 10:00:05 +0200 Subject: [PATCH 061/143] Added day 2019-10 --- 2019/10-Monitoring Station.py | 112 ++++++++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 2019/10-Monitoring Station.py diff --git a/2019/10-Monitoring Station.py b/2019/10-Monitoring Station.py new file mode 100644 index 0000000..05be331 --- /dev/null +++ b/2019/10-Monitoring Station.py @@ -0,0 +1,112 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * +from math import pi + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#..##.###...####### +##.############..##. +.#.######.########.# +.###.#######.####.#. +#####.##.#.##.###.## +..#####..#.######### +#################### +#.####....###.#.#.## +##.################# +#####.##.###..####.. +..######..##.####### +####.##.####...##..# +.#####..#.######.### +##...#.##########... +#.##########.####### +.####.#.###.###.#.## +....##.##.###..##### +.#.#.###########.### +#.#.#.#####.####.### +###.##.####.##.#..##""", + "expected": ["210", "802"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["256", "1707"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +grid = pathfinding.Graph() +grid.grid_to_vertices(puzzle_input, wall=".") + +visible_count = [] +for asteroid in grid.vertices: + visible = set() + for other in grid.vertices: + if other == asteroid: + continue + visible.add(SuperComplex(other - asteroid).phase()) + visible_count.append((len(visible), SuperComplex(asteroid))) + +if part_to_test == 1: + puzzle_actual_result = max(visible_count)[0] + + +else: + station = max(visible_count)[1] + targets = {} + + for target in grid.vertices: + if target == station: + continue + vector = SuperComplex(target - station) + order = ( + pi / 2 - vector.phase() + if vector.phase() <= pi / 2 + else 10 * pi / 4 - vector.phase() + ) + try: + targets[order].append((vector.amplitude(), target)) + except: + targets[order] = [(vector.amplitude(), target)] + + phases = list(targets.keys()) + phases.sort() + destroyed = 0 + while destroyed < 200: + for phase in phases: + if phase in targets and len(targets[phase]) > 0: + targets[phase].sort(key=lambda a: a[0]) + target = targets[phase][0][1] + del targets[phase][0] + destroyed += 1 + if destroyed == 200: + break + + puzzle_actual_result = int(target.real * 100 - target.imag) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 3b45a669d61d1ff6102ef7f862792522f6bc93c3 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 17 Aug 2020 21:02:44 +0200 Subject: [PATCH 062/143] Made pathfinding a bit more safe --- 2019/pathfinding.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2019/pathfinding.py b/2019/pathfinding.py index 2a4572a..4ee9a55 100644 --- a/2019/pathfinding.py +++ b/2019/pathfinding.py @@ -86,7 +86,7 @@ def grid_search(self, grid, items): Searches the grid for some items :param string grid: The grid in which to search - :param Boolean items: The items to search + :param list items: The items to search :return: A dictionnary of the items found """ items_found = {} @@ -126,7 +126,7 @@ def vertices_to_grid(self, mark_coords={}, wall="#"): grid += "X" else: try: - grid += self.vertices.get(x + y * j, wall) + grid += str(self.vertices.get(x + y * j, wall)) except AttributeError: if x + y * j in self.vertices: grid += "." From 79014434f86abb3777572c7e00beae756be3cfc6 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 17 Aug 2020 21:03:08 +0200 Subject: [PATCH 063/143] Added days 2019-11, 2019-12 and 2019-13 --- 2019/11-Space Police.py | 74 +++++++++++++++++++ 2019/12-The N-Body Problem.py | 122 ++++++++++++++++++++++++++++++ 2019/13-Care Package.py | 135 ++++++++++++++++++++++++++++++++++ 3 files changed, 331 insertions(+) create mode 100644 2019/11-Space Police.py create mode 100644 2019/12-The N-Body Problem.py create mode 100644 2019/13-Care Package.py diff --git a/2019/11-Space Police.py b/2019/11-Space Police.py new file mode 100644 index 0000000..b8c24a3 --- /dev/null +++ b/2019/11-Space Police.py @@ -0,0 +1,74 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["1934", "RKURGKGK"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +position = 0 +direction = north +if part_to_test == 1: + panels = {0: 0} +else: + panels = {0: 1} + + +computer = IntCode.IntCode(puzzle_input) + +while computer.state != "Stopped": + if position in panels: + computer.add_input(panels[position]) + else: + computer.add_input(0) + computer.restart() + computer.run() + color, dir = computer.outputs[-2:] + panels[position] = color + direction *= ( + relative_directions["left"] if dir == 0 else relative_directions["right"] + ) + position += direction + +if part_to_test == 1: + puzzle_actual_result = len(panels) +else: + grid = pathfinding.Graph() + grid.vertices = {x: "X" if panels[x] == 1 else " " for x in panels} + puzzle_actual_result = "\n" + grid.vertices_to_grid(wall=" ") + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/12-The N-Body Problem.py b/2019/12-The N-Body Problem.py new file mode 100644 index 0000000..76c8383 --- /dev/null +++ b/2019/12-The N-Body Problem.py @@ -0,0 +1,122 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, re, math, copy + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """ + + +""", + "expected": ["179 after 10 steps", "2772"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["12773", "306798770391636"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +stars = [] +for string in puzzle_input.split("\n"): + x, y, z = map(int, re.findall("[-0-9]{1,}", string)) + stars.append([x, y, z, 0, 0, 0]) + +if part_to_test == 1: + for step in range(1000): + for star_id in range(len(stars)): + for coord in range(3): + stars[star_id][3 + coord] += sum( + [1 for other in stars if stars[star_id][coord] < other[coord]] + ) + stars[star_id][3 + coord] += sum( + [-1 for other in stars if stars[star_id][coord] > other[coord]] + ) + + for star_id in range(len(stars)): + for coord in range(3): + stars[star_id][coord] += stars[star_id][3 + coord] + + energy = sum( + [ + (abs(x) + abs(y) + abs(z)) * (abs(dx) + abs(dy) + abs(dz)) + for (x, y, z, dx, dy, dz) in stars + ] + ) + puzzle_actual_result = energy + +else: + + # 1st trick: For this part, do the computation on each axis independently (since they're independent) + # 2nd trick: the function state => next state is invertible, so any repetition will go through the initial state (we can't have 3>0>1>0>1>0>1, it has to be something like 3>0>1>3>0>1) + repeats = [] + for coord in range(3): + step = -1 + repeat = 0 + stars_pos_vel = [ + [stars[star_id][coord], stars[star_id][coord + 3]] + for star_id in range(len(stars)) + ] + init_stars_pos_vel = [ + [stars[star_id][coord], stars[star_id][coord + 3]] + for star_id in range(len(stars)) + ] + + while repeat == 0: # and step < 20: + step += 1 + for star_id in range(len(stars)): + stars_pos_vel[star_id][1] += sum( + [ + 1 + for other in stars_pos_vel + if stars_pos_vel[star_id][0] < other[0] + ] + ) + stars_pos_vel[star_id][1] -= sum( + [ + 1 + for other in stars_pos_vel + if stars_pos_vel[star_id][0] > other[0] + ] + ) + + for star_id in range(len(stars)): + stars_pos_vel[star_id][0] += stars_pos_vel[star_id][1] + + if stars_pos_vel == init_stars_pos_vel: + repeat = step + 1 + + repeats.append(repeat) + + lcm = repeats[0] + for val in repeats: + lcm = lcm * val // math.gcd(lcm, val) + + puzzle_actual_result = lcm + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/13-Care Package.py b/2019/13-Care Package.py new file mode 100644 index 0000000..8d2e1c8 --- /dev/null +++ b/2019/13-Care Package.py @@ -0,0 +1,135 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["462", "23981"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +tiles = {0: " ", 1: "#", 2: "ø", 3: "_", 4: "o"} +grid = pathfinding.Graph() +computer = IntCode.IntCode(puzzle_input) + +if part_to_test == 1: + computer.run() + grid.vertices = {} + for i in range(len(computer.outputs) // 3): + position = SuperComplex( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + grid.vertices[position] = tiles[computer.outputs[i * 3 + 2]] + + puzzle_actual_result = sum([1 for val in grid.vertices.values() if val == "ø"]) + + +else: + computer.instructions[0] = 2 + blocks_left = 1 + score = 0 + + vertices = {} + + while blocks_left > 0 and computer.state != "Failure": + computer.run() + + # Check if we can still play + blocks_left = 0 + ball_position = 0 + paddle_position = 0 + for i in range(len(computer.outputs) // 3): + + vertices[ + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ] = computer.outputs[i * 3 + 2] + # The ball has not fallen + if computer.outputs[i * 3 + 2] == 4: + ball_position = ( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + if ball_position.imag < -21: + print("Failed") + computer.state = "Failure" + break + # Check the score + elif computer.outputs[i * 3] == -1 and computer.outputs[i * 3 + 1] == 0: + score = computer.outputs[i * 3 + 2] + + # Store the paddle position + elif computer.outputs[i * 3 + 2] == 3: + paddle_position = ( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + + # There are still blocks to break + blocks_left = len([x for x in vertices if vertices[x] == 2]) + + # Move paddle + if paddle_position.real < ball_position.real: + joystick = 1 + elif paddle_position.real > ball_position.real: + joystick = -1 + else: + joystick = 0 + computer.add_input(joystick) + + if verbose_level >= 2: + print( + "Movements", + len(computer.all_inputs), + " - Score", + score, + " - Blocks left", + blocks_left, + " - Ball", + ball_position, + " - Paddle", + paddle_position, + " - Direction", + joystick, + ) + + # 'Restart' the computer to process the input + computer.restart() + + # Outputs the grid (just for fun) + grid.vertices = {x: tiles.get(vertices[x], vertices[x]) for x in vertices} + print(grid.vertices_to_grid()) + + puzzle_actual_result = score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 2ebcb1d445044d22c7230e9e3822988353609ad1 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 19 Aug 2020 21:10:11 +0200 Subject: [PATCH 064/143] Added days 2019-14 and 2019-15 --- 2019/14-Space Stoichiometry.py | 130 +++++++++++++++++++++++++++++++++ 2019/15-Oxygen System.py | 121 ++++++++++++++++++++++++++++++ 2 files changed, 251 insertions(+) create mode 100644 2019/14-Space Stoichiometry.py create mode 100644 2019/15-Oxygen System.py diff --git a/2019/14-Space Stoichiometry.py b/2019/14-Space Stoichiometry.py new file mode 100644 index 0000000..ecf4475 --- /dev/null +++ b/2019/14-Space Stoichiometry.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, re + +from complex_utils import * +from math import ceil + +test_data = {} + +test = 1 +test_data[test] = { + "input": """10 ORE => 10 A +1 ORE => 1 B +7 A, 1 B => 1 C +7 A, 1 C => 1 D +7 A, 1 D => 1 E +7 A, 1 E => 1 FUEL +6 HTRFP, 1 FVXV, 4 JKLNF, 1 TXFCS, 2 PXBP => 4 JRBFT""", + "expected": ["31", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """157 ORE => 5 NZVS +165 ORE => 6 DCFZ +44 XJWVT, 5 KHKGT, 1 QDVJ, 29 NZVS, 9 GPVTF, 48 HKGWZ => 1 FUEL +12 HKGWZ, 1 GPVTF, 8 PSHF => 9 QDVJ +179 ORE => 7 PSHF +177 ORE => 5 HKGWZ +7 DCFZ, 7 PSHF => 2 XJWVT +165 ORE => 2 GPVTF +3 DCFZ, 7 NZVS, 5 HKGWZ, 10 PSHF => 8 KHKGT""", + "expected": ["13312", "82892753"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["1037742", "1572358"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def execute_reaction(stock, reaction, required): + global ore_required + target = reaction[1] + nb_reactions = ceil((required[target] - stock.get(target, 0)) / reaction[0]) + + # Impact on target material + stock[target] = stock.get(target, 0) + nb_reactions * reaction[0] - required[target] + del required[target] + + # Impact on other materials + for i in range(len(reaction[2]) // 2): + nb_required, mat = reaction[2][i * 2 : i * 2 + 2] + nb_required = int(nb_required) * nb_reactions + if mat == "ORE" and part_to_test == 1: + ore_required += nb_required + elif stock.get(mat, 0) >= nb_required: + stock[mat] -= nb_required + else: + missing = nb_required - stock.get(mat, 0) + stock[mat] = 0 + required[mat] = required.get(mat, 0) + missing + + +reactions = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + + source, target = string.split(" => ") + nb, target = target.split(" ") + nb = int(nb) + + sources = source.replace(",", "").split(" ") + + reactions[target] = (nb, target, sources) + + +if part_to_test == 1: + required = {"FUEL": 1} + ore_required = 0 + stock = {} + while len(required) > 0: + material = list(required.keys())[0] + execute_reaction(stock, reactions[material], required) + + puzzle_actual_result = ore_required + + +else: + below, above = 1000000000000 // 1037742, 1000000000000 + + while below != above - 1: + required = {"FUEL": (below + above) // 2} + stock = {"ORE": 1000000000000} + while len(required) > 0 and "ORE" not in required: + material = list(required.keys())[0] + execute_reaction(stock, reactions[material], required) + + if stock["ORE"] == 0 or "ORE" in required: + above = (below + above) // 2 + else: + below = (below + above) // 2 + + puzzle_actual_result = below + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/15-Oxygen System.py b/2019/15-Oxygen System.py new file mode 100644 index 0000000..92e4775 --- /dev/null +++ b/2019/15-Oxygen System.py @@ -0,0 +1,121 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode, copy + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["366", "384"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def breadth_first_search(self, start, end=None): + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start[0]: 0} + self.came_from = {start[0]: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + + try: + neighbors = self.neighbors(vertex) + except pathfinding.TargetFound as e: + raise pathfinding.TargetFound(current_distance, e.args[0]) + + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor[0] in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor[0]] = current_distance + self.came_from[neighbor[0]] = vertex[0] + + +def neighbors(self, vertex): + position, program = vertex + possible = [] + neighbors = [] + for dir in directions_straight: + if position + dir not in self.vertices: + possible.append(dir) + new_program = copy.deepcopy(program) + new_program.add_input(movements[dir]) + new_program.restart() + new_program.run() + result = new_program.outputs.pop() + if result == 2: + self.vertices[position + dir] = "O" + if not start_from_oxygen: + raise pathfinding.TargetFound(new_program) + elif result == 1: + self.vertices[position + dir] = "." + neighbors.append([position + dir, new_program]) + else: + self.vertices[position + dir] = "#" + return neighbors + + +pathfinding.Graph.breadth_first_search = breadth_first_search +pathfinding.Graph.neighbors = neighbors + + +movements = {north: 1, south: 2, west: 3, east: 4} +position = 0 +droid = IntCode.IntCode(puzzle_input) +start_from_oxygen = False + +grid = pathfinding.Graph() +grid.vertices = {} + +status = 0 +try: + grid.breadth_first_search((0, droid)) +except pathfinding.TargetFound as e: + if part_to_test == 1: + puzzle_actual_result = e.args[0] + else: + start_from_oxygen = True + oxygen_program = e.args[1] + grid.reset_search() + grid.vertices = {} + grid.breadth_first_search((0, oxygen_program)) + puzzle_actual_result = max(grid.distance_from_start.values()) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 5bd2006803251cb1110c4898ad2be7292a5df901 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 20 Aug 2020 18:40:56 +0200 Subject: [PATCH 065/143] Added days 2019-16 and 2019-17 --- 2019/16-Flawed Frequency Transmission.py | 102 +++++++++++++++ 2019/17-Set and Forget.py | 152 +++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 2019/16-Flawed Frequency Transmission.py create mode 100644 2019/17-Set and Forget.py diff --git a/2019/16-Flawed Frequency Transmission.py b/2019/16-Flawed Frequency Transmission.py new file mode 100644 index 0000000..a011b5c --- /dev/null +++ b/2019/16-Flawed Frequency Transmission.py @@ -0,0 +1,102 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """12345678""", + "expected": ["01029498 after 4 phases", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """80871224585914546619083218645595""", + "expected": ["24176176", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """03036732577212944063491565474664""", + "expected": ["Unknown", "84462026"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["27229269", "26857164"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +base_pattern = [0, 1, 0, -1] + +if part_to_test == 1: + signal = [int(x) for x in puzzle_input] + + for phase in range(100): + output = [0] * len(signal) + for i in range(len(signal)): + pattern = [] + for j in range(len(base_pattern)): + pattern += [base_pattern[j]] * (i + 1) + + while len(pattern) < len(signal) + 1: + pattern += pattern + del pattern[0] + + output[i] = sum([pattern[j] * signal[j] for j in range(len(signal))]) + output[i] = abs(output[i]) % 10 + signal = output[:] + + puzzle_actual_result = "".join(map(str, output[:8])) + + +else: + # The signal's length is 650 * 10000 = 6500000 + # The first 7 digits of the input are 5978261 + # Therefore, the first number to be calculated will ignore the first 5978261 of the input + # Also, since 5978261 < 6500000 < 5978261*2, the part with '0, -1' in the pattern is after the signal's length + # Therefore it can be ignored + signal = [int(x) for x in puzzle_input] * 10 ** 4 + start = int(puzzle_input[:7]) + signal = signal[start:] + + sum_signal = sum([int(x) for x in puzzle_input]) % 10 + len_signal = len(puzzle_input) + + output = [0] * len(signal) + + for phase in range(100): + output[-1] = signal[-1] + for i in range(1, len(signal)): + output[-i - 1] = output[-i] + signal[-i - 1] + + signal = [x % 10 for x in output] + + puzzle_actual_result = "".join(map(str, signal[:8])) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/17-Set and Forget.py b/2019/17-Set and Forget.py new file mode 100644 index 0000000..bb827da --- /dev/null +++ b/2019/17-Set and Forget.py @@ -0,0 +1,152 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["5068", "1415975"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 0 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +position = 0 + +droid = IntCode.IntCode(puzzle_input) +droid.run() +grid = [] +for output in droid.outputs: + if chr(output) == "#": + grid.append(position) + elif chr(output) in ["^", "v", ">", "<"]: + droid_pos = [position, accent_to_dir[chr(output)]] + + if chr(output) == "\n": + position = j * (position.imag - 1) + else: + position += 1 + + +if part_to_test == 1: + alignment_parameter = 0 + for x in range(1, int(max_real(grid))): + for y in range(int(min_imag(grid)), -1): + if x + y * j in grid: + if all([x + y * j + dir in grid for dir in directions_straight]): + alignment_parameter += x * -y + + puzzle_actual_result = alignment_parameter + + +else: + steps = [] + visited = [] + + # Find the path, in the long form (L,12,R,8,.....) + while True: + position, direction = droid_pos + visited.append(position) + if position + direction in grid: + steps[-1] += 1 + droid_pos[0] += droid_pos[1] + else: + option = [ + (turn[0].upper(), direction * relative_directions[turn]) + for turn in relative_directions + if position + direction * relative_directions[turn] in grid + if position + direction * relative_directions[turn] not in visited + ] + if len(option) > 1: + print("error") + raise Exception(position, direction, option) + + if option: + option = option[0] + steps += [option[0], 1] + droid_pos[1] = option[1] + droid_pos[0] += droid_pos[1] + else: + break + + steps = list(map(str, steps)) + steps_inline = ",".join(steps) + + # Shorten the path + subprograms = [] + nb_to_letter = {0: "A", 1: "B", 2: "C"} + + offset = 0 + for i in range(3): + while len(subprograms) == i: + nb_steps = min(20, len(steps) - offset) + subprogram = steps[offset : offset + nb_steps] + subprogram_inline = ",".join(subprogram) + + # The limits of 3 is arbitrary + while ( + steps_inline.count(subprogram_inline) < 3 or len(subprogram_inline) > 20 + ): + # Shorten subprogram for test + if len(subprogram) <= 2: + break + else: + if subprogram[-1] in ("A", "B", "C"): + del subprogram[-1] + else: + del subprogram[-2:] + + subprogram_inline = ",".join(subprogram) + + # Found one! + if steps_inline.count(subprogram_inline) >= 3 and len(subprogram) > 2: + subprograms.append(subprogram_inline) + steps_inline = steps_inline.replace(subprogram_inline, nb_to_letter[i]) + steps = steps_inline.split(",") + else: + if steps[offset] in ["A", "B", "C"]: + offset += 1 + else: + offset += 2 + offset = 0 + + # Now send all that to the robot + droid.instructions[0] = 2 + inputs = ( + steps_inline + "\n" + "\n".join(subprograms) + "\nn\n" + ) # the last n is for the video + for letter in inputs: + droid.add_input(ord(letter)) + droid.restart() + droid.run() + + puzzle_actual_result = droid.outputs.pop() + if verbose_level: + for output in droid.outputs: + print(chr(output), end="") + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From d35d457cd03829b453ea69d9b0408bc2c6d79d46 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 20 Aug 2020 18:41:22 +0200 Subject: [PATCH 066/143] Complex utils: added directions as symbols: < > ^ v --- 2019/complex_utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/2019/complex_utils.py b/2019/complex_utils.py index 5120b12..4df2e78 100644 --- a/2019/complex_utils.py +++ b/2019/complex_utils.py @@ -78,6 +78,10 @@ def amplitude(self): southwest, ] +# Easy way of representing direction +accent_to_dir = {"^": north, "v": south, ">": east, "<": west} +dir_to_accent = {accent_to_dir[x]: x for x in accent_to_dir} + # To be multiplied by the current cartinal direction relative_directions = { "left": j, From dd568b50f526b4f1cab10f20add3dc9763f80194 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 22 Aug 2020 16:34:53 +0200 Subject: [PATCH 067/143] Pathfinding: Added optimization for Dijkstra --- 2019/pathfinding.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/2019/pathfinding.py b/2019/pathfinding.py index 4ee9a55..e7df628 100644 --- a/2019/pathfinding.py +++ b/2019/pathfinding.py @@ -162,7 +162,10 @@ def add_walls(self, vertices): for vertex in vertices: if vertex in self.edges: del self.edges[vertex] - self.vertices.remove(vertex) + if isinstance(self.vertices, list): + self.vertices.remove(vertex) + else: + del self.vertices[vertex] changed = True self.edges = { @@ -489,6 +492,7 @@ def dijkstra(self, start, end=None): heapq.heapify(frontier) self.distance_from_start = {start: 0} self.came_from = {start: None} + min_distance = float("inf") while frontier: current_distance, vertex = heapq.heappop(frontier) @@ -497,6 +501,10 @@ def dijkstra(self, start, end=None): if not neighbors: continue + # No need to explore neighbors if we already found a shorter path to the end + if current_distance > min_distance: + continue + for neighbor, weight in neighbors.items(): # We've already checked that node, and it's not better now if neighbor in self.distance_from_start and self.distance_from_start[ @@ -511,6 +519,9 @@ def dijkstra(self, start, end=None): self.distance_from_start[neighbor] = current_distance + weight self.came_from[neighbor] = vertex + if neighbor == end: + min_distance = min(min_distance, current_distance + weight) + return end is None or end in self.distance_from_start def a_star_search(self, start, end=None): From 7046de16ae2832585296f708352ad575ecb999a7 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 22 Aug 2020 17:02:27 +0200 Subject: [PATCH 068/143] Added day 2019-18 --- 2019/18-Many-Worlds Interpretation.py | 332 ++++++++++++++++++++++++++ 1 file changed, 332 insertions(+) create mode 100644 2019/18-Many-Worlds Interpretation.py diff --git a/2019/18-Many-Worlds Interpretation.py b/2019/18-Many-Worlds Interpretation.py new file mode 100644 index 0000000..9328add --- /dev/null +++ b/2019/18-Many-Worlds Interpretation.py @@ -0,0 +1,332 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, heapq + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """######### +#b.A.@.a# +#########""", + "expected": ["8", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """######################## +#f.D.E.e.C.b.A.@.a.B.c.# +######################.# +#d.....................# +########################""", + "expected": ["86", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """######################## +#...............b.C.D.f# +#.###################### +#.....@.a.B.c.d.A.e.F.g# +########################""", + "expected": ["132", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """################# +#i.G..c...e..H.p# +########.######## +#j.A..b...f..D.o# +########@######## +#k.E..a...g..B.n# +########.######## +#l.F..d...h..C.m# +#################""", + "expected": ["136", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """######################## +#@..............ac.GI.b# +###d#e#f################ +###A#B#C################ +###g#h#i################ +########################""", + "expected": ["81", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """####### +#a.#Cd# +##...## +##.@.## +##...## +#cB#Ab# +#######""", + "expected": ["Unknown", "8"], +} + +test += 1 +test_data[test] = { + "input": """############# +#DcBa.#.GhKl# +#.###...#I### +#e#d#.@.#j#k# +###C#...###J# +#fEbA.#.FgHi# +#############""", + "expected": ["Unknown", "32"], +} + +test += 1 +test_data[test] = { + "input": """############# +#g#f.D#..h#l# +#F###e#E###.# +#dCba...BcIJ# +#####.@.##### +#nK.L...G...# +#M###N#H###.# +#o#m..#i#jk.# +#############""", + "expected": ["Unknown", "72"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["4844", "Unknown"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + self.vertices = {} + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices[x - y * j] = line[x] + y += 1 + + for source in self.vertices: + for direction in directions_straight: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + +pathfinding.Graph.grid_to_vertices = grid_to_vertices + + +def breadth_first_search(self, start, end=None): + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # Stop search when reaching another object + if self.vertices[vertex] not in (".", "@") and vertex != start: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + +pathfinding.Graph.breadth_first_search = breadth_first_search + + +def neighbors_part1(self, vertex): + neighbors = {} + for target_item in edges[vertex[0]]: + if target_item == "@": + neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item] + elif target_item == target_item.lower(): + if target_item in vertex[1]: + neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item] + else: + keys = "".join(sorted([x for x in vertex[1]] + [target_item])) + neighbors[(target_item, keys)] = edges[vertex[0]][target_item] + else: + if target_item.lower() in vertex[1]: + neighbors[(target_item, vertex[1])] = edges[vertex[0]][target_item] + else: + continue + + return neighbors + + +def neighbors_part2(self, vertex): + neighbors = {} + for robot in vertex[0]: + for target_item in edges[robot]: + new_position = vertex[0].replace(robot, target_item) + distance = edges[robot][target_item] + if target_item in "1234": + neighbors[(new_position, vertex[1])] = distance + elif target_item.islower(): + if target_item in vertex[1]: + neighbors[(new_position, vertex[1])] = distance + else: + keys = "".join(sorted([x for x in vertex[1]] + [target_item])) + neighbors[(new_position, keys)] = distance + else: + if target_item.lower() in vertex[1]: + neighbors[(new_position, vertex[1])] = distance + + return neighbors + + +# Only the WeightedGraph method is replaced, so that it doesn't impact the first search +if part_to_test == 1: + pathfinding.WeightedGraph.neighbors = neighbors_part1 +else: + pathfinding.WeightedGraph.neighbors = neighbors_part2 + + +def dijkstra(self, start, end=None): + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + min_distance = float("inf") + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + if current_distance > min_distance: + continue + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # print (vertex, min_distance, len(self.distance_from_start)) + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if len(neighbor[1]) == nb_keys: + min_distance = min(min_distance, current_distance + weight) + + return end is None or end in self.distance_from_start + + +pathfinding.WeightedGraph.dijkstra = dijkstra + + +maze = pathfinding.Graph() +maze.grid_to_vertices(puzzle_input) + +# First, simplify the maze to have only the important items (@, keys, doors) +items = "abcdefghijklmnopqrstuvwxyz" + "abcdefghijklmnopqrstuvwxyz".upper() + "@" +items = maze.grid_search(puzzle_input, items) +nb_keys = len([x for x in items if x in "abcdefghijklmnopqrstuvwxyz"]) + +if part_to_test == 2: + # Separate the start point + start = items["@"][0] + del items["@"] + items["1"] = [start + northwest] + items["2"] = [start + northeast] + items["3"] = [start + southwest] + items["4"] = [start + southeast] + + for dir in directions_straight + [0]: + maze.add_walls([start + dir]) + + +edges = {} +for item in items: + maze.reset_search() + + maze.breadth_first_search(items[item][0]) + edges[item] = {} + for other_item in items: + if other_item == item: + continue + if items[other_item][0] in maze.distance_from_start: + edges[item][other_item] = maze.distance_from_start[items[other_item][0]] + + +# Then, perform Dijkstra on the simplified graph +graph = pathfinding.WeightedGraph() +graph.edges = edges +graph.reset_search() +if part_to_test == 1: + graph.dijkstra(("@", "")) +else: + graph.dijkstra(("1234", "")) + +puzzle_actual_result = min( + [ + graph.distance_from_start[x] + for x in graph.distance_from_start + if len(x[1]) == nb_keys + ] +) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 4c612b23d45b2260c4368ed0d7e9e249f7a3c4cd Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 24 Aug 2020 21:39:54 +0200 Subject: [PATCH 069/143] IntCode: Corrected reset --- 2019/IntCode.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/2019/IntCode.py b/2019/IntCode.py index a0714ee..ca1eda4 100644 --- a/2019/IntCode.py +++ b/2019/IntCode.py @@ -35,6 +35,9 @@ def __init__(self, instructions, reference=""): def reset(self, instructions): self.instructions = list(map(int, instructions.split(","))) + self.inputs = [] + self.all_inputs = [] + self.outputs = [] self.pointer = 0 self.state = "Running" From bb7926951671f0606af94132da4177c4b3d4ef11 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 24 Aug 2020 21:40:11 +0200 Subject: [PATCH 070/143] Pathfinding: Added a small fix (ugly but works) --- 2019/pathfinding.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/2019/pathfinding.py b/2019/pathfinding.py index e7df628..e8f47e7 100644 --- a/2019/pathfinding.py +++ b/2019/pathfinding.py @@ -513,7 +513,12 @@ def dijkstra(self, start, end=None): continue # Adding for future examination - heapq.heappush(frontier, (current_distance + weight, neighbor)) + if type(neighbor) == complex: + heapq.heappush( + frontier, (current_distance + weight, SuperComplex(neighbor)) + ) + else: + heapq.heappush(frontier, (current_distance + weight, neighbor)) # Adding for final search self.distance_from_start[neighbor] = current_distance + weight From 3a6c96c03efc62928a8082b3c21fdb6b20cacc37 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 24 Aug 2020 21:40:39 +0200 Subject: [PATCH 071/143] Added days 2019-19 and 2019-20 --- 2019/19-Tractor Beam.py | 160 +++++++++++++++++++++++++++ 2019/20-Donut Maze.py | 233 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 393 insertions(+) create mode 100644 2019/19-Tractor Beam.py create mode 100644 2019/20-Donut Maze.py diff --git a/2019/19-Tractor Beam.py b/2019/19-Tractor Beam.py new file mode 100644 index 0000000..c33cf36 --- /dev/null +++ b/2019/19-Tractor Beam.py @@ -0,0 +1,160 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode, math + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["169", "7001134"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + beam = IntCode.IntCode(puzzle_input) + + affected = 0 + for x in range(50): + for y in range(50): + beam.reset(puzzle_input) + beam.add_input(x) + beam.add_input(y) + beam.run() + affected += beam.outputs.pop() + + puzzle_actual_result = affected + + +else: + beam = IntCode.IntCode(puzzle_input) + known_points = {} + + def check_tractor(position): + if position not in known_points: + beam.reset(puzzle_input) + beam.add_input(position.real) + beam.add_input(-position.imag) + beam.run() + known_points[position] = beam.outputs.pop() + return known_points[position] == 1 + + # If we call alpha the angle from vertical to the lowest part of the beam + # And beta the angle from vertical to the highest part of the beam + # And x, y the target position + # Then we have: + # x + 100 = y*tan(beta) + # x = (y+100)*tan(alpha) + # Therefore: + # y = 100*(tan (alpha) - 1) / (tan(beta) - tan(alpha)) + # x = y * tan(beta) - 100 + + # First, get an approximation of alpha and beta + def search_x(direction): + y = 1000 + x = 0 if direction == 1 else 10 ** 4 + resolution = 100 + while True: + if check_tractor(x + resolution - j * y) == 1: + if resolution == 1: + break + resolution //= 2 + else: + x += resolution * direction + return x + + alpha = math.atan(search_x(1) / 1000) + beta = math.atan(search_x(-1) / 1000) + + # Then, math! + # Note: We look for size 150 as a safety + y = 150 * (math.tan(alpha) + 1) / (math.tan(beta) - math.tan(alpha)) + x = y * math.tan(beta) - 150 + position = int(x) - int(y) * j + + def corners(position): + # We need to check only those 2 positions + return [position + 99, position - 99 * j] + + valid_position = 0 + checked_positions = [] + best_position = position + resolution = 100 + + while True: + box = corners(position) + checked_positions.append(position) + + new_position = position + if check_tractor(box[0]) and check_tractor(box[1]): + if manhattan_distance(0, best_position) > manhattan_distance(0, position): + best_position = position + # If I move the box just by 1, it fails + if ( + not check_tractor(box[0] + 1) + and not check_tractor(box[0] + 1 * j) + and not check_tractor(box[1] + 1 * j) + and not check_tractor(box[1] + 1 * j) + ): + break + new_position += resolution * j + elif check_tractor(box[0]): + new_position += resolution + elif check_tractor(box[1]): + new_position -= resolution + else: + new_position -= resolution * j + + # This means we have already checked the new position + # So, either we reduce the resolution, or we check closer + if new_position in checked_positions: + if resolution != 1: + resolution //= 2 + else: + # This means we are close + # So now, check the 10*10 grid closer to the emitter + found = False + for dx in range(10, 0, -1): + for dy in range(10, 0, -1): + test = best_position - dx + dy * j + box = corners(test) + if check_tractor(box[0]) and check_tractor(box[1]): + new_position = test + found = True + break + + if not found: + break + position = new_position + puzzle_actual_result = int(best_position.real * 10 ** 4 - best_position.imag) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/20-Donut Maze.py b/2019/20-Donut Maze.py new file mode 100644 index 0000000..92fc552 --- /dev/null +++ b/2019/20-Donut Maze.py @@ -0,0 +1,233 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """ A + A + #################.############# + #.#...#...................#.#.# + #.#.#.###.###.###.#########.#.# + #.#.#.......#...#.....#.#.#...# + #.#########.###.#####.#.#.###.# + #.............#.#.....#.......# + ###.###########.###.#####.#.#.# + #.....# A C #.#.#.# + ####### S P #####.# + #.#...# #......VT + #.#.#.# #.##### + #...#.# YN....#.# + #.###.# #####.# +DI....#.# #.....# + #####.# #.###.# +ZZ......# QG....#..AS + ###.### ####### +JO..#.#.# #.....# + #.#.#.# ###.#.# + #...#..DI BU....#..LF + #####.# #.##### +YN......# VT..#....QG + #.###.# #.###.# + #.#...# #.....# + ###.### J L J #.#.### + #.....# O F P #.#...# + #.###.#####.#.#####.#####.###.# + #...#.#.#...#.....#.....#.#...# + #.#####.###.###.#.#.#########.# + #...#.#.....#...#.#.#.#.....#.# + #.###.#####.###.###.#.#.####### + #.#.........#...#.............# + #########.###.###.############# + B J C + U P P """, + "expected": ["58", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """ Z L X W C + Z P Q B K + ###########.#.#.#.#######.############### + #...#.......#.#.......#.#.......#.#.#...# + ###.#.#.#.#.#.#.#.###.#.#.#######.#.#.### + #.#...#.#.#...#.#.#...#...#...#.#.......# + #.###.#######.###.###.#.###.###.#.####### + #...#.......#.#...#...#.............#...# + #.#########.#######.#.#######.#######.### + #...#.# F R I Z #.#.#.# + #.###.# D E C H #.#.#.# + #.#...# #...#.# + #.###.# #.###.# + #.#....OA WB..#.#..ZH + #.###.# #.#.#.# +CJ......# #.....# + ####### ####### + #.#....CK #......IC + #.###.# #.###.# + #.....# #...#.# + ###.### #.#.#.# +XF....#.# RF..#.#.# + #####.# ####### + #......CJ NM..#...# + ###.#.# #.###.# +RE....#.# #......RF + ###.### X X L #.#.#.# + #.....# F Q P #.#.#.# + ###.###########.###.#######.#########.### + #.....#...#.....#.......#...#.....#.#...# + #####.#.###.#######.#######.###.###.#.#.# + #.......#.......#.#.#.#.#...#...#...#.#.# + #####.###.#####.#.#.#.#.###.###.#.###.### + #.......#.....#.#...#...............#...# + #############.#.#.###.################### + A O F N + A A D M """, + "expected": ["Unknown", "396"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["642", "7492"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 2 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): + self.vertices = {} + y = 0 + + for line in grid.splitlines(): + for x in range(len(line)): + if line[x] != wall: + self.vertices[x - y * j] = line[x] + y += 1 + + for source in self.vertices: + for direction in directions_straight: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source][target] = 1 + else: + self.edges[source] = {target: 1} + + return True + + +pathfinding.WeightedGraph.grid_to_vertices = grid_to_vertices + + +grid = pathfinding.WeightedGraph() +grid.grid_to_vertices(puzzle_input.replace(" ", "#")) +width, height = max_real(grid.vertices), -min_imag(grid.vertices) +letters = grid.grid_search(puzzle_input, "abcdefghijklmnopqrstuvwxyz".upper()) +portals = {} +for letter in letters: + for position in letters[letter]: + # Vertical portal + if ( + grid.vertices.get(position + south, "#") + in "abcdefghijklmnopqrstuvwxyz".upper() + ): + portal = letter + grid.vertices[position + south] + if grid.vertices.get(position + south * 2, "#") == ".": + portal_position = position + south * 2 + else: + portal_position = position - south + + # Horizontal portal + elif ( + grid.vertices.get(position + east, "#") + in "abcdefghijklmnopqrstuvwxyz".upper() + ): + portal = letter + grid.vertices[position + east] + if grid.vertices.get(position + east * 2, "#") == ".": + portal_position = position + east * 2 + else: + portal_position = position - east + else: + continue + + portal_position = SuperComplex(portal_position) + + # Find whether we're at the center or not (I don't care for AA or ZZ) + if portal in ("AA", "ZZ"): + portals[portal] = portal_position + elif portal_position.real == 2 or portal_position.real == width - 2: + portals[(portal, "out")] = portal_position + elif portal_position.imag == -2 or portal_position.imag == -(height - 2): + portals[(portal, "out")] = portal_position + else: + portals[(portal, "in")] = portal_position + + +if part_to_test == 1: + for portal in portals: + if len(portal) == 2 and portal[1] == "in": + portal_in = portals[portal] + portal_out = portals[(portal[0], "out")] + grid.edges[portal_in][portal_out] = 1 + grid.edges[portal_in][portal_out] = 1 + + grid.dijkstra(portals["AA"], portals["ZZ"]) + puzzle_actual_result = grid.distance_from_start[portals["ZZ"]] + + +else: + edges = {} + for portal in portals: + grid.reset_search() + grid.dijkstra(portals[portal]) + for other_portal in portals: + if portal == other_portal: + continue + if not portals[other_portal] in grid.distance_from_start: + continue + distance = grid.distance_from_start[portals[other_portal]] + for level in range(20): + if portal in ("AA", "ZZ") and level != 0: + break + if other_portal in ("AA", "ZZ") and level != 0: + break + if (portal, level) in edges: + edges[(portal, level)].update({(other_portal, level): distance}) + else: + edges[(portal, level)] = {(other_portal, level): distance} + + if len(portal) == 2 and portal[1] == "in": + portal_out = (portal[0], "out") + edges[(portal, level)].update({(portal_out, level + 1): 1}) + elif len(portal) == 2 and portal[1] == "out" and level != 0: + portal_in = (portal[0], "in") + edges[(portal, level)].update({(portal_in, level - 1): 1}) + + grid = pathfinding.WeightedGraph({}, edges) + + grid.dijkstra(("AA", 0), ("ZZ", 0)) + puzzle_actual_result = grid.distance_from_start[("ZZ", 0)] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 13768a85507841b3b2e2a8dba43e7bb0c26ad252 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 26 Aug 2020 21:07:26 +0200 Subject: [PATCH 072/143] Added days 2019-21, 2019-22 and 2019-23 --- 2019/21-Springdroid Adventure.py | 84 +++++++++++++++++++ 2019/22-Slam Shuffle.py | 138 +++++++++++++++++++++++++++++++ 2019/23-Category Six.py | 114 +++++++++++++++++++++++++ 3 files changed, 336 insertions(+) create mode 100644 2019/21-Springdroid Adventure.py create mode 100644 2019/22-Slam Shuffle.py create mode 100644 2019/23-Category Six.py diff --git a/2019/21-Springdroid Adventure.py b/2019/21-Springdroid Adventure.py new file mode 100644 index 0000000..f40f948 --- /dev/null +++ b/2019/21-Springdroid Adventure.py @@ -0,0 +1,84 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["19352638", "1141251258"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def add_ascii_input(self, value): + self.add_input([ord(x) for x in value]) + + +IntCode.IntCode.add_ascii_input = add_ascii_input + + +if part_to_test == 1: + instructions = [ + "NOT A T", + "NOT B J", + "OR T J", + "NOT C T", + "OR T J", + "AND D J", + "WALK", + ] +else: + instructions = [ + "NOT A T", + "NOT B J", + "OR T J", + "NOT C T", + "OR T J", + "AND D J", + "NOT H T", + "NOT T T", + "OR E T", + "AND T J", + "RUN", + ] + + +droid = IntCode.IntCode(puzzle_input) + + +for instruction in instructions: + droid.add_ascii_input(instruction + "\n") + +droid.run() +for output in droid.outputs: + if output > 256: + puzzle_actual_result = output + else: + print(chr(output), end="") + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/22-Slam Shuffle.py b/2019/22-Slam Shuffle.py new file mode 100644 index 0000000..dd82406 --- /dev/null +++ b/2019/22-Slam Shuffle.py @@ -0,0 +1,138 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": ( + """deal into new stack +cut -2 +deal with increment 7 +cut 8 +cut -4 +deal with increment 7 +cut 3 +deal with increment 9 +deal with increment 3 +cut -1""", + 10, + ), + "expected": ["9 2 5 8 1 4 7 0 3 6", "9 2 5 8 1 4 7 0 3 6"], +} + +test += 1 +test_data[test] = { + "input": ( + """cut 6 +deal with increment 7 +deal into new stack""", + 10, + ), + "expected": ["3 0 7 4 1 8 5 2 9 6", "3 0 7 4 1 8 5 2 9 6"], +} + +test += 1 +test_data[test] = { + "input": ( + """deal with increment 7 +cut 3 +deal into new stack""", + 10, + ), + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": (open(input_file, "r+").read(), 119315717514047), + "expected": ["2480", "62416301438548"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +nb_cards = puzzle_input[1] + +if part_to_test == 1: + deck = [x for x in range(nb_cards)] + + for string in puzzle_input[0].split("\n"): + if string == "": + continue + if string == "deal into new stack": + deck = deck[::-1] + elif string[0:4] == "deal": + number = int(string.split(" ")[-1]) + new_deck = [0] * nb_cards + for i in range(0, nb_cards * number, number): + new_deck[i % nb_cards] = deck[i // number] + deck = new_deck[:] + else: + number = int(string.split(" ")[-1]) + deck = deck[number:] + deck[:number] + + # print (string, deck) + + print(deck) + puzzle_actual_result = deck.index(2019) + + +else: + nb_shuffles = 101741582076661 + # Then the goal is to find a, b and x so that after 1 deal means: + # a*initial_position + b = [output] % nb_cards + # a and b can be found by analyzing the movements done + a, b = 1, 0 + for string in puzzle_input[0].split("\n")[::-1]: + if string == "": + continue + if string == "deal into new stack": + a *= -1 + b *= -1 + b -= 1 # Not sure why it's needed... + elif string[0:4] == "deal": + number = int(string.split(" ")[-1]) + a *= pow(number, -1, nb_cards) + b *= pow(number, -1, nb_cards) + else: + number = int(string.split(" ")[-1]) + b += number + + a, b = a % nb_cards, b % nb_cards + + # This function applies the shuffles nb_shuffles times + # This is the equation a^nb_shuffles * position + sum[a^k * b for k in range(0, nb_shuffles-1)] % nb_cards + # This translated to a^nb_shuffles * position + b * (1-a^nb_shuffles) / (1-a) % nb_cards + + def shuffles(a, b, position, nb_shuffles, nb_cards): + value = pow(a, nb_shuffles, nb_cards) * position + value += b * (1 - pow(a, nb_shuffles, nb_cards)) * pow(1 - a, -1, nb_cards) + value %= nb_cards + return value + + puzzle_actual_result = shuffles(a, b, 2020, nb_shuffles, nb_cards) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/23-Category Six.py b/2019/23-Category Six.py new file mode 100644 index 0000000..c7b0a71 --- /dev/null +++ b/2019/23-Category Six.py @@ -0,0 +1,114 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["23266", "17493"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 0 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +computers = [0] * 50 +queue = [] +nat_queue = [] +nat_y_values_sent = [] +for i in range(len(computers)): + computers[i] = IntCode.IntCode(puzzle_input, i) + computers[i].add_input(i) + computers[i].reception_duration = 0 + + +total_outputs = 0 +while puzzle_actual_result == "Unknown": + for computer in computers: + computer.run(1) + + if computer.outputs: + computer.reception_duration = 0 + + if len(computer.outputs) == 3: + total_outputs += len(computer.outputs) + queue += [computer.outputs] + computer.outputs = [] + + if verbose_level >= 1 and queue: + print("Queue contains", queue) + print("# outputs from computers", total_outputs) + + while queue: + packet = queue.pop(0) + if packet[0] == 255 and part_to_test == 1: + puzzle_actual_result = packet[2] + break + elif packet[0] == 255: + nat_queue = packet[1:] + else: + computers[packet[0]].add_input(packet[1:]) + computers[packet[0]].restart() + + for computer in computers: + if computer.state == "Paused": + computer.reception_duration += 1 + + senders = [ + computer.reference for computer in computers if computer.reception_duration < 5 + ] + inputs = [computer.reference for computer in computers if len(computer.inputs) != 0] + + if ( + all( + [ + computer.reception_duration > 5 and len(computer.inputs) == 0 + for computer in computers + ] + ) + and nat_queue + ): + computers[0].add_input(nat_queue) + y_sent = nat_queue[-1] + + print("NAT sends", nat_queue, "- Previous Y values sent:", nat_y_values_sent) + nat_queue = [] + if nat_y_values_sent and y_sent == nat_y_values_sent[-1]: + puzzle_actual_result = y_sent + nat_y_values_sent.append(y_sent) + else: + for computer in computers: + if computer.state == "Paused": + computer.add_input(-1) + computer.restart() + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From cbdba8d8499ee993e111e009a782519e538715c5 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 26 Aug 2020 21:07:50 +0200 Subject: [PATCH 073/143] IntCode: added limit of instructions processed --- 2019/IntCode.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/2019/IntCode.py b/2019/IntCode.py index ca1eda4..c3b641a 100644 --- a/2019/IntCode.py +++ b/2019/IntCode.py @@ -175,8 +175,10 @@ def op_99(self, instr): self.pointer += self.instr_length["99"] self.state = "Stopped" - def run(self): - while self.state == "Running": + def run(self, nb_instructions=float("inf")): + i = 0 + while self.state == "Running" and i < nb_instructions: + i += 1 opcode_full = self.get_opcode() opcode = opcode_full[-2:] self.modes = opcode_full[:-2] From 3b4a38a8cca6fe987b51fe5de6d25b1e71aca030 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 27 Aug 2020 20:31:42 +0200 Subject: [PATCH 074/143] Pathfinding: fixed some bugs --- 2019/pathfinding.py | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/2019/pathfinding.py b/2019/pathfinding.py index e8f47e7..14c67b8 100644 --- a/2019/pathfinding.py +++ b/2019/pathfinding.py @@ -12,14 +12,9 @@ class NegativeWeightCycle(Exception): class Graph: - vertices = [] - edges = {} - distance_from_start = {} - came_from = {} - def __init__(self, vertices=[], edges={}): - self.vertices = vertices - self.edges = edges + self.vertices = vertices.copy() + self.edges = edges.copy() def neighbors(self, vertex): """ @@ -57,6 +52,7 @@ def grid_to_vertices(self, grid, diagonals_allowed=False, wall="#"): :return: True if the grid was converted """ self.vertices = [] + self.edges = {} y = 0 for line in grid.splitlines(): @@ -151,7 +147,7 @@ def add_traps(self, vertices): return changed - def add_walls(self, vertices): + def add_walls(self, walls): """ Adds walls - useful for modification of map @@ -159,7 +155,7 @@ def add_walls(self, vertices): :return: True if successful, False if no vertex found """ changed = False - for vertex in vertices: + for vertex in walls: if vertex in self.edges: del self.edges[vertex] if isinstance(self.vertices, list): @@ -169,7 +165,7 @@ def add_walls(self, vertices): changed = True self.edges = { - source: [target for target in self.edges[source] if target not in vertices] + source: [target for target in self.edges[source] if target not in walls] for source in self.edges } From 3a51a0f1b3659e0629d1b75c84119d8a89af8d0f Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 27 Aug 2020 20:31:57 +0200 Subject: [PATCH 075/143] Added days 2019-24 and 2019-25 --- 2019/24-Planet of Discord.py | 210 +++++++++++++++++++++++++++++++++++ 2019/25-Cryostasis.py | 62 +++++++++++ 2 files changed, 272 insertions(+) create mode 100644 2019/24-Planet of Discord.py create mode 100644 2019/25-Cryostasis.py diff --git a/2019/24-Planet of Discord.py b/2019/24-Planet of Discord.py new file mode 100644 index 0000000..8134ee1 --- /dev/null +++ b/2019/24-Planet of Discord.py @@ -0,0 +1,210 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """....# +#..#. +#..## +..#.. +#....""", + "expected": ["2129920", "99"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["20751345", "1983"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def grid_to_vertices(self, grid): + self.vertices = {} + y = 0 + for line in grid.splitlines(): + for x in range(len(line)): + self.vertices[x - y * j] = line[x] + y += 1 + + for source in self.vertices: + for direction in directions_straight: + target = source + direction + if target in self.vertices: + if source in self.edges: + self.edges[source].append(target) + else: + self.edges[source] = [target] + + return True + + +pathfinding.Graph.grid_to_vertices = grid_to_vertices + + +def biodiversity_rating(self): + rating = 0 + for y in range(int(min_imag(self.vertices)), int(max_imag(self.vertices) + 1)): + for x in range(int(min_real(self.vertices)), int(max_real(self.vertices) + 1)): + if self.vertices[x + y * j] == "#": + rating += pow(2, -y * (max_real(self.vertices) + 1) + x) + + return int(rating) + + +pathfinding.Graph.biodiversity_rating = biodiversity_rating + + +if part_to_test == 1: + empty_grid = ("." * 5 + "\n") * 5 + area = pathfinding.Graph() + new_area = pathfinding.Graph() + area.grid_to_vertices(puzzle_input) + + previous_ratings = [] + while area.biodiversity_rating() not in previous_ratings: + previous_ratings.append(area.biodiversity_rating()) + new_area.grid_to_vertices(empty_grid) + for position in area.vertices: + if area.vertices[position] == "#": + living_neighbors = len( + [ + neighbor + for neighbor in area.neighbors(position) + if area.vertices[neighbor] == "#" + ] + ) + if living_neighbors == 1: + new_area.vertices[position] = "#" + else: + new_area.vertices[position] = "." + else: + living_neighbors = len( + [ + neighbor + for neighbor in area.neighbors(position) + if area.vertices[neighbor] == "#" + ] + ) + if living_neighbors in (1, 2): + new_area.vertices[position] = "#" + else: + new_area.vertices[position] = "." + + area.vertices = new_area.vertices.copy() + + puzzle_actual_result = area.biodiversity_rating() + +else: + + def neighbors(self, vertex): + neighbors = [] + position, level = vertex + for dir in directions_straight: + if (position + dir, level) in self.vertices: + neighbors.append((position + dir, level)) + + # Connection to lower (outside) levels + if position.imag == 0: + neighbors.append((2 - 1 * j, level - 1)) + elif position.imag == -4: + neighbors.append((2 - 3 * j, level - 1)) + if position.real == 0: + neighbors.append((1 - 2 * j, level - 1)) + elif position.real == 4: + neighbors.append((3 - 2 * j, level - 1)) + + # Connection to higher (inside) levels + if position == 2 - 1 * j: + neighbors += [(x, level + 1) for x in range(5)] + elif position == 2 - 3 * j: + neighbors += [(x - 4 * j, level + 1) for x in range(5)] + elif position == 1 - 2 * j: + neighbors += [(-y * j, level + 1) for y in range(5)] + elif position == 3 - 2 * j: + neighbors += [(4 - y * j, level + 1) for y in range(5)] + + return neighbors + + pathfinding.Graph.neighbors = neighbors + + empty_grid = ("." * 5 + "\n") * 5 + area = pathfinding.Graph() + area.grid_to_vertices(puzzle_input) + area.add_walls([2 - 2 * j]) + + nb_minutes = 200 if case_to_test == "real" else 10 + + recursive = pathfinding.Graph() + recursive.vertices = { + (position, level): "." + for position in area.vertices + for level in range(-nb_minutes // 2, nb_minutes // 2 + 1) + } + + recursive.vertices.update( + {(position, 0): area.vertices[position] for position in area.vertices} + ) + + for generation in range(nb_minutes): + new_grids = pathfinding.Graph() + new_grids.vertices = {} + for position in recursive.vertices: + if recursive.vertices[position] == "#": + living_neighbors = len( + [ + neighbor + for neighbor in recursive.neighbors(position) + if recursive.vertices.get(neighbor, ".") == "#" + ] + ) + if living_neighbors == 1: + new_grids.vertices[position] = "#" + else: + new_grids.vertices[position] = "." + else: + living_neighbors = len( + [ + neighbor + for neighbor in recursive.neighbors(position) + if recursive.vertices.get(neighbor, ".") == "#" + ] + ) + if living_neighbors in (1, 2): + new_grids.vertices[position] = "#" + else: + new_grids.vertices[position] = "." + + recursive.vertices = new_grids.vertices.copy() + + puzzle_actual_result = len( + [x for x in recursive.vertices if recursive.vertices[x] == "#"] + ) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/25-Cryostasis.py b/2019/25-Cryostasis.py new file mode 100644 index 0000000..3a7ad8c --- /dev/null +++ b/2019/25-Cryostasis.py @@ -0,0 +1,62 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": "Objects: coin, shell, space heater, fuel cell - code : 805306888", +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +droid = IntCode.IntCode(puzzle_input) +droid.run() + +while True: + for number in droid.outputs: + print(chr(number), end="") + + data = input() + for letter in data: + print(data) + droid.add_input(ord(letter)) + droid.add_input(ord("\n")) + droid.restart() + droid.run() + + # north, south, east, or west. + # take + # drop + # inv + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From e63baf5785991fa68a03d0c1eb71e001828a962b Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 29 Aug 2020 18:29:35 +0200 Subject: [PATCH 076/143] Removed various prints --- 2015/01-Not Quite Lisp.py | 117 +++++++++++++++------------ 2017/23-Coprocessor Conflagration.py | 75 ++++++++--------- 2019/23-Category Six.py | 5 +- 3 files changed, 107 insertions(+), 90 deletions(-) diff --git a/2015/01-Not Quite Lisp.py b/2015/01-Not Quite Lisp.py index 046d17d..6b6464c 100644 --- a/2015/01-Not Quite Lisp.py +++ b/2015/01-Not Quite Lisp.py @@ -3,87 +3,98 @@ test_data = {} test = 1 -test_data[test] = {"input": '(())', - "expected": ['0', ''], - } +test_data[test] = { + "input": "(())", + "expected": ["0", ""], +} test += 1 -test_data[test] = {"input": '()()', - "expected": ['0', ''], - } +test_data[test] = { + "input": "()()", + "expected": ["0", ""], +} test += 1 -test_data[test] = {"input": '(((', - "expected": ['3', ''], - } +test_data[test] = { + "input": "(((", + "expected": ["3", ""], +} test += 1 -test_data[test] = {"input": '(()(()(', - "expected": ['3', ''], - } +test_data[test] = { + "input": "(()(()(", + "expected": ["3", ""], +} test += 1 -test_data[test] = {"input": '))(((((', - "expected": ['3', ''], - } +test_data[test] = { + "input": "))(((((", + "expected": ["3", ""], +} test += 1 -test_data[test] = {"input": '())', - "expected": ['-1', ''], - } +test_data[test] = { + "input": "())", + "expected": ["-1", ""], +} test += 1 -test_data[test] = {"input": '))(', - "expected": ['-1', ''], - } +test_data[test] = { + "input": "))(", + "expected": ["-1", ""], +} test += 1 -test_data[test] = {"input": ')))', - "expected": ['-3', ''], - } +test_data[test] = { + "input": ")))", + "expected": ["-3", ""], +} test += 1 -test_data[test] = {"input": ')())())', - "expected": ['-3', ''], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read(), - "expected": ['232', '1783'], - } +test_data[test] = { + "input": ")())())", + "expected": ["-3", ""], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["232", "1783"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' +case_to_test = "real" part_to_test = 2 -verbose_level = 3 +verbose_level = 0 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # if part_to_test == 1: - puzzle_actual_result = puzzle_input.count('(') - puzzle_input.count(')') + puzzle_actual_result = puzzle_input.count("(") - puzzle_input.count(")") else: - count_plus = 0 - count_minus = 0 - i = 0 - while count_plus >= count_minus and i < len(puzzle_input): - count_plus += 1 if puzzle_input[i] == '(' else 0 - count_minus += 1 if puzzle_input[i] == ')' else 0 - i += 1 - puzzle_actual_result = i - + count_plus = 0 + count_minus = 0 + i = 0 + while count_plus >= count_minus and i < len(puzzle_input): + count_plus += 1 if puzzle_input[i] == "(" else 0 + count_minus += 1 if puzzle_input[i] == ")" else 0 + i += 1 + puzzle_actual_result = i # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/23-Coprocessor Conflagration.py b/2017/23-Coprocessor Conflagration.py index 698822d..4c9e9b1 100644 --- a/2017/23-Coprocessor Conflagration.py +++ b/2017/23-Coprocessor Conflagration.py @@ -4,49 +4,56 @@ test_data = {} test = 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['6724', '903'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["6724", "903"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -def val_get (registers, value): + +def val_get(registers, value): try: return int(value) except ValueError: return registers[value] -def get_divisors (value): - small_divisors = [d for d in range (1, int(math.sqrt(value))+1) if value % d == 0 ] - big_divisors = [value // d for d in small_divisors if not d**2 == value] - return set(small_divisors + big_divisors) +def get_divisors(value): + small_divisors = [d for d in range(1, int(math.sqrt(value)) + 1) if value % d == 0] + big_divisors = [value // d for d in small_divisors if not d ** 2 == value] + return set(small_divisors + big_divisors) -instructions = [(string.split(' ')) for string in puzzle_input.split('\n')] +instructions = [(string.split(" ")) for string in puzzle_input.split("\n")] i = 0 -registers = {x:0 for x in 'abcdefgh'} -registers['a'] = part_to_test - 1 +registers = {x: 0 for x in "abcdefgh"} +registers["a"] = part_to_test - 1 count_mul = 0 val_h = 1 nb_instructions = 0 @@ -55,19 +62,19 @@ def get_divisors (value): while i < len(instructions): instr = instructions[i] - if instr[0] == 'set': + if instr[0] == "set": registers.update({instr[1]: val_get(registers, instr[2])}) - elif instr[0] == 'sub': + elif instr[0] == "sub": registers.setdefault(instr[1], 0) registers[instr[1]] -= val_get(registers, instr[2]) - elif instr[0] == 'mul': + elif instr[0] == "mul": registers.setdefault(instr[1], 0) registers[instr[1]] *= val_get(registers, instr[2]) count_mul += 1 - elif instr[0] == 'mod': + elif instr[0] == "mod": registers.setdefault(instr[1], 0) registers[instr[1]] %= val_get(registers, instr[2]) - elif instr[0] == 'jnz': + elif instr[0] == "jnz": if val_get(registers, instr[1]) != 0: i += val_get(registers, instr[2]) - 1 @@ -82,9 +89,9 @@ def get_divisors (value): else: count_composite = 0 - for i in range (84*100+100000, 84*100+100000+17000+1, 17): + for i in range(84 * 100 + 100000, 84 * 100 + 100000 + 17000 + 1, 17): if len(get_divisors(i)) != 2: - print (i, get_divisors(i)) + # print (i, get_divisors(i)) count_composite += 1 puzzle_actual_result = count_composite @@ -96,10 +103,6 @@ def get_divisors (value): # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/23-Category Six.py b/2019/23-Category Six.py index c7b0a71..3f0cbb7 100644 --- a/2019/23-Category Six.py +++ b/2019/23-Category Six.py @@ -96,7 +96,10 @@ computers[0].add_input(nat_queue) y_sent = nat_queue[-1] - print("NAT sends", nat_queue, "- Previous Y values sent:", nat_y_values_sent) + if verbose_level >= 1: + print( + "NAT sends", nat_queue, "- Previous Y values sent:", nat_y_values_sent + ) nat_queue = [] if nat_y_values_sent and y_sent == nat_y_values_sent[-1]: puzzle_actual_result = y_sent From 8fcb2d3667253966f3345a4b5aa68faa2299ff34 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 29 Aug 2020 18:30:25 +0200 Subject: [PATCH 077/143] Performance improvements -- all days run below 60 sec now --- 2015/22-Wizard Simulator 20XX.py | 390 ++++++++-------------------- 2015/22-Wizard Simulator 20XX.v1.py | 315 ++++++++++++++++++++++ 2016/14-One-Time Pad.py | 102 ++++---- 2016/14-One-Time Pad.v1.py | 105 ++++++++ 2017/21-Fractal Art.py | 157 ++++++----- 2017/21-Fractal Art.v1.py | 108 ++++++++ 2019/13-Care Package.py | 48 +--- 2019/13-Care Package.v1.py | 135 ++++++++++ 8 files changed, 934 insertions(+), 426 deletions(-) create mode 100644 2015/22-Wizard Simulator 20XX.v1.py create mode 100644 2016/14-One-Time Pad.v1.py create mode 100644 2017/21-Fractal Art.v1.py create mode 100644 2019/13-Care Package.v1.py diff --git a/2015/22-Wizard Simulator 20XX.py b/2015/22-Wizard Simulator 20XX.py index 52f16cb..f852ff4 100644 --- a/2015/22-Wizard Simulator 20XX.py +++ b/2015/22-Wizard Simulator 20XX.py @@ -1,301 +1,141 @@ # -------------------------------- Input data -------------------------------- # -import os, itertools, random +import os, heapq test_data = {} -test = 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -test_data[test] = {"input": '', - "expected": ['900', '1216'], - } +test = "real" +test_data[test] = { + "input": "", + "expected": ["900", "1216"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 1 -part_to_test = 2 -verbose_level = 1 +case_to_test = "real" +part_to_test = 2 +verbose_level = 0 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # spells = { - # Cost, Duration, Damage, Heal, Armor, Mana - 'M': [53, 1, 4, 0, 0, 0], - 'D': [73, 1, 2, 2, 0, 0], - 'S': [113, 6, 0, 0, 7, 0], - 'P': [173, 6, 3, 0, 0, 0], - 'R': [229, 5, 0, 0, 0, 101], - } - -# Mana, HP, Armor -init_player_stats = [500, 50, 0] -# HP, Damage -init_boss_stats = [51, 9] -init_counters = {'S': 0, 'P': 0, 'R': 0} - -# Maximum mana used - initially 10 ** 6, reduced with manual tests / strategy -min_mana_used = 1300 - - -def apply_effects (counters, player_stats, boss_stats): - global spells - - for effect in counters: - if counters[effect] == 0: - if effect == 'S': - player_stats[2] = 0 - continue + # Cost, Duration, Damage, Heal, Armor, Mana + "M": [53, 1, 4, 0, 0, 0], + "D": [73, 1, 2, 2, 0, 0], + "S": [113, 6, 0, 0, 7, 0], + "P": [173, 6, 3, 0, 0, 0], + "R": [229, 5, 0, 0, 0, 101], +} + + +# Order: +# Player mana, HP, Armor +# Boss HP and damage +# Counters for the 3 spells: Shield, Poison, Recharge +state = ["", 500, 50, 0, 51, 9, 0, 0, 0] +i_moves, i_mana, i_hp, i_armor, i_bhp, i_bdamage, i_cs, i_cp, i_cr = range(len(state)) + + +def apply_effects(state): + # Shield + if state[i_cs] > 0: + state[i_armor] = 7 + state[i_cs] -= 1 else: - if effect == 'S': - player_stats[2] = spells[effect][4] - else: - boss_stats[0] -= spells[effect][2] - player_stats[0] += spells[effect][5] - - counters[effect] -= 1 - - return [counters, player_stats, boss_stats] - -if part_to_test == 1: - count_strategies = 5 ** 10 - for strategy in itertools.product(spells.keys(), repeat=10): - count_strategies -= 1 - print ('Min mana :', min_mana_used, '###### Strategy #', count_strategies, ':', strategy) - if 'S' not in strategy[0:5] or 'R' not in strategy[0:5]: - continue - counters = init_counters.copy() - player_stats = init_player_stats.copy() - boss_stats = init_boss_stats.copy() - mana_used = 0 - - - - for player_action in strategy: - # Player turn - if part_to_test == 2: - player_stats[1] -= 1 - if player_stats[1] <= 0: - if verbose_level >=2: - print ('Boss wins') - break - - # Apply effects - counters, player_stats, boss_stats = apply_effects(counters, player_stats, boss_stats) - if verbose_level >=2: - print ('### Player turn - Player casts', player_action) - print (counters, player_stats, boss_stats) - - # Apply player move - if spells[player_action][0] > player_stats[0]: - if verbose_level >=2: - print ('Aborting: not enough mana') - break - if spells[player_action][1] == 1: - player_stats[1] += spells[player_action][3] - boss_stats[0] -= spells[player_action][2] - else: - if counters[player_action] != 0: - if verbose_level >=2: - print ('Aborting: reused ' + player_action) - break - else: - counters[player_action] = spells[player_action][1] - # Mana usage - player_stats[0] -= spells[player_action][0] - mana_used += spells[player_action][0] - if verbose_level >=2: - print (counters, player_stats, boss_stats) - - if boss_stats[0] <= 0: - if verbose_level >=2: - print ('Player wins with', mana_used, 'mana used') - min_mana_used = min (min_mana_used, mana_used) - break - if mana_used > min_mana_used: - print ('Aborting: too much mana used') - break - - - # Boss turn - # Apply effects - counters, player_stats, boss_stats = apply_effects(counters, player_stats, boss_stats) - if verbose_level >=2: - print ('### Boss turn') - print (counters, player_stats, boss_stats) - - player_stats[1] -= boss_stats[1] - player_stats[2] - if verbose_level >=2: - print (counters, player_stats, boss_stats) - - if player_stats[1] <= 0: - if verbose_level >=2: - print ('Boss wins') - break -else: - max_moves = 15 - pruned_strategies = [] - count_strategies = 5 ** max_moves - - # This code is not very efficient, becuase it changes the last spells first (and those are likely not to be used because we finish the combat or our mana before that)... - - for strategy in itertools.product(spells.keys(), repeat=max_moves): - count_strategies -= 1 - if 'S' not in strategy[0:4] or 'R' not in strategy[0:5]: - if verbose_level >=2: - print (' Missing Shield or Recharge') - continue - if any ([True for i in range(1, max_moves) if strategy[0:i] in pruned_strategies]): - print (' Pruned') - continue - - if verbose_level >=2: - print ('Min mana :', min_mana_used, '###### Strategy #', count_strategies,'- pruned: ', len(pruned_strategies), '-', strategy) - shield_left = 0 - poison_left = 0 - recharge_left = 0 - player_hp = 50 - player_mana = 500 - player_armor = 0 - mana_used = 0 - boss_hp = 51 - boss_dmg = 9 - - - for player_action in strategy: - - # Player turn - player_hp -= 1 - if player_hp <= 0: - if verbose_level >=2: - print ('Boss wins') -# pruned_strategies.append(tuple(actions_done)) - break - - -# actions_done += tuple(player_action) - - # Apply effects - if shield_left > 0: - player_armor = 7 - shield_left -= 1 - else: - player_armor = 0 - if poison_left > 0: - boss_hp -= 3 - poison_left -= 0 - if recharge_left: - player_mana += 101 - recharge_left -= 1 - - - # Apply player move - if spells[player_action][0] > player_mana: - if verbose_level >=2: - print ('Aborting: not enough mana') -# pruned_strategies.append(actions_done) - break - # Missile - if player_action == 'M': - player_mana -= 53 - mana_used += 53 - boss_hp -= 4 - # Drain - elif player_action == 'D': - player_mana -= 73 - mana_used += 73 - boss_hp -= 2 - player_hp += 2 - # Shield - elif player_action == 'S': - if shield_left != 0: - if verbose_level >=2: - print ('Aborting: reused ' + player_action) -# pruned_strategies.append(actions_done) - break - else: - shield_left = 6 - # Poison - elif player_action == 'P': - if poison_left != 0: - if verbose_level >=2: - print ('Aborting: reused ' + player_action) -# pruned_strategies.append(actions_done) - break - else: - poison_left = 6 - # Recharge - elif player_action == 'R': - if recharge_left != 0: - if verbose_level >=2: - print ('Aborting: reused ' + player_action) -# pruned_strategies.append(actions_done) - break - else: - shield_left = 5 - - if boss_hp <= 0: - if verbose_level >=2: - print ('Player wins with', mana_used, 'mana used') - min_mana_used = min (min_mana_used, mana_used) - break - if mana_used > min_mana_used: - if verbose_level >=2: - print ('Aborting: too much mana used') - break - - - # Boss turn - # Apply effects - if shield_left > 0: - player_armor = 7 - shield_left -= 1 - else: - player_armor = 0 - if poison_left > 0: - boss_hp -= 3 - poison_left -= 0 - if recharge_left: - player_mana += 101 - recharge_left -= 1 - - player_hp -= boss_dmg - player_armor - - if player_hp <= 0: - if verbose_level >=2: - print ('Boss wins') -# pruned_strategies.append(actions_done) - break + state[i_armor] = 0 + # Poison + if state[i_cp] > 0: + state[i_bhp] -= 3 + # Recharge + if state[i_cr] > 0: + state[i_mana] += 101 + + state[-2:] = [0 if x <= 1 else x - 1 for x in state[-2:]] + + +def player_turn(state, spell): + if spell in "MD": + state[i_mana] -= spells[spell][0] + state[i_bhp] -= spells[spell][2] + state[i_hp] += spells[spell][3] else: - unknown_result.append(strategy) -# print ('Pruned : ', pruned_strategies) - print ('Unknown : ', unknown_result) -puzzle_actual_result = min_mana_used + state[i_mana] -= spells[spell][0] + state[-3 + "SPR".index(spell)] = spells[spell][1] +def boss_move(state): + state[i_hp] -= max(state[i_bdamage] - state[i_armor], 1) -# -------------------------------- Outputs / results -------------------------------- # -if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) +min_mana = 10 ** 6 +frontier = [(0, state)] +heapq.heapify(frontier) + +while frontier: + mana_used, state = heapq.heappop(frontier) + + if mana_used > min_mana: + continue + + if part_to_test == 2: + state[i_hp] -= 1 + if state[i_hp] <= 0: + continue + + # Apply effects before player turn + apply_effects(state) + if state[i_bhp] <= 0: + min_mana = min(min_mana, mana_used) + continue + for spell in spells: + # Exclude if mana < 0 + if spells[spell][0] > state[i_mana]: + continue + # Exclude if mana > max mana found + if mana_used + spells[spell][0] > min_mana: + continue + # Exclude if spell already active + if spell in "SPR": + if state[-3 + "SPR".index(spell)] != 0: + continue + neighbor = state.copy() + neighbor[0] += spell + # Player moves + player_turn(neighbor, spell) + if neighbor[i_bhp] <= 0: + min_mana = min(min_mana, mana_used + spells[spell][0]) + continue + # Apply effects + apply_effects(neighbor) + if neighbor[i_bhp] <= 0: + min_mana = min(min_mana, mana_used + spells[spell][0]) + continue + + # Boss moves + boss_move(neighbor) + if neighbor[i_hp] <= 0: + continue + + # Adding for future examination + heapq.heappush(frontier, (mana_used + spells[spell][0], neighbor)) + +puzzle_actual_result = min_mana + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2015/22-Wizard Simulator 20XX.v1.py b/2015/22-Wizard Simulator 20XX.v1.py new file mode 100644 index 0000000..ef43c41 --- /dev/null +++ b/2015/22-Wizard Simulator 20XX.v1.py @@ -0,0 +1,315 @@ +# -------------------------------- Input data -------------------------------- # +import os, itertools, random + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +test_data[test] = { + "input": "", + "expected": ["900", "1216"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = 1 +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + + +spells = { + # Cost, Duration, Damage, Heal, Armor, Mana + "M": [53, 1, 4, 0, 0, 0], + "D": [73, 1, 2, 2, 0, 0], + "S": [113, 6, 0, 0, 7, 0], + "P": [173, 6, 3, 0, 0, 0], + "R": [229, 5, 0, 0, 0, 101], +} + +# Mana, HP, Armor +init_player_stats = [500, 50, 0] +# HP, Damage +init_boss_stats = [51, 9] +init_counters = {"S": 0, "P": 0, "R": 0} + +# Maximum mana used - initially 10 ** 6, reduced with manual tests / strategy +min_mana_used = 1300 + + +def apply_effects(counters, player_stats, boss_stats): + global spells + + for effect in counters: + if counters[effect] == 0: + if effect == "S": + player_stats[2] = 0 + continue + else: + if effect == "S": + player_stats[2] = spells[effect][4] + else: + boss_stats[0] -= spells[effect][2] + player_stats[0] += spells[effect][5] + + counters[effect] -= 1 + + return [counters, player_stats, boss_stats] + + +if part_to_test == 1: + count_strategies = 5 ** 10 + for strategy in itertools.product(spells.keys(), repeat=10): + count_strategies -= 1 + print( + "Min mana :", + min_mana_used, + "###### Strategy #", + count_strategies, + ":", + strategy, + ) + if "S" not in strategy[0:5] or "R" not in strategy[0:5]: + continue + counters = init_counters.copy() + player_stats = init_player_stats.copy() + boss_stats = init_boss_stats.copy() + mana_used = 0 + + for player_action in strategy: + # Player turn + if part_to_test == 2: + player_stats[1] -= 1 + if player_stats[1] <= 0: + if verbose_level >= 2: + print("Boss wins") + break + + # Apply effects + counters, player_stats, boss_stats = apply_effects( + counters, player_stats, boss_stats + ) + if verbose_level >= 2: + print("### Player turn - Player casts", player_action) + print(counters, player_stats, boss_stats) + + # Apply player move + if spells[player_action][0] > player_stats[0]: + if verbose_level >= 2: + print("Aborting: not enough mana") + break + if spells[player_action][1] == 1: + player_stats[1] += spells[player_action][3] + boss_stats[0] -= spells[player_action][2] + else: + if counters[player_action] != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + break + else: + counters[player_action] = spells[player_action][1] + # Mana usage + player_stats[0] -= spells[player_action][0] + mana_used += spells[player_action][0] + if verbose_level >= 2: + print(counters, player_stats, boss_stats) + + if boss_stats[0] <= 0: + if verbose_level >= 2: + print("Player wins with", mana_used, "mana used") + min_mana_used = min(min_mana_used, mana_used) + break + if mana_used > min_mana_used: + print("Aborting: too much mana used") + break + + # Boss turn + # Apply effects + counters, player_stats, boss_stats = apply_effects( + counters, player_stats, boss_stats + ) + if verbose_level >= 2: + print("### Boss turn") + print(counters, player_stats, boss_stats) + + player_stats[1] -= boss_stats[1] - player_stats[2] + if verbose_level >= 2: + print(counters, player_stats, boss_stats) + + if player_stats[1] <= 0: + if verbose_level >= 2: + print("Boss wins") + break +else: + max_moves = 15 + pruned_strategies = [] + count_strategies = 5 ** max_moves + + # This code is not very efficient, becuase it changes the last spells first (and those are likely not to be used because we finish the combat or our mana before that)... + + for strategy in itertools.product(spells.keys(), repeat=max_moves): + count_strategies -= 1 + if "S" not in strategy[0:4] or "R" not in strategy[0:5]: + if verbose_level >= 2: + print(" Missing Shield or Recharge") + continue + if any( + [True for i in range(1, max_moves) if strategy[0:i] in pruned_strategies] + ): + print(" Pruned") + continue + + if verbose_level >= 2: + print( + "Min mana :", + min_mana_used, + "###### Strategy #", + count_strategies, + "- pruned: ", + len(pruned_strategies), + "-", + strategy, + ) + shield_left = 0 + poison_left = 0 + recharge_left = 0 + player_hp = 50 + player_mana = 500 + player_armor = 0 + mana_used = 0 + boss_hp = 51 + boss_dmg = 9 + + for player_action in strategy: + + # Player turn + player_hp -= 1 + if player_hp <= 0: + if verbose_level >= 2: + print("Boss wins") + # pruned_strategies.append(tuple(actions_done)) + break + + # actions_done += tuple(player_action) + + # Apply effects + if shield_left > 0: + player_armor = 7 + shield_left -= 1 + else: + player_armor = 0 + if poison_left > 0: + boss_hp -= 3 + poison_left -= 0 + if recharge_left: + player_mana += 101 + recharge_left -= 1 + + # Apply player move + if spells[player_action][0] > player_mana: + if verbose_level >= 2: + print("Aborting: not enough mana") + # pruned_strategies.append(actions_done) + break + # Missile + if player_action == "M": + player_mana -= 53 + mana_used += 53 + boss_hp -= 4 + # Drain + elif player_action == "D": + player_mana -= 73 + mana_used += 73 + boss_hp -= 2 + player_hp += 2 + # Shield + elif player_action == "S": + if shield_left != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + # pruned_strategies.append(actions_done) + break + else: + shield_left = 6 + # Poison + elif player_action == "P": + if poison_left != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + # pruned_strategies.append(actions_done) + break + else: + poison_left = 6 + # Recharge + elif player_action == "R": + if recharge_left != 0: + if verbose_level >= 2: + print("Aborting: reused " + player_action) + # pruned_strategies.append(actions_done) + break + else: + shield_left = 5 + + if boss_hp <= 0: + if verbose_level >= 2: + print("Player wins with", mana_used, "mana used") + min_mana_used = min(min_mana_used, mana_used) + break + if mana_used > min_mana_used: + if verbose_level >= 2: + print("Aborting: too much mana used") + break + + # Boss turn + # Apply effects + if shield_left > 0: + player_armor = 7 + shield_left -= 1 + else: + player_armor = 0 + if poison_left > 0: + boss_hp -= 3 + poison_left -= 0 + if recharge_left: + player_mana += 101 + recharge_left -= 1 + + player_hp -= boss_dmg - player_armor + + if player_hp <= 0: + if verbose_level >= 2: + print("Boss wins") + # pruned_strategies.append(actions_done) + break + else: + unknown_result.append(strategy) + # print ('Pruned : ', pruned_strategies) + print("Unknown : ", unknown_result) +puzzle_actual_result = min_mana_used + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/14-One-Time Pad.py b/2016/14-One-Time Pad.py index c09aa98..f170f98 100644 --- a/2016/14-One-Time Pad.py +++ b/2016/14-One-Time Pad.py @@ -4,49 +4,55 @@ test_data = {} test = 1 -test_data[test] = {"input": """abc""", - "expected": ['22728', '22551'], - } +test_data[test] = { + "input": """abc""", + "expected": ["22728", "22551"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} -test = 'real' -test_data[test] = {"input": 'qzyelonm', - "expected": ['15168', '20864'], - } +test = "real" +test_data[test] = { + "input": "qzyelonm", + "expected": ["15168", "20864"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 1 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # + if part_to_test == 1: index = 0 found_keys = 0 while True: index += 1 - init_hash = hashlib.md5((puzzle_input + str(index)).encode('utf-8')).hexdigest() - triplets = [x for x in '0123456789abcdef' if x*3 in init_hash] + init_hash = hashlib.md5((puzzle_input + str(index)).encode("utf-8")).hexdigest() + triplets = [x for x in "0123456789abcdef" if x * 3 in init_hash] if triplets: - first_triplet_position = min ([init_hash.find(x*3) for x in triplets]) + first_triplet_position = min([init_hash.find(x * 3) for x in triplets]) triplet = init_hash[first_triplet_position] - for i in range (1, 1000): - new_hash = hashlib.md5((puzzle_input + str(index + i)).encode('utf-8')).hexdigest() + for i in range(1, 1000): + new_hash = hashlib.md5( + (puzzle_input + str(index + i)).encode("utf-8") + ).hexdigest() if triplet * 5 in new_hash: found_keys += 1 break @@ -57,50 +63,40 @@ else: - hashes = [] + # hashes = [] hashes_first_triplet = {} - hashes_quintuplets = {} - index = -1 - found_keys = 0 + hashes_quintuplets = [] + keys_found = 0 - for i in range (30000): + i = 0 + while keys_found < 64: hash_text = puzzle_input + str(i) - for y in range (2017): - hash_text = hashlib.md5(hash_text.encode('utf-8')).hexdigest() - hashes.append(hash_text) + for _ in range(2017): + hash_text = hashlib.md5(hash_text.encode("utf-8")).hexdigest() - triplets = [x for x in '0123456789abcdef' if x*3 in hash_text] + triplets = [x for x in "0123456789abcdef" if x * 3 in hash_text] if triplets: - first_triplet_position = min ([hash_text.find(x*3) for x in triplets]) + first_triplet_position = min([hash_text.find(x * 3) for x in triplets]) hashes_first_triplet[i] = hash_text[first_triplet_position] - quintuplets = [x for x in '0123456789abcdef' if x*5 in hash_text] - - if quintuplets: - hashes_quintuplets[i] = quintuplets - - - for index, triplet in hashes_first_triplet.items(): - for i in range (1, 1000): - if index + i in hashes_quintuplets: - if triplet in hashes_quintuplets[index + i]: - found_keys += 1 - break - - if found_keys == 64: - puzzle_actual_result = index - break + hashes_quintuplets.append( + "".join(x for x in "0123456789abcdef" if x * 5 in hash_text) + ) + if i > 1000: + if i - 1001 in hashes_first_triplet: + if hashes_first_triplet[i - 1001] in "".join( + hashes_quintuplets[i - 1000 :] + ): + keys_found += 1 + i += 1 + puzzle_actual_result = i - 1002 # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/14-One-Time Pad.v1.py b/2016/14-One-Time Pad.v1.py new file mode 100644 index 0000000..55f196f --- /dev/null +++ b/2016/14-One-Time Pad.v1.py @@ -0,0 +1,105 @@ +# -------------------------------- Input data -------------------------------- # +import os, hashlib + +test_data = {} + +test = 1 +test_data[test] = { + "input": """abc""", + "expected": ["22728", "22551"], +} + +test += 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +test_data[test] = { + "input": "qzyelonm", + "expected": ["15168", "20864"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +if part_to_test == 1: + index = 0 + found_keys = 0 + while True: + index += 1 + init_hash = hashlib.md5((puzzle_input + str(index)).encode("utf-8")).hexdigest() + triplets = [x for x in "0123456789abcdef" if x * 3 in init_hash] + + if triplets: + first_triplet_position = min([init_hash.find(x * 3) for x in triplets]) + triplet = init_hash[first_triplet_position] + + for i in range(1, 1000): + new_hash = hashlib.md5( + (puzzle_input + str(index + i)).encode("utf-8") + ).hexdigest() + if triplet * 5 in new_hash: + found_keys += 1 + break + + if found_keys == 64: + puzzle_actual_result = index + break + + +else: + hashes = [] + hashes_first_triplet = {} + hashes_quintuplets = {} + index = -1 + found_keys = 0 + + for i in range(30000): + hash_text = puzzle_input + str(i) + for y in range(2017): + hash_text = hashlib.md5(hash_text.encode("utf-8")).hexdigest() + hashes.append(hash_text) + + triplets = [x for x in "0123456789abcdef" if x * 3 in hash_text] + + if triplets: + first_triplet_position = min([hash_text.find(x * 3) for x in triplets]) + hashes_first_triplet[i] = hash_text[first_triplet_position] + + quintuplets = [x for x in "0123456789abcdef" if x * 5 in hash_text] + + if quintuplets: + hashes_quintuplets[i] = quintuplets + + for index, triplet in hashes_first_triplet.items(): + for i in range(1, 1000): + if index + i in hashes_quintuplets: + if triplet in hashes_quintuplets[index + i]: + found_keys += 1 + break + + if found_keys == 64: + puzzle_actual_result = index + break + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/21-Fractal Art.py b/2017/21-Fractal Art.py index 8c4cee1..c7d03e0 100644 --- a/2017/21-Fractal Art.py +++ b/2017/21-Fractal Art.py @@ -4,40 +4,43 @@ test_data = {} test = 1 -test_data[test] = {"input": """../.# => ##./#../... +test_data[test] = { + "input": """../.# => ##./#../... .#./..#/### => #..#/..../..../#..#""", - "expected": ['12', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['139', '1857134'], - } + "expected": ["12", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["139", "1857134"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -pattern = '''.#. +pattern = """.#. ..# -###''' - -grid = drawing.text_to_grid(pattern) -parts = drawing.split_in_parts(grid, 2, 2) -merged_grid = drawing.merge_parts(parts, 2, 2) - +###""".split( + "\n" +) if case_to_test == 1: iterations = 2 @@ -48,60 +51,86 @@ enhancements = {} -for string in puzzle_input.split('\n'): - if string == '': +for string in puzzle_input.split("\n"): + if string == "": continue - source, _, target = string.split(' ') - source = source.replace('/', '\n') - target = target.replace('/', '\n') + source, _, target = string.split(" ") + source = tuple(source.split("/")) + target = target.split("/") - source_grid = drawing.text_to_grid(source) enhancements[source] = target - for rotated_source in drawing.rotate(source_grid): - rotated_source_text = drawing.grid_to_text(rotated_source) - enhancements[rotated_source_text] = target - - for flipped_source in drawing.flip(rotated_source): - flipped_source_text = drawing.grid_to_text(flipped_source) - enhancements[flipped_source_text] = target + def rotate_flip(source): + sources = [] + size = len(source) + new = list(source).copy() + for rotate in range(4): + new = [ + "".join(new[x][size - y - 1] for x in range(size)) for y in range(size) + ] + sources.append("/".join(new)) + new_flipx = [ + "".join(new[y][size - x - 1] for x in range(size)) for y in range(size) + ] + new_flipy = [ + "".join(new[size - y - 1][x] for x in range(size)) for y in range(size) + ] + sources.append("/".join(new_flipx)) + sources.append("/".join(new_flipy)) + return set(sources) + + for sources in rotate_flip(source): + enhancements[sources] = target -pattern_grid = drawing.text_to_grid(pattern) for i in range(iterations): + if verbose_level >= 2: + print("Iteration", i) + size = len(pattern) - grid_x, grid_y = zip(*pattern_grid.keys()) - grid_width = max(grid_x) - min(grid_x) + 1 - - if grid_width % 2 == 0: - parts = drawing.split_in_parts(pattern_grid, 2, 2) + if size % 2 == 0: + block_size = 2 else: - parts = drawing.split_in_parts(pattern_grid, 3, 3) - - grid_size = int(math.sqrt(len(parts))) - - new_parts = [] - for part in parts: - part_text = drawing.grid_to_text(part) - new_parts.append(drawing.text_to_grid(enhancements[part_text])) - - new_grid = drawing.merge_parts(new_parts, grid_size, grid_size) - - pattern_grid = new_grid - -grid_text = drawing.grid_to_text(pattern_grid) - -puzzle_actual_result = grid_text.count('#') - + block_size = 3 + + nb_blocks = size // block_size + + blocks = [ + [ + "/".join( + "".join( + pattern[y + iy * block_size][x + ix * block_size] + for x in range(block_size) + ) + for y in range(block_size) + ) + for ix in range(nb_blocks) + ] + for iy in range(nb_blocks) + ] + + new_blocks = [ + [enhancements[block] for block in blocks[y]] for y in range(len(blocks)) + ] + + pattern = [ + "".join( + new_blocks[iy][ix][y][x] + for ix in range(nb_blocks) + for x in range(block_size + 1) + ) + for iy in range(nb_blocks) + for y in range(block_size + 1) + ] + if verbose_level >= 2: + print("\n".join(pattern)) + +puzzle_actual_result = "".join(pattern).count("#") # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/21-Fractal Art.v1.py b/2017/21-Fractal Art.v1.py new file mode 100644 index 0000000..f478d72 --- /dev/null +++ b/2017/21-Fractal Art.v1.py @@ -0,0 +1,108 @@ +# -------------------------------- Input data -------------------------------- # +import os, drawing, itertools, math + +test_data = {} + +test = 1 +test_data[test] = { + "input": """../.# => ##./#../... +.#./..#/### => #..#/..../..../#..#""", + "expected": ["12", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["139", "1857134"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +pattern = """.#. +..# +###""" + +grid = drawing.text_to_grid(pattern) +parts = drawing.split_in_parts(grid, 2, 2) +merged_grid = drawing.merge_parts(parts, 2, 2) + + +if case_to_test == 1: + iterations = 2 +elif part_to_test == 1: + iterations = 5 +else: + iterations = 18 + + +enhancements = {} +for string in puzzle_input.split("\n"): + if string == "": + continue + + source, _, target = string.split(" ") + source = source.replace("/", "\n") + target = target.replace("/", "\n") + + source_grid = drawing.text_to_grid(source) + enhancements[source] = target + + for rotated_source in drawing.rotate(source_grid): + rotated_source_text = drawing.grid_to_text(rotated_source) + enhancements[rotated_source_text] = target + + for flipped_source in drawing.flip(rotated_source): + flipped_source_text = drawing.grid_to_text(flipped_source) + enhancements[flipped_source_text] = target + +pattern_grid = drawing.text_to_grid(pattern) +for i in range(iterations): + + grid_x, grid_y = zip(*pattern_grid.keys()) + grid_width = max(grid_x) - min(grid_x) + 1 + + if grid_width % 2 == 0: + parts = drawing.split_in_parts(pattern_grid, 2, 2) + else: + parts = drawing.split_in_parts(pattern_grid, 3, 3) + + grid_size = int(math.sqrt(len(parts))) + + new_parts = [] + for part in parts: + part_text = drawing.grid_to_text(part) + new_parts.append(drawing.text_to_grid(enhancements[part_text])) + + new_grid = drawing.merge_parts(new_parts, grid_size, grid_size) + + pattern_grid = new_grid + +grid_text = drawing.grid_to_text(pattern_grid) + +puzzle_actual_result = grid_text.count("#") + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2019/13-Care Package.py b/2019/13-Care Package.py index 8d2e1c8..e0f0762 100644 --- a/2019/13-Care Package.py +++ b/2019/13-Care Package.py @@ -55,49 +55,33 @@ else: computer.instructions[0] = 2 - blocks_left = 1 score = 0 - vertices = {} - - while blocks_left > 0 and computer.state != "Failure": + while computer.state != "Stopped": computer.run() - # Check if we can still play - blocks_left = 0 - ball_position = 0 - paddle_position = 0 + ball_x = 0 + paddle_x = 0 for i in range(len(computer.outputs) // 3): - - vertices[ - computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] - ] = computer.outputs[i * 3 + 2] - # The ball has not fallen + # Ball position if computer.outputs[i * 3 + 2] == 4: - ball_position = ( - computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] - ) - if ball_position.imag < -21: - print("Failed") - computer.state = "Failure" - break + ball_x = computer.outputs[i * 3] + # Paddle position + elif computer.outputs[i * 3 + 2] == 3: + paddle_x = computer.outputs[i * 3] + # Check the score elif computer.outputs[i * 3] == -1 and computer.outputs[i * 3 + 1] == 0: score = computer.outputs[i * 3 + 2] + computer.outputs = [] - # Store the paddle position - elif computer.outputs[i * 3 + 2] == 3: - paddle_position = ( - computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] - ) - - # There are still blocks to break - blocks_left = len([x for x in vertices if vertices[x] == 2]) + if computer.state == "Stopped": + break # Move paddle - if paddle_position.real < ball_position.real: + if paddle_x < ball_x: joystick = 1 - elif paddle_position.real > ball_position.real: + elif paddle_x > ball_x: joystick = -1 else: joystick = 0 @@ -122,10 +106,6 @@ # 'Restart' the computer to process the input computer.restart() - # Outputs the grid (just for fun) - grid.vertices = {x: tiles.get(vertices[x], vertices[x]) for x in vertices} - print(grid.vertices_to_grid()) - puzzle_actual_result = score diff --git a/2019/13-Care Package.v1.py b/2019/13-Care Package.v1.py new file mode 100644 index 0000000..8d2e1c8 --- /dev/null +++ b/2019/13-Care Package.v1.py @@ -0,0 +1,135 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, pathfinding, IntCode + +from complex_utils import * + +test_data = {} + +test = 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["462", "23981"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +tiles = {0: " ", 1: "#", 2: "ø", 3: "_", 4: "o"} +grid = pathfinding.Graph() +computer = IntCode.IntCode(puzzle_input) + +if part_to_test == 1: + computer.run() + grid.vertices = {} + for i in range(len(computer.outputs) // 3): + position = SuperComplex( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + grid.vertices[position] = tiles[computer.outputs[i * 3 + 2]] + + puzzle_actual_result = sum([1 for val in grid.vertices.values() if val == "ø"]) + + +else: + computer.instructions[0] = 2 + blocks_left = 1 + score = 0 + + vertices = {} + + while blocks_left > 0 and computer.state != "Failure": + computer.run() + + # Check if we can still play + blocks_left = 0 + ball_position = 0 + paddle_position = 0 + for i in range(len(computer.outputs) // 3): + + vertices[ + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ] = computer.outputs[i * 3 + 2] + # The ball has not fallen + if computer.outputs[i * 3 + 2] == 4: + ball_position = ( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + if ball_position.imag < -21: + print("Failed") + computer.state = "Failure" + break + # Check the score + elif computer.outputs[i * 3] == -1 and computer.outputs[i * 3 + 1] == 0: + score = computer.outputs[i * 3 + 2] + + # Store the paddle position + elif computer.outputs[i * 3 + 2] == 3: + paddle_position = ( + computer.outputs[i * 3] - j * computer.outputs[i * 3 + 1] + ) + + # There are still blocks to break + blocks_left = len([x for x in vertices if vertices[x] == 2]) + + # Move paddle + if paddle_position.real < ball_position.real: + joystick = 1 + elif paddle_position.real > ball_position.real: + joystick = -1 + else: + joystick = 0 + computer.add_input(joystick) + + if verbose_level >= 2: + print( + "Movements", + len(computer.all_inputs), + " - Score", + score, + " - Blocks left", + blocks_left, + " - Ball", + ball_position, + " - Paddle", + paddle_position, + " - Direction", + joystick, + ) + + # 'Restart' the computer to process the input + computer.restart() + + # Outputs the grid (just for fun) + grid.vertices = {x: tiles.get(vertices[x], vertices[x]) for x in vertices} + print(grid.vertices_to_grid()) + + puzzle_actual_result = score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From d737eb6de1973d2cbeefa4ff44762167a3a02fce Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 29 Aug 2020 18:56:03 +0200 Subject: [PATCH 078/143] Further performance improvement --- 2016/12-Leonardo's Monorail.py | 98 +++++++++++++++---------------- 2016/12-Leonardo's Monorail.v1.py | 96 ++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 51 deletions(-) create mode 100644 2016/12-Leonardo's Monorail.v1.py diff --git a/2016/12-Leonardo's Monorail.py b/2016/12-Leonardo's Monorail.py index 835817d..94f2188 100644 --- a/2016/12-Leonardo's Monorail.py +++ b/2016/12-Leonardo's Monorail.py @@ -4,93 +4,89 @@ test_data = {} test = 1 -test_data[test] = {"input": """cpy 41 a +test_data[test] = { + "input": """cpy 41 a inc a inc a dec a jnz a 2 dec a""", - "expected": ['42', 'Unknown'], - } + "expected": ["42", "Unknown"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['318083', '9227737'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["318083", "9227737"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -registers = {'a':0, 'b':0, 'c':0, 'd':0} +registers = {"a": 0, "b": 0, "c": 0, "d": 0} if part_to_test == 2: - registers['c'] = 1 + registers["c"] = 1 -instructions = puzzle_input.split('\n') +instructions = [line.split(" ") for line in puzzle_input.split("\n")] i = 0 while True: - instruction = instructions[i] + ins = instructions[i] i += 1 - if instruction[0:3] == 'cpy': - _, val, target = instruction.split(' ') + if ins[0] == "cpy": try: - registers[target] = int(val) + registers[ins[2]] = int(ins[1]) except ValueError: - registers[target] = registers[val] - - elif instruction[0:3] == 'inc': - _, target = instruction.split(' ') - registers[target] += 1 - elif instruction[0:3] == 'dec': - _, target = instruction.split(' ') - registers[target] -= 1 - - elif instruction[0:3] == 'jnz': - _, target, jump = instruction.split(' ') - if target == '0': + registers[ins[2]] = registers[ins[1]] + + elif ins[0] == "inc": + registers[ins[1]] += 1 + elif ins[0] == "dec": + registers[ins[1]] -= 1 + + elif ins[0] == "jnz": + if ins[1] == "0": pass else: try: - if int(target): - i = i + int(jump) - 1 # -1 to compensate for what we added before + if int(ins[1]): + i += int(ins[2]) - 1 # -1 to compensate for what we added before except ValueError: - if registers[target] != 0: - i = i + int(jump) - 1 # -1 to compensate for what we added before + if registers[ins[1]] != 0: + i += int(ins[2]) - 1 # -1 to compensate for what we added before if i >= len(instructions): break -puzzle_actual_result = registers['a'] - - - +puzzle_actual_result = registers["a"] # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/12-Leonardo's Monorail.v1.py b/2016/12-Leonardo's Monorail.v1.py new file mode 100644 index 0000000..5524e96 --- /dev/null +++ b/2016/12-Leonardo's Monorail.v1.py @@ -0,0 +1,96 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """cpy 41 a +inc a +inc a +dec a +jnz a 2 +dec a""", + "expected": ["42", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["318083", "9227737"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # +registers = {"a": 0, "b": 0, "c": 0, "d": 0} +if part_to_test == 2: + registers["c"] = 1 + + +instructions = puzzle_input.split("\n") +i = 0 +while True: + instruction = instructions[i] + i += 1 + + if instruction[0:3] == "cpy": + _, val, target = instruction.split(" ") + try: + registers[target] = int(val) + except ValueError: + registers[target] = registers[val] + + elif instruction[0:3] == "inc": + _, target = instruction.split(" ") + registers[target] += 1 + elif instruction[0:3] == "dec": + _, target = instruction.split(" ") + registers[target] -= 1 + + elif instruction[0:3] == "jnz": + _, target, jump = instruction.split(" ") + if target == "0": + pass + else: + try: + if int(target): + i = i + int(jump) - 1 # -1 to compensate for what we added before + except ValueError: + if registers[target] != 0: + i = i + int(jump) - 1 # -1 to compensate for what we added before + + if i >= len(instructions): + break + +puzzle_actual_result = registers["a"] + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 63af5e1e07c0061551665040204840426404ddd8 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 1 Dec 2020 08:21:14 +0100 Subject: [PATCH 079/143] Started 2020, added day 2020-01 and various utilities --- .gitignore | 1 + 2016/24-Air Duct Spelunking.py | 70 ++--- 2020/01-Report Repair.py | 82 +++++ 2020/assembly.py | 546 +++++++++++++++++++++++++++++++++ 2020/compass.py | 35 +++ 2020/dot.py | 222 ++++++++++++++ 2020/graph.py | 446 +++++++++++++++++++++++++++ 2020/grid.py | 335 ++++++++++++++++++++ 8 files changed, 1699 insertions(+), 38 deletions(-) create mode 100644 2020/01-Report Repair.py create mode 100644 2020/assembly.py create mode 100644 2020/compass.py create mode 100644 2020/dot.py create mode 100644 2020/graph.py create mode 100644 2020/grid.py diff --git a/.gitignore b/.gitignore index ac5e756..078bdd3 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ Inputs/ template.py __pycache__ parse/ +download.py \ No newline at end of file diff --git a/2016/24-Air Duct Spelunking.py b/2016/24-Air Duct Spelunking.py index a7173ac..2764334 100644 --- a/2016/24-Air Duct Spelunking.py +++ b/2016/24-Air Duct Spelunking.py @@ -4,50 +4,54 @@ test_data = {} test = 1 -test_data[test] = {"input": """########### +test_data[test] = { + "input": """########### #0.1.....2# #.#######.# #4.......3# ###########""", - "expected": ['Unknown', 'Unknown'], - } - -test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['442', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["442", "660"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # grid = puzzle_input -graph = pathfinding.WeightedGraph () -graph.grid_to_vertices(re.sub('[0-9]', '.', puzzle_input)) +graph = pathfinding.WeightedGraph() +graph.grid_to_vertices(re.sub("[0-9]", ".", puzzle_input)) waypoints = {} -for i in range (10): +for i in range(10): if str(i) in grid: - waypoints[i] = (grid.find(str(i)) % (len(grid.split('\n')[0])+1), grid.find(str(i)) // (len(grid.split('\n')[0])+1)) + waypoints[i] = ( + grid.find(str(i)) % (len(grid.split("\n")[0]) + 1), + grid.find(str(i)) // (len(grid.split("\n")[0]) + 1), + ) -edges = {waypoints[x]:{} for x in waypoints} +edges = {waypoints[x]: {} for x in waypoints} for a in waypoints: for b in waypoints: if waypoints[a] <= waypoints[b]: @@ -59,7 +63,7 @@ edges[waypoints[b]][waypoints[a]] = graph.distance_from_start[waypoints[b]] graph.reset_search() -min_length = 10**6 +min_length = 10 ** 6 for order in itertools.permutations([waypoints[x] for x in waypoints if x != 0]): length = 0 current_waypoint = waypoints[0] @@ -74,19 +78,9 @@ puzzle_actual_result = min_length - - - - - - # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/01-Report Repair.py b/2020/01-Report Repair.py new file mode 100644 index 0000000..47a3c56 --- /dev/null +++ b/2020/01-Report Repair.py @@ -0,0 +1,82 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # t hanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1721 +979 +366 +299 +675 +1456""", + "expected": ["514579", "241861950"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["997899", "131248694"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +values = sorted(ints(puzzle_input)) + +for a in itertools.combinations(values, part_to_test + 1): + if sum(a) == 2020: + puzzle_actual_result = math.prod(a) + break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/assembly.py b/2020/assembly.py new file mode 100644 index 0000000..a07534f --- /dev/null +++ b/2020/assembly.py @@ -0,0 +1,546 @@ +import json + +# -------------------------------- Notes ----------------------------- # + + +# This program will run pseudo-assembly code based on provided instructions +# It can handle a set of instructions (which are writable), a stack and registers + + +# -------------------------------- Program flow exceptions ----------------------------- # + + +class MissingInput(RuntimeError): + pass + + +class ProgramHalt(RuntimeError): + pass + + +# -------------------------------- Main program class ----------------------------- # +class Program: + + # Whether to print outputs + print_output = False + # Print outputs in a detailed way (useful when debugging is detailed) + print_output_verbose = False + # Print outputs when input is required (useful for text-based games) + print_output_before_input = False + + # Whether to print the inputs received (useful for predefined inputs) + print_input = False + # Print inputs in a detailed way (useful when debugging is detailed) + print_input_verbose = False + + # Whether to print the instructions before execution + print_details_before = False + # Whether to print the instructions after execution + print_details_after = False + + # Output format - for all instructions + print_format = "{pointer:5}-{opcode:15} {instr:50} - R: {registers} - Stack ({stack_len:4}): {stack}" + # Output format for numbers + print_format_numbers = "{val:5}" + + # Whether inputs and outputs are ASCII codes or not + input_ascii = True + output_ascii = True + + # Whether to ask user for input or not (if not, will raise exception) + input_from_terminal = True + + # Bit length used for NOT operation (bitwise inverse) + bit_length = 15 + + # Where to store saves + save_data_file = "save.txt" + + # Maximum number of instructions executed + max_instructions = 10 ** 7 + + # Sets up the program based on the provided instructions + def __init__(self, program): + self.instructions = program.copy() + self.registers = [0] * 8 + self.stack = [] + self.pointer = 0 + self.state = "Running" + self.output = [] + self.input = [] + self.instructions_done = 0 + + ################### Main program body ################### + + def run(self): + while ( + self.state == "Running" and self.instructions_done < self.max_instructions + ): + self.instructions_done += 1 + # Get details of current operation + opcode = self.instructions[self.pointer] + current_instr = self.get_instruction(opcode) + + # Outputs operation details before its execution + if self.print_details_before: + self.print_operation(opcode, current_instr) + + self.operation_codes[opcode][2](self, current_instr) + + # Outputs operation details after its execution + if self.print_details_after: + self.print_operation(opcode, self.get_instruction(opcode)) + + # Moves the pointer + if opcode not in self.operation_jumps and self.state == "Running": + self.pointer += self.operation_codes[opcode][1] + + print("instructions", i) + + # Gets all parameters for the current instruction + def get_instruction(self, opcode): + args_order = self.operation_codes[opcode][3] + values = [opcode] + [ + self.instructions[self.pointer + order + 1] for order in args_order + ] + print([self.pointer + order + 1 for order in args_order]) + + print(args_order, values, self.operation_codes[opcode]) + + return values + + # Prints the details of an operation according to the specified format + def print_operation(self, opcode, instr): + params = instr.copy() + # Remove opcode + del params[0] + + # Handle stack operations + if opcode in self.operation_stack and self.stack: + params.append(self.stack[-1]) + elif opcode in self.operation_stack: + params.append("Empty") + + # Format the numbers + params = list(map(self.format_numbers, params)) + + data = {} + data["opcode"] = opcode + data["pointer"] = self.pointer + data["registers"] = ",".join(map(self.format_numbers, self.registers)) + data["stack"] = ",".join(map(self.format_numbers, self.stack)) + data["stack_len"] = len(self.stack) + + instr_output = self.operation_codes[opcode][0].format(*params, **data) + final_output = self.print_format.format(instr=instr_output, **data) + print(final_output) + + # Outputs all stored data and resets it + def print_output_data(self): + if self.output and self.print_output_before_input: + if self.output_ascii: + print("".join(self.output), sep="", end="") + else: + print(self.output, end="") + self.output = [] + + # Formats numbers + def format_numbers(self, code): + return self.print_format_numbers.format(val=code) + + # Sets a log level based on predefined rules + def log_level(self, level): + self.print_output = False + self.print_output_verbose = False + self.print_output_before_input = False + + self.print_input = False + self.print_input_verbose = False + + self.print_details_before = False + self.print_details_after = False + + if level >= 1: + self.print_output = True + self.print_input = True + + if level >= 2: + self.print_output_verbose = True + self.print_output_before_input = True + self.print_input_verbose = True + self.print_details_before = True + + if level >= 3: + self.print_details_after = True + + ################### Get and set registers and memory ################### + + # Reads a "normal" value based on the provided reference + def get_register(self, reference): + return self.registers[reference] + + # Writes a value to a register + def set_register(self, reference, value): + self.registers[reference] = value + + # Reads a memory value based on the code + def get_memory(self, code): + return self.instructions[code] + + # Writes a value to the memory + def set_memory(self, reference, value): + self.instructions[reference] = value + + ################### Start / Stop the program ################### + + # halt: Stop execution and terminate the program + def op_halt(self, instr): + self.state = "Stopped" + raise ProgramHalt("Reached Halt instruction") + + # pass 21: No operation + def op_pass(self, instr): + return + + ################### Basic operations ################### + + # add a b c: Assign into the sum of and ", + def op_add(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) + self.get_register(instr[3]) + ) + + # mult a b c: store into the product of and ", + def op_multiply(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) * self.get_register(instr[3]) + ) + + # mod a b c: store into the remainder of divided by ", + def op_modulo(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) % self.get_register(instr[3]) + ) + + # set a b: set register to the value of + def op_set(self, instr): + self.set_register(instr[1], self.get_register(instr[2])) + + ################### Comparisons ################### + + # eq a b c: set to 1 if is equal to ; set it to 0 otherwise", + def op_equal(self, instr): + self.set_register( + instr[1], + 1 if self.get_register(instr[2]) == self.get_register(instr[3]) else 0, + ) + + # gt a b c: set to 1 if is greater than ; set it to 0 otherwise", + def op_greater_than(self, instr): + self.set_register( + instr[1], + 1 if self.get_register(instr[2]) > self.get_register(instr[3]) else 0, + ) + + ################### Binary operations ################### + + # and a b c: stores into the bitwise and of and ", + def op_and(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) & self.get_register(instr[3]) + ) + + # or a b c: stores into the bitwise or of and ", + def op_or(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) | self.get_register(instr[3]) + ) + + # not a b: stores 15-bit bitwise inverse of in ", + def op_not(self, instr): + self.set_register( + instr[1], ~self.get_register(instr[2]) & int("1" * self.bit_length, 2) + ) + + ################### Jumps ################### + + # jmp a: jump to ", + def op_jump(self, instr): + self.pointer = self.get_register(instr[1]) + + # jt a b: if is nonzero, jump to ", + def op_jump_if_true(self, instr): + self.pointer = ( + self.get_register(instr[2]) + if self.get_register(instr[1]) != 0 + else self.pointer + self.operation_codes["jump_if_true"][1] + ) + + # jf a b: if is zero, jump to ", + def op_jump_if_false(self, instr): + self.pointer = ( + self.get_register(instr[2]) + if self.get_register(instr[1]) == 0 + else self.pointer + self.operation_codes["jump_if_false"][1] + ) + + ################### Memory-related operations ################### + + # rmem a b: read memory at address and write it to ", + def op_read_memory(self, instr): + self.set_register(instr[1], self.get_memory(self.get_register(instr[2]))) + + # wmem a b: write the value from into memory at address ", + def op_write_memory(self, instr): + self.set_memory(self.get_register(instr[1]), self.get_register(instr[2])) + + ################### Stack-related operations ################### + + # push a: push onto the stack", + def op_push(self, instr): + self.stack.append(self.get_register(instr[1])) + + # pop a: remove the top element from the stack and write it into ; empty stack = error", + def op_pop(self, instr): + if not self.stack: + self.state = "Error" + else: + self.set_register(instr[1], self.stack.pop()) + + # ret: remove the top element from the stack and jump to it; empty stack = halt", + def op_jump_to_stack(self, instr): + if not self.stack: + raise RuntimeError("No stack available for jump") + else: + self.pointer = self.stack.pop() + + ################### Input and output ################### + + # in a: read a character from the terminal and write its ascii code to + def op_input(self, instr): + self.print_output_data() + + self.custom_commands() + while not self.input: + if self.input_from_terminal: + self.add_input(input() + "\n") + else: + raise MissingInput() + + if self.input[0] == "?": + self.custom_commands() + + letter = self.input.pop(0) + + # Print what we received? + if self.print_input_verbose: + print(" Input: ", letter) + elif self.print_input: + print(letter, end="") + + # Actually write the input to the registers + if self.input_ascii: + self.set_register(instr[1], ord(letter)) + else: + self.set_register(instr[1], letter) + + # out a: write the character represented by ascii code to the terminal", + def op_output(self, instr): + # Determine what to output + if self.output_ascii: + letter = chr(self.get_register(instr[1])) + else: + letter = self.get_register(instr[1]) + + # Store for future use + self.output += letter + + # Display output immediatly? + if self.print_output_verbose: + print(" Output:", letter) + elif self.print_output: + print(letter, end="") + + ################### Save and restore ################### + + def save_state(self): + data = [ + self.instructions, + self.registers, + self.stack, + self.pointer, + self.state, + self.output, + self.input, + ] + with open(self.save_data_file, "w") as f: + json.dump(data, f) + + def restore_state(self): + with open(self.save_data_file, "r") as f: + data = json.load(f) + + ( + self.instructions, + self.registers, + self.stack, + self.pointer, + self.state, + self.output, + self.input, + ) = data + + ################### Adding manual inputs ################### + + def add_input(self, input_data, convert_ascii=True): + try: + self.input += input_data + except TypeError: + self.input.append(input_data) + + ################### Custom commands ################### + + # Pause until input provided + def custom_pause(self, instr): + print("Program paused. Press Enter to continue.") + input() + + # Pause until input provided + def custom_stop(self, instr): + self.op_halt(instr) + + # Save + def custom_save(self, instr): + self.save_state() + if self.print_output: + print("\nSaved game.") + + # Restore + def custom_restore(self, instr): + self.restore_state() + if self.print_output: + print("\nRestored the game.") + + # set a b: set register to the value of + def custom_write(self, instr): + self.op_set([instr[0]] + list(map(int, instr[1:]))) + + # log a: sets the log level to X + def custom_log(self, instr): + self.log_level(int(instr[1])) + if self.print_output: + print("\nChanged log level to", instr[1]) + + # print: prints the current situation in a detailed way + def custom_print(self, instr): + self.print_operation("?print", instr) + + def custom_commands(self): + while self.input and self.input[0] == "?": + command = self.input.pop(0) + while command[-1] != "\n" and self.input: + command += self.input.pop(0) + + if self.print_input: + print(command) + + command = command.replace("\n", "").split(" ") + self.operation_codes[command[0]][2](self, command) + + # ADDING NEW INSTRUCTIONS + # - Create a method with a name starting by op_ + # Its signature must be: op_X (self, instr) + # instr contains the list of values relevant to this operation (raw data from instructions set) + # - Reference this method in the variable operation_codes + # Format of the variable: + # operation code: [ + # debug formatting (used by str.format) + # number of operands (including the operation code) + # method to call + # argument order] ==> [2, 0, 1] means arguments are in provided as c, a, b + # - Include it in operation_jumps or operation_stack if relevant + + # ADDING CUSTOM INSTRUCTIONS + # Those instructions are not interpreted by the run() method + # Therefore: + # - They will NOT move the pointer + # - They will NOT impact the program (unless you make them do so) + # They're processed through the op_input method + # Custom operations are also referenced in the same operation_codes variable + # Custom operations start with ? for easy identification during input processing + + # TL;DR: Format: + # operation code: [ + # debug formatting + # number of operands (including the operation code) + # method to call + # argument order] + operation_codes = { + # Start / Stop + 0: ["halt", 1, op_halt, []], + 21: ["pass", 1, op_pass, []], + # Basic operations + 9: ["add: {0} = {1}+{2}", 4, op_add, [2, 0, 1]], # This means c = a + b + 10: ["mult: {0} = {1}*{2}", 4, op_multiply, [0, 1, 2]], + 11: ["mod: {0} = {1}%{2}", 4, op_modulo, [0, 1, 2]], + 1: ["set: {0} = {1}", 3, op_set, [0, 1]], + # Comparisons + 4: ["eq: {0} = {1} == {2}", 4, op_equal, [0, 1, 2]], + 5: ["gt: {0} = ({1} > {2})", 4, op_greater_than, [0, 1, 2]], + # Binary operations + 12: ["and: {0} = {1}&{2}", 4, op_and, [0, 1, 2]], + 13: ["or: {0} = {1}|{2}", 4, op_or, [0, 1, 2]], + 14: ["not: {0} = ~{1}", 3, op_not, [0, 1]], + # Jumps + 6: ["jump: go to {0}", 2, op_jump, [0]], + 7: ["jump if yes: go to {1} if {0}", 3, op_jump_if_true, [0, 1]], + 8: ["jump if no: go to {1} if !{0}", 3, op_jump_if_false, [0, 1]], + # Memory-related operations + 15: ["rmem: {0} = M{1}", 3, op_read_memory, [0, 1]], + 16: ["wmem: write {1} to M{0}", 3, op_write_memory, [0, 1]], + # Stack-related operations + 2: ["push: stack += {0}", 2, op_push, [0]], + 3: ["pop: {0} = stack.pop() ({1})", 2, op_pop, [0]], + 18: ["pop & jump: jump to stack.pop() ({0})", 2, op_jump_to_stack, []], + # Inputs and outputs + 19: ["out: print {0}", 2, op_output, [0]], + 20: ["in: {0} = input", 2, op_input, [0]], + # Custom operations + "?save": ["Saved data", 2, custom_save, []], + "?write": ["Wrote data", 3, custom_write, []], + "?restore": ["Restored data", 2, custom_restore, []], + "?log": ["Logging enabled", 2, custom_log, []], + "?stop": ["STOP", 2, custom_stop, []], + "?pause": ["Pause", 2, custom_pause, []], + "?print": ["Print data", 1, custom_print, []], + } + # Operations in this list will not move the pointer through the run method + # (this is because they do it themselves) + operation_jumps = ["jump", "jump_if_true", "jump_if_false", "jump_to_stack"] + # Operations in this list use the stack + # (the value taken from stack will be added to debug) + operation_stack = ["pop", "jump_to_stack"] + + +# -------------------------------- Documentation & main variables ----------------------------- # + +# HOW TO MAKE IT WORK +# The program has a set of possible instructions +# The exact list is available in variable operation_codes +# In order to work, you must modify this variable operation_codes so that the key is the code in your computer + +# If you need to override the existing methods, you need to override operation_codes + + +# NOT OPERATION +# This will perform a bitwise inverse +# However, it requires the length (in bits) specific to the program's hardware +# Therefore, update Program.bit_length +# TL;DR: Length in bits used for NOT +Program.bit_length = 15 + +# Save file (stored as JSON) +Program.save_data_file = "save.txt" + +# Maximum instructions to be executed +Program.max_instructions = 10 ** 7 diff --git a/2020/compass.py b/2020/compass.py new file mode 100644 index 0000000..041a2c5 --- /dev/null +++ b/2020/compass.py @@ -0,0 +1,35 @@ +north = 1j +south = -1j +west = -1 +east = 1 +northeast = 1 + 1j +northwest = -1 + 1j +southeast = 1 - 1j +southwest = -1 - 1j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + +text_to_direction = { + "N": north, + "S": south, + "E": east, + "W": west, + "NW": northwest, + "NE": northeast, + "SE": southeast, + "SW": southwest, +} +direction_to_text = {text_to_direction[x]: x for x in text_to_direction} + +relative_directions = { + "left": 1j, + "right": -1j, + "ahead": 1, + "back": -1, +} diff --git a/2020/dot.py b/2020/dot.py new file mode 100644 index 0000000..dd7666f --- /dev/null +++ b/2020/dot.py @@ -0,0 +1,222 @@ +from compass import * +import math + + +def get_dot_position(element): + if isinstance(element, Dot): + return element.position + else: + return element + + +# Defines all directions that can be used (basically, are diagonals allowed?) +all_directions = directions_straight + + +class Dot: + # The first level is the actual terrain + # The second level is, in order: is_walkable, is_waypoint + # Walkable means you can get on that dot and leave it + # Waypoints are just cool points (it's meant for reducting the grid to a smaller graph) + # Isotropic means the direction doesn't matter + terrain_map = { + ".": [True, False], + "#": [False, False], + " ": [False, False], + "^": [True, True], + "v": [True, True], + ">": [True, True], + "<": [True, True], + "+": [True, False], + "|": [True, False], + "-": [True, False], + "/": [True, False], + "\\": [True, False], + "X": [True, True], + } + terrain_default = "X" + + # Override for printing + terrain_print = { + "^": "|", + "v": "|", + ">": "-", + "<": "-", + } + + # Defines which directions are allowed + # The first level is the actual terrain + # The second level is the direction taken to reach the dot + # The third level are the directions allowed to leave it + allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, + } + # This has the same format, except the third level has only 1 option + # Anisotropic grids allow only 1 direction for each (position, source_direction) + # Target direction is the direction in which I'm going + allowed_anisotropic_direction_map = { + ".": {dir: [-dir] for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: [-dir] for dir in all_directions}, + "|": {north: [south], south: [north]}, + "^": {north: [south], south: [north]}, + "v": {north: [south], south: [north]}, + "-": {east: [west], west: [east]}, + ">": {east: [west], west: [east]}, + "<": {east: [west], west: [east]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: [-dir] for dir in all_directions}, + } + # Default allowed directions + direction_default = all_directions + + # How to sort those dots + sorting_map = { + "xy": lambda self, a: (a.real, a.imag), + "yx": lambda self, a: (a.imag, a.real), + "reading": lambda self, a: (-a.imag, a.real), + "manhattan": lambda self, a: (abs(a.real) + abs(a.imag)), + "*": lambda self, a: (a.imag ** 2 + a.real ** 2) ** 0.5, + } + sort_value = sorting_map["*"] + + def __init__(self, grid, position, terrain, source_direction=None): + self.position = position + self.grid = grid + self.set_terrain(terrain) + self.neighbors = {} + if self.grid.is_isotropic: + self.set_directions() + else: + if source_direction: + self.source_direction = source_direction + self.set_directions() + else: + raise ValueError("Anisotropic dots need a source direction") + + self.neighbors_obsolete = True + + # Those functions allow sorting for various purposes + def __lt__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) < self.sort_value(ref) + + def __le__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) <= self.sort_value(ref) + + def __gt__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) > self.sort_value(ref) + + def __ge__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) >= self.sort_value(ref) + + def __repr__(self): + if self.grid.is_isotropic: + return self.terrain + "@" + complex(self.position).__str__() + else: + return ( + self.terrain + + "@" + + complex(self.position).__str__() + + direction_to_text[self.source_direction] + ) + + def __str__(self): + return self.terrain + + def __add__(self, direction): + if not direction in self.allowed_directions: + raise ValueError("Can't add a Dot with forbidden direction") + position = self.position + direction + if self.grid.is_isotropic: + return self.get_dot(position) + else: + # For the target dot, I'm coming from the opposite direction + return self.get_dot((position, -self.allowed_directions[0])) + + def __sub__(self, direction): + return self.__add__(-direction) + + def phase(self, reference=0): + ref = get_dot_position(reference) + return math.atan2(self.position.imag - ref.imag, self.position.real - ref.real) + + def amplitude(self, reference=0): + ref = get_dot_position(reference) + return ( + (self.position.imag - ref.imag) ** 2 + (self.position.real - ref.real) ** 2 + ) ** 0.5 + + def manhattan_distance(self, reference=0): + ref = get_dot_position(reference) + return abs(self.position.imag - ref.imag) + abs(self.position.real - ref.real) + + def set_terrain(self, terrain): + self.terrain = terrain or self.default_terrain + self.is_walkable, self.is_waypoint = self.terrain_map.get( + terrain, self.terrain_map[self.terrain_default] + ) + + def set_directions(self): + terrain = ( + self.terrain + if self.terrain in self.allowed_direction_map + else self.terrain_default + ) + if self.grid.is_isotropic: + self.allowed_directions = self.allowed_direction_map[terrain].copy() + else: + self.allowed_directions = self.allowed_anisotropic_direction_map[ + terrain + ].get(self.source_direction, []) + + def get_dot(self, dot): + return self.grid.dots.get(dot, None) + + def get_neighbors(self): + if self.neighbors_obsolete: + self.neighbors = { + self + direction: 1 + for direction in self.allowed_directions + if (self + direction) and (self + direction).is_walkable + } + + self.neighbors_obsolete = False + return self.neighbors + + def set_trap(self, is_trap): + self.grid.reset_pathfinding() + if is_trap: + self.allowed_directions = [] + self.neighbors = {} + self.neighbors_obsolete = False + else: + self.set_directions() + + def set_wall(self, is_wall): + self.grid.reset_pathfinding() + if is_wall: + self.allowed_directions = [] + self.neighbors = {} + self.neighbors_obsolete = False + self.is_walkable = False + else: + self.set_terrain(self.terrain) + self.set_directions() diff --git a/2020/graph.py b/2020/graph.py new file mode 100644 index 0000000..b2d3f9f --- /dev/null +++ b/2020/graph.py @@ -0,0 +1,446 @@ +import heapq + + +class TargetFound(Exception): + pass + + +class NegativeWeightCycle(Exception): + pass + + +class Graph: + def __init__(self, vertices=[], edges={}): + self.vertices = vertices.copy() + self.edges = edges.copy() + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def estimate_to_complete(self, source_vertex, target_vertex): + return 0 + + def reset_search(self): + self.distance_from_start = {} + self.came_from = {} + + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = set(self.vertices) + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited -= set(newly_visited) + groups.append(newly_visited) + + return groups + + def depth_first_search(self, start, end=None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion(self, current_distance, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort(self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + + return True + + def topological_sort_alphabetical(self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search(self, start, end=None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + if end: + return True + return False + + def greedy_best_first_search(self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def dijkstra(self, start, end=None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + min_distance = float("inf") + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # No need to explore neighbors if we already found a shorter path to the end + if current_distance > min_distance: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + if type(neighbor) == complex: + heapq.heappush( + frontier, (current_distance + weight, SuperComplex(neighbor)) + ) + else: + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + min_distance = min(min_distance, current_distance + weight) + + return end is None or end in self.distance_from_start + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford(self, start, end=None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range(len(self.vertices) - 1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= ( + current_distance + weight + ): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start diff --git a/2020/grid.py b/2020/grid.py new file mode 100644 index 0000000..fa1aa7c --- /dev/null +++ b/2020/grid.py @@ -0,0 +1,335 @@ +from compass import * +from dot import Dot +from graph import WeightedGraph +import heapq + + +class Grid: + # For anisotropic grids, this provides which directions are allowed + possible_source_directions = { + ".": directions_straight, + "#": [], + " ": [], + "^": [north, south], + "v": [north, south], + ">": [east, west], + "<": [east, west], + "+": directions_straight, + "|": [north, south], + "-": [east, west], + "/": directions_straight, + "\\": directions_straight, + } + direction_default = directions_straight + all_directions = directions_straight + + def __init__(self, dots=[], edges={}, isotropic=True): + """ + Creates the grid based on the list of dots and edges provided + + :param sequence dots: Either a list of positions or a dict position:terrain + :param dict edges: Dict of format source:target:distance + :param Boolean isotropic: Whether directions matter + """ + + self.is_isotropic = bool(isotropic) + + if dots: + if isinstance(dots, dict): + if self.is_isotropic: + self.dots = {x: Dot(self, x, dots[x]) for x in dots} + else: + self.dots = {x: Dot(self, x[0], dots[x], x[1]) for x in dots} + else: + if self.is_isotropic: + self.dots = {x: Dot(self, x, None) for x in dots} + else: + self.dots = {x: Dot(self, x[0], None, x[1]) for x in dots} + else: + self.dots = {} + + self.edges = edges.copy() + if edges: + self.set_edges(self.edges) + + self.width = None + self.height = None + + def set_edges(self, edges): + """ + Sets up the edges as neighbors of Dots + + """ + for source in edges: + if not self.dots[source].neighbors: + self.dots[source].neighbors = {} + for target in edges[source]: + self.dots[source].neighbors[self.dots[target]] = edges[source][target] + self.dots[source].neighbors_obsolete = False + + def reset_pathfinding(self): + """ + Resets the pathfinding (= forces recalculation of all neighbors if relevant) + + """ + if self.edges: + self.set_edges(self.edges) + else: + for dot in self.dots.values(): + dot.neighbors_obsolete = True + + def text_to_dots(self, text, ignore_terrain=""): + """ + Converts a text to a set of dots + + The text is expected to be separated by newline characters + The dots will have x - y * 1j as coordinates + + :param string text: The text to convert + :param sequence ignore_terrain: The grid to convert + """ + self.dots = {} + + y = 0 + for line in text.splitlines(): + for x in range(len(line)): + if line[x] not in ignore_terrain: + if self.is_isotropic: + self.dots[x - y * 1j] = Dot(self, x - y * 1j, line[x]) + else: + for dir in self.possible_source_directions.get( + line[x], self.direction_default + ): + self.dots[(x - y * 1j, dir)] = Dot( + self, x - y * 1j, line[x], dir + ) + y += 1 + + def dots_to_text(self, mark_coords={}, void=" "): + """ + Converts dots to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string void: Which character to use when no dot is present + :return: the text + """ + text = "" + + min_x, max_x, min_y, max_y = self.get_box() + + # The imaginary axis is reversed compared to reading order + for y in range(max_y, min_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + text += mark_coords[x + y * 1j] + except (KeyError, TypeError): + if x + y * 1j in mark_coords: + text += "X" + else: + if self.is_isotropic: + text += str(self.dots.get(x + y * 1j, void)) + else: + dots = [dot for dot in self.dots if dot[0] == x + y * 1j] + if dots: + text += str(self.dots.get(dots[0], void)) + else: + text += str(void) + text += "\n" + + return text + + def get_size(self): + """ + Gets the width and height of the grid + + :return: the width and height + """ + + if not self.width: + min_x, max_x, min_y, max_y = self.get_box() + + self.width = max_x - min_x + 1 + self.height = max_y - min_y + 1 + + return (self.width, self.height) + + def get_box(self): + """ + Gets the min/max x and y values + + :return: the minimum and maximum for x and y values + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + return (min_x, max_x, min_y, max_y) + + def add_traps(self, traps): + """ + Adds traps + """ + + for dot in traps: + if self.is_isotropic: + self.dots[dot].set_trap(True) + else: + # print (dot, self.dots.values()) + if dot in self.dots: + self.dots[dot].set_trap(True) + else: + for direction in self.all_directions: + if (dot, direction) in self.dots: + self.dots[(dot, direction)].set_trap(True) + + def add_walls(self, walls): + """ + Adds walls + """ + + for dot in walls: + if self.is_isotropic: + self.dots[dot].set_wall(True) + else: + if dot in self.dots: + self.dots[dot].set_wall(True) + else: + for direction in self.all_directions: + if (dot, direction) in self.dots: + self.dots[(dot, direction)].set_wall(True) + + def crop(self, corners=[], size=0): + """ + Gets the list of dots within a given area + + :param sequence corners: Either one or 2 corners to use + :param int or sequence size: The size (width + height, or simply length) to use + :return: a dict of matching dots + """ + + delta = size - 1 + # top left corner + size are provided + if delta and len(corners) == 1: + # The corner is a Dot + if isinstance(corners[0], Dot): + min_x, max_x = ( + int(corners[0].position.real), + int(corners[0].position.real) + delta, + ) + min_y, max_y = ( + int(corners[0].position.imag) - delta, + int(corners[0].position.imag), + ) + # The corner is a tuple position, direction + elif isinstance(corners[0], tuple): + min_x, max_x = int(corners[0][0].real), int(corners[0][0].real + delta) + min_y, max_y = int(corners[0][0].imag - delta), int(corners[0][0].imag) + # The corner is a complex number + else: + min_x, max_x = int(corners[0].real), int(corners[0].real + delta) + min_y, max_y = int(corners[0].imag - delta), int(corners[0].imag) + + # Multiple corners are provided + else: + # Dots are provided as a Dot instance + if isinstance(corners[0], Dot): + x_vals = set(dot.position.real for dot in corners) + y_vals = set(dot.position.imag for dot in corners) + # Dots are provided as complex numbers + else: + x_vals = set(pos.real for pos in corners) + y_vals = set(pos.imag for pos in corners) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + if self.is_isotropic: + cropped = { + x + y * 1j: self.dots[x + y * 1j] + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + if x + y * 1j in self.dots + } + else: + cropped = { + (x + y * 1j, dir): self.dots[(x + y * 1j, dir)] + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + for dir in self.all_directions + if (x + y * 1j, dir) in self.dots + } + + return cropped + + def dijkstra(self, start): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Dot start: The start dot to consider + """ + current_distance = 0 + if not isinstance(start, Dot): + start = self.dots[start] + frontier = [(0, start)] + heapq.heapify(frontier) + visited = {start: 0} + + while frontier: + current_distance, dot = frontier.pop(0) + neighbors = dot.get_neighbors() + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + if neighbor in visited and visited[neighbor] <= ( + current_distance + weight + ): + continue + # Adding for future examination + frontier.append((current_distance + weight, neighbor)) + + # Adding for final search + visited[neighbor] = current_distance + weight + start.neighbors[neighbor] = current_distance + weight + + def convert_to_graph(self): + """ + Converts the grid in a reduced graph for pathfinding + + :return: a WeightedGraph containing all waypoints and links + """ + + waypoints = [ + self.dots[dot_key] + for dot_key in self.dots + if self.dots[dot_key].is_waypoint + ] + edges = {} + + for waypoint in waypoints: + self.dijkstra(waypoint) + distances = waypoint.get_neighbors() + edges[waypoint] = { + wp: distances[wp] + for wp in distances + if wp != waypoint and wp.is_waypoint + } + + graph = WeightedGraph(waypoints, edges) + graph.neighbors = lambda vertex: vertex.get_neighbors() + + return graph From 1bafc1678ae3c18dbc5945580136805b0698b510 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 3 Dec 2020 07:03:48 +0100 Subject: [PATCH 080/143] Added days 2020-02 and 2020-03 --- 2020/02-Password Philosophy.py | 113 +++++++++++++++++++++++++++++++ 2020/03-Toboggan Trajectory.py | 117 +++++++++++++++++++++++++++++++++ 2 files changed, 230 insertions(+) create mode 100644 2020/02-Password Philosophy.py create mode 100644 2020/03-Toboggan Trajectory.py diff --git a/2020/02-Password Philosophy.py b/2020/02-Password Philosophy.py new file mode 100644 index 0000000..ae72ad7 --- /dev/null +++ b/2020/02-Password Philosophy.py @@ -0,0 +1,113 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, collections + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1-3 a: abcde +1-3 b: cdefg +2-9 c: ccccccccc""", + "expected": ["2", "1"], +} + +test = "WD" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".WD.txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + +test = "Twitter" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".Twitter.txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["447", "Unknown"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "Twitter" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + valid_password = 0 + for string in puzzle_input.split("\n"): + _, letter, password = string.split(" ") + min_c, max_c = positive_ints(string) + if ( + collections.Counter(password)[letter[:1]] >= min_c + and collections.Counter(password)[letter[:1]] <= max_c + ): + valid_password = valid_password + 1 + + puzzle_actual_result = valid_password + + +else: + valid_password = 0 + for string in puzzle_input.split("\n"): + _, letter, password = string.split(" ") + letter = letter[:1] + min_c, max_c = positive_ints(string) + if password[min_c - 1] == letter: + if password[max_c - 1] == letter: + pass + else: + valid_password = valid_password + 1 + else: + if password[max_c - 1] == letter: + valid_password = valid_password + 1 + puzzle_actual_result = valid_password + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2020/03-Toboggan Trajectory.py b/2020/03-Toboggan Trajectory.py new file mode 100644 index 0000000..d2d550b --- /dev/null +++ b/2020/03-Toboggan Trajectory.py @@ -0,0 +1,117 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """..##....... +#...#...#.. +.#....#..#. +..#.#...#.# +.#...##..#. +..#.##..... +.#.#.#....# +.#........# +#.##...#... +#...##....# +.#..#...#.#""", + "expected": ["7", "336"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["153", "2421944712"], +} + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + maze = grid.Grid() + maze.text_to_dots(puzzle_input) + position = 0 + width, height = maze.get_size() + + nb_trees = 0 + while position.imag > -height: + if maze.dots[position].terrain == "#": + nb_trees = nb_trees + 1 + position = position + south + east * 3 + position = position.real % width + 1j * position.imag + + puzzle_actual_result = nb_trees + + +else: + maze = grid.Grid() + maze.text_to_dots(puzzle_input) + position = 0 + width, height = maze.get_size() + + nb_trees = 0 + score = 1 + for direction in [1 - 1j, 3 - 1j, 5 - 1j, 7 - 1j, 1 - 2j]: + while position.imag > -height: + if maze.dots[position].terrain == "#": + nb_trees = nb_trees + 1 + position = position + direction + position = position.real % width + 1j * position.imag + score = score * nb_trees + nb_trees = 0 + position = 0 + + puzzle_actual_result = score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 0955fa3fca582973fc90908a9da3b5e260220b43 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 4 Dec 2020 07:01:32 +0100 Subject: [PATCH 081/143] Added day 2020-04 --- 2020/04-Passport Processing.py | 178 +++++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 2020/04-Passport Processing.py diff --git a/2020/04-Passport Processing.py b/2020/04-Passport Processing.py new file mode 100644 index 0000000..5ba70aa --- /dev/null +++ b/2020/04-Passport Processing.py @@ -0,0 +1,178 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """ecl:gry pid:860033327 eyr:2020 hcl:#fffffd +byr:1937 iyr:2017 cid:147 hgt:183cm + +iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 +hcl:#cfa07d byr:1929 + +hcl:#ae17e1 iyr:2013 +eyr:2024 +ecl:brn pid:760753108 byr:1931 +hgt:179cm + +hcl:#cfa07d eyr:2025 pid:166559648 +iyr:2011 ecl:brn hgt:59in""", + "expected": ["2", "Unknown"], +} +test = 2 +test_data[test] = { + "input": """eyr:1972 cid:100 +hcl:#18171d ecl:amb hgt:170 pid:186cm iyr:2018 byr:1926 + +iyr:2019 +hcl:#602927 eyr:1967 hgt:170cm +ecl:grn pid:012533040 byr:1946 + +hcl:dab227 iyr:2012 +ecl:brn hgt:182cm pid:021572410 eyr:2020 byr:1992 cid:277 + +hgt:59cm ecl:zzz +eyr:2038 hcl:74454a iyr:2023 +pid:3556412378 byr:2007 + +pid:087499704 hgt:74in ecl:grn iyr:2012 eyr:2030 byr:1980 +hcl:#623a2f + +eyr:2029 ecl:blu cid:129 byr:1989 +iyr:2014 pid:896056539 hcl:#a97842 hgt:165cm + +hcl:#888785 +hgt:164cm byr:2001 iyr:2015 cid:88 +pid:545766238 ecl:hzl +eyr:2022 + +iyr:2010 hgt:158cm hcl:#b6652a ecl:blu byr:1944 eyr:2021 pid:093154719""", + "expected": ["Unknown", "4"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["235", "194"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +required_fields = ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] + +passports = [] +i = 0 +for string in puzzle_input.split("\n"): + if len(passports) >= i: + passports.append("") + if string == "": + i = i + 1 + else: + passports[i] = passports[i] + " " + string + +valid_passports = 0 + +if part_to_test == 1: + for passport in passports: + if all([x + ":" in passport for x in required_fields]): + valid_passports = valid_passports + 1 + + +else: + for passport in passports: + if all([x + ":" in passport for x in required_fields]): + fields = passport.split(" ") + score = 0 + for field in fields: + data = field.split(":") + if data[0] == "byr": + year = int(data[1]) + if year >= 1920 and year <= 2002: + score = score + 1 + elif data[0] == "iyr": + year = int(data[1]) + if year >= 2010 and year <= 2020: + score = score + 1 + elif data[0] == "eyr": + year = int(data[1]) + if year >= 2020 and year <= 2030: + score = score + 1 + elif data[0] == "hgt": + size = ints(data[1])[0] + if data[1][-2:] == "cm": + if size >= 150 and size <= 193: + score = score + 1 + elif data[1][-2:] == "in": + if size >= 59 and size <= 76: + score = score + 1 + elif data[0] == "hcl": + if re.match("#[0-9a-f]{6}", data[1]) and len(data[1]) == 7: + score = score + 1 + print(data[0], passport) + elif data[0] == "ecl": + if data[1] in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]: + score = score + 1 + print(data[0], passport) + elif data[0] == "pid": + if re.match("[0-9]{9}", data[1]) and len(data[1]) == 9: + score = score + 1 + print(data[0], passport) + print(passport, score) + if score == 7: + valid_passports = valid_passports + 1 + +puzzle_actual_result = valid_passports + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From e2c0e92d37a32c650848cfd0f969246ef7c9eb30 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 5 Dec 2020 06:59:53 +0100 Subject: [PATCH 082/143] Added day 2020-05 --- 2020/05-Binary Boarding.py | 110 +++++++++++++++++++++++++++++++++++++ 1 file changed, 110 insertions(+) create mode 100644 2020/05-Binary Boarding.py diff --git a/2020/05-Binary Boarding.py b/2020/05-Binary Boarding.py new file mode 100644 index 0000000..f9ce323 --- /dev/null +++ b/2020/05-Binary Boarding.py @@ -0,0 +1,110 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """FBFBBFFRLR +BFFFBBFRRR +FFFBBBFRRR +BBFFBBFRLL""", + "expected": ["357, 567, 119, 820", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["878", "504"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 2: + seat_list = list(range(127 * 8 + 7 + 1)) + +max_seat_id = 0 +for seat in puzzle_input.split("\n"): + row = 0 + column = 0 + row_power = 6 + col_power = 2 + for letter in seat: + if letter == "F": + row_power = row_power - 1 + elif letter == "B": + row = row + 2 ** row_power + row_power = row_power - 1 + + elif letter == "L": + col_power = col_power - 1 + elif letter == "R": + column = column + 2 ** col_power + col_power = col_power - 1 + + seat_id = row * 8 + column + max_seat_id = max(seat_id, max_seat_id) + + if part_to_test == 2: + seat_list.remove(seat_id) + +if part_to_test == 1: + puzzle_actual_result = max_seat_id +else: + seat_list = [x for x in seat_list if x <= max_seat_id] + + puzzle_actual_result = max(seat_list) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 9f626b7b25af9f296e86313c8055a092e4e07f3f Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 6 Dec 2020 06:22:51 +0100 Subject: [PATCH 083/143] Added day 2020-06 --- 2020/06-Custom Customs.py | 107 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 2020/06-Custom Customs.py diff --git a/2020/06-Custom Customs.py b/2020/06-Custom Customs.py new file mode 100644 index 0000000..6b07c61 --- /dev/null +++ b/2020/06-Custom Customs.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """abc + +a +b +c + +ab +ac + +a +a +a +a + +b""", + "expected": ["11", "6"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["6782", "3596"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + total_score = 0 + for group in puzzle_input.split("\n\n"): + group_size = len(group.split("\n")) + answers = Counter(group.replace("\n", "")) + nb_common = len(answers) + total_score = total_score + nb_common + + puzzle_actual_result = total_score + + +else: + total_score = 0 + for group in puzzle_input.split("\n\n"): + group_size = len(group.split("\n")) + answers = Counter(group.replace("\n", "")) + nb_common = len([x for x in answers if answers[x] == group_size]) + total_score = total_score + nb_common + + puzzle_actual_result = total_score + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From c14864c7f317e8595379e2f5f5424171f56f4f1b Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 7 Dec 2020 06:52:59 +0100 Subject: [PATCH 084/143] Added day 2020-07 --- 2020/07-Handy Haversacks.py | 157 ++++++++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 2020/07-Handy Haversacks.py diff --git a/2020/07-Handy Haversacks.py b/2020/07-Handy Haversacks.py new file mode 100644 index 0000000..5d1c168 --- /dev/null +++ b/2020/07-Handy Haversacks.py @@ -0,0 +1,157 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """light red bags contain 1 bright white bag, 2 muted yellow bags. +dark orange bags contain 3 bright white bags, 4 muted yellow bags. +bright white bags contain 1 shiny gold bag. +muted yellow bags contain 2 shiny gold bags, 9 faded blue bags. +shiny gold bags contain 1 dark olive bag, 2 vibrant plum bags. +dark olive bags contain 3 faded blue bags, 4 dotted black bags. +vibrant plum bags contain 5 faded blue bags, 6 dotted black bags. +faded blue bags contain no other bags. +dotted black bags contain no other bags.""", + "expected": ["4", "Unknown"], +} + +test = 2 +test_data[test] = { + "input": """shiny gold bags contain 2 dark red bags. +dark red bags contain 2 dark orange bags. +dark orange bags contain 2 dark yellow bags. +dark yellow bags contain 2 dark green bags. +dark green bags contain 2 dark blue bags. +dark blue bags contain 2 dark violet bags. +dark violet bags contain no other bags.""", + "expected": ["Unknown", "126"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["300", "8030"], +} + + +# -------------------------------- Control program execution ------------------------- # +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + results = [] + for string in puzzle_input.split("\n"): + results.append(re.findall("[a-z ]* bags?", string)) + + combinations = [] + for result in results: + if len(result) == 1: + print("No match for", result) + else: + combinations.append( + { + "out": result[0].replace("bags", "bag"), + "in": [x.replace("bags", "bag")[1:] for x in result[1:]], + } + ) + + contain_gold = set(["shiny gold bag"]) + # There is certainly a clever way to reduce how many loops I do, but I don't know it (yet) + for i in range(len(combinations)): + for combination in combinations: + if any( + [gold_container in combination["in"] for gold_container in contain_gold] + ): + contain_gold.add(combination["out"]) + print(len(contain_gold), i, len(combinations)) + + puzzle_actual_result = len(contain_gold) - 1 + + +else: + results = [] + for string in puzzle_input.split("\n"): + results.append(re.findall("([0-9]* )?([a-z ]*) bags?", string)) + + combinations = [] + for result in results: + if len(result) == 1: + bags = result[0][1].split(" bags contain no ") + combinations.append({"out": bags[0], "in": []}) + else: + combinations.append( + {"out": result[0][1], "in": {x[1]: int(x[0]) for x in result[1:]}} + ) + + gold_contains = defaultdict(int) + gold_contains["shiny gold"] = 1 + gold_contains["total"] = -1 + + while len(gold_contains) > 1: + for combination in combinations: + if combination["out"] in gold_contains: + for containee in combination["in"]: + # Add those bags to the count + gold_contains[containee] += ( + combination["in"][containee] * gold_contains[combination["out"]] + ) + # Add the "out" bag to the count & remove it from the list + # This ensures we don't loop over the same bag twice + gold_contains["total"] += gold_contains[combination["out"]] + del gold_contains[combination["out"]] + + print(sum(gold_contains.values()), gold_contains) + + puzzle_actual_result = gold_contains["total"] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From 9726372195c6700db1be3b84b2aff0202a25be4e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 8 Dec 2020 07:20:28 +0100 Subject: [PATCH 085/143] Added day 2020-08 --- 2020/08-Handheld Halting.py | 154 ++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 2020/08-Handheld Halting.py diff --git a/2020/08-Handheld Halting.py b/2020/08-Handheld Halting.py new file mode 100644 index 0000000..2e42793 --- /dev/null +++ b/2020/08-Handheld Halting.py @@ -0,0 +1,154 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """nop +0 +acc +1 +jmp +4 +acc +3 +jmp -3 +acc -99 +acc +1 +jmp -4 +acc +6""", + "expected": ["5", "8"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1134", "1205"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Program: + def __init__(self, instructions): + self.instructions = [ + [x.split(" ")[0], int(x.split(" ")[1])] for x in instructions.split("\n") + ] + self.accumulator = 0 + self.current_line = 0 + self.operations = { + "nop": self.nop, + "acc": self.acc, + "jmp": self.jmp, + } + + def run(self): + while current_line <= len(self.operations): + self.run_once() + + def run_once(self): + instr = self.instructions[self.current_line] + print("Before", self.current_line, self.accumulator, instr) + self.operations[instr[0]](instr) + + def nop(self, instr): + self.current_line += 1 + pass + + def acc(self, instr): + self.current_line += 1 + self.accumulator += instr[1] + + def jmp(self, instr): + self.current_line += instr[1] + + +if part_to_test == 1: + program = Program(puzzle_input) + + visited = [] + while ( + program.current_line < len(program.instructions) + and program.current_line not in visited + ): + visited.append(program.current_line) + program.run_once() + + puzzle_actual_result = program.accumulator + + +else: + initial_program = Program(puzzle_input) + all_nop_jmp = [ + i + for i, instr in enumerate(initial_program.instructions) + if instr[0] in ("jmp", "nop") + ] + for val in all_nop_jmp: + program = copy.deepcopy(initial_program) + program.instructions[val][0] = ( + "nop" if program.instructions[val][0] == "jpm" else "nop" + ) + + visited = [] + while ( + program.current_line < len(program.instructions) + and program.current_line not in visited + ): + visited.append(program.current_line) + program.run_once() + + if program.current_line == len(program.instructions): + puzzle_actual_result = program.accumulator + break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From b99833b1dccb6931fee5918a41a28d35f7616e9f Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 10 Dec 2020 06:16:12 +0100 Subject: [PATCH 086/143] Added day 2020-09 --- 2020/09-Encoding Error.py | 130 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 2020/09-Encoding Error.py diff --git a/2020/09-Encoding Error.py b/2020/09-Encoding Error.py new file mode 100644 index 0000000..fc5b180 --- /dev/null +++ b/2020/09-Encoding Error.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """35 +20 +15 +25 +47 +40 +62 +55 +65 +95 +102 +117 +150 +182 +127 +219 +299 +277 +309 +576""", + "expected": ["127", "62"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1504371145", "183278487"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +preamble = 25 if case_to_test == "real" else 5 + +numbers = ints(puzzle_input) +sums = [] +for vals in itertools.combinations(numbers[:preamble], 2): + sums.append(sum(vals)) + +i = 0 +while True: + sums = [] + for vals in itertools.combinations(numbers[i : i + preamble], 2): + sums.append(sum(vals)) + if numbers[i + preamble] not in sums: + puzzle_actual_result = numbers[i + preamble] + break + i += 1 + +if part_to_test == 2: + invalid_number = puzzle_actual_result + puzzle_actual_result = "Unknown" + + for a in range(len(numbers)): + number_sum = numbers[a] + if number_sum < invalid_number: + for b in range(1, len(numbers) - a): + number_sum += numbers[a + b] + print(a, b, number_sum, invalid_number) + if number_sum == invalid_number: + puzzle_actual_result = min(numbers[a : a + b + 1]) + max( + numbers[a : a + b + 1] + ) + break + if number_sum > invalid_number: + break + if puzzle_actual_result != "Unknown": + break + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-09 06:14:55.183250 +# Solve part 1: 2020-12-09 06:20:49 +# Solve part 2: 2020-12-09 06:29:07 From 23f11599d35aef35443dd2c39674b5d2baa400ac Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 10 Dec 2020 06:17:03 +0100 Subject: [PATCH 087/143] Added day 2020-10 --- 2020/10-Adapter Array.py | 164 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 2020/10-Adapter Array.py diff --git a/2020/10-Adapter Array.py b/2020/10-Adapter Array.py new file mode 100644 index 0000000..5325792 --- /dev/null +++ b/2020/10-Adapter Array.py @@ -0,0 +1,164 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +from functools import lru_cache + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """16 +10 +15 +5 +1 +11 +7 +19 +6 +12 +4""", + "expected": ["there are 7 differences of 1 jolt and 5 differences of 3 jolts", "8"], +} + +test = 2 +test_data[test] = { + "input": """28 +33 +18 +42 +31 +14 +46 +20 +48 +47 +24 +23 +49 +45 +19 +38 +39 +11 +1 +32 +25 +35 +8 +17 +7 +9 +4 +2 +34 +10 +3""", + "expected": ["22 differences of 1 jolt and 10 differences of 3 jolts", "19208"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + joltages = ints(puzzle_input) + my_joltage = max(joltages) + 3 + outlet = 0 + + diff_3 = 0 + diff_1 = 0 + + current_joltage = outlet + while current_joltage != max(joltages): + next_adapter = min([x for x in joltages if x > current_joltage]) + if next_adapter - current_joltage == 1: + diff_1 += 1 + if next_adapter - current_joltage == 3: + diff_3 += 1 + + current_joltage = next_adapter + + diff_3 += 1 + puzzle_actual_result = (diff_1, diff_3, diff_1 * diff_3) + + +else: + joltages = ints(puzzle_input) + joltages.append(max(joltages) + 3) + joltages.append(0) + edges = defaultdict(list) + + for joltage in joltages: + edges[joltage] = [x for x in joltages if x < joltage and x >= joltage - 3] + + print(edges) + + @lru_cache(maxsize=len(joltages)) + def count_paths(position): + if position == 0: + return 1 + else: + nb_paths = 0 + # print (position, [count_paths(joltage) for joltage in edges[position]], edges[position]) + return sum([count_paths(joltage) for joltage in edges[position]]) + + puzzle_actual_result = count_paths(max(joltages)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-10 06:00:02.437611 From 3afc4a760aa12a77e56353d5c27f99c33b9d7d1c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 10 Dec 2020 08:09:17 +0100 Subject: [PATCH 088/143] Various improvements --- 2020/02-Password Philosophy.py | 19 ++----- 2020/04-Passport Processing.py | 92 ++++++++++++++-------------------- 2020/07-Handy Haversacks.py | 2 +- 2020/08-Handheld Halting.py | 2 +- 2020/09-Encoding Error.py | 2 +- 2020/10-Adapter Array.py | 6 ++- 2020/grid.py | 2 +- 7 files changed, 50 insertions(+), 75 deletions(-) diff --git a/2020/02-Password Philosophy.py b/2020/02-Password Philosophy.py index ae72ad7..7b89b29 100644 --- a/2020/02-Password Philosophy.py +++ b/2020/02-Password Philosophy.py @@ -39,31 +39,20 @@ def words(s: str): "expected": ["2", "1"], } -test = "WD" +test = "real" input_file = os.path.join( os.path.dirname(__file__), "Inputs", - os.path.basename(__file__).replace(".py", ".WD.txt"), + os.path.basename(__file__).replace(".py", ".txt"), ) test_data[test] = { "input": open(input_file, "r+").read(), - "expected": ["Unknown", "Unknown"], -} - -test = "Twitter" -input_file = os.path.join( - os.path.dirname(__file__), - "Inputs", - os.path.basename(__file__).replace(".py", ".Twitter.txt"), -) -test_data[test] = { - "input": open(input_file, "r+").read(), - "expected": ["447", "Unknown"], + "expected": ["447", "249"], } # -------------------------------- Control program execution ------------------------- # -case_to_test = "Twitter" +case_to_test = "real" part_to_test = 2 # -------------------------------- Initialize some variables ------------------------- # diff --git a/2020/04-Passport Processing.py b/2020/04-Passport Processing.py index 5ba70aa..cf5a877 100644 --- a/2020/04-Passport Processing.py +++ b/2020/04-Passport Processing.py @@ -106,68 +106,52 @@ def words(s: str): # -------------------------------- Actual code execution ----------------------------- # -required_fields = ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] + +class Passport: + required_fields = ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] + + validations = { + "byr": lambda year: year in range(1920, 2002 + 1), + "iyr": lambda year: year in range(2010, 2020 + 1), + "eyr": lambda year: year in range(2020, 2030 + 1), + "hgt": lambda data: ( + data[-2:] == "cm" and int(data[:-2]) in range(150, 193 + 1) + ) + or (data[-2:] == "in" and int(data[:-2]) in range(59, 76 + 1)), + "hcl": lambda data: re.match("^#[0-9a-f]{6}$", data), + "ecl": lambda data: data in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"], + "pid": lambda data: re.match("^[0-9]{9}$", data), + } + + def __init__(self, data): + self.fields = defaultdict(str) + for element in data.split(): + if element[:3] in ("byr", "iyr", "eyr"): + try: + self.fields[element[:3]] = int(element[4:]) + except: + self.fields[element[:3]] = element[4:] + else: + self.fields[element[:3]] = element[4:] + + def has_required_data(self): + return all([x in self.fields for x in self.required_fields]) + + def is_valid(self): + return all([self.validations[x](self.fields[x]) for x in self.required_fields]) + passports = [] -i = 0 -for string in puzzle_input.split("\n"): - if len(passports) >= i: - passports.append("") - if string == "": - i = i + 1 - else: - passports[i] = passports[i] + " " + string +for string in puzzle_input.split("\n\n"): + passports.append(Passport(string)) valid_passports = 0 if part_to_test == 1: - for passport in passports: - if all([x + ":" in passport for x in required_fields]): - valid_passports = valid_passports + 1 - + valid_passports = sum([1 for x in passports if x.has_required_data()]) else: - for passport in passports: - if all([x + ":" in passport for x in required_fields]): - fields = passport.split(" ") - score = 0 - for field in fields: - data = field.split(":") - if data[0] == "byr": - year = int(data[1]) - if year >= 1920 and year <= 2002: - score = score + 1 - elif data[0] == "iyr": - year = int(data[1]) - if year >= 2010 and year <= 2020: - score = score + 1 - elif data[0] == "eyr": - year = int(data[1]) - if year >= 2020 and year <= 2030: - score = score + 1 - elif data[0] == "hgt": - size = ints(data[1])[0] - if data[1][-2:] == "cm": - if size >= 150 and size <= 193: - score = score + 1 - elif data[1][-2:] == "in": - if size >= 59 and size <= 76: - score = score + 1 - elif data[0] == "hcl": - if re.match("#[0-9a-f]{6}", data[1]) and len(data[1]) == 7: - score = score + 1 - print(data[0], passport) - elif data[0] == "ecl": - if data[1] in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]: - score = score + 1 - print(data[0], passport) - elif data[0] == "pid": - if re.match("[0-9]{9}", data[1]) and len(data[1]) == 9: - score = score + 1 - print(data[0], passport) - print(passport, score) - if score == 7: - valid_passports = valid_passports + 1 + valid_passports = sum([1 for x in passports if x.is_valid()]) puzzle_actual_result = valid_passports diff --git a/2020/07-Handy Haversacks.py b/2020/07-Handy Haversacks.py index 5d1c168..637adb4 100644 --- a/2020/07-Handy Haversacks.py +++ b/2020/07-Handy Haversacks.py @@ -145,7 +145,7 @@ def words(s: str): gold_contains["total"] += gold_contains[combination["out"]] del gold_contains[combination["out"]] - print(sum(gold_contains.values()), gold_contains) + # print(sum(gold_contains.values()), gold_contains) puzzle_actual_result = gold_contains["total"] diff --git a/2020/08-Handheld Halting.py b/2020/08-Handheld Halting.py index 2e42793..279d1e1 100644 --- a/2020/08-Handheld Halting.py +++ b/2020/08-Handheld Halting.py @@ -92,7 +92,7 @@ def run(self): def run_once(self): instr = self.instructions[self.current_line] - print("Before", self.current_line, self.accumulator, instr) + # print("Before", self.current_line, self.accumulator, instr) self.operations[instr[0]](instr) def nop(self, instr): diff --git a/2020/09-Encoding Error.py b/2020/09-Encoding Error.py index fc5b180..5c2303d 100644 --- a/2020/09-Encoding Error.py +++ b/2020/09-Encoding Error.py @@ -109,7 +109,7 @@ def words(s: str): if number_sum < invalid_number: for b in range(1, len(numbers) - a): number_sum += numbers[a + b] - print(a, b, number_sum, invalid_number) + # print(a, b, number_sum, invalid_number) if number_sum == invalid_number: puzzle_actual_result = min(numbers[a : a + b + 1]) + max( numbers[a : a + b + 1] diff --git a/2020/10-Adapter Array.py b/2020/10-Adapter Array.py index 5325792..b2b66a7 100644 --- a/2020/10-Adapter Array.py +++ b/2020/10-Adapter Array.py @@ -93,7 +93,7 @@ def words(s: str): ) test_data[test] = { "input": open(input_file, "r+").read(), - "expected": ["Unknown", "Unknown"], + "expected": ["2240", "99214346656768"], } @@ -142,7 +142,7 @@ def words(s: str): for joltage in joltages: edges[joltage] = [x for x in joltages if x < joltage and x >= joltage - 3] - print(edges) + # print(edges) @lru_cache(maxsize=len(joltages)) def count_paths(position): @@ -162,3 +162,5 @@ def count_paths(position): print("Expected result : " + str(puzzle_expected_result)) print("Actual result : " + str(puzzle_actual_result)) # Date created: 2020-12-10 06:00:02.437611 +# Part 1: 2020-12-10 06:04:42 +# Part 2: 2020-12-10 06:14:12 diff --git a/2020/grid.py b/2020/grid.py index fa1aa7c..da0ce66 100644 --- a/2020/grid.py +++ b/2020/grid.py @@ -86,7 +86,7 @@ def text_to_dots(self, text, ignore_terrain=""): The dots will have x - y * 1j as coordinates :param string text: The text to convert - :param sequence ignore_terrain: The grid to convert + :param sequence ignore_terrain: Types of terrain to ignore (useful for walls) """ self.dots = {} From 17e9266ad1d52ef5a654d147e423734dd6d4433a Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 11 Dec 2020 08:03:05 +0100 Subject: [PATCH 089/143] Added day 2020-11 --- 2020/11-Seating System.py | 211 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 2020/11-Seating System.py diff --git a/2020/11-Seating System.py b/2020/11-Seating System.py new file mode 100644 index 0000000..1321a3c --- /dev/null +++ b/2020/11-Seating System.py @@ -0,0 +1,211 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +import copy +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """L.LL.LL.LL +LLLLLLL.LL +L.L.L..L.. +LLLL.LL.LL +L.LL.LL.LL +L.LLLLL.LL +..L.L..... +LLLLLLLLLL +L.LLLLLL.L +L.LLLLL.LL""", + "expected": ["37", "26"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2324", "2068"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, +} + + +grid.Grid.all_directions = directions_diagonals + +if part_to_test == 1: + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + + new_seats = grid.Grid() + new_seats.all_directions = directions_diagonals + new_seats.text_to_dots(puzzle_input) + + i = 0 + while True: + i += 1 + watch = [1 - 1j] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 4 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + new_seats.text_to_dots(puzzle_input) + print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +else: + + def get_neighbors_map(dot): + neighbors = [] + if dot.grid.width is None: + dot.grid.get_size() + for direction in dot.allowed_directions: + neighbor = dot + direction + while neighbor is not None: + if neighbor.terrain in ("L", "#"): + neighbors.append(neighbor.position) + break + else: + neighbor += direction + return neighbors + + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + seats.neighbors_map = { + dot: get_neighbors_map(seats.dots[dot]) for dot in seats.dots + } + + new_seats = copy.deepcopy(seats) + + def get_neighbors(self): + return { + self.grid.dots[neighbor]: 1 + for neighbor in self.grid.neighbors_map[self.position] + } + + dot.Dot.get_neighbors = get_neighbors + + i = 0 + + while True: + i += 1 + watch = [2] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 5 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-11 06:00:07.140562 +# Part 1: 2020-12-11 06:22:46 +# Part 2: 2020-12-11 06:37:29 From f6537b74d4a63705baeaa62ddecec526428ad872 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 13 Dec 2020 09:07:00 +0100 Subject: [PATCH 090/143] Added days 2020-12 and 2020-13 --- 2020/12-Rain Risk.py | 131 ++++++++++++++++++++++++++++++++++++++ 2020/13-Shuttle Search.py | 131 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 2020/12-Rain Risk.py create mode 100644 2020/13-Shuttle Search.py diff --git a/2020/12-Rain Risk.py b/2020/12-Rain Risk.py new file mode 100644 index 0000000..feba241 --- /dev/null +++ b/2020/12-Rain Risk.py @@ -0,0 +1,131 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """F10 +N3 +F7 +R90 +F11""", + "expected": ["25", "286"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["820", "66614"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +relative_directions = { + "L": 1j, + "R": -1j, + "F": 1, + "B": -1, +} + + +if part_to_test == 1: + position = 0 + direction = east + for string in puzzle_input.split("\n"): + if string[0] in ("N", "S", "E", "W"): + position += text_to_direction[string[0]] * int(string[1:]) + elif string[0] == "F": + position += direction * int(string[1:]) + elif string[0] in ("L", "R"): + angle = int(string[1:]) % 360 + if angle == 0: + pass + elif angle == 90: + direction *= relative_directions[string[0]] + elif angle == 180: + direction *= -1 + elif angle == 270: + direction *= -1 * relative_directions[string[0]] + + puzzle_actual_result = int(abs(position.real) + abs(position.imag)) + + +else: + ship_pos = 0 + wpt_rel_pos = 10 + 1j + for string in puzzle_input.split("\n"): + if string[0] in ("N", "S", "E", "W"): + wpt_rel_pos += text_to_direction[string[0]] * int(string[1:]) + elif string[0] == "F": + delta = wpt_rel_pos * int(string[1:]) + ship_pos += delta + elif string[0] in ("L", "R"): + angle = int(string[1:]) % 360 + if angle == 0: + pass + elif angle == 90: + wpt_rel_pos *= relative_directions[string[0]] + elif angle == 180: + wpt_rel_pos *= -1 + elif angle == 270: + wpt_rel_pos *= -1 * relative_directions[string[0]] + + puzzle_actual_result = int(abs(ship_pos.real) + abs(ship_pos.imag)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-12 07:21:36.624800 +# Part 1: 2020-12-12 07:28:36 +# Part 2: 2020-12-12 07:34:51 diff --git a/2020/13-Shuttle Search.py b/2020/13-Shuttle Search.py new file mode 100644 index 0000000..30feb47 --- /dev/null +++ b/2020/13-Shuttle Search.py @@ -0,0 +1,131 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """939 +7,13,x,x,59,x,31,19""", + "expected": ["295", "1068781"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2382", "906332393333683"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + data = puzzle_input.split("\n") + curr_time = int(data[0]) + busses = ints(data[1]) + next_time = curr_time * 10 + + for bus in busses: + next_round = bus - curr_time % bus + curr_time + print(next_round, bus, curr_time) + if next_round < next_time: + next_time = next_round + next_bus = bus + + puzzle_actual_result = (next_time - curr_time) * next_bus + + +else: + data = puzzle_input.split("\n") + busses = data[1].split(",") + bus_offsets = {} + + i = 0 + for bus in busses: + if bus == "x": + pass + else: + bus_offsets[int(bus)] = i + i += 1 + + timestamp = 0 + + # I first solved this thanks to a diophantine equation solvers found on Internet + + # Then I looked at the solutions megathread to learn more + # This is the proper algorithm that works in a feasible time + # It's called the Chinese remainder theorem + # See https://crypto.stanford.edu/pbc/notes/numbertheory/crt.html + prod_modulos = math.prod(bus_offsets.keys()) + for bus, offset in bus_offsets.items(): + timestamp += -offset * (prod_modulos // bus) * pow(prod_modulos // bus, -1, bus) + timestamp %= prod_modulos + + # The below algorithm is the brute-force version: very slow but should work + # Since timestamp is calculated above, this won't do anything + # To make it run, uncomment the below line + # timestamp = 0 + + min_bus = min(bus_offsets.keys()) + while True: + if all([(timestamp + bus_offsets[bus]) % bus == 0 for bus in bus_offsets]): + puzzle_actual_result = timestamp + break + else: + timestamp += min_bus + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-13 06:25:25.641468 +# Part 1: 2020-12-13 06:31:06 +# Part 2: 2020-12-13 07:12:10 From e6c39db0b0575b77ab4965919ee9371b575e17b5 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 14 Dec 2020 07:21:36 +0100 Subject: [PATCH 091/143] Added day 2020-14 --- 2020/14-Docking Data.py | 141 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 2020/14-Docking Data.py diff --git a/2020/14-Docking Data.py b/2020/14-Docking Data.py new file mode 100644 index 0000000..9fde54a --- /dev/null +++ b/2020/14-Docking Data.py @@ -0,0 +1,141 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """mask = XXXXXXXXXXXXXXXXXXXXXXXXXXXXX1XXXX0X +mem[8] = 11 +mem[7] = 101 +mem[8] = 0""", + "expected": ["Unknown", "Unknown"], +} + +test = 2 +test_data[test] = { + "input": """mask = 000000000000000000000000000000X1001X +mem[42] = 100 +mask = 00000000000000000000000000000000X0XX +mem[26] = 1""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + data = puzzle_input.split("\n") + + memory = defaultdict(int) + + for string in data: + if string[:4] == "mask": + mask = string[7:] + else: + address, value = ints(string) + # print ('{0:>036b}'.format(value)) + for position, bit in enumerate(mask): + if bit == "X": + pass + elif bit == "1": + str_value = "{0:>036b}".format(value) + str_value = str_value[:position] + "1" + str_value[position + 1 :] + value = int(str_value, 2) + elif bit == "0": + str_value = "{0:>036b}".format(value) + str_value = str_value[:position] + "0" + str_value[position + 1 :] + value = int(str_value, 2) + # print ('{0:>036b}'.format(value)) + memory[address] = value + + puzzle_actual_result = sum(memory.values()) + + +else: + data = puzzle_input.split("\n") + + memory = defaultdict(int) + + for string in data: + if string[:4] == "mask": + mask = string[7:] + else: + address, value = ints(string) + adresses = ["0"] + for position, bit in enumerate(mask): + if bit == "0": + adresses = [ + add + "{0:>036b}".format(address)[position] for add in adresses + ] + elif bit == "1": + adresses = [add + "1" for add in adresses] + elif bit == "X": + adresses = [add + "1" for add in adresses] + [ + add + "0" for add in adresses + ] + for add in set(adresses): + memory[add] = value + + puzzle_actual_result = sum(memory.values()) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-14 06:55:33.216654 +# Part 1: 2020-12-14 07:11:07 +# Part 2: 2020-12-14 07:17:27 From 5c53045ecb2a9f9d5d331e0652a2993d14bc264c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 15 Dec 2020 07:45:13 +0100 Subject: [PATCH 092/143] Added day 2020-15 --- 2020/15-Rambunctious Recitation.py | 130 +++++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 2020/15-Rambunctious Recitation.py diff --git a/2020/15-Rambunctious Recitation.py b/2020/15-Rambunctious Recitation.py new file mode 100644 index 0000000..fdd3714 --- /dev/null +++ b/2020/15-Rambunctious Recitation.py @@ -0,0 +1,130 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0,3,6""", + "expected": ["436", "175594"], +} + +test += 1 +test_data[test] = { + "input": """1,3,2""", + "expected": ["1", "175594"], +} + +test += 1 +test_data[test] = { + "input": """2,1,3""", + "expected": ["10", "3544142"], +} + +test += 1 +test_data[test] = { + "input": """1,2,3""", + "expected": ["27", "261214"], +} + +test += 1 +test_data[test] = { + "input": """2,3,1""", + "expected": ["78", "6895259"], +} + +test += 1 +test_data[test] = { + "input": """3,2,1""", + "expected": ["438", "18"], +} + +test += 1 +test_data[test] = {"input": """3,1,2""", "expected": ["1836", "362"]} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["763", "1876406"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + limit = 2020 +else: + limit = 30000000 + +values = ints(puzzle_input) +last_seen = {val: i + 1 for i, val in enumerate(values[:-1])} +last_nr = values[-1] +for i in range(len(values), limit): + # #print ('before', i, last_nr, last_seen) + if last_nr in last_seen: + new_nr = i - last_seen[last_nr] + last_seen[last_nr] = i + else: + last_seen[last_nr], new_nr = i, 0 + + # #print ('after', i, last_nr, new_nr, last_seen) + # print (i+1, new_nr) + last_nr = new_nr + +puzzle_actual_result = new_nr + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-15 06:30:45.515647 +# Part 1: 2020-12-15 06:40:45 +# Part 2: 2020-12-15 07:33:58 From 7b03bf81681a169b9c40235728279d60aa591512 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 16 Dec 2020 07:01:16 +0100 Subject: [PATCH 093/143] Added day 2020-16 --- 2020/16-Ticket Translation.py | 202 ++++++++++++++++++++++++++++++++++ 1 file changed, 202 insertions(+) create mode 100644 2020/16-Ticket Translation.py diff --git a/2020/16-Ticket Translation.py b/2020/16-Ticket Translation.py new file mode 100644 index 0000000..85ed874 --- /dev/null +++ b/2020/16-Ticket Translation.py @@ -0,0 +1,202 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """class: 1-3 or 5-7 +row: 6-11 or 33-44 +seat: 13-40 or 45-50 + +your ticket: +7,1,14 + +nearby tickets: +7,3,47 +40,4,50 +55,2,20 +38,6,12""", + "expected": ["71", "Unknown"], +} + + +test = 2 +test_data[test] = { + "input": """class: 0-1 or 4-19 +row: 0-5 or 8-19 +seat: 0-13 or 16-19 + +your ticket: +11,12,13 + +nearby tickets: +3,9,18 +15,1,5 +5,14,9""", + "expected": ["Unknown", "row, class, seat ==> 0"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["32835", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +validations = {} + +section = 0 +tickets = [] + +for string in puzzle_input.split("\n"): + if string == "": + section += 1 + else: + if section == 0: + field, numbers = string.split(": ") + numbers = positive_ints(numbers) + validations[field] = list(range(numbers[0], numbers[1] + 1)) + list( + range(numbers[2], numbers[3] + 1) + ) + elif section == 1: + if string == "your ticket:": + pass + else: + my_ticket = ints(string) + elif section == 2: + if string == "nearby tickets:": + pass + else: + tickets.append(ints(string)) + +if part_to_test == 1: + invalid_fields = 0 + for ticket in tickets: + invalid_fields += sum( + [ + field + for field in ticket + if all(field not in val for val in validations.values()) + ] + ) + + puzzle_actual_result = invalid_fields + +else: + valid_tickets = [] + invalid_fields = 0 + for ticket in tickets: + if ( + len( + [ + field + for field in ticket + if all(field not in val for val in validations.values()) + ] + ) + == 0 + ): + valid_tickets.append(ticket) + + field_order = {} + for field in validations.keys(): + possible_order = list(range(len(validations))) + allowed_values = validations[field] + for position in range(len(validations)): + for ticket in valid_tickets: + # #print (field, ticket, position, possible_order, allowed_values) + value = ticket[position] + if value not in allowed_values: + try: + possible_order.remove(position) + except ValueError: + pass + field_order[field] = possible_order + + for val in field_order: + print(field_order[val], val) + while any(len(val) > 1 for val in field_order.values()): + new_field_order = deepcopy(field_order) + for field in field_order: + if len(field_order[field]) == 1: + for field2 in new_field_order: + if field2 == field: + pass + else: + new_field_order[field2] = [ + val + for val in new_field_order[field2] + if val not in field_order[field] + ] + field_order = deepcopy(new_field_order) + + ticket_value = 1 + for val in field_order: + print(field_order[val], val) + for field in validations.keys(): + if field[:9] == "departure": + print( + my_ticket, field, field_order[field], my_ticket[field_order[field][0]] + ) + ticket_value *= my_ticket[field_order[field][0]] + + puzzle_actual_result = ticket_value + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-16 06:05:34.085933 +# Part 1: 2020-12-16 06:23:05 +# Part 2: 2020-12-16 06:59:59 From 0f388d1e706e89467292e1e9db9a67832ed78032 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 17 Dec 2020 06:57:12 +0100 Subject: [PATCH 094/143] Added day 2020-17 --- 2020/17-Conway Cubes.py | 248 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 248 insertions(+) create mode 100644 2020/17-Conway Cubes.py diff --git a/2020/17-Conway Cubes.py b/2020/17-Conway Cubes.py new file mode 100644 index 0000000..1adcae9 --- /dev/null +++ b/2020/17-Conway Cubes.py @@ -0,0 +1,248 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#. +..# +###""", + "expected": ["112", "848"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["348", "2236"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Grid_3D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_3D: + def __init__(self, grid, x, y, z, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.state = state + + def neighbors(self): + return [ + self.grid.dots[(self.x + a, self.y + b, self.z + c)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + if (a, b, c) != (0, 0, 0) + and (self.x + a, self.y + b, self.z + c) in self.grid.dots + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +class Grid_4D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_4D: + def __init__(self, grid, x, y, z, w, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.w = w + self.state = state + + def neighbors(self): + return [ + self.grid.dots[(self.x + a, self.y + b, self.z + c, self.w + d)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + and (self.x + a, self.y + b, self.z + c, self.w + d) in self.grid.dots + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +if part_to_test == 1: + margin = 7 + grid = Grid_3D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + grid.dots[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid.dots[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + + for cycle in range(6): + print("Cycle = ", cycle + 1) + # #print ('Before') + + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (grid.dots[(x, y, z)].state, end='') + # #print ('') + + new_grid = deepcopy(grid) + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid.dots[(0,0,0)].neighbors()]) + + for dot in grid.dots: + if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( + 2, + 3, + ): + new_grid.dots[dot].state = "#" + elif grid.dots[dot].state == "#": + new_grid.dots[dot].state = "." + elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: + new_grid.dots[dot].state = "#" + + # #print ('After') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid.dots[(x, y, z)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + + +else: + margin = 7 + grid = Grid_4D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + for w in range(-margin, size + margin): + grid.dots[(x, y, z, w)] = Dot_4D(grid, x, y, z, w, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid.dots[(x, y, 0, 0)] = Dot_4D(grid, x, y, 0, 0, cell) + + for cycle in range(6): + # #print ('Cycle = ', cycle+1) + # #print ('Before') + + # #for w in range (-margin, size+margin): + # #print ('\n w=' + str(w)) + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z)) + # #level = '' + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #level += grid.dots[(x, y, z, w)].state + # #level += '\n' + # #if '#' in level: + # #print (level) + + new_grid = deepcopy(grid) + watchdot = (1, 0, 0, 0) + # #print (watchdot, grid.dots[watchdot].state, grid.dots[watchdot].active_neighbors()) + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) + ',' + str(neighbor.w) for neighbor in grid.dots[(1,0,0,0)].neighbors()]) + # #print (grid.dots[(1,0,0,0)].active_neighbors()) + + for dot in grid.dots: + if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( + 2, + 3, + ): + new_grid.dots[dot].state = "#" + elif grid.dots[dot].state == "#": + new_grid.dots[dot].state = "." + elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: + new_grid.dots[dot].state = "#" + + # #print (watchdot, new_grid.dots[watchdot].state, new_grid.dots[watchdot].active_neighbors()) + + # #print ('After') + # #for w in range (-margin, size+margin): + # #print ('\nw=' + str(w) + '\n') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid.dots[(x, y, z, w)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-17 06:00:01.401422 +# Part 1: 2020-12-17 06:28:49 +# Part 2: 2020-12-17 06:50:40 From 090d75d47a7f16f563d97a3344106525d37d5d78 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 18 Dec 2020 07:02:39 +0100 Subject: [PATCH 095/143] Removed prints --- 2020/11-Seating System.py | 4 ++-- 2020/14-Docking Data.py | 2 +- 2020/16-Ticket Translation.py | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/2020/11-Seating System.py b/2020/11-Seating System.py index 1321a3c..562a65f 100644 --- a/2020/11-Seating System.py +++ b/2020/11-Seating System.py @@ -131,7 +131,7 @@ def words(s: str): seats = copy.deepcopy(new_seats) new_seats.text_to_dots(puzzle_input) - print(i) + # #print(i) puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) @@ -196,7 +196,7 @@ def get_neighbors(self): break seats = copy.deepcopy(new_seats) - print(i) + # #print(i) puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) diff --git a/2020/14-Docking Data.py b/2020/14-Docking Data.py index 9fde54a..23c1135 100644 --- a/2020/14-Docking Data.py +++ b/2020/14-Docking Data.py @@ -58,7 +58,7 @@ def words(s: str): ) test_data[test] = { "input": open(input_file, "r+").read(), - "expected": ["Unknown", "Unknown"], + "expected": ["11179633149677", "4822600194774"], } diff --git a/2020/16-Ticket Translation.py b/2020/16-Ticket Translation.py index 85ed874..015c969 100644 --- a/2020/16-Ticket Translation.py +++ b/2020/16-Ticket Translation.py @@ -75,7 +75,7 @@ def words(s: str): ) test_data[test] = { "input": open(input_file, "r+").read(), - "expected": ["32835", "Unknown"], + "expected": ["32835", "514662805187"], } @@ -162,8 +162,8 @@ def words(s: str): pass field_order[field] = possible_order - for val in field_order: - print(field_order[val], val) + # #for val in field_order: + # #print(field_order[val], val) while any(len(val) > 1 for val in field_order.values()): new_field_order = deepcopy(field_order) for field in field_order: @@ -180,13 +180,13 @@ def words(s: str): field_order = deepcopy(new_field_order) ticket_value = 1 - for val in field_order: - print(field_order[val], val) + # #for val in field_order: + # #print(field_order[val], val) for field in validations.keys(): if field[:9] == "departure": - print( - my_ticket, field, field_order[field], my_ticket[field_order[field][0]] - ) + # #print( + # #my_ticket, field, field_order[field], my_ticket[field_order[field][0]] + # #) ticket_value *= my_ticket[field_order[field][0]] puzzle_actual_result = ticket_value From 37ef57232627c670389b82c5113e630dc052f23c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 18 Dec 2020 07:02:46 +0100 Subject: [PATCH 096/143] Added day 2020-18 --- 2020/18-Operation Order.py | 219 +++++++++++++++++++++++++++++++++++++ 1 file changed, 219 insertions(+) create mode 100644 2020/18-Operation Order.py diff --git a/2020/18-Operation Order.py b/2020/18-Operation Order.py new file mode 100644 index 0000000..83d73bf --- /dev/null +++ b/2020/18-Operation Order.py @@ -0,0 +1,219 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1 + 2 * 3 + 4 * 5 + 6""", + "expected": ["71", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """1 + (2 * 3) + (4 * (5 + 6))""", + "expected": ["51", "51"], +} + +test += 1 +test_data[test] = { + "input": """2 * 3 + (4 * 5)""", + "expected": ["Unknown", "46"], +} + +test += 1 +test_data[test] = { + "input": """5 * 9 * (7 * 3 * 3 + 9 * 3 + (8 + 6 * 4))""", + "expected": ["Unknown", "669060"], +} + +test += 1 +test_data[test] = { + "input": """4 * 2 + 3""", + "expected": ["11", "20"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["3647606140187", "323802071857594"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def make_math_p1(vals): + # #print ('Calculating', ''.join(map(str, vals))) + i = 0 + if vals[0] != "(": + value = int(vals[0]) + i = 1 + else: + j = 0 + open_par = 1 + closed_par = 0 + while open_par != closed_par: + j += 1 + if vals[i + j] == "(": + open_par += 1 + elif vals[i + j] == ")": + closed_par += 1 + + value = make_math_p1(vals[i + 1 : i + j]) + i += j + 1 + + # #print (value, i, ''.join(vals[i:])) + while i < len(vals) and vals[i] != "": + # #print (i, vals[i], value) + if vals[i] == "(": + j = 0 + open_par = 1 + closed_par = 0 + while open_par != closed_par: + j += 1 + if vals[i + j] == "(": + open_par += 1 + elif vals[i + j] == ")": + closed_par += 1 + + if operator == "+": + value += make_math_p1(vals[i + 1 : i + j]) + i += j + else: + value *= make_math_p1(vals[i + 1 : i + j]) + i += j + elif vals[i] in ["+", "*"]: + operator = vals[i] + else: + if operator == "+": + value += int(vals[i]) + else: + value *= int(vals[i]) + + i += 1 + # #print (''.join(vals), 'returns', value) + return value + + +def make_math_p2(vals): + # #print ('Calculating', ''.join(map(str, vals))) + init = vals.copy() + i = 0 + + while len(vals) != 1: + if "(" not in vals: + plusses = [i for i, val in enumerate(vals) if val == "+"] + for plus in plusses[::-1]: + vals[plus - 1] = int(vals[plus - 1]) + int(vals[plus + 1]) + del vals[plus : plus + 2] + + if "*" in vals: + return math.prod(map(int, vals[::2])) + else: + return int(vals[0]) + else: + i = min([i for i, val in enumerate(vals) if val == "("]) + j = 0 + open_par = 1 + closed_par = 0 + while open_par != closed_par: + j += 1 + if vals[i + j] == "(": + open_par += 1 + elif vals[i + j] == ")": + closed_par += 1 + + vals[i] = make_math_p2(vals[i + 1 : i + j]) + del vals[i + 1 : i + j + 1] + + # #print (init, 'returns', vals[0]) + return vals[0] + + +if part_to_test == 1: + number = 0 + for string in puzzle_input.split("\n"): + if string == "": + continue + string = string.replace("(", " ( ").replace(")", " ) ").replace(" ", " ") + if string[-1] == " ": + string = string[:-1] + if string[0] == " ": + string = string[1:] + + number += make_math_p1(string.split(" ")) + # #print ('-----') + puzzle_actual_result = number + + +else: + number = 0 + for string in puzzle_input.split("\n"): + if string == "": + continue + string = string.replace("(", " ( ").replace(")", " ) ").replace(" ", " ") + if string[-1] == " ": + string = string[:-1] + if string[0] == " ": + string = string[1:] + + number += make_math_p2(string.split(" ")) + # #print ('-----') + puzzle_actual_result = number + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-18 06:00:00.595135 +# Part 1: 2020-12-18 06:33:45 +# Part 2: 2020-12-18 06:58:36 From d83174eea20b11cbd8907f44627b579b097a8094 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 19 Dec 2020 07:56:15 +0100 Subject: [PATCH 097/143] Added day 2020-19 --- 2020/19-Monster Messages.py | 235 ++++++++++++++++++++++++++++++++++++ 1 file changed, 235 insertions(+) create mode 100644 2020/19-Monster Messages.py diff --git a/2020/19-Monster Messages.py b/2020/19-Monster Messages.py new file mode 100644 index 0000000..773dd2a --- /dev/null +++ b/2020/19-Monster Messages.py @@ -0,0 +1,235 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0: 4 1 5 +1: 2 3 | 3 2 +2: 4 4 | 5 5 +3: 4 5 | 5 4 +4: "a" +5: "b" + +ababbb +bababa +abbbab +aaabbb +aaaabbb""", + "expected": ["2", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """42: 9 14 | 10 1 +9: 14 27 | 1 26 +10: 23 14 | 28 1 +1: "a" +11: 42 31 +5: 1 14 | 15 1 +19: 14 1 | 14 14 +12: 24 14 | 19 1 +16: 15 1 | 14 14 +31: 14 17 | 1 13 +6: 14 14 | 1 14 +2: 1 24 | 14 4 +0: 8 11 +13: 14 3 | 1 12 +15: 1 | 14 +17: 14 2 | 1 7 +23: 25 1 | 22 14 +28: 16 1 +4: 1 1 +20: 14 14 | 1 15 +3: 5 14 | 16 1 +27: 1 6 | 14 18 +14: "b" +21: 14 1 | 1 14 +25: 1 1 | 1 14 +22: 14 14 +8: 42 +26: 14 22 | 1 20 +18: 15 15 +7: 14 5 | 1 21 +24: 14 1 + +abbbbbabbbaaaababbaabbbbabababbbabbbbbbabaaaa +bbabbbbaabaabba +babbbbaabbbbbabbbbbbaabaaabaaa +aaabbbbbbaaaabaababaabababbabaaabbababababaaa +bbbbbbbaaaabbbbaaabbabaaa +bbbababbbbaaaaaaaabbababaaababaabab +ababaaaaaabaaab +ababaaaaabbbaba +baabbaaaabbaaaababbaababb +abbbbabbbbaaaababbbbbbaaaababb +aaaaabbaabaaaaababaa +aaaabbaaaabbaaa +aaaabbaabbaaaaaaabbbabbbaaabbaabaaa +babaaabbbaaabaababbaabababaaab +aabbbbbaabbbaaaaaabbbbbababaaaaabbaaabba""", + "expected": ["3", "12"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["198", "372"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + rules_raw, messages = puzzle_input.split("\n\n") + + rules_with_subrules = {} + regexes = {} + for rule in rules_raw.split("\n"): + if '"' in rule: + regexes[int(rule.split(":")[0])] = rule.split('"')[1] + else: + nr, elements = rule.split(": ") + nr = int(nr) + rules_with_subrules[nr] = "( " + elements + " )" + + while rules_with_subrules: + for nr in regexes: + for rule in rules_with_subrules: + rules_with_subrules[rule] = rules_with_subrules[rule].replace( + " " + str(nr) + " ", " ( " + regexes[nr] + " ) " + ) + regexes.update( + { + rule: rules_with_subrules[rule] + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) == 0 + } + ) + rules_with_subrules = { + rule: rules_with_subrules[rule] + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) != 0 + } + + regexes = {rule: regexes[rule].replace(" ", "") for rule in regexes} + messages_OK = sum( + [ + 1 + for message in messages.split("\n") + if re.match("^" + regexes[0] + "$", message) + ] + ) + puzzle_actual_result = messages_OK + + +else: + rules_raw, messages = puzzle_input.split("\n\n") + + rules_with_subrules = {} + regexes = {} + for rule in rules_raw.split("\n"): + if "8:" in rule[:2]: + rule = "8: 42 +" + elif "11:" in rule[:3]: + rule = "11: 42 31 " + for i in range( + 2, 10 + ): # Note: 10 is arbitraty - it works well with 5 as well. + rule += "| " + "42 " * i + "31 " * i + + if '"' in rule: + regexes[int(rule.split(":")[0])] = rule.split('"')[1] + else: + nr, elements = rule.split(": ") + nr = int(nr) + rules_with_subrules[nr] = "( " + elements + " )" + + while rules_with_subrules: + for nr in regexes: + for rule in rules_with_subrules: + rules_with_subrules[rule] = rules_with_subrules[rule].replace( + " " + str(nr) + " ", " ( " + regexes[nr] + " ) " + ) + + regexes.update( + { + rule: rules_with_subrules[rule] + .replace(" ", "") + .replace("(a)", "a") + .replace("(b)", "b") + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) == 0 + } + ) + rules_with_subrules = { + rule: rules_with_subrules[rule] + for rule in rules_with_subrules + if len(ints(rules_with_subrules[rule])) != 0 + } + + regexes = {rule: regexes[rule] for rule in regexes} + messages_OK = sum( + [ + 1 + for message in messages.split("\n") + if re.match("^" + regexes[0] + "$", message) + ] + ) + puzzle_actual_result = messages_OK + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-19 06:00:00.865376 +# Part 1: 2020-12-19 06:24:39 +# Part 1: 2020-12-19 07:22:52 From 749b8e50321af88a0b1a72efce156a4047b00782 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 19 Dec 2020 18:06:16 +0100 Subject: [PATCH 098/143] Improved performance for 2020-11 and 2020-17 --- 2020/11-Seating System.py | 61 ++++----- 2020/11-Seating System.v1.py | 211 +++++++++++++++++++++++++++++ 2020/17-Conway Cubes.py | 149 +++++++-------------- 2020/17-Conway Cubes.v1.py | 248 +++++++++++++++++++++++++++++++++++ 2020/17-Conway Cubes.v2.py | 201 ++++++++++++++++++++++++++++ 5 files changed, 731 insertions(+), 139 deletions(-) create mode 100644 2020/11-Seating System.v1.py create mode 100644 2020/17-Conway Cubes.v1.py create mode 100644 2020/17-Conway Cubes.v2.py diff --git a/2020/11-Seating System.py b/2020/11-Seating System.py index 562a65f..212ad9d 100644 --- a/2020/11-Seating System.py +++ b/2020/11-Seating System.py @@ -138,67 +138,52 @@ def words(s: str): else: - def get_neighbors_map(dot): + def get_neighbors_map(grid, dot): neighbors = [] - if dot.grid.width is None: - dot.grid.get_size() - for direction in dot.allowed_directions: + for direction in directions_diagonals: neighbor = dot + direction - while neighbor is not None: - if neighbor.terrain in ("L", "#"): - neighbors.append(neighbor.position) + while neighbor in grid.dots: + if grid.dots[neighbor] in ("L", "#"): + neighbors.append(neighbor) break else: neighbor += direction return neighbors seats = grid.Grid() - seats.all_directions = directions_diagonals seats.text_to_dots(puzzle_input) - seats.neighbors_map = { - dot: get_neighbors_map(seats.dots[dot]) for dot in seats.dots - } - - new_seats = copy.deepcopy(seats) + seats.width = len(puzzle_input.split("\n")[0]) + seats.height = len(puzzle_input.split("\n")) - def get_neighbors(self): - return { - self.grid.dots[neighbor]: 1 - for neighbor in self.grid.neighbors_map[self.position] - } + seats.dots = {dot: seats.dots[dot].terrain for dot in seats.dots} + seats.neighbors_map = {dot: get_neighbors_map(seats, dot) for dot in seats.dots} - dot.Dot.get_neighbors = get_neighbors + new_seats = grid.Grid() + new_seats.dots = seats.dots.copy() - i = 0 + # #copy.deepcopy(seats) while True: - i += 1 - watch = [2] - for dot in seats.dots: - if seats.dots[dot].terrain == "L" and all( - [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + for dot, terrain in seats.dots.items(): + if terrain == "L" and all( + [seats.dots[d] in ("L", ".") for d in seats.neighbors_map[dot]] ): - new_seats.dots[dot].terrain = "#" + new_seats.dots[dot] = "#" elif ( - seats.dots[dot].terrain == "#" - and sum( - [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] - ) + terrain == "#" + and sum([1 for d in seats.neighbors_map[dot] if seats.dots[d] == "#"]) >= 5 ): - new_seats.dots[dot].terrain = "L" + new_seats.dots[dot] = "L" else: - new_seats.dots[dot].terrain = seats.dots[dot].terrain + new_seats.dots[dot] = terrain - if all( - [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] - ): + if all([seats.dots[d] == new_seats.dots[d] for d in seats.dots]): break - seats = copy.deepcopy(new_seats) - # #print(i) + seats.dots = new_seats.dots.copy() - puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d] == "#"]) # -------------------------------- Outputs / results --------------------------------- # diff --git a/2020/11-Seating System.v1.py b/2020/11-Seating System.v1.py new file mode 100644 index 0000000..562a65f --- /dev/null +++ b/2020/11-Seating System.v1.py @@ -0,0 +1,211 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +import copy +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """L.LL.LL.LL +LLLLLLL.LL +L.L.L..L.. +LLLL.LL.LL +L.LL.LL.LL +L.LLLLL.LL +..L.L..... +LLLLLLLLLL +L.LLLLLL.L +L.LLLLL.LL""", + "expected": ["37", "26"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2324", "2068"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, +} + + +grid.Grid.all_directions = directions_diagonals + +if part_to_test == 1: + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + + new_seats = grid.Grid() + new_seats.all_directions = directions_diagonals + new_seats.text_to_dots(puzzle_input) + + i = 0 + while True: + i += 1 + watch = [1 - 1j] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 4 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + new_seats.text_to_dots(puzzle_input) + # #print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +else: + + def get_neighbors_map(dot): + neighbors = [] + if dot.grid.width is None: + dot.grid.get_size() + for direction in dot.allowed_directions: + neighbor = dot + direction + while neighbor is not None: + if neighbor.terrain in ("L", "#"): + neighbors.append(neighbor.position) + break + else: + neighbor += direction + return neighbors + + seats = grid.Grid() + seats.all_directions = directions_diagonals + seats.text_to_dots(puzzle_input) + seats.neighbors_map = { + dot: get_neighbors_map(seats.dots[dot]) for dot in seats.dots + } + + new_seats = copy.deepcopy(seats) + + def get_neighbors(self): + return { + self.grid.dots[neighbor]: 1 + for neighbor in self.grid.neighbors_map[self.position] + } + + dot.Dot.get_neighbors = get_neighbors + + i = 0 + + while True: + i += 1 + watch = [2] + for dot in seats.dots: + if seats.dots[dot].terrain == "L" and all( + [d.terrain in ("L", ".") for d in seats.dots[dot].get_neighbors()] + ): + new_seats.dots[dot].terrain = "#" + elif ( + seats.dots[dot].terrain == "#" + and sum( + [1 for d in seats.dots[dot].get_neighbors() if d.terrain == "#"] + ) + >= 5 + ): + new_seats.dots[dot].terrain = "L" + else: + new_seats.dots[dot].terrain = seats.dots[dot].terrain + + if all( + [seats.dots[d].terrain == new_seats.dots[d].terrain for d in seats.dots] + ): + break + + seats = copy.deepcopy(new_seats) + # #print(i) + + puzzle_actual_result = sum([1 for d in seats.dots if seats.dots[d].terrain == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-11 06:00:07.140562 +# Part 1: 2020-12-11 06:22:46 +# Part 2: 2020-12-11 06:37:29 diff --git a/2020/17-Conway Cubes.py b/2020/17-Conway Cubes.py index 1adcae9..dbbc692 100644 --- a/2020/17-Conway Cubes.py +++ b/2020/17-Conway Cubes.py @@ -4,6 +4,7 @@ from compass import * from copy import deepcopy +from functools import lru_cache # This functions come from https://github.com/mcpower/adventofcode - Thanks! def lmap(func, *iterables): @@ -82,45 +83,20 @@ def __init__(self, grid, x, y, z, state): def neighbors(self): return [ - self.grid.dots[(self.x + a, self.y + b, self.z + c)] + self.grid[(self.x + a, self.y + b, self.z + c)] for a in range(-1, 2) for b in range(-1, 2) for c in range(-1, 2) if (a, b, c) != (0, 0, 0) - and (self.x + a, self.y + b, self.z + c) in self.grid.dots + and (self.x + a, self.y + b, self.z + c) in self.grid ] def active_neighbors(self): return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) -class Grid_4D: - def __init__(self, dots={}): - self.dots = dots - - -class Dot_4D: - def __init__(self, grid, x, y, z, w, state): - self.grid = grid - self.x = x - self.y = y - self.z = z - self.w = w - self.state = state - - def neighbors(self): - return [ - self.grid.dots[(self.x + a, self.y + b, self.z + c, self.w + d)] - for a in range(-1, 2) - for b in range(-1, 2) - for c in range(-1, 2) - for d in range(-1, 2) - if (a, b, c, d) != (0, 0, 0, 0) - and (self.x + a, self.y + b, self.z + c, self.w + d) in self.grid.dots - ] - - def active_neighbors(self): - return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) +def active_neighbors(active_grid, dot): + return sum([1 for neighbor in neighbors[dot] if neighbor in active_grid]) if part_to_test == 1: @@ -130,11 +106,11 @@ def active_neighbors(self): for x in range(-margin, size + margin): for y in range(-margin, size + margin): for z in range(-margin, size + margin): - grid.dots[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + grid[(x, y, z)] = Dot_3D(grid, x, y, z, ".") for y, line in enumerate(puzzle_input.split("\n")): for x, cell in enumerate(line): - grid.dots[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + grid[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) for cycle in range(6): print("Cycle = ", cycle + 1) @@ -144,98 +120,69 @@ def active_neighbors(self): # #print ('\nz=' + str(z) + '\n') # #for y in range (-margin, size+margin): # #for x in range (-margin, size+margin): - # #print (grid.dots[(x, y, z)].state, end='') + # #print (grid[(x, y, z)].state, end='') # #print ('') - new_grid = deepcopy(grid) - # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid.dots[(0,0,0)].neighbors()]) + new_grid = grid.copy() + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid[(0,0,0)].neighbors()]) - for dot in grid.dots: - if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( - 2, - 3, - ): - new_grid.dots[dot].state = "#" - elif grid.dots[dot].state == "#": - new_grid.dots[dot].state = "." - elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: - new_grid.dots[dot].state = "#" + for dot in grid: + if grid[dot].state == "#" and grid[dot].active_neighbors() in (2, 3,): + new_grid[dot].state = "#" + elif grid[dot].state == "#": + new_grid[dot].state = "." + elif grid[dot].state == "." and grid[dot].active_neighbors() == 3: + new_grid[dot].state = "#" # #print ('After') # #for z in range (-margin, size+margin): # #print ('\nz=' + str(z) + '\n') # #for y in range (-margin, size+margin): # #for x in range (-margin, size+margin): - # #print (new_grid.dots[(x, y, z)].state, end='') + # #print (new_grid[(x, y, z)].state, end='') # #print ('') grid = deepcopy(new_grid) - puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + puzzle_actual_result = sum([1 for dot in grid if grid[dot].state == "#"]) else: - margin = 7 - grid = Grid_4D() size = len(puzzle_input.split("\n")) - for x in range(-margin, size + margin): - for y in range(-margin, size + margin): - for z in range(-margin, size + margin): - for w in range(-margin, size + margin): - grid.dots[(x, y, z, w)] = Dot_4D(grid, x, y, z, w, ".") + active_grid = set() + + @lru_cache(None) + def neighbors(dot): + return set( + (dot[0] + a, dot[1] + b, dot[2] + c, dot[3] + d) + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + ) for y, line in enumerate(puzzle_input.split("\n")): for x, cell in enumerate(line): - grid.dots[(x, y, 0, 0)] = Dot_4D(grid, x, y, 0, 0, cell) + if cell == "#": + active_grid.add((x, y, 0, 0)) for cycle in range(6): - # #print ('Cycle = ', cycle+1) - # #print ('Before') - - # #for w in range (-margin, size+margin): - # #print ('\n w=' + str(w)) - # #for z in range (-margin, size+margin): - # #print ('\nz=' + str(z)) - # #level = '' - # #for y in range (-margin, size+margin): - # #for x in range (-margin, size+margin): - # #level += grid.dots[(x, y, z, w)].state - # #level += '\n' - # #if '#' in level: - # #print (level) - - new_grid = deepcopy(grid) - watchdot = (1, 0, 0, 0) - # #print (watchdot, grid.dots[watchdot].state, grid.dots[watchdot].active_neighbors()) - # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) + ',' + str(neighbor.w) for neighbor in grid.dots[(1,0,0,0)].neighbors()]) - # #print (grid.dots[(1,0,0,0)].active_neighbors()) - - for dot in grid.dots: - if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( - 2, - 3, - ): - new_grid.dots[dot].state = "#" - elif grid.dots[dot].state == "#": - new_grid.dots[dot].state = "." - elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: - new_grid.dots[dot].state = "#" - - # #print (watchdot, new_grid.dots[watchdot].state, new_grid.dots[watchdot].active_neighbors()) - - # #print ('After') - # #for w in range (-margin, size+margin): - # #print ('\nw=' + str(w) + '\n') - # #for z in range (-margin, size+margin): - # #print ('\nz=' + str(z) + '\n') - # #for y in range (-margin, size+margin): - # #for x in range (-margin, size+margin): - # #print (new_grid.dots[(x, y, z, w)].state, end='') - # #print ('') - - grid = deepcopy(new_grid) - - puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + still_active = set( + dot + for dot in active_grid + if sum([1 for n in neighbors(dot) if n in active_grid]) in (2, 3) + ) + # #print (active_grid, still_active) + all_neighbors = set().union(*(neighbors(dot) for dot in active_grid)) + newly_active = set( + dot + for dot in all_neighbors + if sum([1 for n in neighbors(dot) if n in active_grid]) == 3 + ) + active_grid = still_active.union(newly_active) + + puzzle_actual_result = len(active_grid) # -------------------------------- Outputs / results --------------------------------- # diff --git a/2020/17-Conway Cubes.v1.py b/2020/17-Conway Cubes.v1.py new file mode 100644 index 0000000..1adcae9 --- /dev/null +++ b/2020/17-Conway Cubes.v1.py @@ -0,0 +1,248 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#. +..# +###""", + "expected": ["112", "848"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["348", "2236"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Grid_3D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_3D: + def __init__(self, grid, x, y, z, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.state = state + + def neighbors(self): + return [ + self.grid.dots[(self.x + a, self.y + b, self.z + c)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + if (a, b, c) != (0, 0, 0) + and (self.x + a, self.y + b, self.z + c) in self.grid.dots + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +class Grid_4D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_4D: + def __init__(self, grid, x, y, z, w, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.w = w + self.state = state + + def neighbors(self): + return [ + self.grid.dots[(self.x + a, self.y + b, self.z + c, self.w + d)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + and (self.x + a, self.y + b, self.z + c, self.w + d) in self.grid.dots + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +if part_to_test == 1: + margin = 7 + grid = Grid_3D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + grid.dots[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid.dots[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + + for cycle in range(6): + print("Cycle = ", cycle + 1) + # #print ('Before') + + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (grid.dots[(x, y, z)].state, end='') + # #print ('') + + new_grid = deepcopy(grid) + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid.dots[(0,0,0)].neighbors()]) + + for dot in grid.dots: + if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( + 2, + 3, + ): + new_grid.dots[dot].state = "#" + elif grid.dots[dot].state == "#": + new_grid.dots[dot].state = "." + elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: + new_grid.dots[dot].state = "#" + + # #print ('After') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid.dots[(x, y, z)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + + +else: + margin = 7 + grid = Grid_4D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + for w in range(-margin, size + margin): + grid.dots[(x, y, z, w)] = Dot_4D(grid, x, y, z, w, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid.dots[(x, y, 0, 0)] = Dot_4D(grid, x, y, 0, 0, cell) + + for cycle in range(6): + # #print ('Cycle = ', cycle+1) + # #print ('Before') + + # #for w in range (-margin, size+margin): + # #print ('\n w=' + str(w)) + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z)) + # #level = '' + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #level += grid.dots[(x, y, z, w)].state + # #level += '\n' + # #if '#' in level: + # #print (level) + + new_grid = deepcopy(grid) + watchdot = (1, 0, 0, 0) + # #print (watchdot, grid.dots[watchdot].state, grid.dots[watchdot].active_neighbors()) + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) + ',' + str(neighbor.w) for neighbor in grid.dots[(1,0,0,0)].neighbors()]) + # #print (grid.dots[(1,0,0,0)].active_neighbors()) + + for dot in grid.dots: + if grid.dots[dot].state == "#" and grid.dots[dot].active_neighbors() in ( + 2, + 3, + ): + new_grid.dots[dot].state = "#" + elif grid.dots[dot].state == "#": + new_grid.dots[dot].state = "." + elif grid.dots[dot].state == "." and grid.dots[dot].active_neighbors() == 3: + new_grid.dots[dot].state = "#" + + # #print (watchdot, new_grid.dots[watchdot].state, new_grid.dots[watchdot].active_neighbors()) + + # #print ('After') + # #for w in range (-margin, size+margin): + # #print ('\nw=' + str(w) + '\n') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid.dots[(x, y, z, w)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid.dots if grid.dots[dot].state == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-17 06:00:01.401422 +# Part 1: 2020-12-17 06:28:49 +# Part 2: 2020-12-17 06:50:40 diff --git a/2020/17-Conway Cubes.v2.py b/2020/17-Conway Cubes.v2.py new file mode 100644 index 0000000..d5215dd --- /dev/null +++ b/2020/17-Conway Cubes.v2.py @@ -0,0 +1,201 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from copy import deepcopy +from functools import lru_cache + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """.#. +..# +###""", + "expected": ["112", "848"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["348", "2236"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class Grid_3D: + def __init__(self, dots={}): + self.dots = dots + + +class Dot_3D: + def __init__(self, grid, x, y, z, state): + self.grid = grid + self.x = x + self.y = y + self.z = z + self.state = state + + def neighbors(self): + return [ + self.grid[(self.x + a, self.y + b, self.z + c)] + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + if (a, b, c) != (0, 0, 0) + and (self.x + a, self.y + b, self.z + c) in self.grid + ] + + def active_neighbors(self): + return sum([1 for neighbor in self.neighbors() if neighbor.state == "#"]) + + +def active_neighbors(grid, dot): + return sum([1 for neighbor in neighbors[dot] if grid[neighbor] == "#"]) + + +if part_to_test == 1: + margin = 7 + grid = Grid_3D() + size = len(puzzle_input.split("\n")) + for x in range(-margin, size + margin): + for y in range(-margin, size + margin): + for z in range(-margin, size + margin): + grid[(x, y, z)] = Dot_3D(grid, x, y, z, ".") + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid[(x, y, 0)] = Dot_3D(grid, x, y, 0, cell) + + for cycle in range(6): + print("Cycle = ", cycle + 1) + # #print ('Before') + + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (grid[(x, y, z)].state, end='') + # #print ('') + + new_grid = grid.copy() + # #print ([neighbor.state + '@' + str(neighbor.x) + ',' + str(neighbor.y) + ',' + str(neighbor.z) for neighbor in new_grid[(0,0,0)].neighbors()]) + + for dot in grid: + if grid[dot].state == "#" and grid[dot].active_neighbors() in (2, 3,): + new_grid[dot].state = "#" + elif grid[dot].state == "#": + new_grid[dot].state = "." + elif grid[dot].state == "." and grid[dot].active_neighbors() == 3: + new_grid[dot].state = "#" + + # #print ('After') + # #for z in range (-margin, size+margin): + # #print ('\nz=' + str(z) + '\n') + # #for y in range (-margin, size+margin): + # #for x in range (-margin, size+margin): + # #print (new_grid[(x, y, z)].state, end='') + # #print ('') + + grid = deepcopy(new_grid) + + puzzle_actual_result = sum([1 for dot in grid if grid[dot].state == "#"]) + + +else: + margin = 7 + size = len(puzzle_input.split("\n")) + grid = { + (x, y, z, w): "." + for x in range(-margin, size + margin) + for y in range(-margin, size + margin) + for z in range(-margin, size + margin) + for w in range(-margin, size + margin) + } + + neighbors = { + (dot[0], dot[1], dot[2], dot[3]): [ + (dot[0] + a, dot[1] + b, dot[2] + c, dot[3] + d) + for a in range(-1, 2) + for b in range(-1, 2) + for c in range(-1, 2) + for d in range(-1, 2) + if (a, b, c, d) != (0, 0, 0, 0) + and (dot[0] + a, dot[1] + b, dot[2] + c, dot[3] + d) in grid + ] + for dot in grid + } + + for y, line in enumerate(puzzle_input.split("\n")): + for x, cell in enumerate(line): + grid[(x, y, 0, 0)] = cell + + for cycle in range(6): + new_grid = grid.copy() + + for dot in grid: + if grid[dot] == "#" and active_neighbors(grid, dot) in (2, 3,): + new_grid[dot] = "#" + elif grid[dot] == "#": + new_grid[dot] = "." + elif grid[dot] == "." and active_neighbors(grid, dot) == 3: + new_grid[dot] = "#" + + grid = new_grid.copy() + + puzzle_actual_result = Counter(grid.values())["#"] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-17 06:00:01.401422 +# Part 1: 2020-12-17 06:28:49 +# Part 2: 2020-12-17 06:50:40 From c0867622f7b70207db29157f4bb05ef082a2c780 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 20 Dec 2020 17:08:57 +0100 Subject: [PATCH 099/143] Added 2020-20 and new features in grid --- 2020/20-Jurassic Jigsaw.py | 339 +++++++++++++++++++++++++++++++++++++ 2020/grid.py | 199 ++++++++++++++++++++-- 2 files changed, 525 insertions(+), 13 deletions(-) create mode 100644 2020/20-Jurassic Jigsaw.py diff --git a/2020/20-Jurassic Jigsaw.py b/2020/20-Jurassic Jigsaw.py new file mode 100644 index 0000000..bef3873 --- /dev/null +++ b/2020/20-Jurassic Jigsaw.py @@ -0,0 +1,339 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict + +from functools import reduce +from compass import * + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Tile 1: +A-B +| | +D-C + +Tile 2: +C-D +| | +B-A, + +Tile 3: +X-Y +| | +B-A""", + "expected": ["""""", "Unknown"], +} + +test += 1 +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", "-sample.txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["""20899048083289""", "273"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["54755174472007", "1692"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +def matches(cam1, cam2): + if isinstance(cam1, int): + cam1 = set().union(*(cam_borders[cam1].values())) + if isinstance(cam2, int): + cam2 = set().union(*(cam_borders[cam2].values())) + if isinstance(cam1, str): + cam1 = {cam1} + if isinstance(cam2, str): + cam2 = {cam2} + + return [border for border in cam1 if border in cam2] + + +def nb_matches(cam1, cam2): + return len(matches(cam1, cam2)) + + +# This looks for the best orientation of a specific camera, based on its position +# It's possible to filter by angles & by neighbors +def find_best_orientation(cam1, position, possible_neighbors=[]): + # If cam1 is provided as camera number, select all angles + if isinstance(cam1, int): + cam1 = [(cam1, angle1) for angle1 in all_angles] + # If possible neighbors not provided, get them from neighbors + if possible_neighbors == []: + possible_neighbors = [cam2 for c1 in cam1 for cam2 in neighbors[c1]] + + angles = defaultdict(list) + best_angle = 0 + # By looking through all the orientations of cam1 + neighbors, determine all possible combinations + for (cid1, angle1) in cam1: + borders1 = cam_borders[cid1][angle1] + for (cid2, angle2) in possible_neighbors: + cam2 = cam_borders[cid2] + borders2 = cam2[angle2] + for offset, touchpoint in offset_to_border.items(): + # Let's put that corner in top left + if (position + offset).imag > 0 or (position + offset).real < 0: + continue + if borders1[touchpoint[0]] == borders2[touchpoint[1]]: + angles[angle1].append((cid2, angle2, offset)) + + if len(angles.values()) == 0: + return False + + best_angle = max([len(angle) for angle in angles.values()]) + + return { + angle: angles[angle] for angle in angles if len(angles[angle]) == best_angle + } + + +# There are all the relevant "angles" (actually operations) we can do +# Normal +# Normal + flip vertical +# Normal + flip horizontal +# Rotated 90° +# Rotated 90° + flip vertical +# Rotated 90° + flip horizontal +# Rotated 180° +# Rotated 270° +# Flipping the 180° or 270° would give same results as before +all_angles = [ + (0, "N"), + (0, "V"), + (0, "H"), + (90, "N"), + (90, "V"), + (90, "H"), + (180, "N"), + (270, "N"), +] + + +cam_borders = {} +cam_image = {} +cam_size = len(puzzle_input.split("\n\n")[0].split("\n")[1]) +for camera in puzzle_input.split("\n\n"): + camera_id = ints(camera.split("\n")[0])[0] + image = grid.Grid() + image.text_to_dots("\n".join(camera.split("\n")[1:])) + cam_image[camera_id] = image + + borders = {} + for orientation in all_angles: + new_image = image.flip(orientation[1])[0].rotate(orientation[0])[0] + borders.update({orientation: new_image.get_borders()}) + + cam_borders[camera_id] = borders + +match = {} +for camera_id, camera in cam_borders.items(): + value = ( + sum( + [ + nb_matches(camera_id, other_cam) + for other_cam in cam_borders + if other_cam != camera_id + ] + ) + // 2 + ) # Each match is counted twice because borders get flipped and still match + match[camera_id] = value + +corners = [cid for cid in cam_borders if match[cid] == 2] + +if part_to_test == 1: + puzzle_actual_result = reduce(lambda x, y: x * y, corners) + +else: + # This reads as: + # Cam2 is north of cam1: cam1's border 0 must match cam2's border 2 + offset_to_border = {north: (0, 2), east: (1, 3), south: (2, 0), west: (3, 1)} + + # This is the map of the possible neighbors + neighbors = { + (cid1, angle1): { + (cid2, angle2) + for cid2 in cam_borders + for angle2 in all_angles + if cid1 != cid2 + and nb_matches(cam_borders[cid1][angle1], cam_borders[cid2][angle2]) > 0 + } + for cid1 in cam_borders + for angle1 in all_angles + } + + # First, let's choose a corner + cam = corners[0] + image_pieces = {} + + # Then, let's determine its orientation & find some neighbors + angles = find_best_orientation(cam, 0) + possible_angles = { + x: angles[x] + for x in angles + if all([n[2].real >= 0 and n[2].imag <= 0 for n in angles[x]]) + } + # There should be 2 options (one transposed from the other), so we choose one + # Since the whole image will get flipped anyway, it has no impact + chosen_angle = list(possible_angles.keys())[0] + image_pieces[0] = (cam, chosen_angle) + image_pieces[angles[chosen_angle][0][2]] = angles[chosen_angle][0][:2] + image_pieces[angles[chosen_angle][1][2]] = angles[chosen_angle][1][:2] + + del angles, possible_angles, chosen_angle + + # Find all other pieces + grid_size = int(math.sqrt(len(cam_image))) + for x in range(grid_size): + for y in range(grid_size): + cam_pos = x - 1j * y + if cam_pos in image_pieces: + continue + + # Which neighbors do we already have? + neigh_offset = list( + dir for dir in directions_straight if cam_pos + dir in image_pieces + ) + neigh_vals = [image_pieces[cam_pos + dir] for dir in neigh_offset] + + # Based on the neighbors, keep only possible pieces + candidates = neighbors[neigh_vals[0]] + if len(neigh_offset) == 2: + candidates = [c for c in candidates if c in neighbors[neigh_vals[1]]] + + # Remove elements already in image + cameras_in_image = list(map(lambda a: a[0], image_pieces.values())) + candidates = [c for c in candidates if c[0] not in cameras_in_image] + + # Final filter on the orientation + candidates = [ + c for c in candidates if find_best_orientation([c], cam_pos, neigh_vals) + ] + + assert len(candidates) == 1 + + image_pieces[cam_pos] = candidates[0] + + # Merge all the pieces + all_pieces = [] + for y in range(0, -grid_size, -1): + for x in range(grid_size): + base_image = cam_image[image_pieces[x + 1j * y][0]] + orientation = image_pieces[x + 1j * y][1] + new_piece = base_image.flip(orientation[1])[0].rotate(orientation[0])[0] + new_piece = new_piece.crop([1 - 1j, cam_size - 2 - 1j * (cam_size - 2)]) + all_pieces.append(new_piece) + + final_image = grid.merge_grids(all_pieces, grid_size, grid_size) + del all_pieces + del orientation + del image_pieces + + # Let's search for the monsters! + monster = " # \n# ## ## ###\n # # # # # # " + dash_in_monster = Counter(monster)["#"] + monster = monster.replace(" ", ".").split("\n") + monster_width = len(monster[0]) + line_width = (cam_size - 2) * grid_size + + monster_found = defaultdict(int) + for angle in all_angles: + new_image = final_image.flip(angle[1])[0].rotate(angle[0])[0] + text_image = new_image.dots_to_text() + + matches = re.findall(monster[1], text_image) + if matches: + for match in matches: + position = text_image.find(match) + # We're on the first line + if position <= line_width: + continue + if re.match( + monster[0], + text_image[ + position + - (line_width + 1) : position + - (line_width + 1) + + monster_width + ], + ): + if re.match( + monster[2], + text_image[ + position + + (line_width + 1) : position + + (line_width + 1) + + monster_width + ], + ): + monster_found[angle] += 1 + + if len(monster_found) != 1: + # This means there was an error somewhere + print(monster_found) + + puzzle_actual_result = Counter(text_image)["#"] - dash_in_monster * max( + monster_found.values() + ) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-20 06:00:58.382556 +# Part 1: 2020-12-20 06:54:30 +# Part 2: 2020-12-20 16:45:45 diff --git a/2020/grid.py b/2020/grid.py index da0ce66..b3254d1 100644 --- a/2020/grid.py +++ b/2020/grid.py @@ -204,6 +204,139 @@ def add_walls(self, walls): if (dot, direction) in self.dots: self.dots[(dot, direction)].set_wall(True) + def get_borders(self): + """ + Gets the borders of the image + + Only the terrain of the dot will be sent back + This will be returned in left-to-right, up to bottom reading order + Newline characters are not included + + :return: a set of coordinates + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) + y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + borders = [] + borders.append([x + 1j * max_y for x in sorted(x_vals)]) + borders.append([max_x + 1j * y for y in sorted(y_vals)]) + borders.append([x + 1j * min_y for x in sorted(x_vals)]) + borders.append([min_x + 1j * y for y in sorted(y_vals)]) + + borders_text = [] + for border in borders: + borders_text.append( + Grid({pos: self.dots[pos].terrain for pos in border}) + .dots_to_text() + .replace("\n", "") + ) + + return borders_text + + def rotate(self, angles): + """ + Rotates clockwise a grid and returns a list of rotated grids + + :param tuple angles: Which angles to use for rotation + :return: The dots + """ + + rotated_grids = [] + + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x, min_y, max_y = self.get_box() + width, height = self.get_size() + + if isinstance(angles, int): + angles = {angles} + + for angle in angles: + if angle == 0: + rotated_grids.append(self) + elif angle == 90: + rotated_grids.append( + Grid( + { + height - 1 + pos.imag - 1j * pos.real: dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif angle == 180: + rotated_grids.append( + Grid( + { + width + - 1 + - pos.real + - 1j * (height - 1 + pos.imag): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif angle == 270: + rotated_grids.append( + Grid( + { + -pos.imag - 1j * (width - 1 - pos.real): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + + return rotated_grids + + def flip(self, flips): + """ + Flips a grid and returns a list of grids + + :param tuple flips: Which flips to perform + :return: The dots + """ + + flipped_grids = [] + + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x, min_y, max_y = self.get_box() + width, height = self.get_size() + + if isinstance(flips, str): + flips = {flips} + + for flip in flips: + if flip == "N": + flipped_grids.append(self) + elif flip == "H": + flipped_grids.append( + Grid( + { + pos.real - 1j * (height - 1 + pos.imag): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif flip == "V": + flipped_grids.append( + Grid( + { + width - 1 - pos.real + 1j * pos.imag: dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + + return flipped_grids + def crop(self, corners=[], size=0): """ Gets the list of dots within a given area @@ -250,20 +383,24 @@ def crop(self, corners=[], size=0): min_y, max_y = int(min(y_vals)), int(max(y_vals)) if self.is_isotropic: - cropped = { - x + y * 1j: self.dots[x + y * 1j] - for y in range(min_y, max_y + 1) - for x in range(min_x, max_x + 1) - if x + y * 1j in self.dots - } + cropped = Grid( + { + x + y * 1j: self.dots[x + y * 1j].terrain + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + if x + y * 1j in self.dots + } + ) else: - cropped = { - (x + y * 1j, dir): self.dots[(x + y * 1j, dir)] - for y in range(min_y, max_y + 1) - for x in range(min_x, max_x + 1) - for dir in self.all_directions - if (x + y * 1j, dir) in self.dots - } + cropped = Grid( + { + (x + y * 1j, dir): self.dots[(x + y * 1j, dir)].terrain + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + for dir in self.all_directions + if (x + y * 1j, dir) in self.dots + } + ) return cropped @@ -333,3 +470,39 @@ def convert_to_graph(self): graph.neighbors = lambda vertex: vertex.get_neighbors() return graph + + +def merge_grids(grids, width, height): + """ + Merges different grids in a single grid + + All grids are assumed to be of the same size + + :param dict grids: The grids to merge + :param int width: The width, in number of grids + :param int height: The height, in number of grids + :return: The merged grid + """ + + final_grid = Grid() + + part_width, part_height = grids[0].get_size() + if any([not grid.is_isotropic for grid in grids]): + print("This works only for isotropic grids") + return + + grid_nr = 0 + for part_y in range(height): + for part_x in range(width): + offset = part_x * part_width - 1j * part_y * part_height + final_grid.dots.update( + { + (pos + offset): Dot( + final_grid, pos + offset, grids[grid_nr].dots[pos].terrain + ) + for pos in grids[grid_nr].dots + } + ) + grid_nr += 1 + + return final_grid From e7b8a23da470ca1b70b5f0909cb90c739dcc1ff9 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 22 Dec 2020 07:05:47 +0100 Subject: [PATCH 100/143] Added days 2020-21 and 2020-22 --- 2020/21-Allergen Assessment.py | 160 +++++++++++++++++++++++++++++++ 2020/22-Crab Combat.py | 167 +++++++++++++++++++++++++++++++++ 2 files changed, 327 insertions(+) create mode 100644 2020/21-Allergen Assessment.py create mode 100644 2020/22-Crab Combat.py diff --git a/2020/21-Allergen Assessment.py b/2020/21-Allergen Assessment.py new file mode 100644 index 0000000..9e290cc --- /dev/null +++ b/2020/21-Allergen Assessment.py @@ -0,0 +1,160 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """mxmxvkd kfcds sqjhc nhms (contains dairy, fish) +trh fvjkl sbzzf mxmxvkd (contains dairy) +sqjhc fvjkl (contains soy) +sqjhc mxmxvkd sbzzf (contains fish)""", + "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2410", "tmp,pdpgm,cdslv,zrvtg,ttkn,mkpmkx,vxzpfp,flnhl"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +all_ingredients = defaultdict(int) +all_allergens = {} +nb_allergens = defaultdict(int) +allergens_ingredients = {} + +for string in puzzle_input.split("\n"): + if "contains" in string: + ingredients = string.split(" (")[0].split(" ") + allergens = string.split("(contains ")[1][:-1].split(", ") + if isinstance(allergens, str): + allergens = [allergens] + + for allergen in allergens: + nb_allergens[allergen] += 1 + if allergen not in all_allergens: + all_allergens[allergen] = ingredients.copy() + allergens_ingredients[allergen] = defaultdict(int) + allergens_ingredients[allergen].update( + {ingredient: 1 for ingredient in ingredients} + ) + + else: + for ingredient in ingredients: + allergens_ingredients[allergen][ingredient] += 1 + for ingredient in all_allergens[allergen].copy(): + if ingredient not in ingredients: + all_allergens[allergen].remove(ingredient) + + for ingredient in ingredients: + all_ingredients[ingredient] += 1 + + else: + print("does not contain any allergen") + + +for allergen in test: + if allergen != "shellfish": + continue + print( + allergen, + test2[allergen], + [ing for ing, val in test[allergen].items() if val == test2[allergen]], + ) + +sum_ingredients = 0 +for ingredient in all_ingredients: + if not (any(ingredient in val for val in all_allergens.values())): + sum_ingredients += all_ingredients[ingredient] + +if part_to_test == 1: + puzzle_actual_result = sum_ingredients + + +else: + allergens_ingredients = { + aller: [ + ing + for ing, val in allergens_ingredients[aller].items() + if val == nb_allergens[aller] + ] + for aller in nb_allergens + } + final_allergen = {} + while len(final_allergen) != len(nb_allergens): + for allergen, val in allergens_ingredients.items(): + if len(val) == 1: + final_allergen[allergen] = val[0] + + allergens_ingredients = { + aller: [ + ing + for ing in allergens_ingredients[aller] + if ing not in final_allergen.values() + ] + for aller in nb_allergens + } + + print(final_allergen) + ing_list = "" + for aller in sorted(final_allergen.keys()): + ing_list += final_allergen[aller] + "," + puzzle_actual_result = ing_list[:-1] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-21 06:07:34.505688 +# Part 1: 2020-12-21 07:22:36 +# Part 2: 2020-12-21 07:30:15 diff --git a/2020/22-Crab Combat.py b/2020/22-Crab Combat.py new file mode 100644 index 0000000..8fc2d00 --- /dev/null +++ b/2020/22-Crab Combat.py @@ -0,0 +1,167 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, copy, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Player 1: +9 +2 +6 +3 +1 + +Player 2: +5 +8 +4 +7 +10""", + "expected": ["306", "291"], +} + +test += 1 +test_data[test] = { + "input": """Player 1: +43 +19 + +Player 2: +2 +29 +14 + +""", + "expected": ["Unknown", "1 wins"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["30197", "34031"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + while len(cards[0]) != 0 and len(cards[1]) != 0: + if cards[0][0] >= cards[1][0]: + cards[0].append(cards[0].pop(0)) + cards[0].append(cards[1].pop(0)) + else: + cards[1].append(cards[1].pop(0)) + cards[1].append(cards[0].pop(0)) + + winner = cards[0] + cards[1] + + score = sum([card * (len(winner) - i) for i, card in enumerate(winner)]) + + puzzle_actual_result = score + + +else: + + def find_winner(cards): + previous_decks = [] + + while len(cards[0]) != 0 and len(cards[1]) != 0: + # #print ('before', cards) + if cards in previous_decks: + return (0, 0) + previous_decks.append(copy.deepcopy(cards)) + + if cards[0][0] < len(cards[0]) and cards[1][0] < len(cards[1]): + # #print ('subgame') + winner, score = find_winner( + [cards[0][1 : cards[0][0] + 1], cards[1][1 : cards[1][0] + 1]] + ) + # #print ('subgame won by', winner) + cards[winner].append(cards[winner].pop(0)) + cards[winner].append(cards[1 - winner].pop(0)) + + elif cards[0][0] >= cards[1][0]: + cards[0].append(cards[0].pop(0)) + cards[0].append(cards[1].pop(0)) + else: + cards[1].append(cards[1].pop(0)) + cards[1].append(cards[0].pop(0)) + + winner = [i for i in (0, 1) if cards[i] != []][0] + + score = sum( + [card * (len(cards[winner]) - i) for i, card in enumerate(cards[winner])] + ) + + return (winner, score) + + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + # #print (find_winner(cards)) + + puzzle_actual_result = find_winner(cards)[1] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-22 06:31:42.000598 +# Part 1: 2020-12-22 06:38:55 +# Part 2: 2020-12-22 07:01:53 From c642664168e8c542475236ce5d315d6a1948a250 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 22 Dec 2020 07:06:49 +0100 Subject: [PATCH 101/143] Various corrections --- 2015/24-It Hangs in the Balance.py | 74 +++---- 2016/02-Bathroom Security.py | 124 ++++++------ 2016/03-Squares With Three Sides.py | 89 +++++---- 2016/07-Internet Protocol Version 7.py | 156 ++++++++------- ...-Radioisotope Thermoelectric Generators.py | 189 ++++++++++-------- 2018/10-The Stars Align.py | 2 +- 2019/18-Many-Worlds Interpretation.py | 2 +- 7 files changed, 341 insertions(+), 295 deletions(-) diff --git a/2015/24-It Hangs in the Balance.py b/2015/24-It Hangs in the Balance.py index 5201414..a35eb96 100644 --- a/2015/24-It Hangs in the Balance.py +++ b/2015/24-It Hangs in the Balance.py @@ -7,7 +7,8 @@ test_data = {} test = 1 -test_data[test] = {"input": """1 +test_data[test] = { + "input": """1 2 3 4 @@ -17,26 +18,31 @@ 9 10 11""", - "expected": ['99', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['11846773891', 'Unknown'], - } + "expected": ["99", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["11846773891", "80393059"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # @@ -45,26 +51,28 @@ mini_quantum_entanglement = 10 ** 100 -list_packages = [int(x) for x in puzzle_input.split('\n')] +list_packages = [int(x) for x in puzzle_input.split("\n")] total_weight = sum(list_packages) group_weight = total_weight // 3 if part_to_test == 1 else total_weight // 4 -for group1_size in range (1, len(list_packages) - 2): - for group1 in itertools.combinations(list_packages, group1_size): - if sum(group1) != group_weight: - continue - if reduce(mul, group1, 1) >= mini_quantum_entanglement: - continue +for group1_size in range(1, len(list_packages) - 2): + for group1 in itertools.combinations(list_packages, group1_size): + if sum(group1) != group_weight: + continue + if reduce(mul, group1, 1) >= mini_quantum_entanglement: + continue - remaining_packages = [x for x in list_packages if x not in group1] + remaining_packages = [x for x in list_packages if x not in group1] - for group2_size in range (1, len(remaining_packages) - 2): - for group2 in itertools.combinations(remaining_packages, group2_size): - if sum(group2) == group_weight: - mini_quantum_entanglement = min(mini_quantum_entanglement, reduce(mul, group1, 1)) + for group2_size in range(1, len(remaining_packages) - 2): + for group2 in itertools.combinations(remaining_packages, group2_size): + if sum(group2) == group_weight: + mini_quantum_entanglement = min( + mini_quantum_entanglement, reduce(mul, group1, 1) + ) - if mini_quantum_entanglement != 10 ** 100: - break + if mini_quantum_entanglement != 10 ** 100: + break puzzle_actual_result = mini_quantum_entanglement @@ -72,10 +80,6 @@ # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/02-Bathroom Security.py b/2016/02-Bathroom Security.py index e1310ff..1ac9d8c 100644 --- a/2016/02-Bathroom Security.py +++ b/2016/02-Bathroom Security.py @@ -4,101 +4,103 @@ test_data = {} test = 1 -test_data[test] = {"input": """ULL +test_data[test] = { + "input": """ULL RRDDD LURDL UUUUD""", - "expected": ['1985', '5DB3'], - } + "expected": ["1985", "5DB3"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['36629', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["36629", "99C3D"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # -password = '' +password = "" if part_to_test == 1: - keypad = '''123 + keypad = """123 456 -789''' +789""" - x = 1 - y = 1 - for string in puzzle_input.split('\n'): - for letter in string: - if letter == 'U': - y = max(0, y-1) - elif letter == 'D': - y = min(2, y+1) - elif letter == 'L': - x = max(0, x-1) - elif letter == 'R': - x = min(2, x+1) + x = 1 + y = 1 + for string in puzzle_input.split("\n"): + for letter in string: + if letter == "U": + y = max(0, y - 1) + elif letter == "D": + y = min(2, y + 1) + elif letter == "L": + x = max(0, x - 1) + elif letter == "R": + x = min(2, x + 1) - password += keypad.split('\n')[y][x] + password += keypad.split("\n")[y][x] - puzzle_actual_result = password + puzzle_actual_result = password else: - keypad = '''__1__ + keypad = """__1__ _234_ 56789 _ABC_ -__D__''' - - x = 0 - y = 2 - for string in puzzle_input.split('\n'): - for letter in string: - x_new, y_new = x, y - if letter == 'U': - y_new = max(0, y_new-1) - elif letter == 'D': - y_new = min(4, y_new+1) - elif letter == 'L': - x_new = max(0, x_new-1) - elif letter == 'R': - x_new = min(4, x_new+1) +__D__""" - if not keypad.split('\n')[y_new][x_new] == '_': - x, y = x_new, y_new + x = 0 + y = 2 + for string in puzzle_input.split("\n"): + for letter in string: + x_new, y_new = x, y + if letter == "U": + y_new = max(0, y_new - 1) + elif letter == "D": + y_new = min(4, y_new + 1) + elif letter == "L": + x_new = max(0, x_new - 1) + elif letter == "R": + x_new = min(4, x_new + 1) - password += keypad.split('\n')[y][x] + if not keypad.split("\n")[y_new][x_new] == "_": + x, y = x_new, y_new - puzzle_actual_result = password + password += keypad.split("\n")[y][x] + puzzle_actual_result = password # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/03-Squares With Three Sides.py b/2016/03-Squares With Three Sides.py index bcfcd83..2891ff8 100644 --- a/2016/03-Squares With Three Sides.py +++ b/2016/03-Squares With Three Sides.py @@ -4,74 +4,75 @@ test_data = {} test = 1 -test_data[test] = {"input": """5 10 25 +test_data[test] = { + "input": """5 10 25 10 15 12""", - "expected": ['Unknown', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['983', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["983", "1836"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # possible_triangles = 0 if part_to_test == 1: - for string in puzzle_input.split('\n'): - sides = [int(x) for x in string.split(' ') if not x == ''] - sides.sort() - a, b, c = sides - - if c < (a + b): - possible_triangles += 1 + for string in puzzle_input.split("\n"): + sides = [int(x) for x in string.split(" ") if not x == ""] + sides.sort() + a, b, c = sides + if c < (a + b): + possible_triangles += 1 - puzzle_actual_result = possible_triangles + puzzle_actual_result = possible_triangles else: - lines = puzzle_input.split('\n') - for n in range(len(lines)): - lines[n] = [int(x) for x in lines[n].split(' ') if not x == ''] - for n in range(len(lines)//3): - for i in range (3): - sides = [int(lines[n*3+y][i]) for y in range (3)] - sides.sort() - a, b, c = sides + lines = puzzle_input.split("\n") + for n in range(len(lines)): + lines[n] = [int(x) for x in lines[n].split(" ") if not x == ""] + for n in range(len(lines) // 3): + for i in range(3): + sides = [int(lines[n * 3 + y][i]) for y in range(3)] + sides.sort() + a, b, c = sides - if c < (a + b): - possible_triangles += 1 - - puzzle_actual_result = possible_triangles + if c < (a + b): + possible_triangles += 1 + puzzle_actual_result = possible_triangles # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/07-Internet Protocol Version 7.py b/2016/07-Internet Protocol Version 7.py index cd9d547..9c8df09 100644 --- a/2016/07-Internet Protocol Version 7.py +++ b/2016/07-Internet Protocol Version 7.py @@ -4,106 +4,116 @@ test_data = {} test = 1 -test_data[test] = {"input": """abba[mnop]qrst +test_data[test] = { + "input": """abba[mnop]qrst abcd[bddb]xyyx aaaa[qwer]tyui ioxxoj[asdfgh]zxcvbn""", - "expected": ['Unknown', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": """aba[bab]xyz +test_data[test] = { + "input": """aba[bab]xyz xyx[xyx]xyx aaa[kek]eke zazbz[bzb]cdb""", - "expected": ['Unknown', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['115', 'Unknown'], - } + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["115", "231"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # if part_to_test == 1: - count_abba = 0 - for string in puzzle_input.split('\n'): - abba = False - if string == '': - continue - - in_brackets = False - - for index in range(len(string)-3): - if string[index] == '[': - in_brackets = True - continue - elif string[index] == ']': + count_abba = 0 + for string in puzzle_input.split("\n"): + abba = False + if string == "": + continue + in_brackets = False - continue - - if string[index] == string[index+3] and string[index+1] == string[index+2] and string[index] != string[index+1]: - if in_brackets: - abba = False - break - else: - abba = True - if abba: - count_abba += 1 - puzzle_actual_result = count_abba + + for index in range(len(string) - 3): + if string[index] == "[": + in_brackets = True + continue + elif string[index] == "]": + in_brackets = False + continue + + if ( + string[index] == string[index + 3] + and string[index + 1] == string[index + 2] + and string[index] != string[index + 1] + ): + if in_brackets: + abba = False + break + else: + abba = True + if abba: + count_abba += 1 + puzzle_actual_result = count_abba else: - ssl_support = 0 - for string in puzzle_input.split('\n'): - aba_sequences = [] - bab_sequences = [] - if string == '': - continue - - in_brackets = False - - for index in range(len(string)-2): - if string[index] == '[': - in_brackets = True - continue - elif string[index] == ']': - in_brackets = False - continue + ssl_support = 0 + for string in puzzle_input.split("\n"): + aba_sequences = [] + bab_sequences = [] + if string == "": + continue - if string[index] == string[index+2] and string[index] != string[index+1]: - if in_brackets: - aba_sequences.append(string[index:index+3]) - else: - bab_sequences.append(string[index:index+3]) - matching = [x for x in aba_sequences if x[1] + x[0] + x[1] in bab_sequences] + in_brackets = False - if matching: - ssl_support += 1 - puzzle_actual_result = ssl_support + for index in range(len(string) - 2): + if string[index] == "[": + in_brackets = True + continue + elif string[index] == "]": + in_brackets = False + continue + + if ( + string[index] == string[index + 2] + and string[index] != string[index + 1] + ): + if in_brackets: + aba_sequences.append(string[index : index + 3]) + else: + bab_sequences.append(string[index : index + 3]) + matching = [x for x in aba_sequences if x[1] + x[0] + x[1] in bab_sequences] + + if matching: + ssl_support += 1 + puzzle_actual_result = ssl_support # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2016/11-Radioisotope Thermoelectric Generators.py b/2016/11-Radioisotope Thermoelectric Generators.py index 52648c2..fda9dd5 100644 --- a/2016/11-Radioisotope Thermoelectric Generators.py +++ b/2016/11-Radioisotope Thermoelectric Generators.py @@ -5,49 +5,57 @@ test_data = {} test = 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} test += 1 -test_data[test] = {"input": """""", - "expected": ['Unknown', 'Unknown'], - } +test_data[test] = { + "input": """""", + "expected": ["Unknown", "Unknown"], +} -test = 'real' -test_data[test] = {"input": '11112123333', - "expected": ['31', 'Unknown'], - } +test = "real" +test_data[test] = { + "input": "11112123333", + "expected": ["31", "55"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' - - +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # - if part_to_test == 1: # -------------------------------- Graph-related functions -------------------------------- # # Re-implement the heuristic to match this graph - def heuristic (self, current_node, target_node): - return sum([abs(int(target_node[i]) - int(current_node[i])) for i in range (1, len(current_node))]) // 2 - pathfinding.WeightedGraph.heuristic = heuristic + def heuristic(self, current_node, target_node): + return ( + sum( + [ + abs(int(target_node[i]) - int(current_node[i])) + for i in range(1, len(current_node)) + ] + ) + // 2 + ) + pathfinding.WeightedGraph.heuristic = heuristic # How to determine neighbors - def neighbors (self, state): + def neighbors(self, state): global states E = int(state[0]) movables = [x for x in range(1, len(state)) if state[x] == state[0]] @@ -56,21 +64,38 @@ def neighbors (self, state): possible_neighbors = [] for movable in movables: if E > 1: - neighbor = str(E-1) + state[1:movable] + str(int(state[movable])-1) + state[movable+1:] + neighbor = ( + str(E - 1) + + state[1:movable] + + str(int(state[movable]) - 1) + + state[movable + 1 :] + ) possible_neighbors.append(neighbor) if E < 4: - neighbor = str(E+1) + state[1:movable] + str(int(state[movable])+1) + state[movable+1:] + neighbor = ( + str(E + 1) + + state[1:movable] + + str(int(state[movable]) + 1) + + state[movable + 1 :] + ) possible_neighbors.append(neighbor) if len(movables) >= 2: for moved_objects in itertools.combinations(movables, 2): mov1, mov2 = moved_objects # No use to bring 2 items downstairs - # if E > 1: - # neighbor = str(E-1) + state[1:mov1] + str(int(state[mov1])-1) + state[mov1+1:mov2] + str(int(state[mov2])-1) + state[mov2+1:] - # possible_neighbors.append(neighbor) + # if E > 1: + # neighbor = str(E-1) + state[1:mov1] + str(int(state[mov1])-1) + state[mov1+1:mov2] + str(int(state[mov2])-1) + state[mov2+1:] + # possible_neighbors.append(neighbor) if E < 4: - neighbor = str(E+1) + state[1:mov1] + str(int(state[mov1])+1) + state[mov1+1:mov2] + str(int(state[mov2])+1) + state[mov2+1:] + neighbor = ( + str(E + 1) + + state[1:mov1] + + str(int(state[mov1]) + 1) + + state[mov1 + 1 : mov2] + + str(int(state[mov2]) + 1) + + state[mov2 + 1 :] + ) possible_neighbors.append(neighbor) return [x for x in possible_neighbors if x in states] @@ -79,8 +104,8 @@ def neighbors (self, state): def cost(self, current_node, next_node): return 1 - pathfinding.WeightedGraph.cost = cost + pathfinding.WeightedGraph.cost = cost # -------------------------------- Graph construction & execution -------------------------------- # @@ -88,27 +113,43 @@ def cost(self, current_node, next_node): # Forbidden states: Any G + M if G for M is absent # Forbidden transitions: E changes, the rest is identical - states = set([''.join([str(E), str(TG), str(TM), str(PtG), str(PtM), str(SG), str(SM), str(PrG), str(PrM), str(RG), str(RM)]) - for E in range(1, 5) - for TG in range(1, 5) - for TM in range(1, 5) - for PtG in range(1, 5) - for PtM in range(1, 5) - for SG in range(1, 5) - for SM in range(1, 5) - for PrG in range(1, 5) - for PrM in range(1, 5) - for RG in range(1, 5) - for RM in range(1, 5) - - if (TG == TM or TM not in (TG, PtG, SG, PrG, RG)) - and (PtG == PtM or PtM not in (TG, PtG, SG, PrG, RG)) - and (SG == SM or SM not in (TG, PtG, SG, PrG, RG)) - and (PrG == PrM or PrM not in (TG, PtG, SG, PrG, RG)) - and (RG == RM or RM not in (TG, PtG, SG, PrG, RG)) - ]) - - end = '4' * 11 + states = set( + [ + "".join( + [ + str(E), + str(TG), + str(TM), + str(PtG), + str(PtM), + str(SG), + str(SM), + str(PrG), + str(PrM), + str(RG), + str(RM), + ] + ) + for E in range(1, 5) + for TG in range(1, 5) + for TM in range(1, 5) + for PtG in range(1, 5) + for PtM in range(1, 5) + for SG in range(1, 5) + for SM in range(1, 5) + for PrG in range(1, 5) + for PrM in range(1, 5) + for RG in range(1, 5) + for RM in range(1, 5) + if (TG == TM or TM not in (TG, PtG, SG, PrG, RG)) + and (PtG == PtM or PtM not in (TG, PtG, SG, PrG, RG)) + and (SG == SM or SM not in (TG, PtG, SG, PrG, RG)) + and (PrG == PrM or PrM not in (TG, PtG, SG, PrG, RG)) + and (RG == RM or RM not in (TG, PtG, SG, PrG, RG)) + ] + ) + + end = "4" * 11 graph = pathfinding.WeightedGraph() came_from, total_cost = graph.a_star_search(puzzle_input, end) @@ -119,13 +160,13 @@ def cost(self, current_node, next_node): # -------------------------------- Graph-related functions -------------------------------- # # Part 2 was completely rewritten for performance improvements - def valid_state (state): - pairs = [(state[x], state[x+1]) for x in range (1, len(state), 2)] + def valid_state(state): + pairs = [(state[x], state[x + 1]) for x in range(1, len(state), 2)] generators = state[1::2] for pair in pairs: - if pair[0] != pair[1]: # Microchip is not with generator - if pair[1] in generators: # Microchip is with a generator + if pair[0] != pair[1]: # Microchip is not with generator + if pair[1] in generators: # Microchip is with a generator return False return True @@ -133,7 +174,7 @@ def valid_state (state): def visited_state(state): global visited_coded_states - pairs = [(state[x], state[x+1]) for x in range (1, len(state), 2)] + pairs = [(state[x], state[x + 1]) for x in range(1, len(state), 2)] coded_state = [(state[0], pair) for pair in sorted(pairs)] @@ -143,7 +184,6 @@ def visited_state(state): visited_coded_states.append(coded_state) return False - # -------------------------------- BFS implementation -------------------------------- # start = list(map(int, puzzle_input)) + [1] * 4 end = [4] * 15 @@ -157,9 +197,13 @@ def visited_state(state): # Determine potential states to go to elev_position = state[0] # The +1 ignores the elevator - elements_at_level = [item+1 for item, level in enumerate(state[1:]) if level == elev_position] + elements_at_level = [ + item + 1 for item, level in enumerate(state[1:]) if level == elev_position + ] - movables = list(itertools.combinations(elements_at_level, 2)) + elements_at_level + movables = ( + list(itertools.combinations(elements_at_level, 2)) + elements_at_level + ) if elev_position == 1: directions = [1] @@ -175,7 +219,7 @@ def visited_state(state): new_floor = elev_position + direction new_state[0] = new_floor if isinstance(movable, tuple): - # No point in moving 2 items downwards + # No point in moving 2 items downwards if direction == -1: continue new_state[movable[0]] = new_floor @@ -187,39 +231,24 @@ def visited_state(state): if visited_state(new_state): continue else: - frontier.append((new_state, curr_steps+1)) + frontier.append((new_state, curr_steps + 1)) if new_state == end: puzzle_actual_result = curr_steps + 1 break - if puzzle_actual_result != 'Unknown': + if puzzle_actual_result != "Unknown": break - if puzzle_actual_result != 'Unknown': + if puzzle_actual_result != "Unknown": break - - - - - - puzzle_actual_result = curr_steps + 1 - - - - - # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2018/10-The Stars Align.py b/2018/10-The Stars Align.py index 9c89f1b..b71f89b 100644 --- a/2018/10-The Stars Align.py +++ b/2018/10-The Stars Align.py @@ -93,7 +93,7 @@ for x, y, vx, vy in stars_init ] star_map.vertices = vertices - puzzle_actual_result = min_i_galaxy_size + puzzle_actual_result = "See above, the galaxy is of size", min_i_galaxy_size print(star_map.vertices_to_grid(wall=" ")) break diff --git a/2019/18-Many-Worlds Interpretation.py b/2019/18-Many-Worlds Interpretation.py index 9328add..58d105e 100644 --- a/2019/18-Many-Worlds Interpretation.py +++ b/2019/18-Many-Worlds Interpretation.py @@ -104,7 +104,7 @@ ) test_data[test] = { "input": open(input_file, "r+").read().strip(), - "expected": ["4844", "Unknown"], + "expected": ["4844", "1784"], } # -------------------------------- Control program execution ------------------------- # From aa6f8ad480f532c87281b80ba966db87109339b2 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:18:32 +0100 Subject: [PATCH 102/143] Improved performance of 2020-22 --- 2020/22-Crab Combat.py | 82 ++++++++----------- 2020/22-Crab Combat.v1.py | 167 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 200 insertions(+), 49 deletions(-) create mode 100644 2020/22-Crab Combat.v1.py diff --git a/2020/22-Crab Combat.py b/2020/22-Crab Combat.py index 8fc2d00..2ccde94 100644 --- a/2020/22-Crab Combat.py +++ b/2020/22-Crab Combat.py @@ -90,71 +90,55 @@ def words(s: str): # -------------------------------- Actual code execution ----------------------------- # +def find_winner(cards, recursive): + previous_decks = [] + + while cards[0] and cards[1]: + # #print ('before', cards) + if cards in previous_decks: + return (0, None) + previous_decks.append([cards[i].copy() for i in (0, 1)]) + + cards_played = [cards[i].pop(0) for i in (0, 1)] + + if ( + recursive + and cards_played[0] <= len(cards[0]) + and cards_played[1] <= len(cards[1]) + ): + # #print ('subgame') + winner, _ = find_winner([cards[i][: cards_played[i]] for i in (0, 1)], True) + # #print ('subgame won by', winner) -if part_to_test == 1: - players = puzzle_input.split("\n\n") - cards = [ints(player) for i, player in enumerate(players)] - cards[0].pop(0) - cards[1].pop(0) - - while len(cards[0]) != 0 and len(cards[1]) != 0: - if cards[0][0] >= cards[1][0]: - cards[0].append(cards[0].pop(0)) - cards[0].append(cards[1].pop(0)) else: - cards[1].append(cards[1].pop(0)) - cards[1].append(cards[0].pop(0)) - - winner = cards[0] + cards[1] - - score = sum([card * (len(winner) - i) for i, card in enumerate(winner)]) - - puzzle_actual_result = score + winner = cards_played[0] < cards_played[1] + cards[winner].append(cards_played[winner]) + cards[winner].append(cards_played[1 - winner]) -else: - - def find_winner(cards): - previous_decks = [] + winner = [i for i in (0, 1) if cards[i] != []][0] - while len(cards[0]) != 0 and len(cards[1]) != 0: - # #print ('before', cards) - if cards in previous_decks: - return (0, 0) - previous_decks.append(copy.deepcopy(cards)) + score = sum(card * (len(cards[winner]) - i) for i, card in enumerate(cards[winner])) - if cards[0][0] < len(cards[0]) and cards[1][0] < len(cards[1]): - # #print ('subgame') - winner, score = find_winner( - [cards[0][1 : cards[0][0] + 1], cards[1][1 : cards[1][0] + 1]] - ) - # #print ('subgame won by', winner) - cards[winner].append(cards[winner].pop(0)) - cards[winner].append(cards[1 - winner].pop(0)) + return (winner, score) - elif cards[0][0] >= cards[1][0]: - cards[0].append(cards[0].pop(0)) - cards[0].append(cards[1].pop(0)) - else: - cards[1].append(cards[1].pop(0)) - cards[1].append(cards[0].pop(0)) - winner = [i for i in (0, 1) if cards[i] != []][0] +if part_to_test == 1: + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) - score = sum( - [card * (len(cards[winner]) - i) for i, card in enumerate(cards[winner])] - ) + puzzle_actual_result = find_winner(cards, False)[1] - return (winner, score) +else: players = puzzle_input.split("\n\n") cards = [ints(player) for i, player in enumerate(players)] cards[0].pop(0) cards[1].pop(0) - # #print (find_winner(cards)) - - puzzle_actual_result = find_winner(cards)[1] + puzzle_actual_result = find_winner(cards, True)[1] # -------------------------------- Outputs / results --------------------------------- # diff --git a/2020/22-Crab Combat.v1.py b/2020/22-Crab Combat.v1.py new file mode 100644 index 0000000..8fc2d00 --- /dev/null +++ b/2020/22-Crab Combat.v1.py @@ -0,0 +1,167 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, copy, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Player 1: +9 +2 +6 +3 +1 + +Player 2: +5 +8 +4 +7 +10""", + "expected": ["306", "291"], +} + +test += 1 +test_data[test] = { + "input": """Player 1: +43 +19 + +Player 2: +2 +29 +14 + +""", + "expected": ["Unknown", "1 wins"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["30197", "34031"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + while len(cards[0]) != 0 and len(cards[1]) != 0: + if cards[0][0] >= cards[1][0]: + cards[0].append(cards[0].pop(0)) + cards[0].append(cards[1].pop(0)) + else: + cards[1].append(cards[1].pop(0)) + cards[1].append(cards[0].pop(0)) + + winner = cards[0] + cards[1] + + score = sum([card * (len(winner) - i) for i, card in enumerate(winner)]) + + puzzle_actual_result = score + + +else: + + def find_winner(cards): + previous_decks = [] + + while len(cards[0]) != 0 and len(cards[1]) != 0: + # #print ('before', cards) + if cards in previous_decks: + return (0, 0) + previous_decks.append(copy.deepcopy(cards)) + + if cards[0][0] < len(cards[0]) and cards[1][0] < len(cards[1]): + # #print ('subgame') + winner, score = find_winner( + [cards[0][1 : cards[0][0] + 1], cards[1][1 : cards[1][0] + 1]] + ) + # #print ('subgame won by', winner) + cards[winner].append(cards[winner].pop(0)) + cards[winner].append(cards[1 - winner].pop(0)) + + elif cards[0][0] >= cards[1][0]: + cards[0].append(cards[0].pop(0)) + cards[0].append(cards[1].pop(0)) + else: + cards[1].append(cards[1].pop(0)) + cards[1].append(cards[0].pop(0)) + + winner = [i for i in (0, 1) if cards[i] != []][0] + + score = sum( + [card * (len(cards[winner]) - i) for i, card in enumerate(cards[winner])] + ) + + return (winner, score) + + players = puzzle_input.split("\n\n") + cards = [ints(player) for i, player in enumerate(players)] + cards[0].pop(0) + cards[1].pop(0) + + # #print (find_winner(cards)) + + puzzle_actual_result = find_winner(cards)[1] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-22 06:31:42.000598 +# Part 1: 2020-12-22 06:38:55 +# Part 2: 2020-12-22 07:01:53 From f4f4c16aefc0c8b4128182b7b472fec257958fff Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:18:48 +0100 Subject: [PATCH 103/143] Added utility for doubly-linked lists --- 2020/doubly_linked_list.py | 222 +++++++++++++++++++++++++++++++++++++ 1 file changed, 222 insertions(+) create mode 100644 2020/doubly_linked_list.py diff --git a/2020/doubly_linked_list.py b/2020/doubly_linked_list.py new file mode 100644 index 0000000..6bb667c --- /dev/null +++ b/2020/doubly_linked_list.py @@ -0,0 +1,222 @@ +class DoublyLinkedList: + def __init__(self, is_cycle=False): + """ + Creates a list + + :param Boolean is_cycle: Whether the list is a cycle (loops around itself) + """ + self.start_element = None + self.is_cycle = is_cycle + self.elements = {} + + def insert(self, ref_element, new_elements, insert_before=False): + """ + Inserts new elements in the list + + :param Any ref_element: The value of the element where we'll insert data + :param Any new_elements: A list of new elements to insert, or a single element + :param Boolean insert_before: If True, will insert before ref_element. + """ + new_elements_converted = [] + if isinstance(new_elements, (list, tuple, set)): + for i, element in enumerate(new_elements): + if not isinstance(element, DoublyLinkedListElement): + new_element_converted = DoublyLinkedListElement(element) + if i != 0: + new_element_converted.prev_element = new_elements_converted[ + i - 1 + ] + new_element_converted.prev_element.next_element = ( + new_element_converted + ) + else: + new_element_converted = element + if i != 0: + new_element_converted.prev_element = new_elements_converted[ + i - 1 + ] + new_element_converted.prev_element.next_element = ( + new_element_converted + ) + new_elements_converted.append(new_element_converted) + self.elements[new_element_converted.item] = new_element_converted + else: + if not isinstance(new_elements, DoublyLinkedListElement): + new_element_converted = DoublyLinkedListElement(new_elements) + else: + new_element_converted = new_elements + new_elements_converted.append(new_element_converted) + self.elements[new_element_converted.item] = new_element_converted + + if self.start_element == None: + self.start_element = new_elements_converted[0] + for pos, element in enumerate(new_elements_converted): + element.prev_element = new_elements_converted[pos - 1] + element.next_element = new_elements_converted[pos + 1] + + if not self.is_cycle: + new_elements_converted[0].prev_element = None + new_elements_converted[-1].next_element = None + else: + if isinstance(ref_element, DoublyLinkedListElement): + cursor = ref_element + else: + cursor = self.find(ref_element) + + if insert_before: + new_elements_converted[0].prev_element = cursor.prev_element + new_elements_converted[-1].next_element = cursor + + if cursor.prev_element is not None: + cursor.prev_element.next_element = new_elements_converted[0] + cursor.prev_element = new_elements_converted[-1] + if self.start_element == cursor: + self.start_element = new_elements_converted[0] + else: + new_elements_converted[0].prev_element = cursor + new_elements_converted[-1].next_element = cursor.next_element + if cursor.next_element is not None: + cursor.next_element.prev_element = new_elements_converted[-1] + cursor.next_element = new_elements_converted[0] + + def append(self, new_element): + """ + Appends an element in the list + + :param Any new_element: The new element to insert + :param Boolean insert_before: If True, will insert before ref_element. + """ + if not isinstance(new_element, DoublyLinkedListElement): + new_element = DoublyLinkedListElement(new_element) + + self.elements[new_element.item] = new_element + + if self.start_element is None: + self.start_element = new_element + if self.is_cycle: + new_element.next_element = new_element + new_element.prev_element = new_element + else: + if self.is_cycle: + cursor = self.start_element.prev_element + else: + cursor = self.start_element + while cursor.next_element is not None: + if self.is_cycle and cursor.next_element == self.start_element: + break + cursor = cursor.next_element + + new_element.prev_element = cursor + new_element.next_element = cursor.next_element + if cursor.next_element is not None: + cursor.next_element.prev_element = new_element + cursor.next_element = new_element + + def traverse(self, start, end=None): + """ + Gets items based on their values + + :param Any start: The start element + :param Any stop: The end element + """ + output = [] + if self.start_element is None: + return [] + + if not isinstance(start, DoublyLinkedListElement): + start = self.find(start) + cursor = start + + if not isinstance(end, DoublyLinkedListElement): + end = self.find(end) + + while cursor is not None: + if cursor == end: + break + + output.append(cursor) + + cursor = cursor.next_element + + if self.is_cycle and cursor == start: + break + + return output + + def delete_by_value(self, to_delete): + """ + Deletes a given element from the list + + :param Any to_delete: The element to delete + """ + output = [] + if self.start_element is None: + return + + cursor = to_delete + cursor.prev_element.next_element = cursor.next_element + cursor.next_element.prev_element = cursor.prev_element + + def delete_by_position(self, to_delete): + """ + Deletes a given element from the list + + :param Any to_delete: The element to delete + """ + output = [] + if self.start_element is None: + return + + if not isinstance(to_delete, int): + raise TypeError("Position must be an integer") + + cursor = self.start_element + i = -1 + while cursor is not None and i < to_delete: + i += 1 + if i == to_delete: + if cursor.prev_element: + cursor.prev_element.next_element = cursor.next_element + if cursor.next_element: + cursor.next_element.prev_element = cursor.prev_element + + if self.start_element == cursor: + self.start_element = cursor.next_element + + del cursor + return True + + raise ValueError("Element not in list") + + def find(self, needle): + """ + Finds a given item based on its value + + :param Any needle: The element to search + """ + if isinstance(needle, DoublyLinkedListElement): + return needle + else: + if needle in self.elements: + return self.elements[needle] + else: + return False + + +class DoublyLinkedListElement: + def __init__(self, data, prev_element=None, next_element=None): + self.item = data + self.prev_element = prev_element + self.next_element = next_element + + def __repr__(self): + output = [self.item] + if self.prev_element is not None: + output.append(self.prev_element.item) + else: + output.append(None) + if self.next_element is not None: + output.append(self.next_element.item) + else: + output.append(None) + return str(tuple(output)) From 47f11af915dcfd25a25b894f96c021636fdcacb6 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:19:04 +0100 Subject: [PATCH 104/143] Added day 2020-23 --- 2020/23-Crab Cups.py | 153 ++++++++++++++++++++++++++++++++++++++ 2020/23-Crab Cups.v1.py | 161 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 314 insertions(+) create mode 100644 2020/23-Crab Cups.py create mode 100644 2020/23-Crab Cups.v1.py diff --git a/2020/23-Crab Cups.py b/2020/23-Crab Cups.py new file mode 100644 index 0000000..4488850 --- /dev/null +++ b/2020/23-Crab Cups.py @@ -0,0 +1,153 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from simply_linked_list import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """389125467""", + "expected": ["92658374 after 10 moves, 67384529 after 100 moves", "149245887792"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["45286397", "836763710"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +if part_to_test == 1: + moves = 100 + for string in puzzle_input.split("\n"): + cups = [int(x) for x in string] + + for i in range(moves): + cur_cup = cups[0] + pickup = cups[1:4] + del cups[0:4] + + try: + dest_cup = max([x for x in cups if x < cur_cup]) + except: + dest_cup = max([x for x in cups]) + cups[cups.index(dest_cup) + 1 : cups.index(dest_cup) + 1] = pickup + cups.append(cur_cup) + + print(cups) + + pos1 = cups.index(1) + puzzle_actual_result = "".join(map(str, cups[pos1 + 1 :] + cups[:pos1])) + +else: + moves = 10 ** 7 + nb_cups = 10 ** 6 + + class Cup: + def __init__(self, val, next_cup=None): + self.val = val + self.next_cup = next_cup + + string = puzzle_input.split("\n")[0] + next_cup = None + cups = {} + for x in string[::-1]: + cups[x] = Cup(x, next_cup) + next_cup = cups[x] + + next_cup = cups[string[0]] + for x in range(nb_cups, 9, -1): + cups[str(x)] = Cup(str(x), next_cup) + next_cup = cups[str(x)] + + cups[string[-1]].next_cup = cups["10"] + + cur_cup = cups[string[0]] + for i in range(1, moves + 1): + # #print ('----- Move', i) + # #print ('Current', cur_cup.val) + + cups_moved = [ + cur_cup.next_cup, + cur_cup.next_cup.next_cup, + cur_cup.next_cup.next_cup.next_cup, + ] + cups_moved_val = [cup.val for cup in cups_moved] + # #print ('Moved cups', cups_moved_val) + + cur_cup.next_cup = cups_moved[-1].next_cup + + dest_cup_nr = int(cur_cup.val) - 1 + while str(dest_cup_nr) in cups_moved_val or dest_cup_nr <= 0: + dest_cup_nr -= 1 + if dest_cup_nr <= 0: + dest_cup_nr = nb_cups + dest_cup = cups[str(dest_cup_nr)] + + # #print ("Destination", dest_cup_nr) + + cups_moved[-1].next_cup = dest_cup.next_cup + dest_cup.next_cup = cups_moved[0] + + cur_cup = cur_cup.next_cup + + puzzle_actual_result = int(cups["1"].next_cup.val) * int( + cups["1"].next_cup.next_cup.val + ) + # #puzzle_actual_result = cups[(pos1+1)%len(cups)] * cups[(pos1+2)%len(cups)] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-23 06:25:17.546310 diff --git a/2020/23-Crab Cups.v1.py b/2020/23-Crab Cups.v1.py new file mode 100644 index 0000000..6a04b52 --- /dev/null +++ b/2020/23-Crab Cups.v1.py @@ -0,0 +1,161 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * +from doubly_linked_list import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """389125467""", + "expected": ["92658374 after 10 moves, 67384529 after 100 moves", "149245887792"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["45286397", "836763710"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +if part_to_test == 1: + moves = 100 + for string in puzzle_input.split("\n"): + cups = [int(x) for x in string] + + for i in range(moves): + cur_cup = cups[0] + pickup = cups[1:4] + del cups[0:4] + + try: + dest_cup = max([x for x in cups if x < cur_cup]) + except: + dest_cup = max([x for x in cups]) + cups[cups.index(dest_cup) + 1 : cups.index(dest_cup) + 1] = pickup + cups.append(cur_cup) + + print(cups) + + pos1 = cups.index(1) + puzzle_actual_result = "".join(map(str, cups[pos1 + 1 :] + cups[:pos1])) + +else: + moves = 10 ** 7 + nb_cups = 10 ** 6 + cups = DoublyLinkedList(True) + + for string in puzzle_input.split("\n"): + for cup in string: + cups.append(cup) + + new_cups = { + str(i): DoublyLinkedListElement(str(i), None, None) + for i in range(10, nb_cups + 1) + } + for key, cup in new_cups.items(): + if key != "10": + cup.prev_element = new_cups[str(int(key) - 1)] + if key != str(nb_cups): + cup.next_element = new_cups[str(int(key) + 1)] + new_cups["10"].prev_element = cups.elements[string[-1]] + new_cups[str(nb_cups)].next_element = cups.elements[string[0]] + + cups.elements.update(new_cups) + cups.elements[string[-1]].next_element = new_cups["10"] + cups.elements[string[0]].prev_element = new_cups[str(nb_cups)] + + del new_cups + + print([(i, cups.elements[str(i)]) for i in map(str, range(1, 15))]) + + cur_cup = cups.start_element + # #print (cups.elements) + for i in range(1, moves + 1): + print("----- Move", i) + # #print (','.join([x.item for x in cups.traverse(cups.start_element)]), cur_cup.item) + + cups_moved = [ + cur_cup.next_element, + cur_cup.next_element.next_element, + cur_cup.next_element.next_element.next_element, + ] + cups_moved_int = list(map(lambda i: int(i.item), cups_moved)) + # #print ('Moved cups', [x.item for x in cups_moved]) + + cups.delete_by_value(cur_cup.next_element) + cups.delete_by_value(cur_cup.next_element) + cups.delete_by_value(cur_cup.next_element) + + dest_cup_nr = int(cur_cup.item) - 1 + while dest_cup_nr in cups_moved_int or dest_cup_nr <= 0: + dest_cup_nr -= 1 + if dest_cup_nr <= 0: + dest_cup_nr = nb_cups + dest_cup = cups.find(str(dest_cup_nr)) + + # #print ("Destination", dest_cup_nr) + + cups.insert(dest_cup, cups_moved) + cur_cup = cur_cup.next_element + + pos1 = cups.find("1") + puzzle_actual_result = int(pos1.next_element.item) * int( + pos1.next_element.next_element.item + ) + # #puzzle_actual_result = cups[(pos1+1)%len(cups)] * cups[(pos1+2)%len(cups)] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-23 06:25:17.546310 From 2d48023800719c193bf85edfbbab687ac28c3734 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 16:26:37 +0100 Subject: [PATCH 105/143] Added timings for 2020-23 --- 2020/23-Crab Cups.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/2020/23-Crab Cups.py b/2020/23-Crab Cups.py index 4488850..9ad0f81 100644 --- a/2020/23-Crab Cups.py +++ b/2020/23-Crab Cups.py @@ -151,3 +151,5 @@ def __init__(self, val, next_cup=None): print("Expected result : " + str(puzzle_expected_result)) print("Actual result : " + str(puzzle_actual_result)) # Date created: 2020-12-23 06:25:17.546310 +# Part 1: 2020-12-23 06:36:18 +# Part 2: 2020-12-23 15:21:48 From 8f222e48582abb42e2ce9b1ce8cf8b4b1ca96519 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 18:41:09 +0100 Subject: [PATCH 106/143] Added Ford-Fulkerson algorithm for max flow identification --- 2020/graph.py | 66 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 2 deletions(-) diff --git a/2020/graph.py b/2020/graph.py index b2d3f9f..da2933b 100644 --- a/2020/graph.py +++ b/2020/graph.py @@ -196,6 +196,9 @@ def breadth_first_search(self, start, end=None): vertex, current_distance = frontier.pop(0) current_distance += 1 neighbors = self.neighbors(vertex) + # This allows to cover WeightedGraphs + if isinstance(neighbors, dict): + neighbors = list(neighbors.keys()) if not neighbors: continue @@ -212,8 +215,6 @@ def breadth_first_search(self, start, end=None): if neighbor == end: return True - if end: - return True return False def greedy_best_first_search(self, start, end): @@ -444,3 +445,64 @@ def bellman_ford(self, start, end=None): raise NegativeWeightCycle return end is None or end in self.distance_from_start + + def ford_fulkerson(self, start, end): + """ + Searches for the maximum flow using the Ford-Fulkerson algorithm + + The weights of the graph are used as flow limitations + Note: there may be multiple options, this generates only one + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + + if start not in vertices: + return ValueError("Source not in graph") + if end not in vertices: + return ValueError("End not in graph") + + if end not in self.edges: + self.edges[end] = {} + + initial_edges = {a: graph.edges[a].copy() for a in graph.edges} + self.flow_graph = {a: graph.edges[a].copy() for a in graph.edges} + + max_flow = 0 + frontier = [start] + heapq.heapify(frontier) + print(self.edges) + + while self.breadth_first_search(start, end): + path_flow = float("Inf") + cursor = end + while cursor != start: + path_flow = min(path_flow, self.edges[self.came_from[cursor]][cursor]) + cursor = self.came_from[cursor] + + max_flow += path_flow + + # Update the graph to change the flows + cursor = end + while cursor != start: + self.edges[self.came_from[cursor]][cursor] -= path_flow + if self.edges[self.came_from[cursor]][cursor] == 0: + del self.edges[self.came_from[cursor]][cursor] + self.edges[cursor][self.came_from[cursor]] = ( + self.edges[cursor].get(self.came_from[cursor], 0) + path_flow + ) + + cursor = self.came_from[cursor] + + cursor = end + for vertex in self.vertices: + for neighbor, items in self.neighbors(vertex).items(): + if neighbor in self.flow_graph[vertex]: + self.flow_graph[vertex][neighbor] -= self.edges[vertex][neighbor] + if self.flow_graph[vertex][neighbor] == 0: + del self.flow_graph[vertex][neighbor] + + self.edges = initial_edges + + return max_flow From b531317ac81b633342932cc56cc0252e61d7bbfa Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:56:43 +0100 Subject: [PATCH 107/143] Corrections on graph utility --- 2020/graph.py | 50 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 42 insertions(+), 8 deletions(-) diff --git a/2020/graph.py b/2020/graph.py index da2933b..f9b1ca1 100644 --- a/2020/graph.py +++ b/2020/graph.py @@ -455,24 +455,23 @@ def ford_fulkerson(self, start, end): :param Any start: The start vertex to consider :param Any end: The target/end vertex to consider - :return: True when the end vertex is found, False otherwise + :return: The maximum flow """ - if start not in vertices: - return ValueError("Source not in graph") - if end not in vertices: - return ValueError("End not in graph") + if start not in self.vertices: + raise ValueError("Source not in graph") + if end not in self.vertices: + raise ValueError("End not in graph") if end not in self.edges: self.edges[end] = {} - initial_edges = {a: graph.edges[a].copy() for a in graph.edges} - self.flow_graph = {a: graph.edges[a].copy() for a in graph.edges} + initial_edges = {a: self.edges[a].copy() for a in self.edges} + self.flow_graph = {a: self.edges[a].copy() for a in self.edges} max_flow = 0 frontier = [start] heapq.heapify(frontier) - print(self.edges) while self.breadth_first_search(start, end): path_flow = float("Inf") @@ -506,3 +505,38 @@ def ford_fulkerson(self, start, end): self.edges = initial_edges return max_flow + + def bipartite_matching(self, starts, ends): + """ + Performs a bipartite matching using Fold-Fulkerson's algorithm + + :param iterable starts: A list of source vertices + :param iterable ends: A list of target vertices + :return: The maximum matches found + """ + + start_point = "A" + while start_point in self.vertices: + start_point += "A" + self.edges[start_point] = {} + self.vertices += start_point + for start in starts: + if start not in self.vertices: + return ValueError("Source not in graph") + self.edges[start_point].update({start: 1}) + + end_point = "Z" + while end_point in self.vertices: + end_point += "Z" + self.vertices.append(end_point) + for end in ends: + if end not in self.vertices: + return ValueError("End not in graph") + if end not in self.edges: + self.edges[end] = {} + self.edges[end].update({end_point: 1}) + + value = self.ford_fulkerson(start_point, end_point) + self.vertices.remove(end_point) + self.vertices.remove(start_point) + return value From bff2058e902beb404a8ce436a78de6bf8b69dd6c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 23 Dec 2020 20:57:05 +0100 Subject: [PATCH 108/143] Added second method for 2020-21 --- 2020/21-Allergen Assessment.py | 102 +++++++------------ 2020/21-Allergen Assessment.v1.py | 160 ++++++++++++++++++++++++++++++ 2 files changed, 196 insertions(+), 66 deletions(-) create mode 100644 2020/21-Allergen Assessment.v1.py diff --git a/2020/21-Allergen Assessment.py b/2020/21-Allergen Assessment.py index 9e290cc..4dd8c93 100644 --- a/2020/21-Allergen Assessment.py +++ b/2020/21-Allergen Assessment.py @@ -41,6 +41,15 @@ def words(s: str): "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], } +test += 1 +test_data[test] = { + "input": """mxmxvkd kfcds sqjhc nhms (contains dairy, fish) +trh fvjkl sbzzf mxmxvkd (contains dairy) +sqjhc fvjkl (contains soy) +sqjhc mxmxvkd sbzzf (contains fish)""", + "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], +} + test = "real" input_file = os.path.join( os.path.dirname(__file__), @@ -67,88 +76,49 @@ def words(s: str): # -------------------------------- Actual code execution ----------------------------- # -all_ingredients = defaultdict(int) -all_allergens = {} -nb_allergens = defaultdict(int) -allergens_ingredients = {} +all_allergens = set() +all_ingredients = {} +allergen_graph = graph.WeightedGraph() +allergen_graph.vertices = set() for string in puzzle_input.split("\n"): if "contains" in string: ingredients = string.split(" (")[0].split(" ") allergens = string.split("(contains ")[1][:-1].split(", ") - if isinstance(allergens, str): - allergens = [allergens] - for allergen in allergens: - nb_allergens[allergen] += 1 - if allergen not in all_allergens: - all_allergens[allergen] = ingredients.copy() - allergens_ingredients[allergen] = defaultdict(int) - allergens_ingredients[allergen].update( - {ingredient: 1 for ingredient in ingredients} - ) + all_allergens = all_allergens.union(allergens) + all_ingredients.update( + {ing: all_ingredients.get(ing, 0) + 1 for ing in ingredients} + ) + for allergen in allergens: + if allergen not in allergen_graph.edges: + allergen_graph.edges[allergen] = {x: 1 for x in ingredients} else: - for ingredient in ingredients: - allergens_ingredients[allergen][ingredient] += 1 - for ingredient in all_allergens[allergen].copy(): - if ingredient not in ingredients: - all_allergens[allergen].remove(ingredient) - - for ingredient in ingredients: - all_ingredients[ingredient] += 1 + for ing in allergen_graph.edges[allergen].copy(): + if ing not in ingredients: + del allergen_graph.edges[allergen][ing] else: print("does not contain any allergen") - -for allergen in test: - if allergen != "shellfish": - continue - print( - allergen, - test2[allergen], - [ing for ing, val in test[allergen].items() if val == test2[allergen]], - ) - -sum_ingredients = 0 -for ingredient in all_ingredients: - if not (any(ingredient in val for val in all_allergens.values())): - sum_ingredients += all_ingredients[ingredient] +allergen_graph.vertices = list(all_allergens.union(set(all_ingredients.keys()))) +allergen_graph.bipartite_matching(all_allergens, all_ingredients) if part_to_test == 1: - puzzle_actual_result = sum_ingredients - + safe_ingredients = [ + x for x in allergen_graph.vertices if allergen_graph.flow_graph[x] == {} + ] + safe_number = sum(all_ingredients[x] for x in safe_ingredients) + puzzle_actual_result = safe_number else: - allergens_ingredients = { - aller: [ - ing - for ing, val in allergens_ingredients[aller].items() - if val == nb_allergens[aller] - ] - for aller in nb_allergens - } - final_allergen = {} - while len(final_allergen) != len(nb_allergens): - for allergen, val in allergens_ingredients.items(): - if len(val) == 1: - final_allergen[allergen] = val[0] - - allergens_ingredients = { - aller: [ - ing - for ing in allergens_ingredients[aller] - if ing not in final_allergen.values() - ] - for aller in nb_allergens - } - - print(final_allergen) - ing_list = "" - for aller in sorted(final_allergen.keys()): - ing_list += final_allergen[aller] + "," - puzzle_actual_result = ing_list[:-1] + dangerous_ingredients = [ + list(allergen_graph.flow_graph[aller].keys())[0] + for aller in sorted(all_allergens) + ] + puzzle_actual_result = ",".join(dangerous_ingredients) + # -------------------------------- Outputs / results --------------------------------- # diff --git a/2020/21-Allergen Assessment.v1.py b/2020/21-Allergen Assessment.v1.py new file mode 100644 index 0000000..9e290cc --- /dev/null +++ b/2020/21-Allergen Assessment.v1.py @@ -0,0 +1,160 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """mxmxvkd kfcds sqjhc nhms (contains dairy, fish) +trh fvjkl sbzzf mxmxvkd (contains dairy) +sqjhc fvjkl (contains soy) +sqjhc mxmxvkd sbzzf (contains fish)""", + "expected": ["5", "mxmxvkd,sqjhc,fvjkl"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["2410", "tmp,pdpgm,cdslv,zrvtg,ttkn,mkpmkx,vxzpfp,flnhl"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +all_ingredients = defaultdict(int) +all_allergens = {} +nb_allergens = defaultdict(int) +allergens_ingredients = {} + +for string in puzzle_input.split("\n"): + if "contains" in string: + ingredients = string.split(" (")[0].split(" ") + allergens = string.split("(contains ")[1][:-1].split(", ") + if isinstance(allergens, str): + allergens = [allergens] + + for allergen in allergens: + nb_allergens[allergen] += 1 + if allergen not in all_allergens: + all_allergens[allergen] = ingredients.copy() + allergens_ingredients[allergen] = defaultdict(int) + allergens_ingredients[allergen].update( + {ingredient: 1 for ingredient in ingredients} + ) + + else: + for ingredient in ingredients: + allergens_ingredients[allergen][ingredient] += 1 + for ingredient in all_allergens[allergen].copy(): + if ingredient not in ingredients: + all_allergens[allergen].remove(ingredient) + + for ingredient in ingredients: + all_ingredients[ingredient] += 1 + + else: + print("does not contain any allergen") + + +for allergen in test: + if allergen != "shellfish": + continue + print( + allergen, + test2[allergen], + [ing for ing, val in test[allergen].items() if val == test2[allergen]], + ) + +sum_ingredients = 0 +for ingredient in all_ingredients: + if not (any(ingredient in val for val in all_allergens.values())): + sum_ingredients += all_ingredients[ingredient] + +if part_to_test == 1: + puzzle_actual_result = sum_ingredients + + +else: + allergens_ingredients = { + aller: [ + ing + for ing, val in allergens_ingredients[aller].items() + if val == nb_allergens[aller] + ] + for aller in nb_allergens + } + final_allergen = {} + while len(final_allergen) != len(nb_allergens): + for allergen, val in allergens_ingredients.items(): + if len(val) == 1: + final_allergen[allergen] = val[0] + + allergens_ingredients = { + aller: [ + ing + for ing in allergens_ingredients[aller] + if ing not in final_allergen.values() + ] + for aller in nb_allergens + } + + print(final_allergen) + ing_list = "" + for aller in sorted(final_allergen.keys()): + ing_list += final_allergen[aller] + "," + puzzle_actual_result = ing_list[:-1] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-21 06:07:34.505688 +# Part 1: 2020-12-21 07:22:36 +# Part 2: 2020-12-21 07:30:15 From 08478c1fa0432add44d2a954b48936f1d9891125 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 24 Dec 2020 07:14:08 +0100 Subject: [PATCH 109/143] Added Hex grid compass --- 2020/compass.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/2020/compass.py b/2020/compass.py index 041a2c5..e144fab 100644 --- a/2020/compass.py +++ b/2020/compass.py @@ -33,3 +33,24 @@ "ahead": 1, "back": -1, } + + +class hexcompass: + west = -1 + east = 1 + northeast = 0.5 + 1j + northwest = -0.5 + 1j + southeast = 0.5 - 1j + southwest = -0.5 - 1j + + all_directions = [northwest, southwest, west, northeast, southeast, east] + + text_to_direction = { + "E": east, + "W": west, + "NW": northwest, + "NE": northeast, + "SE": southeast, + "SW": southwest, + } + direction_to_text = {text_to_direction[x]: x for x in text_to_direction} From 7a12b4ca7cf5d771d3a558214941bb2d943c9bac Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Thu, 24 Dec 2020 07:14:20 +0100 Subject: [PATCH 110/143] Added day 2020-24 --- 2020/24-Lobby Layout.py | 163 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 2020/24-Lobby Layout.py diff --git a/2020/24-Lobby Layout.py b/2020/24-Lobby Layout.py new file mode 100644 index 0000000..8693370 --- /dev/null +++ b/2020/24-Lobby Layout.py @@ -0,0 +1,163 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """sesenwnenenewseeswwswswwnenewsewsw +neeenesenwnwwswnenewnwwsewnenwseswesw +seswneswswsenwwnwse +nwnwneseeswswnenewneswwnewseswneseene +swweswneswnenwsewnwneneseenw +eesenwseswswnenwswnwnwsewwnwsene +sewnenenenesenwsewnenwwwse +wenwwweseeeweswwwnwwe +wsweesenenewnwwnwsenewsenwwsesesenwne +neeswseenwwswnwswswnw +nenwswwsewswnenenewsenwsenwnesesenew +enewnwewneswsewnwswenweswnenwsenwsw +sweneswneswneneenwnewenewwneswswnese +swwesenesewenwneswnwwneseswwne +enesenwswwswneneswsenwnewswseenwsese +wnwnesenesenenwwnenwsewesewsesesew +nenewswnwewswnenesenwnesewesw +eneswnwswnwsenenwnwnwwseeswneewsenese +neswnwewnwnwseenwseesewsenwsweewe +wseweeenwnesenwwwswnew""", + "expected": ["10", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["538", "4259"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # +west = -1 +east = 1 +northeast = 0.5 + 1j +northwest = -0.5 + 1j +southeast = 0.5 - 1j +southwest = -0.5 - 1j + +text_to_direction = { + "e": east, + "w": west, + "nw": northwest, + "ne": northeast, + "se": southeast, + "sw": southwest, +} +direction_to_text = {text_to_direction[x]: x for x in text_to_direction} + +relative_directions = { + "left": 1j, + "right": -1j, + "ahead": 1, + "back": -1, +} + + +def neighbors(tile): + return [tile + direction for direction in all_directions] + + +all_directions = [northeast, northwest, west, east, southeast, southwest] + +tiles = defaultdict(int) + +for string in puzzle_input.split("\n"): + i = 0 + position = 0 + while i < len(string): + if string[i] in ("n", "s"): + direction = string[i : i + 2] + i += 2 + else: + direction = string[i] + i += 1 + position += text_to_direction[direction] + + if position in tiles: + tiles[position] = 1 - tiles[position] + else: + tiles[position] = 1 + +if part_to_test == 1: + puzzle_actual_result = sum(tiles.values()) + + +else: + for day in range(1, 100 + 1): + all_tiles_to_check = set([x for tile in tiles for x in neighbors(tile)]).union( + set(tiles.keys()) + ) + new_tiles = defaultdict(int) + for tile in all_tiles_to_check: + black_neighbors = sum(tiles[neighbor] for neighbor in neighbors(tile)) + + if not tiles[tile] and black_neighbors == 2: + new_tiles[tile] = 1 + elif tiles[tile] and black_neighbors in (1, 2): + new_tiles[tile] = 1 + + tiles = new_tiles.copy() + puzzle_actual_result = sum(tiles.values()) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-24 06:11:40.071704 +# Part 1: 2020-12-24 06:21:59 +# Part 2: 2020-12-24 07:07:55 From 7675ea539cbb85f28892ca30341c95a060a1e86d Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 25 Dec 2020 07:04:46 +0100 Subject: [PATCH 111/143] Added day 2020-25 --- 2020/25-Combo Breaker.py | 107 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 2020/25-Combo Breaker.py diff --git a/2020/25-Combo Breaker.py b/2020/25-Combo Breaker.py new file mode 100644 index 0000000..0eec7a8 --- /dev/null +++ b/2020/25-Combo Breaker.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """5764801 +17807724""", + "expected": ["14897079", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["18293391", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +if part_to_test == 1: + card_public_key, door_public_key = ints(puzzle_input) + + number = 1 + i = 1 + card_loop_size = 0 + door_loop_size = 0 + while True: + number *= 7 + number %= 20201227 + + if number == card_public_key: + card_loop_size = i + elif number == door_public_key: + door_loop_size = i + + if card_loop_size != 0 and door_loop_size != 0: + break + i += 1 + + # #print (card_loop_size) + # #print (door_loop_size) + + number = 1 + for i in range(door_loop_size): + number *= card_public_key + number %= 20201227 + encryption_key = number + + puzzle_actual_result = encryption_key + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-25 06:00:01.023157 +# Part 1: 2020-12-25 06:17:12 +# Part 2: 2020-12-25 06:17:23 From 55681fc7e545df1dbe14203020ebf40ea7fe76d2 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:31:24 +0100 Subject: [PATCH 112/143] Added 2021-01, 2021-02, 2021-03 --- 2021/01-Sonar Sweep.py | 107 +++++++ 2021/02-Dive.py | 110 +++++++ 2021/03-Binary Diagnostic.py | 154 ++++++++++ 2021/assembly.py | 546 +++++++++++++++++++++++++++++++++++ 2021/compass.py | 56 ++++ 2021/dot.py | 222 ++++++++++++++ 2021/doubly_linked_list.py | 222 ++++++++++++++ 2021/graph.py | 542 ++++++++++++++++++++++++++++++++++ 2021/grid.py | 508 ++++++++++++++++++++++++++++++++ 9 files changed, 2467 insertions(+) create mode 100644 2021/01-Sonar Sweep.py create mode 100644 2021/02-Dive.py create mode 100644 2021/03-Binary Diagnostic.py create mode 100644 2021/assembly.py create mode 100644 2021/compass.py create mode 100644 2021/dot.py create mode 100644 2021/doubly_linked_list.py create mode 100644 2021/graph.py create mode 100644 2021/grid.py diff --git a/2021/01-Sonar Sweep.py b/2021/01-Sonar Sweep.py new file mode 100644 index 0000000..debbe3b --- /dev/null +++ b/2021/01-Sonar Sweep.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """199 +200 +208 +210 +200 +207 +240 +269 +260 +263""", + "expected": ["7", "5"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1766", "1797"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + val = ints(puzzle_input) + puzzle_actual_result = sum( + [1 if val[n] > val[n - 1] else 0 for n in range(1, len(val))] + ) + + +else: + val = ints(puzzle_input) + puzzle_actual_result = sum( + [ + 1 if sum(val[n - 2 : n + 1]) > sum(val[n - 3 : n]) else 0 + for n in range(3, len(val)) + ] + ) + # puzzle_actual_result = [(sum(val[n-2:n+1]) , sum(val[n-3:n])) for n in range(3, len(val))] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-01 08:11:26.495595 +# Part 1: 2021-12-01 08:15:45 +# Part 2: 2021-12-01 08:20:37 diff --git a/2021/02-Dive.py b/2021/02-Dive.py new file mode 100644 index 0000000..43b8ada --- /dev/null +++ b/2021/02-Dive.py @@ -0,0 +1,110 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """forward 5 +down 5 +forward 8 +up 3 +down 8 +forward 2""", + "expected": ["150", "900"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1962940", "1813664422"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + +dirs = {"forward": 1, "down": -1j, "up": +1j} + +position = 0 +aim = 0 +if part_to_test == 1: + for string in puzzle_input.split("\n"): + direction, delta = string.split(" ") + position += dirs[direction] * int(delta) + + puzzle_actual_result = int(abs(position.imag) * abs(position.real)) + + +else: + for string in puzzle_input.split("\n"): + direction, delta = string.split(" ") + if direction == "down" or direction == "up": + aim += dirs[direction] * int(delta) + else: + position += int(delta) + position += int(delta) * abs(aim.imag) * 1j + + print(string, aim, position) + + puzzle_actual_result = int(abs(position.imag) * abs(position.real)) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-02 07:43:32.238803 +# Part 1: 2021-12-02 07:46:00 +# Part 2: 2021-12-02 07:50:10 diff --git a/2021/03-Binary Diagnostic.py b/2021/03-Binary Diagnostic.py new file mode 100644 index 0000000..e016635 --- /dev/null +++ b/2021/03-Binary Diagnostic.py @@ -0,0 +1,154 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """00100 +11110 +10110 +10111 +10101 +01111 +00111 +11100 +10000 +11001 +00010 +01010""", + "expected": ["198", "230"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["3985686", "2555739"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +length_binary = len(puzzle_input.split("\n")[0]) + +gamma = [0] * length_binary +epsilon = [0] * length_binary +counts = [0] * length_binary + + +def count_binary(source): + zero = [0] * len(source[0]) + ones = [0] * len(source[0]) + for string in source: + for i in range(length_binary): + zero[i] += 1 - int(string[i]) + ones[i] += int(string[i]) + + return (zero, ones) + + +if part_to_test == 1: + for string in puzzle_input.split("\n"): + for i in range(length_binary): + counts[i] += int(string[i]) + + for i in range(length_binary): + if counts[i] >= len(puzzle_input.split("\n")) // 2: + gamma[i] = 1 + else: + epsilon[i] = 1 + + gamma = int("".join(map(str, gamma)), 2) + epsilon = int("".join(map(str, epsilon)), 2) + + puzzle_actual_result = (gamma, epsilon, gamma * epsilon)[2] + + +else: + oxygen = puzzle_input.split("\n") + co2 = puzzle_input.split("\n") + + for i in range(length_binary): + if len(oxygen) != 1: + zero, ones = count_binary(oxygen) + + if ones[i] >= zero[i]: + oxygen = [n for n in oxygen if int(n[i]) == 1] + else: + oxygen = [n for n in oxygen if int(n[i]) == 0] + + if len(co2) != 1: + zero, ones = count_binary(co2) + if ones[i] >= zero[i]: + co2 = [n for n in co2 if int(n[i]) == 0] + else: + co2 = [n for n in co2 if int(n[i]) == 1] + + if len(oxygen) != 1 or len(co2) != 1: + print("error") + + oxygen = int("".join(map(str, oxygen)), 2) + co2 = int("".join(map(str, co2)), 2) + + puzzle_actual_result = (oxygen, co2, oxygen * co2)[2] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-03 08:08:06.750713 +# Part 1: 2021-12-03 08:14:39 +# Part 2: 2021-12-03 08:25:28 diff --git a/2021/assembly.py b/2021/assembly.py new file mode 100644 index 0000000..a07534f --- /dev/null +++ b/2021/assembly.py @@ -0,0 +1,546 @@ +import json + +# -------------------------------- Notes ----------------------------- # + + +# This program will run pseudo-assembly code based on provided instructions +# It can handle a set of instructions (which are writable), a stack and registers + + +# -------------------------------- Program flow exceptions ----------------------------- # + + +class MissingInput(RuntimeError): + pass + + +class ProgramHalt(RuntimeError): + pass + + +# -------------------------------- Main program class ----------------------------- # +class Program: + + # Whether to print outputs + print_output = False + # Print outputs in a detailed way (useful when debugging is detailed) + print_output_verbose = False + # Print outputs when input is required (useful for text-based games) + print_output_before_input = False + + # Whether to print the inputs received (useful for predefined inputs) + print_input = False + # Print inputs in a detailed way (useful when debugging is detailed) + print_input_verbose = False + + # Whether to print the instructions before execution + print_details_before = False + # Whether to print the instructions after execution + print_details_after = False + + # Output format - for all instructions + print_format = "{pointer:5}-{opcode:15} {instr:50} - R: {registers} - Stack ({stack_len:4}): {stack}" + # Output format for numbers + print_format_numbers = "{val:5}" + + # Whether inputs and outputs are ASCII codes or not + input_ascii = True + output_ascii = True + + # Whether to ask user for input or not (if not, will raise exception) + input_from_terminal = True + + # Bit length used for NOT operation (bitwise inverse) + bit_length = 15 + + # Where to store saves + save_data_file = "save.txt" + + # Maximum number of instructions executed + max_instructions = 10 ** 7 + + # Sets up the program based on the provided instructions + def __init__(self, program): + self.instructions = program.copy() + self.registers = [0] * 8 + self.stack = [] + self.pointer = 0 + self.state = "Running" + self.output = [] + self.input = [] + self.instructions_done = 0 + + ################### Main program body ################### + + def run(self): + while ( + self.state == "Running" and self.instructions_done < self.max_instructions + ): + self.instructions_done += 1 + # Get details of current operation + opcode = self.instructions[self.pointer] + current_instr = self.get_instruction(opcode) + + # Outputs operation details before its execution + if self.print_details_before: + self.print_operation(opcode, current_instr) + + self.operation_codes[opcode][2](self, current_instr) + + # Outputs operation details after its execution + if self.print_details_after: + self.print_operation(opcode, self.get_instruction(opcode)) + + # Moves the pointer + if opcode not in self.operation_jumps and self.state == "Running": + self.pointer += self.operation_codes[opcode][1] + + print("instructions", i) + + # Gets all parameters for the current instruction + def get_instruction(self, opcode): + args_order = self.operation_codes[opcode][3] + values = [opcode] + [ + self.instructions[self.pointer + order + 1] for order in args_order + ] + print([self.pointer + order + 1 for order in args_order]) + + print(args_order, values, self.operation_codes[opcode]) + + return values + + # Prints the details of an operation according to the specified format + def print_operation(self, opcode, instr): + params = instr.copy() + # Remove opcode + del params[0] + + # Handle stack operations + if opcode in self.operation_stack and self.stack: + params.append(self.stack[-1]) + elif opcode in self.operation_stack: + params.append("Empty") + + # Format the numbers + params = list(map(self.format_numbers, params)) + + data = {} + data["opcode"] = opcode + data["pointer"] = self.pointer + data["registers"] = ",".join(map(self.format_numbers, self.registers)) + data["stack"] = ",".join(map(self.format_numbers, self.stack)) + data["stack_len"] = len(self.stack) + + instr_output = self.operation_codes[opcode][0].format(*params, **data) + final_output = self.print_format.format(instr=instr_output, **data) + print(final_output) + + # Outputs all stored data and resets it + def print_output_data(self): + if self.output and self.print_output_before_input: + if self.output_ascii: + print("".join(self.output), sep="", end="") + else: + print(self.output, end="") + self.output = [] + + # Formats numbers + def format_numbers(self, code): + return self.print_format_numbers.format(val=code) + + # Sets a log level based on predefined rules + def log_level(self, level): + self.print_output = False + self.print_output_verbose = False + self.print_output_before_input = False + + self.print_input = False + self.print_input_verbose = False + + self.print_details_before = False + self.print_details_after = False + + if level >= 1: + self.print_output = True + self.print_input = True + + if level >= 2: + self.print_output_verbose = True + self.print_output_before_input = True + self.print_input_verbose = True + self.print_details_before = True + + if level >= 3: + self.print_details_after = True + + ################### Get and set registers and memory ################### + + # Reads a "normal" value based on the provided reference + def get_register(self, reference): + return self.registers[reference] + + # Writes a value to a register + def set_register(self, reference, value): + self.registers[reference] = value + + # Reads a memory value based on the code + def get_memory(self, code): + return self.instructions[code] + + # Writes a value to the memory + def set_memory(self, reference, value): + self.instructions[reference] = value + + ################### Start / Stop the program ################### + + # halt: Stop execution and terminate the program + def op_halt(self, instr): + self.state = "Stopped" + raise ProgramHalt("Reached Halt instruction") + + # pass 21: No operation + def op_pass(self, instr): + return + + ################### Basic operations ################### + + # add a b c: Assign into the sum of and ", + def op_add(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) + self.get_register(instr[3]) + ) + + # mult a b c: store into the product of and ", + def op_multiply(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) * self.get_register(instr[3]) + ) + + # mod a b c: store into the remainder of divided by ", + def op_modulo(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) % self.get_register(instr[3]) + ) + + # set a b: set register to the value of + def op_set(self, instr): + self.set_register(instr[1], self.get_register(instr[2])) + + ################### Comparisons ################### + + # eq a b c: set to 1 if is equal to ; set it to 0 otherwise", + def op_equal(self, instr): + self.set_register( + instr[1], + 1 if self.get_register(instr[2]) == self.get_register(instr[3]) else 0, + ) + + # gt a b c: set to 1 if is greater than ; set it to 0 otherwise", + def op_greater_than(self, instr): + self.set_register( + instr[1], + 1 if self.get_register(instr[2]) > self.get_register(instr[3]) else 0, + ) + + ################### Binary operations ################### + + # and a b c: stores into the bitwise and of and ", + def op_and(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) & self.get_register(instr[3]) + ) + + # or a b c: stores into the bitwise or of and ", + def op_or(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) | self.get_register(instr[3]) + ) + + # not a b: stores 15-bit bitwise inverse of in ", + def op_not(self, instr): + self.set_register( + instr[1], ~self.get_register(instr[2]) & int("1" * self.bit_length, 2) + ) + + ################### Jumps ################### + + # jmp a: jump to ", + def op_jump(self, instr): + self.pointer = self.get_register(instr[1]) + + # jt a b: if is nonzero, jump to ", + def op_jump_if_true(self, instr): + self.pointer = ( + self.get_register(instr[2]) + if self.get_register(instr[1]) != 0 + else self.pointer + self.operation_codes["jump_if_true"][1] + ) + + # jf a b: if is zero, jump to ", + def op_jump_if_false(self, instr): + self.pointer = ( + self.get_register(instr[2]) + if self.get_register(instr[1]) == 0 + else self.pointer + self.operation_codes["jump_if_false"][1] + ) + + ################### Memory-related operations ################### + + # rmem a b: read memory at address and write it to ", + def op_read_memory(self, instr): + self.set_register(instr[1], self.get_memory(self.get_register(instr[2]))) + + # wmem a b: write the value from into memory at address ", + def op_write_memory(self, instr): + self.set_memory(self.get_register(instr[1]), self.get_register(instr[2])) + + ################### Stack-related operations ################### + + # push a: push onto the stack", + def op_push(self, instr): + self.stack.append(self.get_register(instr[1])) + + # pop a: remove the top element from the stack and write it into ; empty stack = error", + def op_pop(self, instr): + if not self.stack: + self.state = "Error" + else: + self.set_register(instr[1], self.stack.pop()) + + # ret: remove the top element from the stack and jump to it; empty stack = halt", + def op_jump_to_stack(self, instr): + if not self.stack: + raise RuntimeError("No stack available for jump") + else: + self.pointer = self.stack.pop() + + ################### Input and output ################### + + # in a: read a character from the terminal and write its ascii code to + def op_input(self, instr): + self.print_output_data() + + self.custom_commands() + while not self.input: + if self.input_from_terminal: + self.add_input(input() + "\n") + else: + raise MissingInput() + + if self.input[0] == "?": + self.custom_commands() + + letter = self.input.pop(0) + + # Print what we received? + if self.print_input_verbose: + print(" Input: ", letter) + elif self.print_input: + print(letter, end="") + + # Actually write the input to the registers + if self.input_ascii: + self.set_register(instr[1], ord(letter)) + else: + self.set_register(instr[1], letter) + + # out a: write the character represented by ascii code to the terminal", + def op_output(self, instr): + # Determine what to output + if self.output_ascii: + letter = chr(self.get_register(instr[1])) + else: + letter = self.get_register(instr[1]) + + # Store for future use + self.output += letter + + # Display output immediatly? + if self.print_output_verbose: + print(" Output:", letter) + elif self.print_output: + print(letter, end="") + + ################### Save and restore ################### + + def save_state(self): + data = [ + self.instructions, + self.registers, + self.stack, + self.pointer, + self.state, + self.output, + self.input, + ] + with open(self.save_data_file, "w") as f: + json.dump(data, f) + + def restore_state(self): + with open(self.save_data_file, "r") as f: + data = json.load(f) + + ( + self.instructions, + self.registers, + self.stack, + self.pointer, + self.state, + self.output, + self.input, + ) = data + + ################### Adding manual inputs ################### + + def add_input(self, input_data, convert_ascii=True): + try: + self.input += input_data + except TypeError: + self.input.append(input_data) + + ################### Custom commands ################### + + # Pause until input provided + def custom_pause(self, instr): + print("Program paused. Press Enter to continue.") + input() + + # Pause until input provided + def custom_stop(self, instr): + self.op_halt(instr) + + # Save + def custom_save(self, instr): + self.save_state() + if self.print_output: + print("\nSaved game.") + + # Restore + def custom_restore(self, instr): + self.restore_state() + if self.print_output: + print("\nRestored the game.") + + # set a b: set register to the value of + def custom_write(self, instr): + self.op_set([instr[0]] + list(map(int, instr[1:]))) + + # log a: sets the log level to X + def custom_log(self, instr): + self.log_level(int(instr[1])) + if self.print_output: + print("\nChanged log level to", instr[1]) + + # print: prints the current situation in a detailed way + def custom_print(self, instr): + self.print_operation("?print", instr) + + def custom_commands(self): + while self.input and self.input[0] == "?": + command = self.input.pop(0) + while command[-1] != "\n" and self.input: + command += self.input.pop(0) + + if self.print_input: + print(command) + + command = command.replace("\n", "").split(" ") + self.operation_codes[command[0]][2](self, command) + + # ADDING NEW INSTRUCTIONS + # - Create a method with a name starting by op_ + # Its signature must be: op_X (self, instr) + # instr contains the list of values relevant to this operation (raw data from instructions set) + # - Reference this method in the variable operation_codes + # Format of the variable: + # operation code: [ + # debug formatting (used by str.format) + # number of operands (including the operation code) + # method to call + # argument order] ==> [2, 0, 1] means arguments are in provided as c, a, b + # - Include it in operation_jumps or operation_stack if relevant + + # ADDING CUSTOM INSTRUCTIONS + # Those instructions are not interpreted by the run() method + # Therefore: + # - They will NOT move the pointer + # - They will NOT impact the program (unless you make them do so) + # They're processed through the op_input method + # Custom operations are also referenced in the same operation_codes variable + # Custom operations start with ? for easy identification during input processing + + # TL;DR: Format: + # operation code: [ + # debug formatting + # number of operands (including the operation code) + # method to call + # argument order] + operation_codes = { + # Start / Stop + 0: ["halt", 1, op_halt, []], + 21: ["pass", 1, op_pass, []], + # Basic operations + 9: ["add: {0} = {1}+{2}", 4, op_add, [2, 0, 1]], # This means c = a + b + 10: ["mult: {0} = {1}*{2}", 4, op_multiply, [0, 1, 2]], + 11: ["mod: {0} = {1}%{2}", 4, op_modulo, [0, 1, 2]], + 1: ["set: {0} = {1}", 3, op_set, [0, 1]], + # Comparisons + 4: ["eq: {0} = {1} == {2}", 4, op_equal, [0, 1, 2]], + 5: ["gt: {0} = ({1} > {2})", 4, op_greater_than, [0, 1, 2]], + # Binary operations + 12: ["and: {0} = {1}&{2}", 4, op_and, [0, 1, 2]], + 13: ["or: {0} = {1}|{2}", 4, op_or, [0, 1, 2]], + 14: ["not: {0} = ~{1}", 3, op_not, [0, 1]], + # Jumps + 6: ["jump: go to {0}", 2, op_jump, [0]], + 7: ["jump if yes: go to {1} if {0}", 3, op_jump_if_true, [0, 1]], + 8: ["jump if no: go to {1} if !{0}", 3, op_jump_if_false, [0, 1]], + # Memory-related operations + 15: ["rmem: {0} = M{1}", 3, op_read_memory, [0, 1]], + 16: ["wmem: write {1} to M{0}", 3, op_write_memory, [0, 1]], + # Stack-related operations + 2: ["push: stack += {0}", 2, op_push, [0]], + 3: ["pop: {0} = stack.pop() ({1})", 2, op_pop, [0]], + 18: ["pop & jump: jump to stack.pop() ({0})", 2, op_jump_to_stack, []], + # Inputs and outputs + 19: ["out: print {0}", 2, op_output, [0]], + 20: ["in: {0} = input", 2, op_input, [0]], + # Custom operations + "?save": ["Saved data", 2, custom_save, []], + "?write": ["Wrote data", 3, custom_write, []], + "?restore": ["Restored data", 2, custom_restore, []], + "?log": ["Logging enabled", 2, custom_log, []], + "?stop": ["STOP", 2, custom_stop, []], + "?pause": ["Pause", 2, custom_pause, []], + "?print": ["Print data", 1, custom_print, []], + } + # Operations in this list will not move the pointer through the run method + # (this is because they do it themselves) + operation_jumps = ["jump", "jump_if_true", "jump_if_false", "jump_to_stack"] + # Operations in this list use the stack + # (the value taken from stack will be added to debug) + operation_stack = ["pop", "jump_to_stack"] + + +# -------------------------------- Documentation & main variables ----------------------------- # + +# HOW TO MAKE IT WORK +# The program has a set of possible instructions +# The exact list is available in variable operation_codes +# In order to work, you must modify this variable operation_codes so that the key is the code in your computer + +# If you need to override the existing methods, you need to override operation_codes + + +# NOT OPERATION +# This will perform a bitwise inverse +# However, it requires the length (in bits) specific to the program's hardware +# Therefore, update Program.bit_length +# TL;DR: Length in bits used for NOT +Program.bit_length = 15 + +# Save file (stored as JSON) +Program.save_data_file = "save.txt" + +# Maximum instructions to be executed +Program.max_instructions = 10 ** 7 diff --git a/2021/compass.py b/2021/compass.py new file mode 100644 index 0000000..e144fab --- /dev/null +++ b/2021/compass.py @@ -0,0 +1,56 @@ +north = 1j +south = -1j +west = -1 +east = 1 +northeast = 1 + 1j +northwest = -1 + 1j +southeast = 1 - 1j +southwest = -1 - 1j + +directions_straight = [north, south, west, east] +directions_diagonals = directions_straight + [ + northeast, + northwest, + southeast, + southwest, +] + +text_to_direction = { + "N": north, + "S": south, + "E": east, + "W": west, + "NW": northwest, + "NE": northeast, + "SE": southeast, + "SW": southwest, +} +direction_to_text = {text_to_direction[x]: x for x in text_to_direction} + +relative_directions = { + "left": 1j, + "right": -1j, + "ahead": 1, + "back": -1, +} + + +class hexcompass: + west = -1 + east = 1 + northeast = 0.5 + 1j + northwest = -0.5 + 1j + southeast = 0.5 - 1j + southwest = -0.5 - 1j + + all_directions = [northwest, southwest, west, northeast, southeast, east] + + text_to_direction = { + "E": east, + "W": west, + "NW": northwest, + "NE": northeast, + "SE": southeast, + "SW": southwest, + } + direction_to_text = {text_to_direction[x]: x for x in text_to_direction} diff --git a/2021/dot.py b/2021/dot.py new file mode 100644 index 0000000..dd7666f --- /dev/null +++ b/2021/dot.py @@ -0,0 +1,222 @@ +from compass import * +import math + + +def get_dot_position(element): + if isinstance(element, Dot): + return element.position + else: + return element + + +# Defines all directions that can be used (basically, are diagonals allowed?) +all_directions = directions_straight + + +class Dot: + # The first level is the actual terrain + # The second level is, in order: is_walkable, is_waypoint + # Walkable means you can get on that dot and leave it + # Waypoints are just cool points (it's meant for reducting the grid to a smaller graph) + # Isotropic means the direction doesn't matter + terrain_map = { + ".": [True, False], + "#": [False, False], + " ": [False, False], + "^": [True, True], + "v": [True, True], + ">": [True, True], + "<": [True, True], + "+": [True, False], + "|": [True, False], + "-": [True, False], + "/": [True, False], + "\\": [True, False], + "X": [True, True], + } + terrain_default = "X" + + # Override for printing + terrain_print = { + "^": "|", + "v": "|", + ">": "-", + "<": "-", + } + + # Defines which directions are allowed + # The first level is the actual terrain + # The second level is the direction taken to reach the dot + # The third level are the directions allowed to leave it + allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, + } + # This has the same format, except the third level has only 1 option + # Anisotropic grids allow only 1 direction for each (position, source_direction) + # Target direction is the direction in which I'm going + allowed_anisotropic_direction_map = { + ".": {dir: [-dir] for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: [-dir] for dir in all_directions}, + "|": {north: [south], south: [north]}, + "^": {north: [south], south: [north]}, + "v": {north: [south], south: [north]}, + "-": {east: [west], west: [east]}, + ">": {east: [west], west: [east]}, + "<": {east: [west], west: [east]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: [-dir] for dir in all_directions}, + } + # Default allowed directions + direction_default = all_directions + + # How to sort those dots + sorting_map = { + "xy": lambda self, a: (a.real, a.imag), + "yx": lambda self, a: (a.imag, a.real), + "reading": lambda self, a: (-a.imag, a.real), + "manhattan": lambda self, a: (abs(a.real) + abs(a.imag)), + "*": lambda self, a: (a.imag ** 2 + a.real ** 2) ** 0.5, + } + sort_value = sorting_map["*"] + + def __init__(self, grid, position, terrain, source_direction=None): + self.position = position + self.grid = grid + self.set_terrain(terrain) + self.neighbors = {} + if self.grid.is_isotropic: + self.set_directions() + else: + if source_direction: + self.source_direction = source_direction + self.set_directions() + else: + raise ValueError("Anisotropic dots need a source direction") + + self.neighbors_obsolete = True + + # Those functions allow sorting for various purposes + def __lt__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) < self.sort_value(ref) + + def __le__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) <= self.sort_value(ref) + + def __gt__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) > self.sort_value(ref) + + def __ge__(self, other): + ref = get_dot_position(other) + return self.sort_value(self.position) >= self.sort_value(ref) + + def __repr__(self): + if self.grid.is_isotropic: + return self.terrain + "@" + complex(self.position).__str__() + else: + return ( + self.terrain + + "@" + + complex(self.position).__str__() + + direction_to_text[self.source_direction] + ) + + def __str__(self): + return self.terrain + + def __add__(self, direction): + if not direction in self.allowed_directions: + raise ValueError("Can't add a Dot with forbidden direction") + position = self.position + direction + if self.grid.is_isotropic: + return self.get_dot(position) + else: + # For the target dot, I'm coming from the opposite direction + return self.get_dot((position, -self.allowed_directions[0])) + + def __sub__(self, direction): + return self.__add__(-direction) + + def phase(self, reference=0): + ref = get_dot_position(reference) + return math.atan2(self.position.imag - ref.imag, self.position.real - ref.real) + + def amplitude(self, reference=0): + ref = get_dot_position(reference) + return ( + (self.position.imag - ref.imag) ** 2 + (self.position.real - ref.real) ** 2 + ) ** 0.5 + + def manhattan_distance(self, reference=0): + ref = get_dot_position(reference) + return abs(self.position.imag - ref.imag) + abs(self.position.real - ref.real) + + def set_terrain(self, terrain): + self.terrain = terrain or self.default_terrain + self.is_walkable, self.is_waypoint = self.terrain_map.get( + terrain, self.terrain_map[self.terrain_default] + ) + + def set_directions(self): + terrain = ( + self.terrain + if self.terrain in self.allowed_direction_map + else self.terrain_default + ) + if self.grid.is_isotropic: + self.allowed_directions = self.allowed_direction_map[terrain].copy() + else: + self.allowed_directions = self.allowed_anisotropic_direction_map[ + terrain + ].get(self.source_direction, []) + + def get_dot(self, dot): + return self.grid.dots.get(dot, None) + + def get_neighbors(self): + if self.neighbors_obsolete: + self.neighbors = { + self + direction: 1 + for direction in self.allowed_directions + if (self + direction) and (self + direction).is_walkable + } + + self.neighbors_obsolete = False + return self.neighbors + + def set_trap(self, is_trap): + self.grid.reset_pathfinding() + if is_trap: + self.allowed_directions = [] + self.neighbors = {} + self.neighbors_obsolete = False + else: + self.set_directions() + + def set_wall(self, is_wall): + self.grid.reset_pathfinding() + if is_wall: + self.allowed_directions = [] + self.neighbors = {} + self.neighbors_obsolete = False + self.is_walkable = False + else: + self.set_terrain(self.terrain) + self.set_directions() diff --git a/2021/doubly_linked_list.py b/2021/doubly_linked_list.py new file mode 100644 index 0000000..6bb667c --- /dev/null +++ b/2021/doubly_linked_list.py @@ -0,0 +1,222 @@ +class DoublyLinkedList: + def __init__(self, is_cycle=False): + """ + Creates a list + + :param Boolean is_cycle: Whether the list is a cycle (loops around itself) + """ + self.start_element = None + self.is_cycle = is_cycle + self.elements = {} + + def insert(self, ref_element, new_elements, insert_before=False): + """ + Inserts new elements in the list + + :param Any ref_element: The value of the element where we'll insert data + :param Any new_elements: A list of new elements to insert, or a single element + :param Boolean insert_before: If True, will insert before ref_element. + """ + new_elements_converted = [] + if isinstance(new_elements, (list, tuple, set)): + for i, element in enumerate(new_elements): + if not isinstance(element, DoublyLinkedListElement): + new_element_converted = DoublyLinkedListElement(element) + if i != 0: + new_element_converted.prev_element = new_elements_converted[ + i - 1 + ] + new_element_converted.prev_element.next_element = ( + new_element_converted + ) + else: + new_element_converted = element + if i != 0: + new_element_converted.prev_element = new_elements_converted[ + i - 1 + ] + new_element_converted.prev_element.next_element = ( + new_element_converted + ) + new_elements_converted.append(new_element_converted) + self.elements[new_element_converted.item] = new_element_converted + else: + if not isinstance(new_elements, DoublyLinkedListElement): + new_element_converted = DoublyLinkedListElement(new_elements) + else: + new_element_converted = new_elements + new_elements_converted.append(new_element_converted) + self.elements[new_element_converted.item] = new_element_converted + + if self.start_element == None: + self.start_element = new_elements_converted[0] + for pos, element in enumerate(new_elements_converted): + element.prev_element = new_elements_converted[pos - 1] + element.next_element = new_elements_converted[pos + 1] + + if not self.is_cycle: + new_elements_converted[0].prev_element = None + new_elements_converted[-1].next_element = None + else: + if isinstance(ref_element, DoublyLinkedListElement): + cursor = ref_element + else: + cursor = self.find(ref_element) + + if insert_before: + new_elements_converted[0].prev_element = cursor.prev_element + new_elements_converted[-1].next_element = cursor + + if cursor.prev_element is not None: + cursor.prev_element.next_element = new_elements_converted[0] + cursor.prev_element = new_elements_converted[-1] + if self.start_element == cursor: + self.start_element = new_elements_converted[0] + else: + new_elements_converted[0].prev_element = cursor + new_elements_converted[-1].next_element = cursor.next_element + if cursor.next_element is not None: + cursor.next_element.prev_element = new_elements_converted[-1] + cursor.next_element = new_elements_converted[0] + + def append(self, new_element): + """ + Appends an element in the list + + :param Any new_element: The new element to insert + :param Boolean insert_before: If True, will insert before ref_element. + """ + if not isinstance(new_element, DoublyLinkedListElement): + new_element = DoublyLinkedListElement(new_element) + + self.elements[new_element.item] = new_element + + if self.start_element is None: + self.start_element = new_element + if self.is_cycle: + new_element.next_element = new_element + new_element.prev_element = new_element + else: + if self.is_cycle: + cursor = self.start_element.prev_element + else: + cursor = self.start_element + while cursor.next_element is not None: + if self.is_cycle and cursor.next_element == self.start_element: + break + cursor = cursor.next_element + + new_element.prev_element = cursor + new_element.next_element = cursor.next_element + if cursor.next_element is not None: + cursor.next_element.prev_element = new_element + cursor.next_element = new_element + + def traverse(self, start, end=None): + """ + Gets items based on their values + + :param Any start: The start element + :param Any stop: The end element + """ + output = [] + if self.start_element is None: + return [] + + if not isinstance(start, DoublyLinkedListElement): + start = self.find(start) + cursor = start + + if not isinstance(end, DoublyLinkedListElement): + end = self.find(end) + + while cursor is not None: + if cursor == end: + break + + output.append(cursor) + + cursor = cursor.next_element + + if self.is_cycle and cursor == start: + break + + return output + + def delete_by_value(self, to_delete): + """ + Deletes a given element from the list + + :param Any to_delete: The element to delete + """ + output = [] + if self.start_element is None: + return + + cursor = to_delete + cursor.prev_element.next_element = cursor.next_element + cursor.next_element.prev_element = cursor.prev_element + + def delete_by_position(self, to_delete): + """ + Deletes a given element from the list + + :param Any to_delete: The element to delete + """ + output = [] + if self.start_element is None: + return + + if not isinstance(to_delete, int): + raise TypeError("Position must be an integer") + + cursor = self.start_element + i = -1 + while cursor is not None and i < to_delete: + i += 1 + if i == to_delete: + if cursor.prev_element: + cursor.prev_element.next_element = cursor.next_element + if cursor.next_element: + cursor.next_element.prev_element = cursor.prev_element + + if self.start_element == cursor: + self.start_element = cursor.next_element + + del cursor + return True + + raise ValueError("Element not in list") + + def find(self, needle): + """ + Finds a given item based on its value + + :param Any needle: The element to search + """ + if isinstance(needle, DoublyLinkedListElement): + return needle + else: + if needle in self.elements: + return self.elements[needle] + else: + return False + + +class DoublyLinkedListElement: + def __init__(self, data, prev_element=None, next_element=None): + self.item = data + self.prev_element = prev_element + self.next_element = next_element + + def __repr__(self): + output = [self.item] + if self.prev_element is not None: + output.append(self.prev_element.item) + else: + output.append(None) + if self.next_element is not None: + output.append(self.next_element.item) + else: + output.append(None) + return str(tuple(output)) diff --git a/2021/graph.py b/2021/graph.py new file mode 100644 index 0000000..889fd6d --- /dev/null +++ b/2021/graph.py @@ -0,0 +1,542 @@ +import heapq + + +class TargetFound(Exception): + pass + + +class NegativeWeightCycle(Exception): + pass + + +class Graph: + def __init__(self, vertices=[], edges={}): + self.vertices = vertices.copy() + self.edges = edges.copy() + + def neighbors(self, vertex): + """ + Returns the neighbors of a given vertex + + :param Any vertex: The vertex to consider + :return: The neighbor and its weight if any + """ + if vertex in self.edges: + return self.edges[vertex] + else: + return False + + def estimate_to_complete(self, source_vertex, target_vertex): + return 0 + + def reset_search(self): + self.distance_from_start = {} + self.came_from = {} + + def dfs_groups(self): + """ + Groups vertices based on depth-first search + + :return: A list of groups + """ + groups = [] + unvisited = set(self.vertices) + + while unvisited: + start = unvisited.pop() + self.depth_first_search(start) + + newly_visited = list(self.distance_from_start.keys()) + unvisited -= set(newly_visited) + groups.append(newly_visited) + + return groups + + def depth_first_search(self, start, end=None): + """ + Performs a depth-first search based on a start node + + The end node can be used for an early exit. + DFS will explore the graph by going as deep as possible first + The exploration path is a star, with each branch explored one by one + It'll not yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + try: + self.depth_first_search_recursion(0, start, end) + except TargetFound: + return True + if end: + return False + return False + + def depth_first_search_recursion(self, current_distance, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + # Examine the neighbor immediatly + self.depth_first_search_recursion(current_distance, neighbor, end) + + if neighbor == end: + raise TargetFound + + def topological_sort(self): + """ + Performs a topological sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_nodes = sorted(x for x in not_visited if x not in sum(edges.values(), [])) + current_distance = 0 + + while not_visited: + for next_node in next_nodes: + self.distance_from_start[next_node] = current_distance + + not_visited -= set(next_nodes) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_nodes = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + + return True + + def topological_sort_alphabetical(self): + """ + Performs a topological sort with alphabetical sort + + Topological sort is based on dependencies + All nodes are traversed, based on their dependencies + When multiple choices are available, the first one will be taken (no parallel work) + The "distance from start" is the order to use + + :return: True when all is explored + """ + self.distance_from_start = {} + + not_visited = set(self.vertices) + edges = self.edges.copy() + + next_node = sorted(x for x in not_visited if x not in sum(edges.values(), []))[ + 0 + ] + current_distance = 0 + + while not_visited: + self.distance_from_start[next_node] = current_distance + + not_visited.remove(next_node) + current_distance += 1 + edges = {x: edges[x] for x in edges if x in not_visited} + next_node = sorted( + x for x in not_visited if not x in sum(edges.values(), []) + ) + if len(next_node): + next_node = next_node[0] + + return True + + def breadth_first_search(self, start, end=None): + """ + Performs a breath-first search based on a start node + + This algorithm is appropriate for "One source, Multiple targets" + The end node can be used for an early exit. + BFS will explore the graph in concentric circles + This is useful when controlling the depth is needed + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found or all is explored or False if not + """ + current_distance = 0 + frontier = [(start, 0)] + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + vertex, current_distance = frontier.pop(0) + current_distance += 1 + neighbors = self.neighbors(vertex) + # This allows to cover WeightedGraphs + if isinstance(neighbors, dict): + neighbors = list(neighbors.keys()) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + # Adding for future examination + frontier.append((neighbor, current_distance)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def greedy_best_first_search(self, start, end): + """ + Performs a greedy best-first search based on a start node + + This algorithm is appropriate for the search "One source, One target" + Greedy BFS will explore by always taking the best direction available + This direction is estimated based on the estimate_to_complete function + Not everything will be explored + Does NOT provide the shortest path, but quite quick to run + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(self.estimate_to_complete(start, end), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + current_distance += 1 + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor in neighbors: + if neighbor in self.distance_from_start: + continue + + # Adding for future examination + heapq.heappush( + frontier, + ( + self.estimate_to_complete(neighbor, end), + neighbor, + current_distance, + ), + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return False + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + target_vertex = self.came_from[target_vertex] + path.append(target_vertex) + + path.reverse() + + return path + + +class WeightedGraph(Graph): + def dijkstra(self, start, end=None): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + min_distance = float("inf") + + while frontier: + current_distance, vertex = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + # No need to explore neighbors if we already found a shorter path to the end + if current_distance > min_distance: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + if type(neighbor) == complex: + heapq.heappush( + frontier, (current_distance + weight, SuperComplex(neighbor)) + ) + else: + heapq.heappush(frontier, (current_distance + weight, neighbor)) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + min_distance = min(min_distance, current_distance + weight) + + return end is None or end in self.distance_from_start + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + while frontier: + _, vertex, current_distance = heapq.heappop(frontier) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if neighbor == end: + return True + + return end in self.distance_from_start + + def bellman_ford(self, start, end=None): + """ + Applies the Bellman–Ford algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive or negative weigths / costs of travelling. + + The algorithm is basically Dijkstra, but it runs V-1 times (V = number of vertices) + Unless there is a neigative-weight cycle (meaning there is no possible minimum), it'll yield a result + It'll yield exact result for the path-finding + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + self.distance_from_start = {start: 0} + self.came_from = {start: None} + + for i in range(len(self.vertices) - 1): + for vertex in self.vertices: + current_distance = self.distance_from_start[vertex] + for neighbor, weight in self.neighbors(vertex).items(): + # We've already checked that node, and it's not better now + if ( + neighbor in self.distance_from_start + and self.distance_from_start[neighbor] + <= (current_distance + weight) + ): + continue + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + # Check for cycles + for vertex in self.vertices: + for neighbor, weight in self.neighbors(vertex).items(): + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + raise NegativeWeightCycle + + return end is None or end in self.distance_from_start + + def ford_fulkerson(self, start, end): + """ + Searches for the maximum flow using the Ford-Fulkerson algorithm + + The weights of the graph are used as flow limitations + Note: there may be multiple options, this generates only one + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: The maximum flow + """ + + if start not in self.vertices: + raise ValueError("Source not in graph") + if end not in self.vertices: + raise ValueError("End not in graph") + + if end not in self.edges: + self.edges[end] = {} + + initial_edges = {a: self.edges[a].copy() for a in self.edges} + self.flow_graph = {a: self.edges[a].copy() for a in self.edges} + + max_flow = 0 + frontier = [start] + heapq.heapify(frontier) + + while self.breadth_first_search(start, end): + path_flow = float("Inf") + cursor = end + while cursor != start: + path_flow = min(path_flow, self.edges[self.came_from[cursor]][cursor]) + cursor = self.came_from[cursor] + + max_flow += path_flow + + # Update the graph to change the flows + cursor = end + while cursor != start: + self.edges[self.came_from[cursor]][cursor] -= path_flow + if self.edges[self.came_from[cursor]][cursor] == 0: + del self.edges[self.came_from[cursor]][cursor] + self.edges[cursor][self.came_from[cursor]] = ( + self.edges[cursor].get(self.came_from[cursor], 0) + path_flow + ) + + cursor = self.came_from[cursor] + + cursor = end + for vertex in self.vertices: + for neighbor, items in self.neighbors(vertex).items(): + if neighbor in self.flow_graph[vertex]: + self.flow_graph[vertex][neighbor] -= self.edges[vertex][neighbor] + if self.flow_graph[vertex][neighbor] == 0: + del self.flow_graph[vertex][neighbor] + + self.edges = initial_edges + + return max_flow + + def bipartite_matching(self, starts, ends): + """ + Performs a bipartite matching using Fold-Fulkerson's algorithm + + :param iterable starts: A list of source vertices + :param iterable ends: A list of target vertices + :return: The maximum matches found + """ + + start_point = "A" + while start_point in self.vertices: + start_point += "A" + self.edges[start_point] = {} + self.vertices += start_point + for start in starts: + if start not in self.vertices: + return ValueError("Source not in graph") + self.edges[start_point].update({start: 1}) + + end_point = "Z" + while end_point in self.vertices: + end_point += "Z" + self.vertices.append(end_point) + for end in ends: + if end not in self.vertices: + return ValueError("End not in graph") + if end not in self.edges: + self.edges[end] = {} + self.edges[end].update({end_point: 1}) + + value = self.ford_fulkerson(start_point, end_point) + self.vertices.remove(end_point) + self.vertices.remove(start_point) + return value diff --git a/2021/grid.py b/2021/grid.py new file mode 100644 index 0000000..b3254d1 --- /dev/null +++ b/2021/grid.py @@ -0,0 +1,508 @@ +from compass import * +from dot import Dot +from graph import WeightedGraph +import heapq + + +class Grid: + # For anisotropic grids, this provides which directions are allowed + possible_source_directions = { + ".": directions_straight, + "#": [], + " ": [], + "^": [north, south], + "v": [north, south], + ">": [east, west], + "<": [east, west], + "+": directions_straight, + "|": [north, south], + "-": [east, west], + "/": directions_straight, + "\\": directions_straight, + } + direction_default = directions_straight + all_directions = directions_straight + + def __init__(self, dots=[], edges={}, isotropic=True): + """ + Creates the grid based on the list of dots and edges provided + + :param sequence dots: Either a list of positions or a dict position:terrain + :param dict edges: Dict of format source:target:distance + :param Boolean isotropic: Whether directions matter + """ + + self.is_isotropic = bool(isotropic) + + if dots: + if isinstance(dots, dict): + if self.is_isotropic: + self.dots = {x: Dot(self, x, dots[x]) for x in dots} + else: + self.dots = {x: Dot(self, x[0], dots[x], x[1]) for x in dots} + else: + if self.is_isotropic: + self.dots = {x: Dot(self, x, None) for x in dots} + else: + self.dots = {x: Dot(self, x[0], None, x[1]) for x in dots} + else: + self.dots = {} + + self.edges = edges.copy() + if edges: + self.set_edges(self.edges) + + self.width = None + self.height = None + + def set_edges(self, edges): + """ + Sets up the edges as neighbors of Dots + + """ + for source in edges: + if not self.dots[source].neighbors: + self.dots[source].neighbors = {} + for target in edges[source]: + self.dots[source].neighbors[self.dots[target]] = edges[source][target] + self.dots[source].neighbors_obsolete = False + + def reset_pathfinding(self): + """ + Resets the pathfinding (= forces recalculation of all neighbors if relevant) + + """ + if self.edges: + self.set_edges(self.edges) + else: + for dot in self.dots.values(): + dot.neighbors_obsolete = True + + def text_to_dots(self, text, ignore_terrain=""): + """ + Converts a text to a set of dots + + The text is expected to be separated by newline characters + The dots will have x - y * 1j as coordinates + + :param string text: The text to convert + :param sequence ignore_terrain: Types of terrain to ignore (useful for walls) + """ + self.dots = {} + + y = 0 + for line in text.splitlines(): + for x in range(len(line)): + if line[x] not in ignore_terrain: + if self.is_isotropic: + self.dots[x - y * 1j] = Dot(self, x - y * 1j, line[x]) + else: + for dir in self.possible_source_directions.get( + line[x], self.direction_default + ): + self.dots[(x - y * 1j, dir)] = Dot( + self, x - y * 1j, line[x], dir + ) + y += 1 + + def dots_to_text(self, mark_coords={}, void=" "): + """ + Converts dots to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string void: Which character to use when no dot is present + :return: the text + """ + text = "" + + min_x, max_x, min_y, max_y = self.get_box() + + # The imaginary axis is reversed compared to reading order + for y in range(max_y, min_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + text += mark_coords[x + y * 1j] + except (KeyError, TypeError): + if x + y * 1j in mark_coords: + text += "X" + else: + if self.is_isotropic: + text += str(self.dots.get(x + y * 1j, void)) + else: + dots = [dot for dot in self.dots if dot[0] == x + y * 1j] + if dots: + text += str(self.dots.get(dots[0], void)) + else: + text += str(void) + text += "\n" + + return text + + def get_size(self): + """ + Gets the width and height of the grid + + :return: the width and height + """ + + if not self.width: + min_x, max_x, min_y, max_y = self.get_box() + + self.width = max_x - min_x + 1 + self.height = max_y - min_y + 1 + + return (self.width, self.height) + + def get_box(self): + """ + Gets the min/max x and y values + + :return: the minimum and maximum for x and y values + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + return (min_x, max_x, min_y, max_y) + + def add_traps(self, traps): + """ + Adds traps + """ + + for dot in traps: + if self.is_isotropic: + self.dots[dot].set_trap(True) + else: + # print (dot, self.dots.values()) + if dot in self.dots: + self.dots[dot].set_trap(True) + else: + for direction in self.all_directions: + if (dot, direction) in self.dots: + self.dots[(dot, direction)].set_trap(True) + + def add_walls(self, walls): + """ + Adds walls + """ + + for dot in walls: + if self.is_isotropic: + self.dots[dot].set_wall(True) + else: + if dot in self.dots: + self.dots[dot].set_wall(True) + else: + for direction in self.all_directions: + if (dot, direction) in self.dots: + self.dots[(dot, direction)].set_wall(True) + + def get_borders(self): + """ + Gets the borders of the image + + Only the terrain of the dot will be sent back + This will be returned in left-to-right, up to bottom reading order + Newline characters are not included + + :return: a set of coordinates + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) + y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + borders = [] + borders.append([x + 1j * max_y for x in sorted(x_vals)]) + borders.append([max_x + 1j * y for y in sorted(y_vals)]) + borders.append([x + 1j * min_y for x in sorted(x_vals)]) + borders.append([min_x + 1j * y for y in sorted(y_vals)]) + + borders_text = [] + for border in borders: + borders_text.append( + Grid({pos: self.dots[pos].terrain for pos in border}) + .dots_to_text() + .replace("\n", "") + ) + + return borders_text + + def rotate(self, angles): + """ + Rotates clockwise a grid and returns a list of rotated grids + + :param tuple angles: Which angles to use for rotation + :return: The dots + """ + + rotated_grids = [] + + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x, min_y, max_y = self.get_box() + width, height = self.get_size() + + if isinstance(angles, int): + angles = {angles} + + for angle in angles: + if angle == 0: + rotated_grids.append(self) + elif angle == 90: + rotated_grids.append( + Grid( + { + height - 1 + pos.imag - 1j * pos.real: dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif angle == 180: + rotated_grids.append( + Grid( + { + width + - 1 + - pos.real + - 1j * (height - 1 + pos.imag): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif angle == 270: + rotated_grids.append( + Grid( + { + -pos.imag - 1j * (width - 1 - pos.real): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + + return rotated_grids + + def flip(self, flips): + """ + Flips a grid and returns a list of grids + + :param tuple flips: Which flips to perform + :return: The dots + """ + + flipped_grids = [] + + x_vals = set(dot.position.real for dot in self.dots.values()) + y_vals = set(dot.position.imag for dot in self.dots.values()) + + min_x, max_x, min_y, max_y = self.get_box() + width, height = self.get_size() + + if isinstance(flips, str): + flips = {flips} + + for flip in flips: + if flip == "N": + flipped_grids.append(self) + elif flip == "H": + flipped_grids.append( + Grid( + { + pos.real - 1j * (height - 1 + pos.imag): dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + elif flip == "V": + flipped_grids.append( + Grid( + { + width - 1 - pos.real + 1j * pos.imag: dot.terrain + for pos, dot in self.dots.items() + } + ) + ) + + return flipped_grids + + def crop(self, corners=[], size=0): + """ + Gets the list of dots within a given area + + :param sequence corners: Either one or 2 corners to use + :param int or sequence size: The size (width + height, or simply length) to use + :return: a dict of matching dots + """ + + delta = size - 1 + # top left corner + size are provided + if delta and len(corners) == 1: + # The corner is a Dot + if isinstance(corners[0], Dot): + min_x, max_x = ( + int(corners[0].position.real), + int(corners[0].position.real) + delta, + ) + min_y, max_y = ( + int(corners[0].position.imag) - delta, + int(corners[0].position.imag), + ) + # The corner is a tuple position, direction + elif isinstance(corners[0], tuple): + min_x, max_x = int(corners[0][0].real), int(corners[0][0].real + delta) + min_y, max_y = int(corners[0][0].imag - delta), int(corners[0][0].imag) + # The corner is a complex number + else: + min_x, max_x = int(corners[0].real), int(corners[0].real + delta) + min_y, max_y = int(corners[0].imag - delta), int(corners[0].imag) + + # Multiple corners are provided + else: + # Dots are provided as a Dot instance + if isinstance(corners[0], Dot): + x_vals = set(dot.position.real for dot in corners) + y_vals = set(dot.position.imag for dot in corners) + # Dots are provided as complex numbers + else: + x_vals = set(pos.real for pos in corners) + y_vals = set(pos.imag for pos in corners) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + if self.is_isotropic: + cropped = Grid( + { + x + y * 1j: self.dots[x + y * 1j].terrain + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + if x + y * 1j in self.dots + } + ) + else: + cropped = Grid( + { + (x + y * 1j, dir): self.dots[(x + y * 1j, dir)].terrain + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + for dir in self.all_directions + if (x + y * 1j, dir) in self.dots + } + ) + + return cropped + + def dijkstra(self, start): + """ + Applies the Dijkstra algorithm to a given search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is based on concentric shapes + The frontier elements have identical / similar cost from start + It'll yield exact result for the path-finding, but it's quite slow + + :param Dot start: The start dot to consider + """ + current_distance = 0 + if not isinstance(start, Dot): + start = self.dots[start] + frontier = [(0, start)] + heapq.heapify(frontier) + visited = {start: 0} + + while frontier: + current_distance, dot = frontier.pop(0) + neighbors = dot.get_neighbors() + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + if neighbor in visited and visited[neighbor] <= ( + current_distance + weight + ): + continue + # Adding for future examination + frontier.append((current_distance + weight, neighbor)) + + # Adding for final search + visited[neighbor] = current_distance + weight + start.neighbors[neighbor] = current_distance + weight + + def convert_to_graph(self): + """ + Converts the grid in a reduced graph for pathfinding + + :return: a WeightedGraph containing all waypoints and links + """ + + waypoints = [ + self.dots[dot_key] + for dot_key in self.dots + if self.dots[dot_key].is_waypoint + ] + edges = {} + + for waypoint in waypoints: + self.dijkstra(waypoint) + distances = waypoint.get_neighbors() + edges[waypoint] = { + wp: distances[wp] + for wp in distances + if wp != waypoint and wp.is_waypoint + } + + graph = WeightedGraph(waypoints, edges) + graph.neighbors = lambda vertex: vertex.get_neighbors() + + return graph + + +def merge_grids(grids, width, height): + """ + Merges different grids in a single grid + + All grids are assumed to be of the same size + + :param dict grids: The grids to merge + :param int width: The width, in number of grids + :param int height: The height, in number of grids + :return: The merged grid + """ + + final_grid = Grid() + + part_width, part_height = grids[0].get_size() + if any([not grid.is_isotropic for grid in grids]): + print("This works only for isotropic grids") + return + + grid_nr = 0 + for part_y in range(height): + for part_x in range(width): + offset = part_x * part_width - 1j * part_y * part_height + final_grid.dots.update( + { + (pos + offset): Dot( + final_grid, pos + offset, grids[grid_nr].dots[pos].terrain + ) + for pos in grids[grid_nr].dots + } + ) + grid_nr += 1 + + return final_grid From bb18e8e0d64f93d6a50aca8da8d85f87dd510754 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:33:53 +0100 Subject: [PATCH 113/143] New files to ignore --- .gitignore | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 078bdd3..798ec50 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,8 @@ Inputs/ template.py __pycache__ parse/ -download.py \ No newline at end of file +download.py +timings.ods +time.txt +time_calc.sh +timings.txt \ No newline at end of file From ca6aaccb8aebefa10b058c9902645886a9259ad0 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Fri, 3 Dec 2021 08:34:35 +0100 Subject: [PATCH 114/143] New version for 2020-04 --- 2020/04-Passport Processing.v1.py | 178 ++++++++++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 2020/04-Passport Processing.v1.py diff --git a/2020/04-Passport Processing.v1.py b/2020/04-Passport Processing.v1.py new file mode 100644 index 0000000..5ba70aa --- /dev/null +++ b/2020/04-Passport Processing.v1.py @@ -0,0 +1,178 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """ecl:gry pid:860033327 eyr:2020 hcl:#fffffd +byr:1937 iyr:2017 cid:147 hgt:183cm + +iyr:2013 ecl:amb cid:350 eyr:2023 pid:028048884 +hcl:#cfa07d byr:1929 + +hcl:#ae17e1 iyr:2013 +eyr:2024 +ecl:brn pid:760753108 byr:1931 +hgt:179cm + +hcl:#cfa07d eyr:2025 pid:166559648 +iyr:2011 ecl:brn hgt:59in""", + "expected": ["2", "Unknown"], +} +test = 2 +test_data[test] = { + "input": """eyr:1972 cid:100 +hcl:#18171d ecl:amb hgt:170 pid:186cm iyr:2018 byr:1926 + +iyr:2019 +hcl:#602927 eyr:1967 hgt:170cm +ecl:grn pid:012533040 byr:1946 + +hcl:dab227 iyr:2012 +ecl:brn hgt:182cm pid:021572410 eyr:2020 byr:1992 cid:277 + +hgt:59cm ecl:zzz +eyr:2038 hcl:74454a iyr:2023 +pid:3556412378 byr:2007 + +pid:087499704 hgt:74in ecl:grn iyr:2012 eyr:2030 byr:1980 +hcl:#623a2f + +eyr:2029 ecl:blu cid:129 byr:1989 +iyr:2014 pid:896056539 hcl:#a97842 hgt:165cm + +hcl:#888785 +hgt:164cm byr:2001 iyr:2015 cid:88 +pid:545766238 ecl:hzl +eyr:2022 + +iyr:2010 hgt:158cm hcl:#b6652a ecl:blu byr:1944 eyr:2021 pid:093154719""", + "expected": ["Unknown", "4"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["235", "194"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +required_fields = ["byr", "iyr", "eyr", "hgt", "hcl", "ecl", "pid"] + +passports = [] +i = 0 +for string in puzzle_input.split("\n"): + if len(passports) >= i: + passports.append("") + if string == "": + i = i + 1 + else: + passports[i] = passports[i] + " " + string + +valid_passports = 0 + +if part_to_test == 1: + for passport in passports: + if all([x + ":" in passport for x in required_fields]): + valid_passports = valid_passports + 1 + + +else: + for passport in passports: + if all([x + ":" in passport for x in required_fields]): + fields = passport.split(" ") + score = 0 + for field in fields: + data = field.split(":") + if data[0] == "byr": + year = int(data[1]) + if year >= 1920 and year <= 2002: + score = score + 1 + elif data[0] == "iyr": + year = int(data[1]) + if year >= 2010 and year <= 2020: + score = score + 1 + elif data[0] == "eyr": + year = int(data[1]) + if year >= 2020 and year <= 2030: + score = score + 1 + elif data[0] == "hgt": + size = ints(data[1])[0] + if data[1][-2:] == "cm": + if size >= 150 and size <= 193: + score = score + 1 + elif data[1][-2:] == "in": + if size >= 59 and size <= 76: + score = score + 1 + elif data[0] == "hcl": + if re.match("#[0-9a-f]{6}", data[1]) and len(data[1]) == 7: + score = score + 1 + print(data[0], passport) + elif data[0] == "ecl": + if data[1] in ["amb", "blu", "brn", "gry", "grn", "hzl", "oth"]: + score = score + 1 + print(data[0], passport) + elif data[0] == "pid": + if re.match("[0-9]{9}", data[1]) and len(data[1]) == 9: + score = score + 1 + print(data[0], passport) + print(passport, score) + if score == 7: + valid_passports = valid_passports + 1 + +puzzle_actual_result = valid_passports + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From db960457dd277f30cf0e839bdaa2bb786b3bbd60 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 5 Dec 2021 20:38:13 +0100 Subject: [PATCH 115/143] Added 2021-04 & 2021-05 + additions to grid module --- 2021/04-Giant Squid.py | 203 ++++++++++++++++++++++++++++++++ 2021/05-Hydrothermal Venture.py | 139 ++++++++++++++++++++++ 2021/grid.py | 71 ++++++++++- 3 files changed, 412 insertions(+), 1 deletion(-) create mode 100644 2021/04-Giant Squid.py create mode 100644 2021/05-Hydrothermal Venture.py diff --git a/2021/04-Giant Squid.py b/2021/04-Giant Squid.py new file mode 100644 index 0000000..62bc5b4 --- /dev/null +++ b/2021/04-Giant Squid.py @@ -0,0 +1,203 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """7,4,9,5,11,17,23,2,0,14,21,24,10,16,13,6,15,25,12,22,18,20,8,19,3,26,1 + +22 13 17 11 0 + 8 2 23 4 24 +21 9 14 16 7 + 6 10 3 18 5 + 1 12 20 15 19 + + 3 15 0 2 22 + 9 18 13 17 5 +19 8 7 25 23 +20 11 10 24 4 +14 21 16 12 6 + +14 21 17 24 4 +10 16 15 9 19 +18 8 23 26 20 +22 11 13 6 5 + 2 0 12 3 7""", + "expected": ["4512", "1924"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["39984", "8468"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + numbers_drawn = ints(puzzle_input.split("\n")[0]) + + cards_init = {} + cards = {} + + for i, card in enumerate(puzzle_input.split("\n\n")[1:]): + cards_init[i] = {} + cards[i] = {} + for r, row in enumerate(card.split("\n")): + cards_init[i][r] = ints(row) + cards[i][r] = ints(row) + + for n in numbers_drawn: + cards = { + i: {r: [c if c != n else "x" for c in cards[i][r]] for r in cards[i]} + for i in cards + } + + # Check rows + for i in cards: + for r in cards[i]: + if cards[i][r] == ["x", "x", "x", "x", "x"]: + winner_numbers = [ + cards_init[i][row][col] + for row in cards[i] + for col in range(5) + if cards[i][row][col] != "x" + ] + puzzle_actual_result = sum(winner_numbers) * int(n) + break + if puzzle_actual_result != "Unknown": + break + if puzzle_actual_result != "Unknown": + break + + # Check columns + for i in cards: + for c in range(5): + if all(cards[i][r][c] == "x" for r in range(5)): + winner_numbers = [ + cards_init[i][row][col] + for row in cards[i] + for col in range(5) + if cards[i][row][col] != "x" + ] + puzzle_actual_result = sum(winner_numbers) * int(n) + break + if puzzle_actual_result != "Unknown": + break + if puzzle_actual_result != "Unknown": + break + + +else: + numbers_drawn = ints(puzzle_input.split("\n")[0]) + + cards_init = {} + cards = {} + + last_card = "Unknown" + + for i, card in enumerate(puzzle_input.split("\n\n")[1:]): + cards_init[i] = {} + cards[i] = {} + for r, row in enumerate(card.split("\n")): + cards_init[i][r] = ints(row) + cards[i][r] = ints(row) + + for n in numbers_drawn: + cards = { + i: {r: [c if c != n else "x" for c in cards[i][r]] for r in cards[i]} + for i in cards + } + + # Check rows + to_remove = [] + for i in cards: + for r in cards[i]: + if cards[i][r] == ["x", "x", "x", "x", "x"]: + to_remove.append(i) + break + + # Check columns + for i in cards: + for c in range(5): + if all(cards[i][r][c] == "x" for r in range(5)): + to_remove.append(i) + break + + if len(cards) == 1: + last_card = list(cards.keys())[0] + if last_card in to_remove: + winner_numbers = [ + cards_init[last_card][row][col] + for row in range(5) + for col in range(5) + if cards[last_card][row][col] != "x" + ] + puzzle_actual_result = sum(winner_numbers) * int(n) + break + + cards = {i: cards[i] for i in cards if i not in to_remove} + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-05 18:08:14.982011 +# Part 1 : 2021-12-05 19:05:21 +# Part 2 : 2021-12-05 19:16:15 diff --git a/2021/05-Hydrothermal Venture.py b/2021/05-Hydrothermal Venture.py new file mode 100644 index 0000000..5f31f82 --- /dev/null +++ b/2021/05-Hydrothermal Venture.py @@ -0,0 +1,139 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """0,9 -> 5,9 +8,0 -> 0,8 +9,4 -> 3,4 +2,2 -> 2,1 +7,0 -> 7,4 +6,4 -> 2,0 +0,9 -> 2,9 +3,4 -> 1,4 +0,0 -> 8,8 +5,5 -> 8,2""", + "expected": ["5", "12"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["7438", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + dots = {} + for string in puzzle_input.split("\n"): + x1, y1, x2, y2 = ints(string) + x1, x2 = min(x1, x2), max(x1, x2) + y1, y2 = min(y1, y2), max(y1, y2) + + if x1 != x2 and y1 != y2: + continue + new_dots = [x + 1j * y for x in range(x1, x2 + 1) for y in range(y1, y2 + 1)] + dots.update({pos: 1 if pos not in dots else 2 for pos in new_dots}) + + puzzle_actual_result = len([x for x in dots if dots[x] != 1]) + + +else: + dots = {} + for string in puzzle_input.split("\n"): + x1, y1, x2, y2 = ints(string) + + if x1 != x2 and y1 != y2: + if x1 > x2: + if y1 > y2: + new_dots = [ + x1 + n - 1j * (y1 + n) for n in range(0, x2 - x1 - 1, -1) + ] + else: + new_dots = [ + x1 + n - 1j * (y1 - n) for n in range(0, x2 - x1 - 1, -1) + ] + else: + if y1 > y2: + new_dots = [x1 + n - 1j * (y1 - n) for n in range(x2 - x1 + 1)] + else: + new_dots = [x1 + n - 1j * (y1 + n) for n in range(x2 - x1 + 1)] + + else: + x1, x2 = min(x1, x2), max(x1, x2) + y1, y2 = min(y1, y2), max(y1, y2) + new_dots = [ + x - 1j * y for x in range(x1, x2 + 1) for y in range(y1, y2 + 1) + ] + # print (string, new_dots) + dots.update({pos: 1 if pos not in dots else dots[pos] + 1 for pos in new_dots}) + + # print (dots) + # grid = grid.Grid({i: str(dots[i]) for i in dots}) + # print (grid.dots_to_text()) + puzzle_actual_result = len([x for x in dots if dots[x] != 1]) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-05 20:13:00 +# Part 1: 2021-12-05 20:22:20 +# Part 1: 2021-12-05 20:36:20 diff --git a/2021/grid.py b/2021/grid.py index b3254d1..6fa1c2b 100644 --- a/2021/grid.py +++ b/2021/grid.py @@ -105,6 +105,33 @@ def text_to_dots(self, text, ignore_terrain=""): ) y += 1 + def words_to_dots(self, text, convert_to_int=False): + """ + Converts a text to a set of dots + + The text is expected to be separated by newline characters + The dots will have x - y * 1j as coordinates + Dots are words (rather than letters, like in text_to_dots) + + :param string text: The text to convert + :param sequence ignore_terrain: Types of terrain to ignore (useful for walls) + """ + self.dots = {} + + y = 0 + for line in text.splitlines(): + for x in line.split(" "): + for dir in self.possible_source_directions.get( + x, self.direction_default + ): + if convert_to_int: + self.dots[(x - y * 1j, dir)] = Dot( + self, x - y * 1j, int(x), dir + ) + else: + self.dots[(x - y * 1j, dir)] = Dot(self, x - y * 1j, x, dir) + y += 1 + def dots_to_text(self, mark_coords={}, void=" "): """ Converts dots to a text @@ -212,7 +239,7 @@ def get_borders(self): This will be returned in left-to-right, up to bottom reading order Newline characters are not included - :return: a set of coordinates + :return: a text representing a border """ if not self.dots: @@ -239,6 +266,48 @@ def get_borders(self): return borders_text + def get_columns(self): + """ + Gets the columns of the image + + :return: a dict of dots + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) + y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + columns = {} + for x in x_vals: + columns[x] = [x + 1j * y for y in y_vals if x + 1j * y in self.dots] + + return columns + + def get_rows(self): + """ + Gets the rows of the image + + :return: a dict of dots + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) + y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + rows = {} + for y in y_vals: + rows[y] = [x + 1j * y for x in x_vals if x + 1j * y in self.dots] + + return rows + def rotate(self, angles): """ Rotates clockwise a grid and returns a list of rotated grids From 3c96b32d0a020520b58cdde57deb24e821830542 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 6 Dec 2021 09:43:13 +0100 Subject: [PATCH 116/143] Deleted .gitignore --- .gitignore | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 798ec50..0000000 --- a/.gitignore +++ /dev/null @@ -1,9 +0,0 @@ -Inputs/ -template.py -__pycache__ -parse/ -download.py -timings.ods -time.txt -time_calc.sh -timings.txt \ No newline at end of file From f7e470aafd45cda28690136bd97e5405b1644fbb Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 6 Dec 2021 09:45:59 +0100 Subject: [PATCH 117/143] Added day 2021-06 --- 2021/06-Lanternfish.py | 107 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 2021/06-Lanternfish.py diff --git a/2021/06-Lanternfish.py b/2021/06-Lanternfish.py new file mode 100644 index 0000000..b7c4ada --- /dev/null +++ b/2021/06-Lanternfish.py @@ -0,0 +1,107 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """3,4,3,1,2""", + "expected": ["26 @ day 18, 5934 @ day 80", "26984457539"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["396210", "1770823541496"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + +fishes = defaultdict(lambda: 0) +new_fish_plus_1 = defaultdict(lambda: 0) +new_fish_plus_2 = defaultdict(lambda: 0) + + +if part_to_test == 1: + nb_gen = 80 +else: + nb_gen = 256 +for fish in ints(puzzle_input): + fishes[fish] += 1 + +for day in range(nb_gen + 1): + new_fish = defaultdict(lambda: 0) + for i in fishes: + if day % 7 == i: + new_fish[(day + 2) % 7] += fishes[day % 7] + + for i in new_fish_plus_2: + fishes[i] += new_fish_plus_2[i] + new_fish_plus_2 = new_fish_plus_1.copy() + new_fish_plus_1 = new_fish.copy() + + print("End of day", day, ":", sum(fishes.values()) + sum(new_fish_plus_2.values())) + + puzzle_actual_result = sum(fishes.values()) + sum(new_fish_plus_2.values()) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-06 08:17:14.668559 +# Part 1: 2021-12-06 09:36:08 (60 min for meetings + shower) +# Part 2: 2021-12-06 09:37:07 (60 min for meetings + shower) From fc94585086352079e64a93eb5ad0a2d29eb3d786 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 8 Dec 2021 17:30:00 +0100 Subject: [PATCH 118/143] Added day 2021-07 & 2021-08 --- 2021/07-The Treachery of Whales.py | 103 +++++++++++ 2021/08-Seven Segment Search.py | 285 +++++++++++++++++++++++++++++ 2 files changed, 388 insertions(+) create mode 100644 2021/07-The Treachery of Whales.py create mode 100644 2021/08-Seven Segment Search.py diff --git a/2021/07-The Treachery of Whales.py b/2021/07-The Treachery of Whales.py new file mode 100644 index 0000000..2c2f641 --- /dev/null +++ b/2021/07-The Treachery of Whales.py @@ -0,0 +1,103 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, statistics +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """16,1,2,0,4,2,7,1,2,14""", + "expected": ["37", "168"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["347449", "98039527"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + crabs = ints(puzzle_input) + target = statistics.median(crabs) + fuel = int(sum([abs(crab - target) for crab in crabs])) + + puzzle_actual_result = fuel + + +else: + crabs = ints(puzzle_input) + square_crabs = sum([crab ** 2 for crab in crabs]) + sum_crabs = sum(crabs) + min_crabs = min(crabs) + max_crabs = max(crabs) + fuel = min( + [ + sum([abs(crab - t) * (abs(crab - t) + 1) / 2 for crab in crabs]) + for t in range(min_crabs, max_crabs) + ] + ) + + puzzle_actual_result = int(fuel) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-07 08:14:33.977835 +# Part 1 : 2021-12-07 08:16:08 +# Part 2 : 2021-12-07 08:33:12 diff --git a/2021/08-Seven Segment Search.py b/2021/08-Seven Segment Search.py new file mode 100644 index 0000000..37f13d5 --- /dev/null +++ b/2021/08-Seven Segment Search.py @@ -0,0 +1,285 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """acedgfb cdfbe gcdfa fbcad dab cefabd cdfgeb eafb cagedb ab | cdfeb fcadb cdfeb cdbaf""", + "expected": ["Unknown", "5353"], +} + +test = 2 +test_data[test] = { + "input": """be cfbegad cbdgef fgaecd cgeb fdcge agebfd fecdb fabcd edb | fdgacbe cefdb cefbgd gcbe +edbfga begcd cbg gc gcadebf fbgde acbgfd abcde gfcbed gfec | fcgedb cgb dgebacf gc +fgaebd cg bdaec gdafb agbcfd gdcbef bgcad gfac gcb cdgabef | cg cg fdcagb cbg +fbegcd cbd adcefb dageb afcb bc aefdc ecdab fgdeca fcdbega | efabcd cedba gadfec cb +aecbfdg fbg gf bafeg dbefa fcge gcbea fcaegb dgceab fcbdga | gecf egdcabf bgf bfgea +fgeab ca afcebg bdacfeg cfaedg gcfdb baec bfadeg bafgc acf | gebdcfa ecba ca fadegcb +dbcfg fgd bdegcaf fgec aegbdf ecdfab fbedc dacgb gdcebf gf | cefg dcbef fcge gbcadfe +bdfegc cbegaf gecbf dfcage bdacg ed bedf ced adcbefg gebcd | ed bcgafe cdgba cbgef +egadfb cdbfeg cegd fecab cgb gbdefca cg fgcdab egfdb bfceg | gbdfcae bgc cg cgb +gcafb gcf dcaebfg ecagb gf abcdeg gaef cafbge fdbac fegbdc | fgae cfgab fg bagce""", + "expected": [ + "26", + "8394, 9781, 1197, 9361, 4873, 8418, 4548, 1625, 8717, 4315 ==> 61229", + ], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + nb_digits = 0 + for string in puzzle_input.split("\n"): + output = words(string)[-4:] + nb_digits += len([x for x in output if len(x) in [2, 3, 4, 7]]) + + puzzle_actual_result = nb_digits + + +else: + digit_to_real_segments = { + "0": "abcefg", + "1": "cf", + "2": "acdeg", + "3": "acdfg", + "4": "bcdf", + "5": "abdfg", + "6": "abdefg", + "7": "acf", + "8": "abcdefg", + "9": "abcdfg", + } + digit_container = { + "0": ["8"], + "1": ["0", "3", "4", "7", "8", "9"], + "2": ["8"], + "3": ["8", "9"], + "4": ["8", "9"], + "5": ["6", "8", "9"], + "6": ["8"], + "7": ["0", "3", "8", "9"], + "8": [], + "9": ["8"], + } + shared_segments = { + digit1: { + digit2: len( + [ + segment + for segment in digit_to_real_segments[digit2] + if segment in digit_to_real_segments[digit1] + ] + ) + for digit2 in digit_to_real_segments + } + for digit1 in digit_to_real_segments + } + nb_segments = { + digit: len(digit_to_real_segments[digit]) for digit in digit_to_real_segments + } + for digit in digit_to_real_segments: + digit_to_real_segments[digit] = [ + "r_" + x for x in digit_to_real_segments[digit] + ] + digit_to_real_segments[digit].sort() + + digits = [str(i) for i in range(10)] + + sum_displays = 0 + + for string in puzzle_input.split("\n"): + signals = ["".join(sorted(x)) for x in words(string.replace("| ", ""))[:-4]] + displayed_words = ["".join(sorted(x)) for x in words(string)[-4:]] + + edges = {} + vertices = signals + digits + for word in signals: + edges[word] = [ + digit for digit in nb_segments if nb_segments[digit] == len(word) + ] + + mapping = {} + i = 0 + while len(mapping) != 9 and i != 5: + i += 1 + changed = True + while changed: + changed = False + for word in edges: + if len(edges[word]) == 1: + mapping[word] = edges[word][0] + edges = { + w: [edge for edge in edges[w] if edge != mapping[word]] + for w in edges + } + changed = True + del edges[word] + + for known_word in mapping: # abd + digit = mapping[known_word][0] # 7 + + for word in edges: # bcdef + same_letters = len([x for x in word if x in known_word]) + for possible_digit in edges[word]: # '2', '3', '5' + if shared_segments[digit][possible_digit] != same_letters: + edges[word].remove(possible_digit) + + # exit() + + # Second try, not the right approach (easier to do with shared_segments) + + # for known_word in mapping: # abd + # digit = mapping[known_word][0] # 7 + # #print ('known_word', known_word, '- digit', digit, 'container', digit_container[digit]) + # if digit_container[digit] == []: + # continue + # for word in edges: # bcdef + # #print ('tried word', word, '- digits', edges[word]) + # for possible_digit in edges[word]: # '2', '3', '5' + # #print ('possible_digit', possible_digit, possible_digit in digit_container[digit]) + # if possible_digit in digit_container[digit]: # '0', '3', '8', '9' + # #print ([(letter, letter in word) for letter in known_word]) + # if not all([letter in word for letter in known_word]): + # edges[word].remove(possible_digit) + + # print (edges, mapping) + output = "" + for displayed_word in displayed_words: + output += "".join(mapping[displayed_word]) + + sum_displays += int(output) + + puzzle_actual_result = sum_displays + +# First try, too complex + +# for string in puzzle_input.split("\n"): +# randomized_words = words(string.replace('| ', '')) +# randomized_displayed_words = words(string)[-4:] + +# randomized_segments = [x for x in 'abcdefg'] +# real_segments = ['r_'+x for x in 'abcdefg'] +# edges = {randomized: {real:1 for real in real_segments} for randomized in randomized_segments} +# vertices = randomized_segments + real_segments + +# for randomized_word in randomized_words: +# for randomized_segment in randomized_word: +# possible_segments = [] +# for digit in nb_segments: +# if nb_segments[digit] == len(randomized_word): +# possible_segments += digit_to_real_segments[digit] +# possible_segments = set(possible_segments) + + +# for real_segment in real_segments: +# if real_segment in possible_segments: +# continue +# if randomized_segment in edges: +# if real_segment in edges[randomized_segment]: +# del edges[randomized_segment][real_segment] + +# #if randomized_segment in 'be': +# #print (randomized_word, digit, nb_segments[digit], randomized_segment, possible_segments, edges[randomized_segment]) +# print (randomized_words) +# print ([x for x in randomized_words if len(x) in [2,3,4,7]]) +# print ({x: list(edges[x].keys()) for x in edges}) + +# mapping = graph.WeightedGraph(vertices, edges) +# result = mapping.bipartite_matching(randomized_segments, real_segments) +# print ('flow_graph ', mapping.flow_graph) +# segment_mapping = {} +# for randomized_segment in mapping.flow_graph: +# segment_mapping[randomized_segment] = mapping.flow_graph[randomized_segment] + +# final_number = '' +# for randomized_word in randomized_displayed_words: +# print('') +# real_segments = [] +# for letter in randomized_word: +# real_segments.append(''.join([k for k in mapping.flow_graph[letter]])) +# print ('real_segments', real_segments) +# real_segments = list(set(real_segments)) +# real_segments.sort() +# real_segments = ''.join(real_segments) + + +# final_number += ''.join([str(key) for key in digit_to_real_segments if ''.join(digit_to_real_segments[key]) == real_segments]) +# print ('real_segments', real_segments) +# print (randomized_word, [(str(key), ''.join(digit_to_real_segments[key])) for key in digit_to_real_segments]) +# print (randomized_word, final_number) + +# print (final_number) + + +# break + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-08 08:11:57.138188 +# Part 1 : 2021-12-08 08:13:56 +# Part 2 : 2021-12-08 14:12:15 From a386be1c1e120b3f896148924b0c5f434642b3ad Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 11 Dec 2021 13:20:37 +0100 Subject: [PATCH 119/143] Grid can now have integer values --- 2021/dot.py | 6 +++--- 2021/grid.py | 14 +++++++++----- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/2021/dot.py b/2021/dot.py index dd7666f..58c762e 100644 --- a/2021/dot.py +++ b/2021/dot.py @@ -129,17 +129,17 @@ def __ge__(self, other): def __repr__(self): if self.grid.is_isotropic: - return self.terrain + "@" + complex(self.position).__str__() + return str(self.terrain) + "@" + complex(self.position).__str__() else: return ( - self.terrain + str(self.terrain) + "@" + complex(self.position).__str__() + direction_to_text[self.source_direction] ) def __str__(self): - return self.terrain + return str(self.terrain) def __add__(self, direction): if not direction in self.allowed_directions: diff --git a/2021/grid.py b/2021/grid.py index 6fa1c2b..fe54dc3 100644 --- a/2021/grid.py +++ b/2021/grid.py @@ -78,7 +78,7 @@ def reset_pathfinding(self): for dot in self.dots.values(): dot.neighbors_obsolete = True - def text_to_dots(self, text, ignore_terrain=""): + def text_to_dots(self, text, ignore_terrain="", convert_to_int=False): """ Converts a text to a set of dots @@ -94,14 +94,18 @@ def text_to_dots(self, text, ignore_terrain=""): for line in text.splitlines(): for x in range(len(line)): if line[x] not in ignore_terrain: + if convert_to_int: + value = int(line[x]) + else: + value = line[x] if self.is_isotropic: - self.dots[x - y * 1j] = Dot(self, x - y * 1j, line[x]) + self.dots[x - y * 1j] = Dot(self, x - y * 1j, value) else: for dir in self.possible_source_directions.get( - line[x], self.direction_default + value, self.direction_default ): self.dots[(x - y * 1j, dir)] = Dot( - self, x - y * 1j, line[x], dir + self, x - y * 1j, value, dir ) y += 1 @@ -150,7 +154,7 @@ def dots_to_text(self, mark_coords={}, void=" "): for y in range(max_y, min_y - 1, -1): for x in range(min_x, max_x + 1): try: - text += mark_coords[x + y * 1j] + text += str(mark_coords[x + y * 1j]) except (KeyError, TypeError): if x + y * 1j in mark_coords: text += "X" From 753fcd193fdc5649eb6305e8c4bb397b8d4d54af Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 12 Dec 2021 10:14:27 +0100 Subject: [PATCH 120/143] Added answer for 2021-08 --- 2021/08-Seven Segment Search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2021/08-Seven Segment Search.py b/2021/08-Seven Segment Search.py index 37f13d5..41f9cab 100644 --- a/2021/08-Seven Segment Search.py +++ b/2021/08-Seven Segment Search.py @@ -64,7 +64,7 @@ def words(s: str): ) test_data[test] = { "input": open(input_file, "r+").read(), - "expected": ["Unknown", "Unknown"], + "expected": ["543", "994266"], } From 80e258e0d5dfa1988c8a0012fea674ef3e7fbd81 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 12 Dec 2021 10:15:12 +0100 Subject: [PATCH 121/143] Added multiple path finding in Graph library --- 2021/graph.py | 58 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/2021/graph.py b/2021/graph.py index 889fd6d..1c8f575 100644 --- a/2021/graph.py +++ b/2021/graph.py @@ -107,6 +107,64 @@ def depth_first_search_recursion(self, current_distance, vertex, end=None): if neighbor == end: raise TargetFound + def find_all_paths(self, start, end=None): + """ + Searches for all possible paths + + To avoid loops, function is_vertex_valid_for_path must be set + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: A list of paths + """ + self.paths = [] + + return self.dfs_all_paths([start], start, end) + + def is_vertex_valid_for_path(self, path, vertex): + """ + Determines whether a vertex can be added to a path + + The goal is to avoid loops + + :param Any path: The current path + :param Any vertex: The vertex to be added to the path + :return: True if the vertex can be added + """ + return False + + def dfs_all_paths(self, path, vertex, end=None): + """ + Recurrence function for depth-first search + + This function will be called each time additional depth is needed + The recursion stack corresponds to the exploration path + + :param integer current_distance: The distance from start of the current vertex + :param Any vertex: The vertex being explored + :param Any end: The target/end vertex to consider + :return: nothing + """ + + neighbors = self.neighbors(vertex) + if not neighbors: + return + + for neighbor in neighbors: + if not self.is_vertex_valid_for_path(path, neighbor): + continue + + new_path = path.copy() + + # Adding to path + new_path.append(neighbor) + + # Examine the neighbor immediatly + self.dfs_all_paths(new_path, neighbor, end) + + if neighbor == end: + self.paths.append(new_path) + def topological_sort(self): """ Performs a topological sort From 034feeca440220a5a0bf4f70fbc2b44f37725bad Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 13 Dec 2021 08:32:15 +0100 Subject: [PATCH 122/143] Added days 2021-09, 2021-10, 2021-11, 2021-12 and 2021-13 --- 2021/09-Smoke Basin.py | 111 ++++++++++++++++ 2021/10-Syntax Scoring.py | 158 ++++++++++++++++++++++ 2021/11-Dumbo Octopus.py | 230 +++++++++++++++++++++++++++++++++ 2021/12-Passage Pathing.py | 183 ++++++++++++++++++++++++++ 2021/13-Transparent Origami.py | 155 ++++++++++++++++++++++ 5 files changed, 837 insertions(+) create mode 100644 2021/09-Smoke Basin.py create mode 100644 2021/10-Syntax Scoring.py create mode 100644 2021/11-Dumbo Octopus.py create mode 100644 2021/12-Passage Pathing.py create mode 100644 2021/13-Transparent Origami.py diff --git a/2021/09-Smoke Basin.py b/2021/09-Smoke Basin.py new file mode 100644 index 0000000..92caa0d --- /dev/null +++ b/2021/09-Smoke Basin.py @@ -0,0 +1,111 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """2199943210 +3987894921 +9856789892 +8767896789 +9899965678""", + "expected": ["15", "1134"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["508", "1564640"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + area = grid.Grid() + area.text_to_dots(puzzle_input) + risk_level = 0 + for dot in area.dots: + if all( + [ + int(neighbor.terrain) > int(area.dots[dot].terrain) + for neighbor in area.dots[dot].get_neighbors() + ] + ): + risk_level += int(area.dots[dot].terrain) + 1 + + puzzle_actual_result = risk_level + + +else: + areas = puzzle_input.replace("9", "#") + area = grid.Grid() + area.text_to_dots(areas) + + area_graph = area.convert_to_graph() + basins = area_graph.dfs_groups() + sizes = sorted([len(x) for x in basins]) + + puzzle_actual_result = sizes[-1] * sizes[-2] * sizes[-3] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-09 18:13:45.008055 +# Part 1: 2021-12-09 18:18:53 +# Part 2: 2021-12-09 18:25:25 diff --git a/2021/10-Syntax Scoring.py b/2021/10-Syntax Scoring.py new file mode 100644 index 0000000..3ff7fc3 --- /dev/null +++ b/2021/10-Syntax Scoring.py @@ -0,0 +1,158 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, statistics +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """[({(<(())[]>[[{[]{<()<>> +[(()[<>])]({[<{<<[]>>( +{([(<{}[<>[]}>{[]{[(<()> +(((({<>}<{<{<>}{[]{[]{} +[[<[([]))<([[{}[[()]]] +[{[{({}]{}}([{[{{{}}([] +{<[[]]>}<{[{[{[]{()[[[] +[<(<(<(<{}))><([]([]() +<{([([[(<>()){}]>(<<{{ +<{([{{}}[<[[[<>{}]]]>[]]""", + "expected": ["26397", "288957"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["268845", "4038824534"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + symbols = ["()", "[]", "<>", "{}"] + opening_symbols = ["(", "[", "<", "{"] + match = {"(": ")", "[": "]", "<": ">", "{": "}"} + score = {")": 3, "]": 57, ">": 25137, "}": 1197} + syntax_score = 0 + for string in puzzle_input.split("\n"): + for i in range(15): + for symbol in symbols: + string = string.replace(symbol, "") + + while string != "" and string[-1] in opening_symbols: + string = string[:-1] + + if string == "": + continue + + for i in range(len(string)): + if string[i] in opening_symbols: + last_character = string[i] + else: + if string[i] == match[last_character]: + print("Cant compute") + else: + syntax_score += score[string[i]] + break + + puzzle_actual_result = syntax_score + + +else: + symbols = ["()", "[]", "<>", "{}"] + opening_symbols = ["(", "[", "<", "{"] + match = {"(": ")", "[": "]", "<": ">", "{": "}"} + score = {")": 1, "]": 2, ">": 4, "}": 3} + all_scores = [] + print_it = False + for string in puzzle_input.split("\n"): + syntax_score = 0 + string2 = string + # Determine whether it's an incomplete or erroneous line + for i in range(10): + for symbol in symbols: + string2 = string2.replace(symbol, "") + + while string2 != "" and string2[-1] in opening_symbols: + string2 = string2[:-1] + + if string2 != "": + continue + + # Remove matching elements + for i in range(15): + for symbol in symbols: + string = string.replace(symbol, "") + + missing_letters = "" + for letter in string: + if letter in match: + missing_letters = match[letter] + missing_letters + + for letter in missing_letters: + syntax_score *= 5 + syntax_score += score[letter] + + all_scores.append(syntax_score) + + puzzle_actual_result = statistics.median(all_scores) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-10 07:58:18.043288 +# Part 1: 2021-12-10 08:06:21 +# Part 2: 2021-12-10 08:30:02 diff --git a/2021/11-Dumbo Octopus.py b/2021/11-Dumbo Octopus.py new file mode 100644 index 0000000..5ce9c3c --- /dev/null +++ b/2021/11-Dumbo Octopus.py @@ -0,0 +1,230 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """11111 +19991 +19191 +19991 +11111""", + "expected": [ + """After step 1: +34543 +40004 +50005 +40004 +34543 + +After step 2: +45654 +51115 +61116 +51115 +45654""", + "Unknown", + ], +} + +test += 1 +test_data[test] = { + "input": """5483143223 +2745854711 +5264556173 +6141336146 +6357385478 +4167524645 +2176841721 +6882881134 +4846848554 +5283751526""", + "expected": ["""1656""", "195"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["1599", "418"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + +dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {}, + " ": {}, + "+": {dir: all_directions for dir in all_directions}, + "|": {north: [north, south], south: [north, south]}, + "^": {north: [north, south], south: [north, south]}, + "v": {north: [north, south], south: [north, south]}, + "-": {east: [east, west], west: [east, west]}, + ">": {east: [east, west], west: [east, west]}, + "<": {east: [east, west], west: [east, west]}, + "\\": {north: [east], east: [north], south: [west], west: [south]}, + "/": {north: [west], east: [south], south: [east], west: [north]}, + "X": {dir: all_directions for dir in all_directions}, +} + + +grid.Grid.all_directions = directions_diagonals + +if part_to_test == 1: + area = grid.Grid() + area.all_directions = directions_diagonals + area.direction_default = directions_diagonals + + area.text_to_dots(puzzle_input, convert_to_int=True) + nb_flashes = 0 + + for i in range(100): + for position in area.dots: + area.dots[position].terrain += 1 + + all_flashes = [] + while any( + [ + area.dots[position].terrain > 9 + for position in area.dots + if position not in all_flashes + ] + ): + flashes = [ + position + for position in area.dots + if area.dots[position].terrain > 9 and position not in all_flashes + ] + nb_flashes += len(flashes) + + neighbors = { + dot: 0 for flash in flashes for dot in area.dots[flash].get_neighbors() + } + for flash in flashes: + for neighbor in area.dots[flash].get_neighbors(): + neighbors[neighbor] += 1 + + for neighbor in neighbors: + neighbor.terrain += neighbors[neighbor] + + all_flashes += flashes + + for flash in all_flashes: + area.dots[flash].terrain = 0 + + puzzle_actual_result = nb_flashes + + +else: + area = grid.Grid() + area.all_directions = directions_diagonals + area.direction_default = directions_diagonals + + area.text_to_dots(puzzle_input, convert_to_int=True) + nb_flashes = 0 + + i = 0 + while True and i <= 500: + for position in area.dots: + area.dots[position].terrain += 1 + + all_flashes = [] + while any( + [ + area.dots[position].terrain > 9 + for position in area.dots + if position not in all_flashes + ] + ): + flashes = [ + position + for position in area.dots + if area.dots[position].terrain > 9 and position not in all_flashes + ] + nb_flashes += len(flashes) + + neighbors = { + dot: 0 for flash in flashes for dot in area.dots[flash].get_neighbors() + } + for flash in flashes: + for neighbor in area.dots[flash].get_neighbors(): + neighbors[neighbor] += 1 + + for neighbor in neighbors: + neighbor.terrain += neighbors[neighbor] + + all_flashes += flashes + + for flash in all_flashes: + area.dots[flash].terrain = 0 + + i += 1 + + if all([area.dots[position].terrain == 0 for position in area.dots]): + break + + puzzle_actual_result = i + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-11 10:42:26.736695 +# Part 1: 2021-12-11 13:17:05 (1h45 outsite) +# Part 2: 2021-12-11 13:18:45 diff --git a/2021/12-Passage Pathing.py b/2021/12-Passage Pathing.py new file mode 100644 index 0000000..3b6eb58 --- /dev/null +++ b/2021/12-Passage Pathing.py @@ -0,0 +1,183 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """start-A +start-b +A-c +A-b +b-d +A-end +b-end""", + "expected": ["10", "36"], +} + +test += 1 +test_data[test] = { + "input": """dc-end +HN-start +start-kj +dc-start +dc-HN +LN-dc +HN-end +kj-sa +kj-HN +kj-dc""", + "expected": ["19", "103"], +} + +test += 1 +test_data[test] = { + "input": """fs-end +he-DX +fs-he +start-DX +pj-DX +end-zg +zg-sl +zg-pj +pj-he +RW-he +fs-DX +pj-RW +zg-RW +start-pj +he-WI +zg-he +pj-fs +start-RW""", + "expected": ["226", "3509"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["4011", "108035"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + edges = {} + vertices = set() + for string in puzzle_input.split("\n"): + a, b = string.split("-") + if not a in edges: + edges[a] = {} + if a != "end": + edges[a].update({b: 1}) + if b not in edges: + edges[b] = {} + if b != "end": + edges[b].update({a: 1}) + vertices.add(a) + vertices.add(b) + + caves = graph.Graph(vertices, edges) + caves.is_vertex_valid_for_path = ( + lambda path, vertex: vertex.isupper() or not vertex in path + ) + caves.find_all_paths("start", "end") + puzzle_actual_result = len(caves.paths) + + +else: + edges = {} + vertices = set() + for string in puzzle_input.split("\n"): + a, b = string.split("-") + if not a in edges: + edges[a] = {} + if a != "end": + edges[a].update({b: 1}) + if b not in edges: + edges[b] = {} + if b != "end": + edges[b].update({a: 1}) + vertices.add(a) + vertices.add(b) + + caves = graph.Graph(vertices, edges) + small_caves = [a for a in edges if a.islower()] + + def is_vertex_valid_for_path(path, vertex): + if vertex.isupper(): + return True + + if vertex == "start": + return False + + if vertex in path: + visited = Counter(path) + + return all([visited[a] < 2 for a in small_caves]) + + return True + + caves.is_vertex_valid_for_path = is_vertex_valid_for_path + caves.find_all_paths("start", "end") + puzzle_actual_result = len(caves.paths) + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-12 09:16:38.023299 +# Part 1: 2021-12-12 09:57:38 +# Part 2: 2021-12-12 10:07:46 diff --git a/2021/13-Transparent Origami.py b/2021/13-Transparent Origami.py new file mode 100644 index 0000000..69c0c07 --- /dev/null +++ b/2021/13-Transparent Origami.py @@ -0,0 +1,155 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """6,10 +0,14 +9,10 +0,3 +10,4 +4,11 +6,0 +6,12 +4,1 +0,13 +10,12 +3,4 +3,0 +8,4 +1,10 +2,14 +8,10 +9,0 + +fold along y=7 +fold along x=5""", + "expected": ["17", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["695", "GJZGLUPJ"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +if part_to_test == 1: + dots_str, folds = puzzle_input.split("\n\n") + dots = [] + for dot in dots_str.split("\n"): + coords = ints(dot) + dots.append(coords[0] - 1j * coords[1]) + + fold = folds.split("\n")[0] + coords = fold.split("=") + if coords[0] == "fold along x": + coords = int(coords[1]) + dots = [ + dot if dot.real <= coords else 2 * coords - dot.real + 1j * dot.imag + for dot in dots + ] + else: + coords = -int(coords[1]) + dots = [ + dot if dot.imag >= coords else dot.real + 1j * (2 * coords - dot.imag) + for dot in dots + ] + + dots = set(dots) + + puzzle_actual_result = len(dots) + + +else: + dots_str, folds = puzzle_input.split("\n\n") + dots = [] + for dot in dots_str.split("\n"): + coords = ints(dot) + dots.append(coords[0] - 1j * coords[1]) + + for fold in folds.split("\n"): + coords = fold.split("=") + if coords[0] == "fold along x": + coords = int(coords[1]) + dots = [ + dot if dot.real <= coords else 2 * coords - dot.real + 1j * dot.imag + for dot in dots + ] + else: + coords = -int(coords[1]) + dots = [ + dot if dot.imag >= coords else dot.real + 1j * (2 * coords - dot.imag) + for dot in dots + ] + + dots = set(dots) + + zone = grid.Grid(dots) + print(zone.dots_to_text()) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-13 08:13:03.925958 +# Part 1: 2021-12-13 08:23:33 +# Part 2: 2021-12-13 08:26:24 From 1f84a2a565831346574fce9f4a4bf5fd56303f44 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 13 Dec 2021 08:32:51 +0100 Subject: [PATCH 123/143] Fixed issue in library dot.py --- 2021/dot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/2021/dot.py b/2021/dot.py index 58c762e..448f6ef 100644 --- a/2021/dot.py +++ b/2021/dot.py @@ -169,7 +169,7 @@ def manhattan_distance(self, reference=0): return abs(self.position.imag - ref.imag) + abs(self.position.real - ref.real) def set_terrain(self, terrain): - self.terrain = terrain or self.default_terrain + self.terrain = terrain or self.terrain_default self.is_walkable, self.is_waypoint = self.terrain_map.get( terrain, self.terrain_map[self.terrain_default] ) From 7dd94871f71798fa62f1f77a3f92a1d49c53dc92 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 15 Dec 2021 09:50:05 +0100 Subject: [PATCH 124/143] Added days 2021-14 and 2021-15 --- 2021/14-Extended Polymerization.py | 144 +++++++++++++++++++++++++++++ 2021/15-Chiton.py | 120 ++++++++++++++++++++++++ 2 files changed, 264 insertions(+) create mode 100644 2021/14-Extended Polymerization.py create mode 100644 2021/15-Chiton.py diff --git a/2021/14-Extended Polymerization.py b/2021/14-Extended Polymerization.py new file mode 100644 index 0000000..8110805 --- /dev/null +++ b/2021/14-Extended Polymerization.py @@ -0,0 +1,144 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """NNCB + +CH -> B +HH -> N +CB -> H +NH -> C +HB -> C +HC -> B +HN -> C +NN -> C +BH -> H +NC -> B +NB -> B +BN -> B +BB -> N +BC -> B +CC -> N +CN -> C""", + "expected": ["1588", "2188189693529"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["3259", "3459174981021"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + +nb_counts = 10 if part_to_test == 1 else 40 + + +# This was the first, obvious solution +# Works well for part 1, not for part 2 +# source = puzzle_input.split("\n\n")[0] +# maps = puzzle_input.split("\n\n")[1] +# mapping = {} +# for string in maps.split("\n"): +# mapping[string.split(' -> ')[0]] = string.split(' -> ')[1] + string[1] + +# word = source +# for j in range(nb_counts): +# target = word[0] +# target += ''.join([mapping[word[i:i+2]] if word[i:i+2] in mapping else word[i+1] for i in range(len(word)-1)]) + +# word = target + + +# occurrences = Counter(word) +# print (occurrences) +# puzzle_actual_result = max(occurrences.values()) - min(occurrences.values()) + + +source = puzzle_input.split("\n\n")[0] +maps = puzzle_input.split("\n\n")[1] +mapping = {} +for string in maps.split("\n"): + mapping[string.split(" -> ")[0]] = string.split(" -> ")[1] + +elem_count = Counter(source) +pair_count = defaultdict(int) +for i in range(len(source) - 1): + pair_count[source[i : i + 2]] += 1 + +print(elem_count, pair_count) + +for j in range(nb_counts): + for pair, nb_pair in pair_count.copy().items(): + pair_count[pair] -= nb_pair + new_elem = mapping[pair] + pair_count[pair[0] + new_elem] += nb_pair + pair_count[new_elem + pair[1]] += nb_pair + elem_count[new_elem] += nb_pair + + +puzzle_actual_result = max(elem_count.values()) - min(elem_count.values()) + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-14 08:37:51.348152 +# Part 1: 2021-12-14 08:42:56 +# Part 2: 2021-12-14 08:56:13 diff --git a/2021/15-Chiton.py b/2021/15-Chiton.py new file mode 100644 index 0000000..f618240 --- /dev/null +++ b/2021/15-Chiton.py @@ -0,0 +1,120 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """1163751742 +1381373672 +2136511328 +3694931569 +7463417111 +1319128137 +1359912421 +3125421639 +1293138521 +2311944581""", + "expected": ["40", "315"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["769", "2963"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +caves = grid.Grid() +caves.text_to_dots(puzzle_input, convert_to_int=True) + +width, height = caves.get_size() + +if part_to_test == 2: + list_caves = [] + for x in range(5): + for y in range(5): + new_cave = copy.deepcopy(caves) + for dot in new_cave.dots: + new_cave.dots[dot].terrain = ( + new_cave.dots[dot].terrain + x + y - 1 + ) % 9 + 1 + list_caves.append(new_cave) + caves = grid.merge_grids(list_caves, 5, 5) + +edges = {} +for dot in caves.dots: + neighbors = caves.dots[dot].get_neighbors() + edges[caves.dots[dot]] = {target: target.terrain for target in neighbors} + +min_x, max_x, min_y, max_y = caves.get_box() +start = caves.dots[min_x + 1j * max_y] +end = caves.dots[max_x + 1j * min_y] + +caves_graph = graph.WeightedGraph(caves.dots, edges) +caves_graph.dijkstra(start, end) +puzzle_actual_result = caves_graph.distance_from_start[end] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-15 08:16:43.421298 +# Part 1: 2021-12-15 08:38:06 +# Part 2: 2021-12-15 09:48:14 From 93a9d3c5c9087c070523b4e9c9e211ef065acab7 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 15 Dec 2021 09:50:17 +0100 Subject: [PATCH 125/143] Fixed issue in graph --- 2021/graph.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/2021/graph.py b/2021/graph.py index 1c8f575..1d3652c 100644 --- a/2021/graph.py +++ b/2021/graph.py @@ -387,9 +387,7 @@ def dijkstra(self, start, end=None): # Adding for future examination if type(neighbor) == complex: - heapq.heappush( - frontier, (current_distance + weight, SuperComplex(neighbor)) - ) + heapq.heappush(frontier, (current_distance + weight, neighbor)) else: heapq.heappush(frontier, (current_distance + weight, neighbor)) From 7a9a27e162e7257241af73b082b065091b50b23c Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 15 Dec 2021 21:21:43 +0100 Subject: [PATCH 126/143] Removed several prints --- 2021/02-Dive.py | 2 +- 2021/06-Lanternfish.py | 2 +- 2021/14-Extended Polymerization.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/2021/02-Dive.py b/2021/02-Dive.py index 43b8ada..19389b7 100644 --- a/2021/02-Dive.py +++ b/2021/02-Dive.py @@ -95,7 +95,7 @@ def words(s: str): position += int(delta) position += int(delta) * abs(aim.imag) * 1j - print(string, aim, position) + # print(string, aim, position) puzzle_actual_result = int(abs(position.imag) * abs(position.real)) diff --git a/2021/06-Lanternfish.py b/2021/06-Lanternfish.py index b7c4ada..690c5d5 100644 --- a/2021/06-Lanternfish.py +++ b/2021/06-Lanternfish.py @@ -92,7 +92,7 @@ def words(s: str): new_fish_plus_2 = new_fish_plus_1.copy() new_fish_plus_1 = new_fish.copy() - print("End of day", day, ":", sum(fishes.values()) + sum(new_fish_plus_2.values())) + # print("End of day", day, ":", sum(fishes.values()) + sum(new_fish_plus_2.values())) puzzle_actual_result = sum(fishes.values()) + sum(new_fish_plus_2.values()) diff --git a/2021/14-Extended Polymerization.py b/2021/14-Extended Polymerization.py index 8110805..60dbbe4 100644 --- a/2021/14-Extended Polymerization.py +++ b/2021/14-Extended Polymerization.py @@ -121,7 +121,7 @@ def words(s: str): for i in range(len(source) - 1): pair_count[source[i : i + 2]] += 1 -print(elem_count, pair_count) +# print(elem_count, pair_count) for j in range(nb_counts): for pair, nb_pair in pair_count.copy().items(): From 233f2ed6357b088ff383e090161e0386d53937d2 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 18 Dec 2021 23:55:13 +0100 Subject: [PATCH 127/143] Added days 2021-16, 2021-17, 2021-18 --- 2021/16-Packet Decoder.py | 210 ++++++++++++++++++ 2021/17-Trick Shot.py | 134 ++++++++++++ 2021/18-Snailfish.py | 432 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 776 insertions(+) create mode 100644 2021/16-Packet Decoder.py create mode 100644 2021/17-Trick Shot.py create mode 100644 2021/18-Snailfish.py diff --git a/2021/16-Packet Decoder.py b/2021/16-Packet Decoder.py new file mode 100644 index 0000000..9baea66 --- /dev/null +++ b/2021/16-Packet Decoder.py @@ -0,0 +1,210 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict +from functools import reduce + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """D2FE28""", + "expected": ["number: 2021", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """38006F45291200""", + "expected": ["2 subpackets: 10 & 20", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """EE00D40C823060""", + "expected": ["3 subpackets: 1, 2, 3", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """8A004A801A8002F478""", + "expected": ["16", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """C200B40A82""", + "expected": ["Unknown", "3"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["877", "194435634456"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +def analyze_packet(binary_value): + p_version = int(binary_value[0:3], 2) + p_type = int(binary_value[3:6], 2) + position = 6 + + if p_type == 4: + group = binary_value[position] + number = "" + while binary_value[position] == "1": + number += binary_value[position + 1 : position + 5] + position += 5 + number += binary_value[position + 1 : position + 5] + position += 5 + + return { + "version": p_version, + "type": p_type, + "value": int(number, 2), + "length": position, + } + + else: + length_type = int(binary_value[position], 2) + position += 1 + if length_type == 0: + length_bits = int(binary_value[position : position + 15], 2) + position += 15 + subpackets_bits = binary_value[position : position + length_bits] + + subpacket_position = 0 + subpackets = [] + while subpacket_position < len(subpackets_bits): + subpacket = analyze_packet(subpackets_bits[subpacket_position:]) + subpackets.append(subpacket) + subpacket_position += subpacket["length"] + + else: + nb_packets = int(binary_value[position : position + 11], 2) + position += 11 + subpackets_bits = binary_value[position:] + + subpacket_position = 0 + subpackets = [] + while len(subpackets) != nb_packets: + subpacket = analyze_packet(subpackets_bits[subpacket_position:]) + subpackets.append(subpacket) + subpacket_position += subpacket["length"] + + if p_type == 0: + value = sum([p["value"] for p in subpackets]) + elif p_type == 1: + value = reduce(lambda x, y: x * y, [p["value"] for p in subpackets]) + elif p_type == 2: + value = min([p["value"] for p in subpackets]) + elif p_type == 3: + value = max([p["value"] for p in subpackets]) + elif p_type == 5: + value = 1 if subpackets[0]["value"] > subpackets[1]["value"] else 0 + elif p_type == 6: + value = 1 if subpackets[0]["value"] < subpackets[1]["value"] else 0 + elif p_type == 7: + value = 1 if subpackets[0]["value"] == subpackets[1]["value"] else 0 + + return { + "version": p_version, + "type": p_type, + "value": value, + "length": position + subpacket_position, + "subpackets": subpackets, + } + + +def sum_version(packet): + total_version = packet["version"] + if "subpackets" in packet: + total_version += sum([sum_version(p) for p in packet["subpackets"]]) + + return total_version + + +def operate_packet(packet): + if "value" in packet: + return packet["value"] + + else: + + total_version += sum([sum_version(p) for p in packet["subpackets"]]) + + return total_version + + +message = "{0:b}".format(int(puzzle_input, 16)) +while len(message) % 4 != 0: + message = "0" + message + + +packets = analyze_packet(message) + +if part_to_test == 1: + puzzle_actual_result = sum_version(packets) + +else: + puzzle_actual_result = packets["value"] + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-16 08:09:42.385082 +# Past 1: 2021-12-16 08:43:04 +# Past 2: 2021-12-16 09:10:53 diff --git a/2021/17-Trick Shot.py b/2021/17-Trick Shot.py new file mode 100644 index 0000000..a3c4ec8 --- /dev/null +++ b/2021/17-Trick Shot.py @@ -0,0 +1,134 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """target area: x=20..30, y=-10..-5""", + "expected": ["45", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +x_min, x_max, y_min, y_max = ints(puzzle_input) + +possible_x = [] +for x_speed_init in range(1, 252): # 251 is the max x from my puzzle input + x = 0 + step = 0 + x_speed = x_speed_init + while x <= x_max: + x += x_speed + if x_speed > 0: + x_speed -= 1 + step += 1 + if x >= x_min and x <= x_max: + possible_x.append((x_speed_init, x_speed, step)) + if x_speed == 0: + break + +possible_y = [] +for y_speed_init in range( + -89, 250 +): # -89 is the min y from my puzzle input, 250 is just a guess + y = 0 + max_y = 0 + step = 0 + y_speed = y_speed_init + while y >= y_min: + y += y_speed + y_speed -= 1 + step += 1 + max_y = max(max_y, y) + if y >= y_min and y <= y_max: + possible_y.append((y_speed_init, y_speed, step, max_y)) + +possible_setup = [] +overall_max_y = 0 +for y_setup in possible_y: + y_speed_init, y_speed, y_step, max_y = y_setup + overall_max_y = max(overall_max_y, max_y) + for x_setup in possible_x: + x_speed_init, x_speed, x_step = x_setup + if y_step == x_step: + possible_setup.append((x_speed_init, y_speed_init)) + elif y_step >= x_step and x_speed == 0: + possible_setup.append((x_speed_init, y_speed_init)) + +possible_setup = sorted(list(set(possible_setup))) + +if part_to_test == 1: + puzzle_actual_result = overall_max_y +else: + # print (''.join([str(x)+','+str(y)+'\n' for (x, y) in possible_setup])) + puzzle_actual_result = len(possible_setup) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-17 07:43:17.756046 +# Part 1: 2021-12-17 08:20:09 +# Part 2: 2021-12-17 09:11:05 diff --git a/2021/18-Snailfish.py b/2021/18-Snailfish.py new file mode 100644 index 0000000..4543c80 --- /dev/null +++ b/2021/18-Snailfish.py @@ -0,0 +1,432 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, json +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """[1,2] +[[1,2],3] +[9,[8,7]]""", + "expected": ["Unknown", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[[[[[9,8],1],2],3],4]""", + "expected": ["[[[[0,9],2],3],4]", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[7,[6,[5,[4,[3,2]]]]]""", + "expected": ["[7,[6,[5,[7,0]]]]", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[[[[4,3],4],4],[7,[[8,4],9]]] +[1,1]""", + "expected": ["[[[[0,7],4],[[7,8],[6,0]]],[8,1]]", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[1,1] +[2,2] +[3,3] +[4,4] +[5,5] +[6,6]""", + "expected": ["[[[[5,0],[7,4]],[5,5]],[6,6]]", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[[[0,[4,5]],[0,0]],[[[4,5],[2,6]],[9,5]]] +[7,[[[3,7],[4,3]],[[6,3],[8,8]]]] +[[2,[[0,8],[3,4]]],[[[6,7],1],[7,[1,6]]]] +[[[[2,4],7],[6,[0,5]]],[[[6,8],[2,8]],[[2,1],[4,5]]]] +[7,[5,[[3,8],[1,4]]]] +[[2,[2,2]],[8,[8,1]]] +[2,9] +[1,[[[9,3],9],[[9,0],[0,7]]]] +[[[5,[7,4]],7],1] +[[[[4,2],2],6],[8,7]]""", + "expected": ["[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[9,1]""", + "expected": ["29", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[[[[8,7],[7,7]],[[8,6],[7,7]]],[[[0,7],[6,6]],[8,7]]]""", + "expected": ["3488", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """[[[0,[5,8]],[[1,7],[9,6]]],[[4,[1,2]],[[1,4],2]]] +[[[5,[2,8]],4],[5,[[9,9],0]]] +[6,[[[6,2],[5,6]],[[7,6],[4,7]]]] +[[[6,[0,7]],[0,9]],[4,[9,[9,0]]]] +[[[7,[6,4]],[3,[1,3]]],[[[5,5],1],9]] +[[6,[[7,3],[3,2]]],[[[3,8],[5,7]],4]] +[[[[5,4],[7,7]],8],[[8,3],8]] +[[9,3],[[9,9],[6,[4,9]]]] +[[2,[[7,7],7]],[[5,8],[[9,3],[0,2]]]] +[[[[5,2],5],[8,[3,7]]],[[5,[7,5]],[4,4]]]""", + "expected": ["4140", "3993"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["3486", "4747"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +class BinaryTreeNode: + def __init__(self, data, parent): + self.left = None + self.right = None + self.data = data + self.parent = parent + + def neighbor_left(self): + parent = self.parent + child = self + if parent.left == child: + while parent.left == child: + child = parent + parent = parent.parent + if parent == None: + return None + + parent = parent.left + + while parent.right != None: + parent = parent.right + return parent + + def neighbor_right(self): + parent = self.parent + child = self + if parent.right == child: + while parent.right == child: + child = parent + parent = parent.parent + if parent == None: + return None + + parent = parent.right + + while parent.left != None: + parent = parent.left + return parent + + def __repr__(self): + return "Node : " + str(self.data) + " - ID : " + str(id(self)) + + +def convert_to_tree(node, number): + a, b = number + if type(a) == list: + node.left = convert_to_tree(BinaryTreeNode("", node), a) + else: + node.left = BinaryTreeNode(a, node) + if type(b) == list: + node.right = convert_to_tree(BinaryTreeNode("", node), b) + else: + node.right = BinaryTreeNode(b, node) + return node + + +def explode_tree(node, depth=0): + if node.left != None and type(node.left.data) != int: + explode_tree(node.left, depth + 1) + if node.right != None and type(node.right.data) != int: + explode_tree(node.right, depth + 1) + + if depth >= 4 and type(node.left.data) == int and type(node.right.data) == int: + add_to_left = node.left.neighbor_left() + if add_to_left != None: + add_to_left.data += node.left.data + add_to_right = node.right.neighbor_right() + if add_to_right != None: + add_to_right.data += node.right.data + node.data = 0 + del node.left + del node.right + node.left = None + node.right = None + + has_exploded = True + return node + + +def split_tree(node): + global has_split + if has_split: + return + + if type(node.data) == int and node.data >= 10: + node.left = BinaryTreeNode(node.data // 2, node) + node.right = BinaryTreeNode(node.data // 2 + node.data % 2, node) + node.data = "" + has_split = True + + elif node.data == "": + split_tree(node.left) + split_tree(node.right) + + +def print_tree(node, string=""): + if type(node.left.data) == int: + string = "[" + str(node.left.data) + else: + string = "[" + print_tree(node.left) + + string += "," + + if type(node.right.data) == int: + string += str(node.right.data) + "]" + else: + string += print_tree(node.right) + "]" + + return string + + +def calculate_magnitude(node): + if node.data == "": + return 3 * calculate_magnitude(node.left) + 2 * calculate_magnitude(node.right) + else: + return node.data + + +if part_to_test == 1: + root = "" + for string in puzzle_input.split("\n"): + number = json.loads(string) + if root == "": + root = BinaryTreeNode("", None) + convert_to_tree(root, number) + else: + old_root = root + root = BinaryTreeNode("", None) + root.left = old_root + old_root.parent = root + root.right = BinaryTreeNode("", root) + convert_to_tree(root.right, json.loads(string)) + + has_exploded = True + has_split = True + while has_exploded or has_split: + has_exploded = False + has_split = False + root = explode_tree(root) + split_tree(root) + + # print (print_tree(root)) + + print(print_tree(root)) + puzzle_actual_result = calculate_magnitude(root) + + +else: + max_magnitude = 0 + for combination in itertools.permutations(puzzle_input.split("\n"), 2): + root = "" + for string in combination: + number = json.loads(string) + if root == "": + root = BinaryTreeNode("", None) + convert_to_tree(root, number) + else: + old_root = root + root = BinaryTreeNode("", None) + root.left = old_root + old_root.parent = root + root.right = BinaryTreeNode("", root) + convert_to_tree(root.right, json.loads(string)) + + has_exploded = True + has_split = True + while has_exploded or has_split: + has_exploded = False + has_split = False + root = explode_tree(root) + split_tree(root) + + magnitude = calculate_magnitude(root) + + max_magnitude = max(max_magnitude, magnitude) + + puzzle_actual_result = max_magnitude + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) + + +################################################# + +# This was the first attempt +# It just doesn't work. Way too error-prone... + +################################################# + +# def explode_number(number, depth=0, a='_', b='_'): +# global has_exploded +# print ('start explode', depth, number) + +# left, right = number +# if type(left) == list: +# left, a, b = explode_number(left, depth+1, a, b) +# if type(right) == list: +# right, a, b = explode_number(right, depth+1, a, b) +# # This will recurse until left and right are the innermost numbers +# # Once a and b are identified (from innermost numbers), then left or right == _ + +# if depth > 3: +# has_exploded = True +# a = left +# b = right +# print ('found', a, b) +# return ('_', a, b) + +# print ('temp1', a, left, b, right) + +# if a != '_' and type(left) == int: +# left += a +# a = '_' +# elif a == '_' and b != '_' and type(left) == int: +# left += b +# b = '_' +# if b != '_' and type(right) == int: +# right += b +# b = '_' +# elif b == '_' and a != '_' and type(right) == int: +# right += a +# a = '_' + +# print ('temp2', a, left, b, right) + +# left = 0 if left=='_' else left +# right = 0 if right=='_' else right + +# print ('end', depth, [left, right]) + +# return ([left, right], a, b) + + +# def split_number(number): +# global has_split +# print ('start split', number) + +# left, right = number +# if type(left) == list: +# left = split_number(left) +# if type(right) == list: +# right = split_number(right) + +# if type(left) == int and left >= 10: +# has_split = True +# left = [ left //2,left//2+left%2] +# if type(right) == int and right >= 10: +# has_split = True +# right = [ right //2,right//2+right%2] + +# print ('end split', number) + +# return [left, right] + + +# if part_to_test == 1: +# number = [] +# for string in puzzle_input.split("\n"): +# if number == []: +# number = json.loads(string) +# else: +# number = [number, json.loads(string)] + +# depth = 0 +# a = '' +# b = '' +# has_exploded = True +# has_split = True +# i = 0 +# while (has_exploded or has_split) and i != 5: +# i += 1 +# has_exploded = False +# has_split = False +# number = explode_number(number)[0] +# number = split_number(number) + + +# print (number) + + +# Date created: 2021-12-18 11:47:53.521779 +# Part 1: 2021-12-18 23:38:34 +# Part 2: 2021-12-18 23:53:07 From b38c2a2adc881b8fa7466db18908e27150795807 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sun, 19 Dec 2021 17:37:54 +0100 Subject: [PATCH 128/143] Added day 2021-19 --- 2021/19-Beacon Scanner.py | 446 +++++++++++++++++++++++ 2021/19-Beacon Scanner.v1 (fails).py | 507 +++++++++++++++++++++++++++ 2 files changed, 953 insertions(+) create mode 100644 2021/19-Beacon Scanner.py create mode 100644 2021/19-Beacon Scanner.v1 (fails).py diff --git a/2021/19-Beacon Scanner.py b/2021/19-Beacon Scanner.py new file mode 100644 index 0000000..e2ad4dd --- /dev/null +++ b/2021/19-Beacon Scanner.py @@ -0,0 +1,446 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, math +from collections import Counter, deque, defaultdict +from functools import lru_cache + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """--- scanner 0 --- +404,-588,-901 +528,-643,409 +-838,591,734 +390,-675,-793 +-537,-823,-458 +-485,-357,347 +-345,-311,381 +-661,-816,-575 +-876,649,763 +-618,-824,-621 +553,345,-567 +474,580,667 +-447,-329,318 +-584,868,-557 +544,-627,-890 +564,392,-477 +455,729,728 +-892,524,684 +-689,845,-530 +423,-701,434 +7,-33,-71 +630,319,-379 +443,580,662 +-789,900,-551 +459,-707,401 + +--- scanner 1 --- +686,422,578 +605,423,415 +515,917,-361 +-336,658,858 +95,138,22 +-476,619,847 +-340,-569,-846 +567,-361,727 +-460,603,-452 +669,-402,600 +729,430,532 +-500,-761,534 +-322,571,750 +-466,-666,-811 +-429,-592,574 +-355,545,-477 +703,-491,-529 +-328,-685,520 +413,935,-424 +-391,539,-444 +586,-435,557 +-364,-763,-893 +807,-499,-711 +755,-354,-619 +553,889,-390 + +--- scanner 2 --- +649,640,665 +682,-795,504 +-784,533,-524 +-644,584,-595 +-588,-843,648 +-30,6,44 +-674,560,763 +500,723,-460 +609,671,-379 +-555,-800,653 +-675,-892,-343 +697,-426,-610 +578,704,681 +493,664,-388 +-671,-858,530 +-667,343,800 +571,-461,-707 +-138,-166,112 +-889,563,-600 +646,-828,498 +640,759,510 +-630,509,768 +-681,-892,-333 +673,-379,-804 +-742,-814,-386 +577,-820,562 + +--- scanner 3 --- +-589,542,597 +605,-692,669 +-500,565,-823 +-660,373,557 +-458,-679,-417 +-488,449,543 +-626,468,-788 +338,-750,-386 +528,-832,-391 +562,-778,733 +-938,-730,414 +543,643,-506 +-524,371,-870 +407,773,750 +-104,29,83 +378,-903,-323 +-778,-728,485 +426,699,580 +-438,-605,-362 +-469,-447,-387 +509,732,623 +647,635,-688 +-868,-804,481 +614,-800,639 +595,780,-596 + +--- scanner 4 --- +727,592,562 +-293,-554,779 +441,611,-461 +-714,465,-776 +-743,427,-804 +-660,-479,-426 +832,-632,460 +927,-485,-438 +408,393,-506 +466,436,-512 +110,16,151 +-258,-428,682 +-393,719,612 +-211,-452,876 +808,-476,-593 +-575,615,604 +-485,667,467 +-680,325,-822 +-627,-443,-432 +872,-547,-609 +833,512,582 +807,604,487 +839,-516,451 +891,-625,532 +-652,-548,-490 +30,-46,-14""", + "expected": ["79", "3621"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["355", "10842"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +@lru_cache +def cos(deg): + return int( + math.cos(math.radians(deg)) + if abs(math.cos(math.radians(deg))) >= 10 ** -15 + else 0 + ) + + +@lru_cache +def sin(deg): + return int( + math.sin(math.radians(deg)) + if abs(math.sin(math.radians(deg))) >= 10 ** -15 + else 0 + ) + + +# All possible rotations (formula from Wikipedia) +rotations_raw = [ + [ + [ + cos(alpha) * cos(beta), + cos(alpha) * sin(beta) * sin(gamma) - sin(alpha) * cos(gamma), + cos(alpha) * sin(beta) * cos(gamma) + sin(alpha) * sin(gamma), + ], + [ + sin(alpha) * cos(beta), + sin(alpha) * sin(beta) * sin(gamma) + cos(alpha) * cos(gamma), + sin(alpha) * sin(beta) * cos(gamma) - cos(alpha) * sin(gamma), + ], + [-sin(beta), cos(beta) * sin(gamma), cos(beta) * cos(gamma)], + ] + for alpha in (0, 90, 180, 270) + for beta in (0, 90, 180, 270) + for gamma in (0, 90, 180, 270) +] + +rotations = [] +for rot in rotations_raw: + if rot not in rotations: + rotations.append(rot) + +# Positionning of items in space (beacons or scanners) +class Point: + def __init__(self, position): + self.position = position + self.distances_cache = "" + + # Manhattan distance for part 2 + @lru_cache + def manhattan_distance(self, other): + distance = sum([abs(other.position[i] - self.position[i]) for i in (0, 1, 2)]) + return distance + + # Regular distance + @lru_cache + def distance(self, other): + distance = sum([(other.position[i] - self.position[i]) ** 2 for i in (0, 1, 2)]) + return distance + + def distances(self, others): + if not self.distances_cache: + self.distances_cache = {self.distance(other) for other in others} + return self.distances_cache + + def rotate(self, rotation): + return Point( + [ + sum(rotation[i][j] * self.position[j] for j in (0, 1, 2)) + for i in (0, 1, 2) + ] + ) + + def __add__(self, other): + return Point([self.position[i] + other.position[i] for i in (0, 1, 2)]) + + def __sub__(self, other): + return Point([self.position[i] - other.position[i] for i in (0, 1, 2)]) + + def __repr__(self): + return self.position.__repr__() + + +# Scanners: has a list of beacons + an abolute position (if it's known) +class Scanner: + def __init__(self, name, position=None): + self.name = name + if position: + self.position = Point(position) + else: + self.position = "" + self.beacons = [] + + # Useful for debug + def __repr__(self): + name = "Scanner " + str(self.name) + " at " + position = self.position.__repr__() if self.position else "Unknown" + name += position + name += " with " + str(len(self.beacons)) + " beacons" + + return name + + # Lazy version - calls Point's manhattan distante + def manhattan_distance(self, other): + return self.position.manhattan_distance(other.position) + + +# Parse the data +scanners = [] +for scanner in puzzle_input.split("\n\n"): + for beacon_id, beacon in enumerate(scanner.split("\n")): + if beacon_id == 0: + if scanners == []: + scanners.append(Scanner(beacon.split(" ")[2], [0, 0, 0])) + else: + scanners.append(Scanner(beacon.split(" ")[2])) + continue + scanners[-1].beacons.append(Point(ints(beacon))) + +# At this point, we have a list of scanners + their beacons in relative position +# Only scanners[0] has an absolute position +# print (scanners) + +# Match scanners between them +already_tested = [] +while [s for s in scanners if s.position == ""]: + for scanner1 in [ + s for s in scanners if s.position != "" and s not in already_tested + ]: + # print () + # print ('scanning from', scanner1) + already_tested.append(scanner1) + for scanner2 in [s for s in scanners if s.position == ""]: + # print ('scanning to ', scanner2) + found_match = False + pairs = [] + # Calculate distances for 2 beacons (1 in each scanner) + # If there are 12 matching distances, we have found a pair of scanners + # We need 2 beacons from each scanner to deduce rotation and position + for s1beacon in scanner1.beacons: + distances1 = s1beacon.distances(scanner1.beacons) + for s2beacon in scanner2.beacons: + distances2 = s2beacon.distances(scanner2.beacons) + if len(distances1.intersection(distances2)) == 12: + pairs.append((s1beacon, s2beacon)) + + if len(pairs) == 2: + break + if len(pairs) == 2: + break + if len(pairs) == 2: + # print ('Found matching scanners', scanner1, scanner2) + found_match = True + + s1_a = pairs[0][0] + s1_b = pairs[1][0] + + # print (pairs) + + found_rotation_match = False + for i in [0, 1]: + # The 2 beacons may not be in the right order (since we check distances) + s2_a = pairs[i][1] + s2_b = pairs[1 - i][1] + # Search for the proper rotation + for rotation in rotations: + # print ((s2_a.rotate(rotation) - s1_a), (s2_b.rotate(rotation) - s1_b), rotation) + # We rotate S2 so that it matches the orientation of S1 + # When it matches, then S2.B1 - S1.B1 = S2.B2 - S1.B2 (in terms of x,y,z position) + if (s2_a.rotate(rotation) - s1_a).position == ( + s2_b.rotate(rotation) - s1_b + ).position: + # print ('Found rotation match', rotation) + # print ('Found delta', s1_a - s2_a.rotate(rotation)) + + # We found the rotation, let's move S2 + scanner2.position = s1_a - s2_a.rotate(rotation) + # print ('Scanner '+scanner2.name+' is at', scanner2.position) + # print () + # print ('s1_a', s1_a) + # print ('s2_a', s2_a) + # print ('s2_a.rotate(rotation)', s2_a.rotate(rotation)) + # print ('s2_a.rotate(rotation) + s2.position', s2_a.rotate(rotation)+scanner2.position) + # print ('s1_b', s1_b) + # print ('s2_b', s2_b) + # print ('s2_b.rotate(rotation)', s2_b.rotate(rotation)) + # print ('s2_b.rotate(rotation) + s2.position', s2_b.rotate(rotation)+scanner2.position) + + # And rotate + move S2's beacons + # Rotation must happen first, because it's a rotation compared to S2 + for i, s2beacons in enumerate(scanner2.beacons): + scanner2.beacons[i] = ( + scanner2.beacons[i].rotate(rotation) + + scanner2.position + ) + found_rotation_match = True + break + if found_rotation_match: + found_rotation_match = False + break + if found_match: + break + # print ('remaining_scanners', [s for s in scanners if s.position =='']) + + +# print (scanners) + +if case_to_test == 1: + assert scanners[1].position.position == [68, -1246, -43] + assert scanners[2].position.position == [1105, -1205, 1229] + assert scanners[3].position.position == [-92, -2380, -20] + assert scanners[4].position.position == [-20, -1133, 1061] + +unique_beacons = [] +for scanner in scanners: + unique_beacons += [ + beacon.position + for beacon in scanner.beacons + if beacon.position not in unique_beacons + ] + +if part_to_test == 1: + puzzle_actual_result = len(unique_beacons) + +else: + max_distance = 0 + for combination in itertools.combinations(scanners, 2): + max_distance = max( + max_distance, combination[0].manhattan_distance(combination[1]) + ) + + puzzle_actual_result = max_distance + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-19 09:26:47.573614 +# Part 1: 2021-12-19 17:02:28 +# Part 2: 2021-12-19 17:09:12 diff --git a/2021/19-Beacon Scanner.v1 (fails).py b/2021/19-Beacon Scanner.v1 (fails).py new file mode 100644 index 0000000..d348d77 --- /dev/null +++ b/2021/19-Beacon Scanner.v1 (fails).py @@ -0,0 +1,507 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """--- scanner 0 --- +404,-588,-901 +528,-643,409 +-838,591,734 +390,-675,-793 +-537,-823,-458 +-485,-357,347 +-345,-311,381 +-661,-816,-575 +-876,649,763 +-618,-824,-621 +553,345,-567 +474,580,667 +-447,-329,318 +-584,868,-557 +544,-627,-890 +564,392,-477 +455,729,728 +-892,524,684 +-689,845,-530 +423,-701,434 +7,-33,-71 +630,319,-379 +443,580,662 +-789,900,-551 +459,-707,401 + +--- scanner 1 --- +686,422,578 +605,423,415 +515,917,-361 +-336,658,858 +95,138,22 +-476,619,847 +-340,-569,-846 +567,-361,727 +-460,603,-452 +669,-402,600 +729,430,532 +-500,-761,534 +-322,571,750 +-466,-666,-811 +-429,-592,574 +-355,545,-477 +703,-491,-529 +-328,-685,520 +413,935,-424 +-391,539,-444 +586,-435,557 +-364,-763,-893 +807,-499,-711 +755,-354,-619 +553,889,-390 + +--- scanner 2 --- +649,640,665 +682,-795,504 +-784,533,-524 +-644,584,-595 +-588,-843,648 +-30,6,44 +-674,560,763 +500,723,-460 +609,671,-379 +-555,-800,653 +-675,-892,-343 +697,-426,-610 +578,704,681 +493,664,-388 +-671,-858,530 +-667,343,800 +571,-461,-707 +-138,-166,112 +-889,563,-600 +646,-828,498 +640,759,510 +-630,509,768 +-681,-892,-333 +673,-379,-804 +-742,-814,-386 +577,-820,562 + +--- scanner 3 --- +-589,542,597 +605,-692,669 +-500,565,-823 +-660,373,557 +-458,-679,-417 +-488,449,543 +-626,468,-788 +338,-750,-386 +528,-832,-391 +562,-778,733 +-938,-730,414 +543,643,-506 +-524,371,-870 +407,773,750 +-104,29,83 +378,-903,-323 +-778,-728,485 +426,699,580 +-438,-605,-362 +-469,-447,-387 +509,732,623 +647,635,-688 +-868,-804,481 +614,-800,639 +595,780,-596 + +--- scanner 4 --- +727,592,562 +-293,-554,779 +441,611,-461 +-714,465,-776 +-743,427,-804 +-660,-479,-426 +832,-632,460 +927,-485,-438 +408,393,-506 +466,436,-512 +110,16,151 +-258,-428,682 +-393,719,612 +-211,-452,876 +808,-476,-593 +-575,615,604 +-485,667,467 +-680,325,-822 +-627,-443,-432 +872,-547,-609 +833,512,582 +807,604,487 +839,-516,451 +891,-625,532 +-652,-548,-490 +30,-46,-14""", + "expected": ["79", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """--- scanner 0 --- +33,119,14 +386,794,-527 +847,-773,-432 +494,712,-428 +-435,-718,795 +-295,471,-487 +-816,-544,-567 +734,-774,473 +463,729,497 +-427,366,-518 +398,573,572 +128,-27,104 +-540,492,683 +-363,-696,767 +503,604,588 +685,-758,404 +939,-738,-439 +466,681,-536 +-506,516,563 +-419,574,648 +-762,-635,-608 +-342,-819,826 +825,-767,-571 +-685,-537,-490 +621,-854,416 +-409,412,-368 + +--- scanner 1 --- +-327,375,-825 +-709,-420,-666 +746,-882,512 +823,-973,-754 +373,660,469 +-596,-500,-657 +-45,-13,17 +-285,550,299 +-627,-528,-765 +-281,393,-675 +852,-859,-622 +788,-793,558 +-335,459,414 +622,651,-703 +-286,532,347 +720,728,-585 +858,-881,-761 +93,-97,-111 +629,782,-626 +-382,-902,781 +446,723,455 +-304,-851,678 +-406,-789,799 +484,574,510 +-386,261,-706 +814,-830,578 + +--- scanner 2 --- +542,-384,605 +-711,703,-638 +583,-273,691 +-653,-503,341 +-634,-620,430 +-782,643,-799 +-51,104,-103 +253,506,-758 +-871,-683,-374 +-622,575,792 +-752,636,712 +705,386,563 +-650,688,764 +494,-688,-762 +-654,-468,434 +-922,-610,-355 +474,-714,-799 +271,482,-871 +597,-346,754 +-955,-562,-392 +753,385,581 +374,404,-820 +540,-646,-851 +638,435,490 +-807,794,-687 + +--- scanner 3 --- +-672,354,397 +610,-553,804 +-713,315,598 +-494,-651,526 +-588,-350,-300 +875,454,872 +-529,-652,433 +-755,559,-513 +659,491,-566 +617,-523,-707 +904,497,845 +-789,338,-502 +768,-498,-595 +-636,-383,-263 +787,372,871 +677,-594,-546 +-709,-434,-282 +-814,454,-386 +-646,-671,522 +634,338,-521 +-645,300,459 +-9,-42,-19 +662,-655,856 +680,434,-600 +549,-683,884 + +--- scanner 4 --- +-391,495,669 +582,758,-495 +723,530,865 +-99,-118,110 +-520,-520,711 +316,-654,637 +-616,-611,662 +469,-629,682 +475,-384,-729 +573,724,-480 +539,594,-580 +-544,667,-771 +720,758,898 +-677,-626,-740 +350,-501,-755 +-705,-739,-768 +432,-413,-756 +-427,531,528 +-667,644,-750 +-523,526,611 +-509,713,-703 +13,-12,-24 +-575,-678,-688 +412,-608,716 +707,753,822 +-545,-671,823 + +--- scanner 5 --- +364,-582,469 +-750,-386,504 +-439,-535,-634 +-734,-429,727 +518,-428,-697 +496,-640,500 +-343,-614,-680 +-339,703,-535 +803,534,-662 +744,470,-753 +493,-540,-546 +-576,853,480 +502,554,402 +-611,799,331 +20,1,-135 +415,692,351 +849,636,-772 +-747,-353,732 +-574,726,496 +589,-589,-637 +-496,-569,-655 +-289,730,-701 +-289,644,-607 +464,590,390 +400,-723,505 + +--- scanner 6 --- +633,-271,-850 +-662,603,-547 +-545,-742,658 +786,450,-611 +610,744,448 +-616,396,752 +-637,450,-592 +593,-505,542 +-128,165,-28 +-2,27,121 +-771,-386,-518 +561,-579,435 +-782,446,725 +-710,396,666 +585,-238,-813 +627,864,436 +752,671,-600 +-655,-696,556 +811,566,-727 +-620,-411,-406 +471,803,497 +-683,546,-513 +-564,-637,492 +712,-502,378 +706,-322,-831 +-680,-482,-567""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["355", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 2 +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +def distance_3d(source, target): + return sum((target[i] - source[i]) ** 2 for i in (0, 1, 2)) + + +def count_beacons(origin): + global visited, visited_beacons, nb_beacons + + visited_beacons += [ + (target, beacon) + for target in matching_scanners[origin] + for beacon in matching_beacons[target][origin] + ] + + for target in matching_scanners[origin]: + if target in visited: + continue + visited.append(target) + + added_beacons = [ + beacon + for beacon in beacons[target] + if (target, beacon) not in visited_beacons + ] + visited_beacons += [(target, beacon) for beacon in added_beacons] + + nb_beacons += len(added_beacons) + print(origin, target, added_beacons, len(beacons[target])) + count_beacons(target) + + +if part_to_test == 1: + + beacons = {} + scanners = puzzle_input.split("\n\n") + for scan_id, scanner in enumerate(puzzle_input.split("\n\n")): + beacons[scan_id] = {} + for beacon_id, beacon in enumerate(scanner.split("\n")): + if beacon_id == 0: + continue + beacon_id -= 1 + beacons[scan_id][beacon_id] = ints(beacon) + + distances = {} + for scan_id, beacons_dict in beacons.items(): + pairs = itertools.combinations(beacons_dict, 2) + distances[scan_id] = defaultdict(dict) + for pair in pairs: + distance = distance_3d(beacons_dict[pair[0]], beacons_dict[pair[1]]) + distances[scan_id][pair[0]][pair[1]] = distance + distances[scan_id][pair[1]][pair[0]] = distance + + matching_scanners = {} + matching_beacons = {} + for scan1_id, dist1 in distances.items(): + matching_scanners[scan1_id] = [] + matching_beacons[scan1_id] = {} + for scan2_id, dist2 in distances.items(): + if scan1_id == scan2_id: + continue + next_scanner = False + for s1beacon_id, s1beacon in dist1.items(): + for s2beacon_id, s2beacon in dist2.items(): + if ( + sum( + [ + 1 if s1dist1 in s2beacon.values() else 0 + for s1dist1 in s1beacon.values() + ] + ) + == 11 + ): + matching_scanners[scan1_id].append(scan2_id) + matching_beacons[scan1_id][scan2_id] = set( + [ + s1beacon_id2 + for s1beacon_id2 in s1beacon + if s1beacon[s1beacon_id2] in s2beacon.values() + ] + ) + matching_beacons[scan1_id][scan2_id].add(s1beacon_id) + next_scanner = True + break + if next_scanner: + next_scanner = False + break + + print(matching_scanners) + print(matching_beacons) + nb_beacons = len(beacons[0]) + visited = [0] + visited_beacons = [(0, b_id) for b_id in beacons[0]] + count_beacons(0) + print(visited_beacons) + if len(visited_beacons) != sum([len(beacons[scan_id]) for scan_id in beacons]): + print("error") + + puzzle_actual_result = nb_beacons + + +# Should find 355 + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-19 09:26:47.573614 From 619c8523bb1ba054882cb5771eeb3403f2910f3a Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 10:39:46 +0100 Subject: [PATCH 129/143] Fixed issue in dot library + added border identification to grid --- 2021/dot.py | 2 +- 2021/grid.py | 16 ++++++---------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/2021/dot.py b/2021/dot.py index 448f6ef..aedbdd3 100644 --- a/2021/dot.py +++ b/2021/dot.py @@ -171,7 +171,7 @@ def manhattan_distance(self, reference=0): def set_terrain(self, terrain): self.terrain = terrain or self.terrain_default self.is_walkable, self.is_waypoint = self.terrain_map.get( - terrain, self.terrain_map[self.terrain_default] + self.terrain, self.terrain_map[self.terrain_default] ) def set_directions(self): diff --git a/2021/grid.py b/2021/grid.py index fe54dc3..ad7d89c 100644 --- a/2021/grid.py +++ b/2021/grid.py @@ -255,20 +255,16 @@ def get_borders(self): min_y, max_y = int(min(y_vals)), int(max(y_vals)) borders = [] - borders.append([x + 1j * max_y for x in sorted(x_vals)]) - borders.append([max_x + 1j * y for y in sorted(y_vals)]) - borders.append([x + 1j * min_y for x in sorted(x_vals)]) - borders.append([min_x + 1j * y for y in sorted(y_vals)]) + borders.append([self.dots[x + 1j * max_y] for x in sorted(x_vals)]) + borders.append([self.dots[max_x + 1j * y] for y in sorted(y_vals)]) + borders.append([self.dots[x + 1j * min_y] for x in sorted(x_vals)]) + borders.append([self.dots[min_x + 1j * y] for y in sorted(y_vals)]) borders_text = [] for border in borders: - borders_text.append( - Grid({pos: self.dots[pos].terrain for pos in border}) - .dots_to_text() - .replace("\n", "") - ) + borders_text.append("".join(dot.terrain for dot in border)) - return borders_text + return borders, borders_text def get_columns(self): """ From a64ad6e2a341a1b39807827bbd36b234d25c06aa Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 10:39:56 +0100 Subject: [PATCH 130/143] Added day 2021-20 --- 2021/20-Trench Map.py | 176 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 2021/20-Trench Map.py diff --git a/2021/20-Trench Map.py b/2021/20-Trench Map.py new file mode 100644 index 0000000..c11fffa --- /dev/null +++ b/2021/20-Trench Map.py @@ -0,0 +1,176 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# + +#..#. +#.... +##..# +..#.. +..###""", + "expected": ["35", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["5044", "18074"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +dot.Dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {dir: all_directions for dir in all_directions}, +} +dot.Dot.terrain_map = { + ".": [True, False], + "#": [True, False], + "X": [True, False], +} + + +def get_neighbors(self): + if self.neighbors_obsolete: + self.neighbors = {} + for direction in self.allowed_directions: + if (self + direction) and (self + direction).is_walkable: + self.neighbors[self + direction] = 1 + else: + new_dot = self.__class__(self.grid, self.position + direction, ".") + self.grid.dots[self.position + direction] = new_dot + self.neighbors[self + direction] = 1 + + self.neighbors_obsolete = False + return self.neighbors + + +dot.Dot.get_neighbors = get_neighbors + +grid.Grid.all_directions = directions_diagonals + +dot.Dot.sort_value = dot.Dot.sorting_map["reading"] + +if part_to_test == 1: + generations = 2 +else: + generations = 50 + + +algorithm = puzzle_input.split("\n")[0] + +image = grid.Grid() +image.all_directions = directions_diagonals +image.text_to_dots("\n".join(puzzle_input.split("\n")[2:])) + +# print (image.dots_to_text()) + +for i in range(generations + 5): + dots = image.dots.copy() + [image.dots[x].get_neighbors() for x in dots] + + +for i in range(generations): + # print ('Generation', i) + new_image = grid.Grid() + new_image.dots = { + x: dot.Dot(new_image, image.dots[x].position, image.dots[x].terrain) + for x in image.dots + } + new_image.all_directions = directions_diagonals + + for x in image.dots.copy(): + neighbors = [neighbor for neighbor in image.dots[x].get_neighbors()] + [ + image.dots[x] + ] + text = "".join([neighbor.terrain for neighbor in sorted(neighbors)]) + binary = int(text.replace(".", "0").replace("#", "1"), 2) + new_image.dots[x].set_terrain(algorithm[binary]) + # print (new_image.dots_to_text()) + + # Empty borders so they're not counted later + # They use surrounding data (out of image) that default to . and this messes up the rest + # This is done only for odd generations because that's enough (all non-borders get blanked out due to the "." at the end of the algorithm) + if i % 2 == 1: + borders, _ = new_image.get_borders() + borders = functools.reduce(lambda a, b: a + b, borders) + [dot.set_terrain(".") for dot in borders] + + image.dots = { + x: dot.Dot(image, new_image.dots[x].position, new_image.dots[x].terrain) + for x in new_image.dots + } + + # print ('Lit dots', sum([1 for dot in image.dots if image.dots[dot].terrain == '#'])) + +# Remove the borders that were added (they shouldn't count because they take into account elements outside the image) +borders, _ = image.get_borders() +borders = functools.reduce(lambda a, b: a + b, borders) +image.dots = { + dot: image.dots[dot] for dot in image.dots if image.dots[dot] not in borders +} + +puzzle_actual_result = sum([1 for dot in image.dots if image.dots[dot].terrain == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-20 08:30:35.363096 +# Part 1: 2021-12-20 10:19:36 +# Part 2: 2021-12-20 10:35:25 From 3420f26724acd6207a731edfd0bb76dbf037197e Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:33:43 +0100 Subject: [PATCH 131/143] Added a game of life simulator --- 2021/grid.py | 670 +++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 644 insertions(+), 26 deletions(-) diff --git a/2021/grid.py b/2021/grid.py index ad7d89c..35b5046 100644 --- a/2021/grid.py +++ b/2021/grid.py @@ -1,6 +1,7 @@ from compass import * from dot import Dot from graph import WeightedGraph +from functools import lru_cache, reduce import heapq @@ -124,16 +125,10 @@ def words_to_dots(self, text, convert_to_int=False): y = 0 for line in text.splitlines(): - for x in line.split(" "): - for dir in self.possible_source_directions.get( - x, self.direction_default - ): - if convert_to_int: - self.dots[(x - y * 1j, dir)] = Dot( - self, x - y * 1j, int(x), dir - ) - else: - self.dots[(x - y * 1j, dir)] = Dot(self, x - y * 1j, x, dir) + for x, value in enumerate(line.split(" ")): + if convert_to_int: + value = int(value) + self.dots[x - y * 1j] = Dot(self, x - y * 1j, value) y += 1 def dots_to_text(self, mark_coords={}, void=" "): @@ -278,14 +273,13 @@ def get_columns(self): x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) - min_x, max_x = int(min(x_vals)), int(max(x_vals)) - min_y, max_y = int(min(y_vals)), int(max(y_vals)) - columns = {} + columns_text = {} for x in x_vals: columns[x] = [x + 1j * y for y in y_vals if x + 1j * y in self.dots] + columns_text[x] = "".join([self.dots[position] for position in columns[x]]) - return columns + return columns, columns_text def get_rows(self): """ @@ -299,14 +293,13 @@ def get_rows(self): x_vals = set(map(int, (dot.position.real for dot in self.dots.values()))) y_vals = set(map(int, (dot.position.imag for dot in self.dots.values()))) - min_x, max_x = int(min(x_vals)), int(max(x_vals)) - min_y, max_y = int(min(y_vals)), int(max(y_vals)) - rows = {} + rows_text = {} for y in y_vals: rows[y] = [x + 1j * y for x in x_vals if x + 1j * y in self.dots] + rows_text[x] = "".join([self.dots[position] for position in rows[x]]) - return rows + return rows, rows_text def rotate(self, angles): """ @@ -318,10 +311,6 @@ def rotate(self, angles): rotated_grids = [] - x_vals = set(dot.position.real for dot in self.dots.values()) - y_vals = set(dot.position.imag for dot in self.dots.values()) - - min_x, max_x, min_y, max_y = self.get_box() width, height = self.get_size() if isinstance(angles, int): @@ -373,10 +362,6 @@ def flip(self, flips): flipped_grids = [] - x_vals = set(dot.position.real for dot in self.dots.values()) - y_vals = set(dot.position.imag for dot in self.dots.values()) - - min_x, max_x, min_y, max_y = self.get_box() width, height = self.get_size() if isinstance(flips, str): @@ -541,6 +526,473 @@ def convert_to_graph(self): return graph +class SimpleGrid: + direction_default = directions_straight + all_directions = directions_straight + + default_dot = "." + content_alive = {".": False, "#": True} + + def __init__(self, dots=[]): + """ + Creates the grid based on the list of dots and edges provided + + :param sequence dots: Either a list of positions or a dict position:cell + """ + + self.dots = {} + if dots: + if isinstance(dots, dict): + self.dots = dots.copy() + else: + self.dots = {x: default_dot for x in dots} + + self.width = None + self.height = None + + def text_to_dots(self, text, ignore_terrain="", convert_to_int=False): + """ + Converts a text to a set of dots + + The text is expected to be separated by newline characters + The dots will have x - y * 1j as coordinates + + :param string text: The text to convert + :param sequence ignore_terrain: Types of terrain to ignore (useful for walls) + """ + self.dots = {} + + y = 0 + for line in text.splitlines(): + for x in range(len(line)): + if line[x] not in ignore_terrain: + if convert_to_int: + value = int(line[x]) + else: + value = line[x] + self.dots[x - y * 1j] = value + y += 1 + + def words_to_dots(self, text, convert_to_int=False): + """ + Converts a text to a set of dots + + The text is expected to be separated by newline characters + The dots will have x - y * 1j as coordinates + Dots are words (rather than letters, like in text_to_dots) + + :param string text: The text to convert + :param sequence ignore_terrain: Types of terrain to ignore (useful for walls) + """ + self.dots = {} + + y = 0 + for line in text.splitlines(): + for x, value in enumerate(line.split(" ")): + if convert_to_int: + value = int(value) + self.dots[x - y * 1j] = value + y += 1 + + def dots_to_text(self, mark_coords={}, void=" "): + """ + Converts dots to a text + + The text will be separated by newline characters + + :param dict mark_coords: List of coordinates to mark, with letter to use + :param string void: Which character to use when no dot is present + :return: the text + """ + text = "" + + min_x, max_x, min_y, max_y = self.get_box() + + # The imaginary axis is reversed compared to reading order + for y in range(max_y, min_y - 1, -1): + for x in range(min_x, max_x + 1): + try: + text += str(mark_coords[x + y * 1j]) + except (KeyError, TypeError): + if x + y * 1j in mark_coords: + text += "X" + else: + text += str(self.dots.get(x + y * 1j, void)) + text += "\n" + + return text + + def get_xy_vals(self): + x_vals = sorted(set(int(dot.real) for dot in self.dots)) + + # Reverse sorting because y grows opposite of reading order + y_vals = sorted(set(int(dot.imag) for dot in self.dots))[::-1] + + return (x_vals, y_vals) + + def get_size(self): + """ + Gets the width and height of the grid + + :return: the width and height + """ + + if not self.width: + min_x, max_x, min_y, max_y = self.get_box() + + self.width = max_x - min_x + 1 + self.height = max_y - min_y + 1 + + return (self.width, self.height) + + def get_box(self): + """ + Gets the min/max x and y values + + :return: the minimum and maximum for x and y values + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals, y_vals = self.get_xy_vals() + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + return (min_x, max_x, min_y, max_y) + + def get_borders(self): + """ + Gets the borders of the image + + Only the terrain of the dot will be sent back + This will be returned in left-to-right, up to bottom reading order + Newline characters are not included + + :return: a text representing a border + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals, y_vals = self.get_xy_vals() + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + borders = [] + borders.append([x + 1j * max_y for x in sorted(x_vals)]) + borders.append([max_x + 1j * y for y in sorted(y_vals)]) + borders.append([x + 1j * min_y for x in sorted(x_vals)]) + borders.append([min_x + 1j * y for y in sorted(y_vals)]) + + borders_text = [] + for border in borders: + borders_text.append("".join(self.dots[dot] for dot in border)) + + return borders, borders_text + + def get_columns(self): + """ + Gets the columns of the image + + :return: a dict of dots + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals, y_vals = self.get_xy_vals() + + columns = {} + columns_text = {} + for x in x_vals: + columns[x] = [x + 1j * y for y in y_vals if x + 1j * y in self.dots] + columns_text[x] = "".join([self.dots[position] for position in columns[x]]) + + return columns, columns_text + + def get_rows(self): + """ + Gets the rows of the image + + :return: a dict of dots + """ + + if not self.dots: + return (0, 0, 0, 0) + x_vals, y_vals = self.get_xy_vals() + + rows = {} + rows_text = {} + for y in y_vals: + rows[y] = [x + 1j * y for x in x_vals if x + 1j * y in self.dots] + rows_text[x] = "".join([self.dots[position] for position in columns[x]]) + + rows_text = ["".join(row) for row in rows.values()] + + return rows, rows_text + + def rotate(self, angles=[0, 90, 180, 270]): + """ + Rotates clockwise a grid and returns a list of rotated grids + + :param tuple angles: Which angles to use for rotation + :return: The dots + """ + + rotated_grids = {} + + width, height = self.get_size() + + if isinstance(angles, int): + angles = {angles} + + for angle in angles: + if angle == 0: + rotated_grids[angle] = self + elif angle == 90: + rotated_grids[angle] = Grid( + { + height - 1 + pos.imag - 1j * pos.real: dot + for pos, dot in self.dots.items() + } + ) + elif angle == 180: + rotated_grids[angle] = Grid( + { + width - 1 - pos.real - 1j * (height - 1 + pos.imag): dot + for pos, dot in self.dots.items() + } + ) + + elif angle == 270: + rotated_grids[angle] = Grid( + { + -pos.imag - 1j * (width - 1 - pos.real): dot + for pos, dot in self.dots.items() + } + ) + + return rotated_grids + + def flip(self, flips=["N", "H", "V"]): + """ + Flips a grid and returns a list of grids + + :param tuple flips: Which flips to perform + :return: The dots + """ + + flipped_grids = {} + + width, height = self.get_size() + + if isinstance(flips, str): + flips = {flips} + + for flip in flips: + if flip == "N": + flipped_grids[flip] = self + elif flip == "H": + flipped_grids[flip] = Grid( + { + pos.real - 1j * (height - 1 + pos.imag): dot + for pos, dot in self.dots.items() + } + ) + + elif flip == "V": + flipped_grids[flip] = Grid( + { + width - 1 - pos.real + 1j * pos.imag: dot + for pos, dot in self.dots.items() + } + ) + + return flipped_grids + + def crop(self, corners=[], size=0): + """ + Gets the list of dots within a given area + + :param sequence corners: Either one or 2 corners to use + :param int or sequence size: The size (width + height, or simply length) to use + :return: a dict of matching dots + """ + + delta = size - 1 + if type(corners) == complex: + corners = [corners] + # top left corner + size are provided + if delta and len(corners) == 1: + # The corner is a tuple position, direction + if isinstance(corners[0], tuple): + min_x, max_x = int(corners[0][0].real), int(corners[0][0].real + delta) + min_y, max_y = int(corners[0][0].imag - delta), int(corners[0][0].imag) + # The corner is a complex number + else: + min_x, max_x = int(corners[0].real), int(corners[0].real + delta) + min_y, max_y = int(corners[0].imag - delta), int(corners[0].imag) + + # Multiple corners are provided + else: + # Dots are provided as complex numbers + x_vals = set(pos.real for pos in corners) + y_vals = set(pos.imag for pos in corners) + + min_x, max_x = int(min(x_vals)), int(max(x_vals)) + min_y, max_y = int(min(y_vals)), int(max(y_vals)) + + cropped = Grid( + { + x + y * 1j: self.dots[x + y * 1j] + for y in range(min_y, max_y + 1) + for x in range(min_x, max_x + 1) + if x + y * 1j in self.dots + } + ) + + return cropped + + +class GameOfLife(SimpleGrid): + dot_default = "." + + def __init__(self, is_infinite=False): + """ + Creates the simulator based on the list of dots and edges provided + + :param boolean is_infinite: Whether the grid can grow exponentially + """ + + self.is_infinite = bool(is_infinite) + + self.width = None + self.height = None + + self.nb_neighbors = 4 + self.include_dot = False + + def set_rules(self, rules): + """ + Defines the rules of life/death + + Rules must be a dict with a 4, 5, 8 or 9-dots key and the target dot as value + Rules with 4 dots will use top, left, right and bottom dots as reference + Rules with 4 dots will use top, left, middle, right and bottom dots as reference + Rules with 8 dots will use neighbor dots as reference (in reading order) + Rules with 9 dots will use neighbor dots + the dot itselt as reference (in reading order) + + :param dict rules: The rule book to use + :return: Nothing + """ + self.rules = rules + key_length = len(list(rules.keys())[0]) + self.include_dot = key_length % 4 == 1 + + if key_length in [8, 9]: + self.set_directions(directions_diagonals) + else: + self.set_directions(directions_straight) + + def evolve(self, nb_generations): + """ + Evolves the grid by nb_generations according to provided rules + + :param int nb_generations: The number of generations to evolve + :return: the resulting grid + """ + + for i in range(nb_generations): + if self.is_infinite: + self.extend_grid(1) + + self.dots = {position: self.apply_rules(position) for position in self.dots} + + def apply_rules(self, position): + """ + Applies the rules to a given dot + + :param complex position: The position of the dot + :return: nothing + """ + neighbors = self.get_neighbors(position) + neighbor_text = "".join( + self.dots[neighbor] if neighbor in self.dots else self.dot_default + for neighbor in neighbors + ) + + return self.rules[neighbor_text] + + def set_directions(self, directions): + """ + Defines which directions are used for neighbor calculation + + :param list directions: The directions to use + :return: nothing + """ + self.all_directions = directions + + @lru_cache + def get_neighbors(self, position): + """ + Finds neighbors of a given position. Returns a sorted list of positions + + :param complex position: The central point + :return: sorted list of positions + """ + positions = [] + if self.include_dot: + positions.append(position) + positions += [position + direction for direction in self.all_directions] + + return sorted(positions, key=lambda a: (-a.imag, a.real)) + + def extend_grid(self, size): + """ + Extends the grid by size elements + + :param int size: The number of cells to add on each side + :return: nothing + """ + dots = self.dots.copy() + + for i in range(int(size)): + # Extend the grid + borders, _ = self.get_borders() + borders = reduce(lambda a, b: a + b, borders) + borders = reduce( + lambda a, b: a + b, [self.get_neighbors(pos) for pos in borders] + ) + dots.update({pos: self.default_dot for pos in borders if pos not in dots}) + + # If diagonals are not allowed, the corners will be missed + if self.all_directions == directions_straight: + x_vals, y_vals = self.get_xy_vals() + min_x, max_x = min(x_vals), max(x_vals) + min_y, max_y = min(y_vals), max(y_vals) + + dots[min_x - 1 + 1j * (min_y - 1)] = self.default_dot + dots[min_x - 1 + 1j * (max_y + 1)] = self.default_dot + dots[max_x + 1 + 1j * (min_y - 1)] = self.default_dot + dots[max_x + 1 + 1j * (max_y + 1)] = self.default_dot + + self.dots.update(dots) + + def reduce_grid(self, size): + """ + Extends the grid by size elements + + :param int size: The number of cells to add on each side + :return: nothing + """ + dots = self.dots.copy() + + for i in range(int(size)): + # Extend the grid + borders, _ = self.get_borders() + borders = reduce(lambda a, b: a + b, borders) + [self.dots.pop(position) for position in borders if position in self.dots] + + def merge_grids(grids, width, height): """ Merges different grids in a single grid @@ -575,3 +1027,169 @@ def merge_grids(grids, width, height): grid_nr += 1 return final_grid + + +if __name__ == "__main__": + # Tests for SimpleGrid + dot_grid = """#..#. +#.... +##..# +..#.. +..### +""" + if True: + image = SimpleGrid() + image.all_directions = directions_diagonals + image.text_to_dots(dot_grid) + + # Get basic info + assert image.dots_to_text() == dot_grid + assert image.get_size() == (5, 5) + assert image.get_box() == (0, 4, -4, 0) + assert image.get_borders() == ( + [ + [0j, (1 + 0j), (2 + 0j), (3 + 0j), (4 + 0j)], + [(4 - 4j), (4 - 3j), (4 - 2j), (4 - 1j), (4 + 0j)], + [-4j, (1 - 4j), (2 - 4j), (3 - 4j), (4 - 4j)], + [-4j, -3j, -2j, -1j, 0j], + ], + ["#..#.", "#.#..", "..###", "..###"], + ) + assert image.get_columns() == ( + { + 0: [0j, -1j, -2j, -3j, -4j], + 1: [(1 + 0j), (1 - 1j), (1 - 2j), (1 - 3j), (1 - 4j)], + 2: [(2 + 0j), (2 - 1j), (2 - 2j), (2 - 3j), (2 - 4j)], + 3: [(3 + 0j), (3 - 1j), (3 - 2j), (3 - 3j), (3 - 4j)], + 4: [(4 + 0j), (4 - 1j), (4 - 2j), (4 - 3j), (4 - 4j)], + }, + {0: "###..", 1: "..#..", 2: "...##", 3: "#...#", 4: "..#.#"}, + ) + + if True: + # Transformations + images = image.rotate() + assert images[0].dots_to_text() == dot_grid + assert images[90].dots_to_text() == "..###\n..#..\n##...\n#...#\n#.#..\n" + assert images[180].dots_to_text() == "###..\n..#..\n#..##\n....#\n.#..#\n" + assert images[270].dots_to_text() == "..#.#\n#...#\n...##\n..#..\n###..\n" + + images = image.flip() + assert images["N"].dots_to_text() == dot_grid + assert images["V"].dots_to_text() == ".#..#\n....#\n#..##\n..#..\n###..\n" + assert images["H"].dots_to_text() == "..###\n..#..\n##..#\n#....\n#..#.\n" + + assert image.crop(1 - 1j, 2).dots_to_text() == "..\n#.\n" + assert ( + image.crop([1 - 1j, 3 - 1j, 3 - 3j, 1 - 3j]).dots_to_text() + == "...\n#..\n.#.\n" + ) + + if True: + # Game of life simulator + # Orthogonal grid (no diagonals) + image = GameOfLife(False) + image.text_to_dots(dot_grid) + + assert image.get_neighbors(1 - 1j) == [1, -1j, 2 - 1j, 1 - 2j] + assert image.get_neighbors(0) == [1j, -1, 1, -1j] + + # Diagonal grid (no diagonals) + image = GameOfLife(True) + image.text_to_dots(dot_grid) + image.set_directions(directions_diagonals) + + assert image.get_neighbors(1 - 1j) == [ + 0, + 1, + 2, + -1j, + 2 - 1j, + -2j, + 1 - 2j, + 2 - 2j, + ] + assert image.get_neighbors(0) == [ + -1 + 1j, + 1j, + 1 + 1j, + -1, + 1, + -1 - 1j, + -1j, + 1 - 1j, + ] + + if True: + # Perform actual simulation with limited grid + image = GameOfLife(False) + image.text_to_dots(dot_grid) + image.set_directions(directions_diagonals) + image.set_rules( + { + "....": ".", + "...#": ".", + "..#.": ".", + "..##": "#", + ".#..": ".", + ".#.#": "#", + ".##.": "#", + ".###": "#", + "#...": ".", + "#..#": "#", + "#.#.": "#", + "#.##": "#", + "##..": "#", + "##.#": "#", + "###.": ".", + "####": ".", + } + ) + + assert image.include_dot == False + assert image.all_directions == directions_straight + + image.evolve(1) + assert image.dots_to_text() == ".....\n##...\n#.#..\n.#.##\n..##.\n" + + if True: + # Perform actual simulation with infinite grid + image = GameOfLife(True) + image.text_to_dots(dot_grid) + image.set_directions(directions_diagonals) + image.set_rules( + { + "....": ".", + "...#": ".", + "..#.": ".", + "..##": "#", + ".#..": ".", + ".#.#": "#", + ".##.": "#", + ".###": "#", + "#...": ".", + "#..#": "#", + "#.#.": "#", + "#.##": "#", + "##..": "#", + "##.#": "#", + "###.": ".", + "####": ".", + } + ) + + assert image.include_dot == False + assert image.all_directions == directions_straight + + image.evolve(1) + assert ( + image.dots_to_text() + == ".......\n.......\n.##....\n.#.#...\n..#.##.\n...##..\n.......\n" + ) + image.evolve(1) + + image = SimpleGrid() + word_grid = """word1 word2 word3 + wordA wordB wordC + word9 word8 word7""" + image.words_to_dots(word_grid) From a98177066d33e20c5cbac8009db4795d0a78cf19 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:34:00 +0100 Subject: [PATCH 132/143] Added v2 of 2021-20 --- 2021/20-Trench Map.py | 95 ++++----------------- 2021/20-Trench Map.v1.py | 176 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+), 78 deletions(-) create mode 100644 2021/20-Trench Map.v1.py diff --git a/2021/20-Trench Map.py b/2021/20-Trench Map.py index c11fffa..94de766 100644 --- a/2021/20-Trench Map.py +++ b/2021/20-Trench Map.py @@ -70,100 +70,39 @@ def words(s: str): # -------------------------------- Actual code execution ----------------------------- # -dot.Dot.all_directions = directions_diagonals all_directions = directions_diagonals -dot.Dot.allowed_direction_map = { - ".": {dir: all_directions for dir in all_directions}, - "#": {dir: all_directions for dir in all_directions}, -} -dot.Dot.terrain_map = { - ".": [True, False], - "#": [True, False], - "X": [True, False], -} - - -def get_neighbors(self): - if self.neighbors_obsolete: - self.neighbors = {} - for direction in self.allowed_directions: - if (self + direction) and (self + direction).is_walkable: - self.neighbors[self + direction] = 1 - else: - new_dot = self.__class__(self.grid, self.position + direction, ".") - self.grid.dots[self.position + direction] = new_dot - self.neighbors[self + direction] = 1 - - self.neighbors_obsolete = False - return self.neighbors - - -dot.Dot.get_neighbors = get_neighbors -grid.Grid.all_directions = directions_diagonals - -dot.Dot.sort_value = dot.Dot.sorting_map["reading"] if part_to_test == 1: generations = 2 else: generations = 50 - +# Parsing algorithm algorithm = puzzle_input.split("\n")[0] -image = grid.Grid() -image.all_directions = directions_diagonals -image.text_to_dots("\n".join(puzzle_input.split("\n")[2:])) - -# print (image.dots_to_text()) +rules = {} +for i in range(2 ** 9): + binary = "{0:>09b}".format(i) + text = binary.replace("0", ".").replace("1", "#") + rules[text] = algorithm[i] -for i in range(generations + 5): - dots = image.dots.copy() - [image.dots[x].get_neighbors() for x in dots] +image = grid.GameOfLife(True) +image.set_rules(rules) +image.text_to_dots("\n".join(puzzle_input.split("\n")[2:])) +# Add some margin to make it 'infinite' +image.extend_grid(2) for i in range(generations): - # print ('Generation', i) - new_image = grid.Grid() - new_image.dots = { - x: dot.Dot(new_image, image.dots[x].position, image.dots[x].terrain) - for x in image.dots - } - new_image.all_directions = directions_diagonals - - for x in image.dots.copy(): - neighbors = [neighbor for neighbor in image.dots[x].get_neighbors()] + [ - image.dots[x] - ] - text = "".join([neighbor.terrain for neighbor in sorted(neighbors)]) - binary = int(text.replace(".", "0").replace("#", "1"), 2) - new_image.dots[x].set_terrain(algorithm[binary]) - # print (new_image.dots_to_text()) - - # Empty borders so they're not counted later - # They use surrounding data (out of image) that default to . and this messes up the rest - # This is done only for odd generations because that's enough (all non-borders get blanked out due to the "." at the end of the algorithm) + image.evolve(1) if i % 2 == 1: - borders, _ = new_image.get_borders() - borders = functools.reduce(lambda a, b: a + b, borders) - [dot.set_terrain(".") for dot in borders] - - image.dots = { - x: dot.Dot(image, new_image.dots[x].position, new_image.dots[x].terrain) - for x in new_image.dots - } - - # print ('Lit dots', sum([1 for dot in image.dots if image.dots[dot].terrain == '#'])) - -# Remove the borders that were added (they shouldn't count because they take into account elements outside the image) -borders, _ = image.get_borders() -borders = functools.reduce(lambda a, b: a + b, borders) -image.dots = { - dot: image.dots[dot] for dot in image.dots if image.dots[dot] not in borders -} + image.reduce_grid(2) + image.extend_grid(2) + +image.reduce_grid(2) -puzzle_actual_result = sum([1 for dot in image.dots if image.dots[dot].terrain == "#"]) +puzzle_actual_result = image.dots_to_text().count("#") # -------------------------------- Outputs / results --------------------------------- # diff --git a/2021/20-Trench Map.v1.py b/2021/20-Trench Map.v1.py new file mode 100644 index 0000000..c11fffa --- /dev/null +++ b/2021/20-Trench Map.v1.py @@ -0,0 +1,176 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """..#.#..#####.#.#.#.###.##.....###.##.#..###.####..#####..#....#..#..##..###..######.###...####..#..#####..##..#.#####...##.#.#..#.##..#.#......#.###.######.###.####...#.##.##..#..#..#####.....#.#....###..#.##......#.....#..#..#..##..#...##.######.####.####.#.#...#.......#..#.#.#...####.##.#......#..#...##.#.##..#...##.#.##..###.#......#.#.......#.#.#.####.###.##...#.....####.#..#..#.##.#....##..#.####....##...##..#...#......#.#.......#.......##..####..#...#.#.#...##..#.#..###..#####........#..####......#..# + +#..#. +#.... +##..# +..#.. +..###""", + "expected": ["35", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["5044", "18074"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +dot.Dot.all_directions = directions_diagonals +all_directions = directions_diagonals +dot.Dot.allowed_direction_map = { + ".": {dir: all_directions for dir in all_directions}, + "#": {dir: all_directions for dir in all_directions}, +} +dot.Dot.terrain_map = { + ".": [True, False], + "#": [True, False], + "X": [True, False], +} + + +def get_neighbors(self): + if self.neighbors_obsolete: + self.neighbors = {} + for direction in self.allowed_directions: + if (self + direction) and (self + direction).is_walkable: + self.neighbors[self + direction] = 1 + else: + new_dot = self.__class__(self.grid, self.position + direction, ".") + self.grid.dots[self.position + direction] = new_dot + self.neighbors[self + direction] = 1 + + self.neighbors_obsolete = False + return self.neighbors + + +dot.Dot.get_neighbors = get_neighbors + +grid.Grid.all_directions = directions_diagonals + +dot.Dot.sort_value = dot.Dot.sorting_map["reading"] + +if part_to_test == 1: + generations = 2 +else: + generations = 50 + + +algorithm = puzzle_input.split("\n")[0] + +image = grid.Grid() +image.all_directions = directions_diagonals +image.text_to_dots("\n".join(puzzle_input.split("\n")[2:])) + +# print (image.dots_to_text()) + +for i in range(generations + 5): + dots = image.dots.copy() + [image.dots[x].get_neighbors() for x in dots] + + +for i in range(generations): + # print ('Generation', i) + new_image = grid.Grid() + new_image.dots = { + x: dot.Dot(new_image, image.dots[x].position, image.dots[x].terrain) + for x in image.dots + } + new_image.all_directions = directions_diagonals + + for x in image.dots.copy(): + neighbors = [neighbor for neighbor in image.dots[x].get_neighbors()] + [ + image.dots[x] + ] + text = "".join([neighbor.terrain for neighbor in sorted(neighbors)]) + binary = int(text.replace(".", "0").replace("#", "1"), 2) + new_image.dots[x].set_terrain(algorithm[binary]) + # print (new_image.dots_to_text()) + + # Empty borders so they're not counted later + # They use surrounding data (out of image) that default to . and this messes up the rest + # This is done only for odd generations because that's enough (all non-borders get blanked out due to the "." at the end of the algorithm) + if i % 2 == 1: + borders, _ = new_image.get_borders() + borders = functools.reduce(lambda a, b: a + b, borders) + [dot.set_terrain(".") for dot in borders] + + image.dots = { + x: dot.Dot(image, new_image.dots[x].position, new_image.dots[x].terrain) + for x in new_image.dots + } + + # print ('Lit dots', sum([1 for dot in image.dots if image.dots[dot].terrain == '#'])) + +# Remove the borders that were added (they shouldn't count because they take into account elements outside the image) +borders, _ = image.get_borders() +borders = functools.reduce(lambda a, b: a + b, borders) +image.dots = { + dot: image.dots[dot] for dot in image.dots if image.dots[dot] not in borders +} + +puzzle_actual_result = sum([1 for dot in image.dots if image.dots[dot].terrain == "#"]) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-20 08:30:35.363096 +# Part 1: 2021-12-20 10:19:36 +# Part 2: 2021-12-20 10:35:25 From d3a46fd317702a412ef9e6d506883a2897263841 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 18:34:50 +0100 Subject: [PATCH 133/143] Removed useless & obsoletelibrary --- 2020/23-Crab Cups.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/2020/23-Crab Cups.py b/2020/23-Crab Cups.py index 9ad0f81..576a307 100644 --- a/2020/23-Crab Cups.py +++ b/2020/23-Crab Cups.py @@ -3,7 +3,8 @@ from collections import Counter, deque, defaultdict from compass import * -from simply_linked_list import * + +# from simply_linked_list import * # This functions come from https://github.com/mcpower/adventofcode - Thanks! From 843b6d196ed3dffaefc7ab7f312df149278a5c11 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:55:26 +0100 Subject: [PATCH 134/143] Improved performance of 2017-15 --- 2017/15-Dueling Generators.py | 86 +++++++++++++++-------------- 2017/15-Dueling Generators.v1.py | 95 ++++++++++++++++++++++++++++++++ 2 files changed, 140 insertions(+), 41 deletions(-) create mode 100644 2017/15-Dueling Generators.v1.py diff --git a/2017/15-Dueling Generators.py b/2017/15-Dueling Generators.py index 9fdebc8..223f703 100644 --- a/2017/15-Dueling Generators.py +++ b/2017/15-Dueling Generators.py @@ -4,90 +4,94 @@ test_data = {} test = 1 -test_data[test] = {"input": """Generator A starts with 65 +test_data[test] = { + "input": """Generator A starts with 65 Generator B starts with 8921""", - "expected": ['588', 'Unknown'], - } - -test = 'real' -input_file = os.path.join(os.path.dirname(__file__), 'Inputs', os.path.basename(__file__).replace('.py', '.txt')) -test_data[test] = {"input": open(input_file, "r+").read().strip(), - "expected": ['597', '303'], - } + "expected": ["588", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["597", "303"], +} # -------------------------------- Control program execution -------------------------------- # -case_to_test = 'real' -part_to_test = 2 +case_to_test = "real" +part_to_test = 2 verbose_level = 1 # -------------------------------- Initialize some variables -------------------------------- # -puzzle_input = test_data[case_to_test]['input'] -puzzle_expected_result = test_data[case_to_test]['expected'][part_to_test-1] -puzzle_actual_result = 'Unknown' +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" # -------------------------------- Actual code execution -------------------------------- # divisor = 2147483647 -factors = {'A': 16807, 'B': 48271} -value = {'A': 0, 'B': 0} +factors = {"A": 16807, "B": 48271} +value = {"A": 0, "B": 0} -def gen_a (): +def gen_a(): + x = value["A"] while True: - value['A'] *= factors['A'] - value['A'] %= divisor - if value['A'] % 4 == 0: - yield value['A'] + x *= 16807 + x %= 2147483647 + if x % 4 == 0: + yield x + -def gen_b (): +def gen_b(): + x = value["B"] while True: - value['B'] *= factors['B'] - value['B'] %= divisor - if value['B'] % 8 == 0: - yield value['B'] + x *= 48271 + x %= 2147483647 + if x % 8 == 0: + yield x + if part_to_test == 1: - for string in puzzle_input.split('\n'): + for string in puzzle_input.split("\n"): _, generator, _, _, start_value = string.split() value[generator] = int(start_value) nb_matches = 0 - for i in range (40 * 10 ** 6): + for i in range(40 * 10 ** 6): value = {gen: value[gen] * factors[gen] % divisor for gen in value} - if '{0:b}'.format(value['A'])[-16:] == '{0:b}'.format(value['B'])[-16:]: + if "{0:b}".format(value["A"])[-16:] == "{0:b}".format(value["B"])[-16:]: nb_matches += 1 puzzle_actual_result = nb_matches else: - for string in puzzle_input.split('\n'): + for string in puzzle_input.split("\n"): _, generator, _, _, start_value = string.split() value[generator] = int(start_value) nb_matches = 0 A = gen_a() B = gen_b() - for count_pairs in range (5 * 10**6): + for count_pairs in range(5 * 10 ** 6): a, b = next(A), next(B) - if '{0:b}'.format(a)[-16:] == '{0:b}'.format(b)[-16:]: + if a & 0xFFFF == b & 0xFFFF: nb_matches += 1 - puzzle_actual_result = nb_matches - # -------------------------------- Outputs / results -------------------------------- # if verbose_level >= 3: - print ('Input : ' + puzzle_input) -print ('Expected result : ' + str(puzzle_expected_result)) -print ('Actual result : ' + str(puzzle_actual_result)) - - - - + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) diff --git a/2017/15-Dueling Generators.v1.py b/2017/15-Dueling Generators.v1.py new file mode 100644 index 0000000..0f77a0d --- /dev/null +++ b/2017/15-Dueling Generators.v1.py @@ -0,0 +1,95 @@ +# -------------------------------- Input data -------------------------------- # +import os + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Generator A starts with 65 +Generator B starts with 8921""", + "expected": ["588", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read().strip(), + "expected": ["597", "303"], +} + +# -------------------------------- Control program execution -------------------------------- # + +case_to_test = "real" +part_to_test = 2 +verbose_level = 1 + +# -------------------------------- Initialize some variables -------------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution -------------------------------- # + +divisor = 2147483647 +factors = {"A": 16807, "B": 48271} +value = {"A": 0, "B": 0} + + +def gen_a(): + while True: + value["A"] *= factors["A"] + value["A"] %= divisor + if value["A"] % 4 == 0: + yield value["A"] + + +def gen_b(): + while True: + value["B"] *= factors["B"] + value["B"] %= divisor + if value["B"] % 8 == 0: + yield value["B"] + + +if part_to_test == 1: + for string in puzzle_input.split("\n"): + _, generator, _, _, start_value = string.split() + value[generator] = int(start_value) + + nb_matches = 0 + for i in range(40 * 10 ** 6): + value = {gen: value[gen] * factors[gen] % divisor for gen in value} + if "{0:b}".format(value["A"])[-16:] == "{0:b}".format(value["B"])[-16:]: + nb_matches += 1 + + puzzle_actual_result = nb_matches + + +else: + for string in puzzle_input.split("\n"): + _, generator, _, _, start_value = string.split() + value[generator] = int(start_value) + + nb_matches = 0 + A = gen_a() + B = gen_b() + for count_pairs in range(5 * 10 ** 6): + a, b = next(A), next(B) + if "{0:b}".format(a)[-16:] == "{0:b}".format(b)[-16:]: + nb_matches += 1 + + puzzle_actual_result = nb_matches + + +# -------------------------------- Outputs / results -------------------------------- # + +if verbose_level >= 3: + print("Input : " + puzzle_input) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) From ed8a2e23e5b5379069f667780add1f57ff3fdf34 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 20 Dec 2021 20:55:38 +0100 Subject: [PATCH 135/143] Improved performance of 2020-23 --- 2020/23-Crab Cups.py | 101 +++++++++++--------------- 2020/23-Crab Cups.v2.py | 156 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 197 insertions(+), 60 deletions(-) create mode 100644 2020/23-Crab Cups.v2.py diff --git a/2020/23-Crab Cups.py b/2020/23-Crab Cups.py index 576a307..397d2d2 100644 --- a/2020/23-Crab Cups.py +++ b/2020/23-Crab Cups.py @@ -65,86 +65,67 @@ def words(s: str): # -------------------------------- Actual code execution ----------------------------- # - +string = puzzle_input.split("\n")[0] if part_to_test == 1: moves = 100 - for string in puzzle_input.split("\n"): - cups = [int(x) for x in string] - - for i in range(moves): - cur_cup = cups[0] - pickup = cups[1:4] - del cups[0:4] - - try: - dest_cup = max([x for x in cups if x < cur_cup]) - except: - dest_cup = max([x for x in cups]) - cups[cups.index(dest_cup) + 1 : cups.index(dest_cup) + 1] = pickup - cups.append(cur_cup) - - print(cups) - - pos1 = cups.index(1) - puzzle_actual_result = "".join(map(str, cups[pos1 + 1 :] + cups[:pos1])) + nb_cups = 9 + next_cup = int(string[0]) else: moves = 10 ** 7 nb_cups = 10 ** 6 + next_cup = 10 - class Cup: - def __init__(self, val, next_cup=None): - self.val = val - self.next_cup = next_cup - string = puzzle_input.split("\n")[0] - next_cup = None - cups = {} - for x in string[::-1]: - cups[x] = Cup(x, next_cup) - next_cup = cups[x] +cups = {} +for x in string[::-1]: + cups[int(x)] = next_cup + next_cup = int(x) - next_cup = cups[string[0]] +if part_to_test == 2: + next_cup = int(string[0]) for x in range(nb_cups, 9, -1): - cups[str(x)] = Cup(str(x), next_cup) - next_cup = cups[str(x)] + cups[x] = next_cup + next_cup = x - cups[string[-1]].next_cup = cups["10"] +cur_cup = int(string[0]) +for i in range(moves): + # print ('----- Move', i+1) + # print ('Current', cur_cup) - cur_cup = cups[string[0]] - for i in range(1, moves + 1): - # #print ('----- Move', i) - # #print ('Current', cur_cup.val) + cups_moved = [ + cups[cur_cup], + cups[cups[cur_cup]], + cups[cups[cups[cur_cup]]], + ] + # print ('Moved cups', cups_moved) - cups_moved = [ - cur_cup.next_cup, - cur_cup.next_cup.next_cup, - cur_cup.next_cup.next_cup.next_cup, - ] - cups_moved_val = [cup.val for cup in cups_moved] - # #print ('Moved cups', cups_moved_val) + cups[cur_cup] = cups[cups_moved[-1]] - cur_cup.next_cup = cups_moved[-1].next_cup + dest_cup = cur_cup - 1 + while dest_cup in cups_moved or dest_cup <= 0: + dest_cup -= 1 + if dest_cup <= 0: + dest_cup = nb_cups - dest_cup_nr = int(cur_cup.val) - 1 - while str(dest_cup_nr) in cups_moved_val or dest_cup_nr <= 0: - dest_cup_nr -= 1 - if dest_cup_nr <= 0: - dest_cup_nr = nb_cups - dest_cup = cups[str(dest_cup_nr)] + # print ("Destination", dest_cup) - # #print ("Destination", dest_cup_nr) + cups[cups_moved[-1]] = cups[dest_cup] + cups[dest_cup] = cups_moved[0] - cups_moved[-1].next_cup = dest_cup.next_cup - dest_cup.next_cup = cups_moved[0] + cur_cup = cups[cur_cup] - cur_cup = cur_cup.next_cup +if part_to_test == 1: + text = "" + cup = cups[1] + while cup != 1: + text += str(cup) + cup = cups[cup] - puzzle_actual_result = int(cups["1"].next_cup.val) * int( - cups["1"].next_cup.next_cup.val - ) - # #puzzle_actual_result = cups[(pos1+1)%len(cups)] * cups[(pos1+2)%len(cups)] + puzzle_actual_result = text +else: + puzzle_actual_result = cups[1] * cups[cups[1]] # -------------------------------- Outputs / results --------------------------------- # diff --git a/2020/23-Crab Cups.v2.py b/2020/23-Crab Cups.v2.py new file mode 100644 index 0000000..576a307 --- /dev/null +++ b/2020/23-Crab Cups.v2.py @@ -0,0 +1,156 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools +from collections import Counter, deque, defaultdict + +from compass import * + +# from simply_linked_list import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """389125467""", + "expected": ["92658374 after 10 moves, 67384529 after 100 moves", "149245887792"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["45286397", "836763710"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +if part_to_test == 1: + moves = 100 + for string in puzzle_input.split("\n"): + cups = [int(x) for x in string] + + for i in range(moves): + cur_cup = cups[0] + pickup = cups[1:4] + del cups[0:4] + + try: + dest_cup = max([x for x in cups if x < cur_cup]) + except: + dest_cup = max([x for x in cups]) + cups[cups.index(dest_cup) + 1 : cups.index(dest_cup) + 1] = pickup + cups.append(cur_cup) + + print(cups) + + pos1 = cups.index(1) + puzzle_actual_result = "".join(map(str, cups[pos1 + 1 :] + cups[:pos1])) + +else: + moves = 10 ** 7 + nb_cups = 10 ** 6 + + class Cup: + def __init__(self, val, next_cup=None): + self.val = val + self.next_cup = next_cup + + string = puzzle_input.split("\n")[0] + next_cup = None + cups = {} + for x in string[::-1]: + cups[x] = Cup(x, next_cup) + next_cup = cups[x] + + next_cup = cups[string[0]] + for x in range(nb_cups, 9, -1): + cups[str(x)] = Cup(str(x), next_cup) + next_cup = cups[str(x)] + + cups[string[-1]].next_cup = cups["10"] + + cur_cup = cups[string[0]] + for i in range(1, moves + 1): + # #print ('----- Move', i) + # #print ('Current', cur_cup.val) + + cups_moved = [ + cur_cup.next_cup, + cur_cup.next_cup.next_cup, + cur_cup.next_cup.next_cup.next_cup, + ] + cups_moved_val = [cup.val for cup in cups_moved] + # #print ('Moved cups', cups_moved_val) + + cur_cup.next_cup = cups_moved[-1].next_cup + + dest_cup_nr = int(cur_cup.val) - 1 + while str(dest_cup_nr) in cups_moved_val or dest_cup_nr <= 0: + dest_cup_nr -= 1 + if dest_cup_nr <= 0: + dest_cup_nr = nb_cups + dest_cup = cups[str(dest_cup_nr)] + + # #print ("Destination", dest_cup_nr) + + cups_moved[-1].next_cup = dest_cup.next_cup + dest_cup.next_cup = cups_moved[0] + + cur_cup = cur_cup.next_cup + + puzzle_actual_result = int(cups["1"].next_cup.val) * int( + cups["1"].next_cup.next_cup.val + ) + # #puzzle_actual_result = cups[(pos1+1)%len(cups)] * cups[(pos1+2)%len(cups)] + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2020-12-23 06:25:17.546310 +# Part 1: 2020-12-23 06:36:18 +# Part 2: 2020-12-23 15:21:48 From 4aa9799b135afecc95eac806cd8c438e82ad0343 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Tue, 21 Dec 2021 09:38:39 +0100 Subject: [PATCH 136/143] Added day 2021-21 --- 2021/21-Dirac Dice.py | 154 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 154 insertions(+) create mode 100644 2021/21-Dirac Dice.py diff --git a/2021/21-Dirac Dice.py b/2021/21-Dirac Dice.py new file mode 100644 index 0000000..62ad688 --- /dev/null +++ b/2021/21-Dirac Dice.py @@ -0,0 +1,154 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """Player 1 starting position: 4 +Player 2 starting position: 8""", + "expected": ["745 * 993 = 739785", "444356092776315"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["920580", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +p1_pos = ints(puzzle_input)[1] +p2_pos = ints(puzzle_input)[3] +if part_to_test == 1: + p1_score = 0 + p2_score = 0 + i = 0 + while p1_score < 1000 and p2_score < 1000: + p1_pos += 8 * i + 6 # real= 18*i+6, but 18%10==8 + p1_pos = (p1_pos - 1) % 10 + 1 + p1_score += p1_pos + + if p1_score >= 1000: + i += 0.5 + break + p2_pos += 8 * i + 5 # real = 18*n+15 + p2_pos = (p2_pos - 1) % 10 + 1 + p2_score += p2_pos + + print(i, p1_pos, p1_score, p2_pos, p2_score) + + i += 1 + + puzzle_actual_result = int(min(p1_score, p2_score) * 6 * i) + + +else: + steps = defaultdict(int) + steps[(0, p1_pos, 0, p2_pos, 0)] = 1 + probabilities = dict( + Counter([i + j + k + 3 for i in range(3) for j in range(3) for k in range(3)]) + ) + universes = [0] * 2 + + print(probabilities) + print(steps) + + i = 0 + max_len = 0 + while steps: + i += 1 + step, frequency = next(iter(steps.items())) + del steps[step] + player = step[-1] + # print ('Player', player, 'plays from', step, frequency) + for dice_score, proba in probabilities.items(): + new_step = list(step) + + # Add dice to position + new_step[player * 2 + 1] += dice_score + new_step[player * 2 + 1] = (new_step[player * 2 + 1] - 1) % 10 + 1 + + # Add position to score + new_step[player * 2] += new_step[player * 2 + 1] + + if new_step[player * 2] >= 21: + # print ('Adding', frequency * proba, 'to', player) + universes[player] += frequency * proba + else: + new_step[-1] = 1 - new_step[-1] + # print ('Player', player, 'does', new_step, frequency, proba) + steps[tuple(new_step)] += frequency * proba + + # print (steps.values()) + # if i == 30: + # break + + # print (len(steps), universes) + max_len = max(len(steps), max_len) + # print (max_len) + + puzzle_actual_result = max(universes) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-21 08:13:41.813570 +# Part 1: 2021-12-21 08:41:31 +# Part 1: 2021-12-21 09:35:03 From 06d5f91955675edf75d5ea35c413223bbdcd0482 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Wed, 22 Dec 2021 09:15:01 +0100 Subject: [PATCH 137/143] Added day 2021-22 --- 2021/22-Reactor Reboot.py | 243 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 243 insertions(+) create mode 100644 2021/22-Reactor Reboot.py diff --git a/2021/22-Reactor Reboot.py b/2021/22-Reactor Reboot.py new file mode 100644 index 0000000..1d4c0e3 --- /dev/null +++ b/2021/22-Reactor Reboot.py @@ -0,0 +1,243 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """on x=10..12,y=10..12,z=10..12 +on x=11..13,y=11..13,z=11..13 +off x=9..11,y=9..11,z=9..11 +on x=10..10,y=10..10,z=10..10""", + "expected": ["39", "Unknown"], +} + +test += 1 +test_data[test] = { + "input": """on x=-5..47,y=-31..22,z=-19..33 +on x=-44..5,y=-27..21,z=-14..35 +on x=-49..-1,y=-11..42,z=-10..38 +on x=-20..34,y=-40..6,z=-44..1 +off x=26..39,y=40..50,z=-2..11 +on x=-41..5,y=-41..6,z=-36..8 +off x=-43..-33,y=-45..-28,z=7..25 +on x=-33..15,y=-32..19,z=-34..11 +off x=35..47,y=-46..-34,z=-11..5 +on x=-14..36,y=-6..44,z=-16..29 +on x=-57795..-6158,y=29564..72030,z=20435..90618 +on x=36731..105352,y=-21140..28532,z=16094..90401 +on x=30999..107136,y=-53464..15513,z=8553..71215 +on x=13528..83982,y=-99403..-27377,z=-24141..23996 +on x=-72682..-12347,y=18159..111354,z=7391..80950 +on x=-1060..80757,y=-65301..-20884,z=-103788..-16709 +on x=-83015..-9461,y=-72160..-8347,z=-81239..-26856 +on x=-52752..22273,y=-49450..9096,z=54442..119054 +on x=-29982..40483,y=-108474..-28371,z=-24328..38471 +on x=-4958..62750,y=40422..118853,z=-7672..65583 +on x=55694..108686,y=-43367..46958,z=-26781..48729 +on x=-98497..-18186,y=-63569..3412,z=1232..88485 +on x=-726..56291,y=-62629..13224,z=18033..85226 +on x=-110886..-34664,y=-81338..-8658,z=8914..63723 +on x=-55829..24974,y=-16897..54165,z=-121762..-28058 +on x=-65152..-11147,y=22489..91432,z=-58782..1780 +on x=-120100..-32970,y=-46592..27473,z=-11695..61039 +on x=-18631..37533,y=-124565..-50804,z=-35667..28308 +on x=-57817..18248,y=49321..117703,z=5745..55881 +on x=14781..98692,y=-1341..70827,z=15753..70151 +on x=-34419..55919,y=-19626..40991,z=39015..114138 +on x=-60785..11593,y=-56135..2999,z=-95368..-26915 +on x=-32178..58085,y=17647..101866,z=-91405..-8878 +on x=-53655..12091,y=50097..105568,z=-75335..-4862 +on x=-111166..-40997,y=-71714..2688,z=5609..50954 +on x=-16602..70118,y=-98693..-44401,z=5197..76897 +on x=16383..101554,y=4615..83635,z=-44907..18747 +off x=-95822..-15171,y=-19987..48940,z=10804..104439 +on x=-89813..-14614,y=16069..88491,z=-3297..45228 +on x=41075..99376,y=-20427..49978,z=-52012..13762 +on x=-21330..50085,y=-17944..62733,z=-112280..-30197 +on x=-16478..35915,y=36008..118594,z=-7885..47086 +off x=-98156..-27851,y=-49952..43171,z=-99005..-8456 +off x=2032..69770,y=-71013..4824,z=7471..94418 +on x=43670..120875,y=-42068..12382,z=-24787..38892 +off x=37514..111226,y=-45862..25743,z=-16714..54663 +off x=25699..97951,y=-30668..59918,z=-15349..69697 +off x=-44271..17935,y=-9516..60759,z=49131..112598 +on x=-61695..-5813,y=40978..94975,z=8655..80240 +off x=-101086..-9439,y=-7088..67543,z=33935..83858 +off x=18020..114017,y=-48931..32606,z=21474..89843 +off x=-77139..10506,y=-89994..-18797,z=-80..59318 +off x=8476..79288,y=-75520..11602,z=-96624..-24783 +on x=-47488..-1262,y=24338..100707,z=16292..72967 +off x=-84341..13987,y=2429..92914,z=-90671..-1318 +off x=-37810..49457,y=-71013..-7894,z=-105357..-13188 +off x=-27365..46395,y=31009..98017,z=15428..76570 +off x=-70369..-16548,y=22648..78696,z=-1892..86821 +on x=-53470..21291,y=-120233..-33476,z=-44150..38147 +off x=-93533..-4276,y=-16170..68771,z=-104985..-24507""", + "expected": ["Unknown", "2758514936282235"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["582644", "1263804707062415"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +class ListDict(object): + def __init__(self): + self.item_to_position = {} + self.items = [] + + def add_item(self, item): + if item in self.item_to_position: + return + self.items.append(item) + self.item_to_position[item] = len(self.items) - 1 + + def remove_item(self, item): + if item not in self.item_to_position: + return + position = self.item_to_position.pop(item) + last_item = self.items.pop() + if position != len(self.items): + self.items[position] = last_item + self.item_to_position[last_item] = position + + def __len__(self): + return len(self.items) + + +if part_to_test == 1: + on = ListDict() + for i, string in enumerate(puzzle_input.split("\n")): + coords = ints(string) + if ( + coords[0] < -50 + or coords[1] > 50 + or coords[2] < -50 + or coords[3] > 50 + or coords[4] < -50 + or coords[5] > 50 + ): + print(i, "skipped") + continue + for x in range(coords[0], coords[1] + 1): + if x < -50 or x > 50: + continue + for y in range(coords[2], coords[3] + 1): + if y < -50 or y > 50: + continue + for z in range(coords[4], coords[5] + 1): + if z < -50 or z > 50: + continue + if string[0:3] == "on ": + on.add_item((x, y, z)) + else: + on.remove_item((x, y, z)) + print(i, len(on)) + + puzzle_actual_result = len(on) + + +else: + cuboids = [] + for i, string in enumerate(puzzle_input.split("\n")): + new_cube = ints(string) + new_power = 1 if string[0:3] == "on " else -1 + for cuboid, power in cuboids.copy(): + intersection = [ + max(new_cube[0], cuboid[0]), + min(new_cube[1], cuboid[1]), + max(new_cube[2], cuboid[2]), + min(new_cube[3], cuboid[3]), + max(new_cube[4], cuboid[4]), + min(new_cube[5], cuboid[5]), + ] + # print (cuboid, new_cube, intersection) + if ( + intersection[0] <= intersection[1] + and intersection[2] <= intersection[3] + and intersection[4] <= intersection[5] + ): + cuboids.append((intersection, -power)) + + if new_power == 1: + cuboids.append((new_cube, new_power)) + # print (i, string, len(cuboids)) + # print (cuboids) + nb_on = sum( + [ + (coords[1] - coords[0] + 1) + * (coords[3] - coords[2] + 1) + * (coords[5] - coords[4] + 1) + * power + for coords, power in cuboids + ] + ) + + puzzle_actual_result = nb_on + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-22 08:23:07.073476 +# Part 1: 2021-12-22 08:37:38 +# Part 2: 2021-12-22 09:12:31 From 736ec8f5449f69d31ccac3ea9d2eb6f286641e85 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 25 Dec 2021 02:18:20 +0100 Subject: [PATCH 138/143] Added day 2021-24 --- 2021/24-Arithmetic Logic Unit.ods | Bin 0 -> 13962 bytes 2021/24-Arithmetic Logic Unit.py | 293 ++++++++++++++++++++++++++++++ 2 files changed, 293 insertions(+) create mode 100644 2021/24-Arithmetic Logic Unit.ods create mode 100644 2021/24-Arithmetic Logic Unit.py diff --git a/2021/24-Arithmetic Logic Unit.ods b/2021/24-Arithmetic Logic Unit.ods new file mode 100644 index 0000000000000000000000000000000000000000..f024b9b44e5140cfce61bfdc5813da427bf45f7e GIT binary patch literal 13962 zcmb8W1y~)+wk-?8mwYI6Vjm$nWF*Um!LXHYU#Q_9h1Q z_SP0g2F@0CwhUivjp^+SoGhH^?d(l#jqQwFY)ovO>7DEyO$>~k%uP(3<^K)y9p=9U z^SvcxXKQL<=HmD-G$&>TXFEGp$^TaC-BOqTxi9R$vC!Vm-o^g6 z>i=LP?%(Jc7#W#Zo4gOy?tilL?|S?_C-w%mCf5JU9_=0N%p6Ueoc{M?baFOucKN^P z5&X>`5Eu3u(?4AC9bD^Q3|MfV%-~Zq8@x5hZU~6G&;^a*4Xly!>Fku_W zh!pe-@YQ5;`7;9qgd^o>t7Uc%Td+C=!(}?kW><8AszO28N%s6+l_<+rbi+5G0q$@_ zT}iJps}U91sJMCKF%5S+eQV(w_v&&J4M95Ig=Ju2Hg%h;Ej5>3@>e>JkJeN5d6aJH zR{*&V;@$;L2l0=C4bjjr_)&ruH~U`LT$8&nrfLe#r#Z3icZ9qV8{DNib+86aik%@% zgmM0#qhO>*gPa3#+hnB%Sdy<4xO#JeyF>b&bAfpP{d1b=spZYkk3|Q%+nqIM+JReM zN#-7BMjkG60~cE)iTWALXL2X)JnnBtuk=%ISi98?UxckcPv)CF#vV0X5~zq(%S%B) zH{6-eLV$vR1bzSk`L8|jchmQ;-Du)$K<{Q_9i=1_vqp&2d8elCT#Xsny*PX;$*at3 zy&yRu*(riB-qrw49JzGdUCm$rjXh*Db*Iz)$~4D|lfdO1tV(H7IW!m{vYbsm^Ks z9NM#YonwlepUONQk0-+^<(?75RcH}+q@fUx6R8O6BO~LAyvKTYOo_#WUQZh?Ro9EI z5<40bF;wU3RQY%s?Jb!wmFTUd@+zSHX}~Twx14J1^?Nl2TpPp(l8v@U1w34@w2cIS zu>s0#yO_!2)$5aY(^UL6yuPpFEd?|`{r;Iybwj^1ntNsQe0Rs}WQ@!g-7x*hr^6gj zxHTL-qTu8LkjiFl(JMA9#Dw|e7^G4u$3`;8Q?Ys9_l1OlhWl!blmEW3a{uGP!oKed zCuetS6Q|!x3)Hf)TVqFh?b0JE>8SxtxY=z8>+J7ctn#!9%mvoOK*%UCD@dSHa?3uR z4ykFxlW9cj%~m=DknZw!@Laa?C?-0(d#8{u(&a&ruOtCu0BeF+q(qauG*7<}t25!A zJZ>k*!iYc4u#(!R(O;Az0k?1Q9#hWUD!D>sh#O4`}ZYSa~A+CcoJP2_~xW>h6G=M{w}n)nTcz-`khB`-tX zbef2^_aUAf1ziw>Oq^4-<#m{Wtq&r{`iDMg3wQV~JN{~l5T5vK##BE*K!uD5qC8g< zJXuKC%acCDq@`WwXBQM<_EnPDjc%7akGU?-=Ot% z0*3=xNqR*mp4Su_W?+m3Cb>=Ruu6=MZpu9_S2Jd1RvJExdk>>atTO97Sa1Oz{XM-H z=MUBS0cHwDNolX7d@DA|&;)dZNw>{-FU?~ewk)uwAL4^>cXSw|1a?~$O7TTPLc;o1 zH<*eUh*iO`I_0=<$xU^I)FH8}$yu?ZhTKhNrU#MGdTb;hai54&QR}6WKuqcjtG;Tf z-QtYbOE_9~LSa$(F`z)E@9w{jzM}6J+}HXmMK!4?(VfdiGRq<|91p-^h``4gL2@BB z`x#7xVbCNjc^6v9Ipm>C1j^{tj5`;Q>In-3Z4DG9ZJpo;)s7i z*ECD9beWGZj;-RC%Dwp&g_y3w$Q-UPJt@ZjZid6s{p1Ly#nlpe%oq3Y zIRY=$DlJP>qGdUvogR2vVYk!`=ZSrZ^cvvv8X&gIz)9%a8~0$f{QO~u8&yD>IhOOP-#}SfE|}doGD0eCTGE{I46Jj8 ziy=f+00mcbiXL#W&Dk41 znfcL07|~&Nqo8DY_4>)vV+x(L9d)|R)b+WQoq7{F1tv$TI%m0&Fa|A+?1wF7y1!s$ z@To!Mf8hFBb7Q+5HpeIX2Fl9Qi#76z19AHhLlVY2XhA`djdYLrVI^W=GohN7Ys2x< z1w_=9=iw1XP@QNHHK;kofpGq+nny8nW5QuENJO^*cqjod{`7<#Y!#DRXWW_o8YYH}a~st#*9 zRSE?)?AC*1;fKd*1|zwy)a7V1#=31x15NQ=d|b!CKpW2)B-Do%xL2`tm24P@{Q8l{ zH2K2weL`gh98Z#|ZpKbc+Y-Qq%A_nrZ#}grjzb;OECYXR*Yv|G{(`L;(?S3$YGL^0 z0mTL->q1@5MUx~OqNZeqV|Ukw);f|{)KH`Sh>6x$9a&z%)DDss@`#0W92HSO?(|8QGVj{Yk>wlBnTl z@+A(Oqb(=?QD5!3m5&-V#!6$ZM94?DutXu}EL@rA@ggH1M_~9!$Y42l$-uOSrM^=u zAjWSr%%08^iZ35d!jjarjBp9M6?*YO2iOVh;Fw%>Xwsrz%~ZGNCeEh#6Wxl|o7WUk zVmwUTjQ#j=qCU`69*Mf<ONDfHC2G=`5ky9N506-XFpc>yxWL|>A=HVuM1xZ!<>21R&SDA-X-7ge1gu;i z$vGrvIk`{6bPwV)*~Jb1HMpg{Y4x{39|~n~1%}F|I8E?I<6;*XCF9umNn(_QZ54vE zsE|SK0FlDqR_)&uI1@8_T!+Ey>IApue3}I;R!^;7SbCP|{F504IY82etYeZtdKKG^ z@>xS!^>d|ua+te0;F*U-3uvPPlcRKz35%!B7n%m`dF8V#C@CY|_`n#Fo9&J9LQUj0 zl`D{t>y5!zh${5i(WKWab(%AjjN{WPxpo*un(;UzJpMMW`YERdi=TUB%?FMxL?wsX z+`b`PmJp#(zGL0b>kAue-oE(LHMY)0_TD81pW)xpI^M{Envcf7}-ThTk4r62hv2zcrCU={_Gk<5Ycbk z`@CtZmM+8yUakIV`>(#=}<|gowaOJcoQOb@{57BxjO!Emg05&8roBe}DFc zh=>)UiHI&hQZL*8P{9LYCBGx|A_XX_LRU6=PWp^+PH4q=B1!)66t#i6$t~#rX1N~zw*X~{SDyQ!w}F$f-;xJ%DX^~N!)*&%5A3>xSd;M zcZhn1o}eHC%4YIX;^|*lSp!69!F9D`gLj?9HP^on)iV~}qu5kG#aL@w0xm_ZD#P7; zNi?U^spi37ZK>WV4?9-D?;Dt;VN+6Q-|%N*!49m_AczzTUu8P+R;tD+ThKO8S4&AS zeLI0c^vcnpw*l@Rmao2*-RcG2^`N9j|B{9IjFp&4^sMicdKRY4bf95R(cWY|*tt); zR+rYivLx)M!VsPXnSmz@iP9CfvX5zi%a&V_pkztapINkqTC?zDvB~?YG~XpGT5kCH zIMcWQ)H{et`7N4@CU$iu#&`%UMp0L)@%Hb$w?CoDJS`Pa~$Snj2 z2q@hjzZZW*!g?Ph%&p)-Kz<*;qgKl1E;fd?1{T&%49FsUJ!sKPe;9;?UdvWj* z;=+pW=c)JO4f@^b12G_T?0G-w%S$VZyr1##@W{x>XlZF#Sy_2_c!Y$6q@|^mm6f%$ zw2X|5tgNh@oSZy8Jp%&+BO)Ra5)!_B`<9=dUtV5bUtize-rm>OH$FZ-H#fJowzjvo zcY1nyb#?Xn`udLi_Vy-X&NmMN@^Mf?SU}lr6_^?3xW<<$F8`$> zviN6uW;i%~+H{yn>u5IU?M42Ko4=6`1H}}mn{z`&ZFdK*vb@qIDOtbi>tkC^!XyXz z$Fb}V>D2-NyUy!lnUg}+?e_jd_n`&UUhS6qT4&`ZVR`WZQ%<9-DkO+r{#%9}gpo3f zX3{I9=Y)s1W)>QKl;vi!t#9Z4@!Zxk*izn0p(T+&xxb&>*c>#piNA7*pg11?kw$MD z8auT=A)>n`KG?7j%j;m+7pW(&uDCN^1_jJ``mjZ3nW@oFkJ;u)1NNpg3%-x+TU&`& zCr{sA7PXlN8z>G|NKEc3OOCFKDW+SH9Skr_Rafc)J z6|i3Cv^c;;*a?T`<*0O9Eanh>^1WP}d*Mh#T>~x*23nZ(sLPVKaBB2e@P5q?R@J;eMsL7_ew+sN{ z_R6}q$MpAcz=GB%4(*0uQ#?%&_sp~q>khLLCReMacuW!z(?gA3tMq}I2@5@D4YNay z^jTC&NdBnE>6|!rCOIT>%=0fFV}86T3y5T<9Q~fQ?4vN7rJ9)UPStm|OS6~>^`BWs zh?Lq+?L?=)@5iThsESj=;nJx}W(>hE4C{N&iRBkcf^;4)#m_hII9{0-pzVPm+dlfE zfTgMgiqY8tM!!n~Z1j51F1{UFt1LB?!JRB&4*7T4JH`(${uU_q{ zw`_u2r^Lp3^+-#BcTJV`CM|hZ{mG$q`QfMk48iF@|9B%&F@I)E@k`yb%joyobX$N{ zKpk0y6BVz)4?Xplpa44q>mvi*RW|KuUA15u_(`y-Phbu$4@#4p&!v4y5J#Vju%OJ3 zG&Es8CinI+LdAfYqSkD=<;ALuupP_pf>s9XT&d_8wfYBmz5vB<9|<`_q7j<{FP<;+ zf+H;4Tk?WM5%{j3CvE*szoRx}Cz*lP+Q8nKA+zp^jJNWiW}0o>u|xsKH}Xe0wG-;= zFC#seMA-12>c5v1Zd)#rone$rYre^A_Icf?J}Ry@+-~z(P8aigOJ;er$9xaO!le=0 zUJ0@mjjLGD?4{_T5N9{oF8~n1_37W>CWA>t7B1iu1tncjxrdtM9j&QGJ&4L+w&KG= z7fcS<3sWZp>ne9<-G8F-PA%AFeKi@q7|FHeDhTE{a~@nd%%mAvqH^~c`%Y24!*z4& z?zT_l<~Twp7_3?AJAA(eyKmu+-cD^WvT2uJ3lp71bmwBBpJ63;G z%zKLeaIE`Q6qK1P7BDe_mCNo=5-2Ycn8Ci))ealMcyogzH(^hi)ptL)o}J=MnDz8{ z+AwKVow=E>^<7yjV}OdQeazDs)i=s+Xz#{;|PShGq-=8!_y zveZx4kaMB#FKU@)2gmdjOWGj6UiUxtaulnZ9kfn0n@#ygXJo%GOGU^wld8MI$NtdM zViZ^CVh~$R&l=RTK=S4rrR_EKE%LeOsWp#t^{p$2%lp!di|eL-4wd^+$2pLZ9}|63LgD4yv7vc6KCW+)*@Qdyjx6IUf~ z%UyxjW#01E&HW17Z2qGXtvD^zt^nu-=8d7idC`7);Yn+ZREs4+6YXc2(Rz_wE+huV zlrh}XfpFG_Ith2xA^ydPC_bY|Eg<=U1^_Lhz27HgEt(~^L%PlI4UA2YmZ>>@FidH$ zc7=l*zvoTZVKE{T;QKv$b4gE)mrgDoG9wSd4%&pjJFa@}vW1xLD_KpGes^)OVjjG+ zCVtg$$c=sgUG>#Tm4#|np?u?^6(QD3Y477H`ktccL0Hl9L~yX{RP(^=V~59cCx2mr zT{_pxF#vFg1ZY&DG)f`C(we-@G`l*vDAz^3YQDm$Tsy74TEAIZ>-2$NJMF(wX}$p> zKC)q+6d&y5C^1W(`#I4PD<5A>F`ZPGXS6iGQ2^X;@zKOKe|b^9a1-Q1O?7Q)+B5HdrJ&5?RNY_{paWEkRObL+K+U?A4wev@fJVhEGMdCPNT zCA>rUlFG43#k)YSW!btV-yycsR_6}vrBE|@kO!D~oeKlD>WuFqTwnqur$+aBEK&ex z2{3dv*{)rK&T@e)P1hVPR!9er6#$wJ)iMno`6g9bU(MF~9yXdc&BVmd`9H`50St(s zxwN+pwDD5)-jnswV6C5Bv-(fwE?M^t;M>0<9VFjAc=cf|4T;S;N>s!uX5T$rAXVTZ zy4|-=F1-*1CaPKWVsAyJOnJBsPUt?_C8^0G6=c0S<7Xi{*@`xn0Rh0C;NB{0m?Yi!VKI43pT2OU(pFH2YN+D!&a(PmGiA_qpZeFZ1`0 z%uFesi9!3?eUmuS@Vl#3w;0Jov@^+3%cGf!+0%%}zA=1qPxJ`A+yaExT}@}LVG}kK z;UM0Fq}N)xqardzRzAi*>JWtm#q`M8bn|R+BhE-WjTDaV8tNPGBI0JN)(RJhEjLDO zdsNJOE8&-5rtxUgP%jI&zdUw5>Ob9f3!h9Wd-x1Ov0Sv=S%#b9Dqo2>h_pX>Sj2AW zieDH2dfSaPk748%NB5>eOJkN3b4QvOe&V>{ToyT*(^V8fOLN(Se(}zE3maFLscW+8 zWhOnl%^p~eAl18QSQ2EVgDX#~k9?(|!h5pYa{w6m){wdpBI#eaJWSOzYN}nTBx^e0 zW#r$!(C|Sk+RCjUOke;5?5Zi=~BWF0Bmud z0>BE3fhyR_xjvWN^}L)fQzKW!?z6p*dJ$WXdMS063?c&_DUDxO^BsHUwt5uIpNWdU z8@^eoe${4HbK^_EdUVkz8>zOzJ;{%Upl>J>(Luvsk9edELaXo==byt^{{F)kDEfBl z{el`ottipe8~E55kaX+yl;RVG@4k23$Qxj`5ww2ClmEU|AIo0r8!sMG_MGOb$8e_9 z^?GtI&Y1SD7Hf&dP9Swy?5u^>E4xP!-Kw8kfuS)7285tPpoBupDU` z+2*sXaN4CEC4oF*OsMNv7B;rARyKu&4Zcc}#{yzp&p3}aEI@zmy+8@KveXRH`=+%I z7=&%`F}@5iPx$@Gb6#d@JO>Zkxe8YC3~@VrM`sHHVpOxv8v~oC<+_F2D4dsmVq~Ni z^wJrzX4Tc2Rv&q7u>i^jA0;0BWKtZgvZ;Q)xH{Lzlk)&z7F|*kkJfA#uc34JZ*c(e zowoM0S$z+4=+OgL22v#7YCY|ye!1e)sOLF~cxDSBxFSy z{yWM1o=&Swkg@F}L=wH@`a00WTmUQ*bf_zt|5N~#V`?9J?x!CbzlNdZuv-?6Dz$RK z((#KabmDBj{REmxBL?2{z)BUgNmuJ9_HE}|8UM0v`|34prN6Kvw(AcL=daMmHD?gl=5e-b+LmDflCml49RY5!#Cx!{j^D>W+m{B*IV z_dAFh8us$W^q#k*1^>e-{=0-U-20px+1Wb(uKoC(xYRkVT5Cam-71#X(XZ7K8cO!A z+T@G?77aAXgy*b)y2QIO6Yqneh7ge0_^(H*CDS>b8&MdA3K$ZiCYQ zQ!j7<;Rj_Tqe-`X20L?10J6Y*VLr%1YP6xJuplm~i&*t(i#jj@HyLeC2$P|ZaO5P~kAs%Iq>0gMRb6J6g#$7FL7sYvl@16scmAagq zT<5i|g0mp8Z(2+UJmC-_4ytZ2r(?nep`9>vxju@SVcW;#(4c&o4{JtRig%$Es=vAk zHht^Nmc=UIvGaQdMJg%Rw`=3_OWcCd9I@Fbm>@yEgg+FnMnmkF4p8ZHYg6t+(`n7*1+CBEAt{{tx zNUSIp{%n7_ZtHaRxVjGS$jVmCAXwJ@D;77;YCzVQoS~*$`xnN$YzO5wJ5zVvcem%c zN29aXBbQlb-Q0s!2WJ;14NBHj2ywZvVAZ_%tD1R6R!6a}rnzTrR)zkJr$ky_hZsE) zclKo^`j?{YdodYv!=MW(!-GZaGs7t=v)-`dIq$2?`OU_yN%yBt-G0g}JdKc;PQ;(4 zk4Jq^Hg-y?JhymojU*hjHqiC6JRhX63&aN5f0#7$h(>RzwlMzepfe1tP+8T|ORd4| z%^<}2hS0?Ocq`L{yO?3?-Q)L-na>>i1(6K6wg*}>@>mMs0jwo zpzNH4yHofF_RjI1xvx^kvSHIvHtcK+l#WWkvrOcu@%3f55IuRnqKZj}m0Q?kWLpuV zpL!%4-Qv=Ul#zO`7jk=Z3}S_A4C-mlokmxgdSX|pH3QqLrmK^h$g|x%Mzvv$XS6B2 zjr`*mZch9^P}!Op(uuTcIKy?MqhPRj#V}uN`HYHcD|!n~^9DNZzq`b{SZn|QfK?yr zw`9&@W1M*kt+!);xh4ahI^Okbs5`H^jYYDx^TWlud*k+UR;fZ$B|@e z(-z!9D9q0WJOfv;<(teLwXr?Slg1L7lYiQ;9By2P^=V!bv-ljQ`g)B&7*5fg1yM>W zDjmlpfKepr$_d5eGsYdsen^OSPyDGsaaLyz_)a8x+JVvj70`yiCN+z+W?-@~gtz6A z0UVB!2rfc~HS?ryJ!Zx-th;lAm-uClnzZFcK56+WC`8%BSV zA?n>9Fp+~p?6%38j>3#Zc-9h9ejF{D1^_=tI&amJv8^pxlaAO16GiCbNhi3EJJYpy z(#Y5nBV4Bs=CtCQttK3g5j9e89e)1&O$gLx6W09dY?eLEf@fOa(Dv@p6bOvyqpxf( zAO2>+98`<>mbntBf4w8j7(&-eX(f`Wk9%m#H$BX*=_5Xz51|Y!<-dz8oH@ca92o#yK!@}2 zxT=G@s8>|{)THKfpA_JehjZNL{CGJRfk0TTIwk@3u#L}Hjp#Jw&D4bn7Fj$mp#Zft z9;qI<>b#1b_&r96c%lLOjo~82OCi)c(*}J1<}0Nt7n>dPzJzkiZcxWOQj#W=q(D~c zRT5K+67*HC4%IZ5i7M=k^b8lr9f${78GVnRF2bWX*K{if(urS|`6EUkORY1cGxSie ziDWZL23BtwAx4I%GSUw%9lx_ni89oA#Sigc1TxJVS!2|>=1;b%!-`7Tf#9q%Yjd(- z7T7yqR9Q+yW?B3i1skLTJ9TA;?2Apndrf)SG{XG9#({IyFvcoc`bfg4r=pYbuEKKH zFvhd$L}biuzfNsoAm1aV2W@XNkYOR~P-32$9VmZTjW@1(h&yu(FGF4Sh-36Hs)UED z-$QOch*g6!l_(fT5>HdJk}EYAtWOLjE*K|k`s}oyv}>M@(8Nv}wJFPVWZjZaA;8V+ zm%y@3R~~}Pi8Cy~#t#LNArs+9Y6F4Ci6fC-B@=ui{uu!kp=C-`p3@6P1j65E8sZ0i zc7dcoC5C@K+q~ul$54k+^E{yBLmMf3@ha4w|Ad97k0cTM5%*Zm1AuY>$clR!xw_Mj z-I57s6Jg;%Adj9uZlKN-Z51W|rDEj|uu2%H_w} zl3-vuFzUnWQ`WdTE|D=pA(Y^NrKSj!?%2t^Gd(N)Z`gIKojFBi2UKBSRI!M!NE z{oG*44KLO~Du5u?L5RF?jYo>a>4>URs4M~ZSuLi=pG4OZV$Z%GZrv=w9rq=wncnWW zeeQ9>!2|Dd7RRcRGGoq+Yf?6dyF|*+89YNq9!g2yxd0PU1T&LHCh+i6E^6?1LTbxEup@}cPetuLlxCPR(Zk+O2JS!q9H1Zk09al(Rt;< z$W6jCD#Xw`K^Q;xd#o%=S3H;kvH1;RwQN9Re8UCW#;D2^^@Ws)MY?-M*prYeZt*yK zpqqI4^N>iO%{mG`Z^72=M5;wCg7fXEz%APkN}=rA55g%b$ZQFK`ICsvs7NL?fH>b! z;&Sd4sxl&&b@X_%r;W@!2a3_`Nnqr(d&N^jH0n}xGz4qXydGXtMLS*PApD45&Jfdg z(ybIO1cRH1Cxfl!TGD`=I1-?;E#6VL5&^Of=p70p-X%x`pyGz&+ZTWlLghg*t4vHO zgU-z5X-jGeK;?QG?W>?W@ zp=SbQpSyYZiw5+X?&uoJ~1XqP2u z`hCc(?#V;@(S6{hZV71Gg`dn-`T6~d?u&npI;*jLwg;<}z%IaQe4h>D-?QY8*q!7H{;7onrbYnUw`nvEZ}Xh!X} z!hbQ_C$L30JRmT^nwE+aT=9HiS7{W}Jel}?d+)PBrjt~o!Y@15-0XlxsN{qZjE8m4 z?JgD%cx@fcNr$NrzhcfqSSi_v&w}g1g?5XU#+vpMbX;ENp^{sZ`rXgbuYG`Fmk3Ab z&pzzms?M ztOgc#-V>$tq_G8R9xw)XPocCBMrmt%ETvB6PbMZ%hxNlkWaXc`#nS?q}U z`Y*~%a<06eYieJx=~kr_?)?ls2N(01w7zPX);fWdW0I*K2+7psie9<~9Wq@g{9HW@ zT6P+R3|1TYPa!O`6uW_@@6gF8TiUChy}j!m^wFbX{WXkD9g_B#7SV zCL68Q8yzc_oR}a*j#?NCg6p*}7M{Kgb8+`4C z2^hErL$r3Y_`?0gzZ6&pSlPdOjW=3Ode-$QjC3u8O;i;qQy=S$lV6l3=2zXqe-CMV zlrHQe7hFz7o$DNvn^l=@$K3ej7+J~cs}S8)qI~hI+DKbiT>D6ys;MY=t1CL{+PAr* zc$DT?cU+h^Ih}H{I7s=He2I>l2xW;&ZGnzjp;TVr*ybGnlFYXDuH{ryS-g-F8u| zh+t?`{-bG)o*Q$Lib6Dv)?p<;zZ^mkK_MzFbu8t67b$Ja63ws4)adl;FT{$So4&f! z?v>toIjc}1Qv7ZOS>(-q8pkN30yDF7QqhB4;y-X zzPbctVF|_idlgw>xD>|@>kkNDgI@2Mi07amAaxBr@maX)#yI%3^5-C4x!sm=X&fwm z(4v>D|!CsweoxOp9vuU z-uItc|A`X*Hzdp{3*BZ&Ho(HUkM}sjPsi^@~6q`wkQ{u$>t<>XI60sBAa z=>H&~{1@0CA*;W#D}LAg{V8(qVE-K}d?%#*@eu!^`PcIO-|UM&CHlR#Yq|h_z%I=|AhGK_sH+);GaT3{BN;Bc_|3U-yb5s|Czi884;O(U;Teyg$yeI literal 0 HcmV?d00001 diff --git a/2021/24-Arithmetic Logic Unit.py b/2021/24-Arithmetic Logic Unit.py new file mode 100644 index 0000000..844e6e7 --- /dev/null +++ b/2021/24-Arithmetic Logic Unit.py @@ -0,0 +1,293 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """inp w +add z w +mod z 2 +div w 2 +add y w +mod y 2 +div w 2 +add x w +mod x 2 +div w 2 +mod w 2""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["92928914999991", "91811211611981"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +# The goal of this file is two-fold: +# - The first part outputs a readable 'formula' for each step +# - The second one executes the program for real + +# Based on the 1st part, I manually executed program steps +# Each time a simplification was possible (= values yielding 0), I wrote & applied the corresponding hypothesis +# At the end, I had a set of hypothesis to match & I manually found the 2 corresponding values + + +program = [line.split(" ") for line in puzzle_input.split("\n")] + + +generate_formula = False +if generate_formula: # Generating a formula + + def add(a, b): + if a == "0": + return b + if b == "0": + return a + try: + return str((int(a) + int(b))) + except: + if len(a) <= 2 and len(b) <= 2: + return a + "+" + b + if len(a) <= 2: + return a + "+(" + b + ")" + if len(b) <= 2: + return "(" + a + ")+" + b + return "(" + a + ")+(" + b + ")" + + def mul(a, b): + if a == "0": + return "0" + if b == "0": + return "0" + if a == "1": + return b + if b == "1": + return a + try: + return str((int(a) * int(b))) + except: + if len(a) <= 2 and len(b) <= 2: + return a + "*" + b + if len(a) <= 2: + return a + "*(" + b + ")" + if len(b) <= 2: + return "(" + a + ")*" + b + return "(" + a + ")*(" + b + ")" + + def div(a, b): + if a == "0": + return "0" + if b == "1": + return a + + if len(a) <= 2 and len(b) <= 2: + return a + "//" + b + if len(a) <= 2: + return a + "//(" + b + ")" + if len(b) <= 2: + return "(" + a + ")//" + b + return "(" + a + ")//(" + b + ")" + + def mod(a, b): + if a == "0": + return "0" + + if len(a) <= 2 and len(b) <= 2: + return a + "%" + b + if len(a) <= 2: + return a + "%(" + b + ")" + if len(b) <= 2: + return "(" + a + ")%" + b + return "(" + a + ")%(" + b + ")" + + def eql(a, b): + if a[0] == "i" and b == "0": + return "0" + if b[0] == "i" and a == "0": + return "0" + if a[0] == "i" and len(b) > 1 and all(x in "1234567890" for x in b): + return "0" + if b[0] == "i" and len(a) > 1 and all(x in "1234567890" for x in a): + return "0" + + if all(x in "1234567890" for x in a) and all(x in "1234567890" for x in b): + return str((a == b) * 1) + + if len(a) <= 2 and len(b) <= 2: + return a + "==" + b + if len(a) <= 2: + return a + "==(" + b + ")" + if len(b) <= 2: + return "(" + a + ")==" + b + + return "(" + a + ")==(" + b + ")" + + vals = {i: "0" for i in "wxyz"} + inputs = ["i" + str(i + 1) for i in range(14)] + current_input = 0 + for j, instruction in enumerate(program): + # print ('before', instruction, vals) + if instruction[0] == "inp": + vals[instruction[1]] = inputs[current_input] + current_input += 1 + else: + operands = [] + for i in (1, 2): + if instruction[i].isalpha(): + operands.append(vals[instruction[i]]) + else: + operands.append(instruction[i]) + + operation = {"add": add, "mul": mul, "div": div, "mod": mod, "eql": eql}[ + instruction[0] + ] + + vals[instruction[1]] = functools.reduce(operation, operands) + + # The below are simplifications + # For example if the formula is "input1+10==input2", this is never possible (input2 <= 9) + if j == 25: + vals["x"] = "1" + if j == 39: + vals["x"] = "i2+11" + if j == 43: + vals["x"] = "1" + if j == 57: + vals["x"] = "i3+7" + if j == 58: + vals["z"] = "(i1+4)*26+i2+11" + if j == 61: + vals["x"] = "(i3-7)!=i4" + if j == 78: + vals["x"] = "0" + if j == 93: + vals["x"] = "i5+11" + if j == 95: + vals["x"] = "i5+1" + if j == 97: + vals["x"] = "i5+1!=i6" + if j == 94: + vals[ + "z" + ] = "((((i1+4)*26+i2+11)*(25*((i3-7)!=i4)+1))+((i4+2)*((i3-7)!=i4)))" + if j == 115 or j == 133: + vals["x"] = "1" + if j == 147: + vals["x"] = "i8+12" + if j == 155: + vals["x"] = "(i8+5)!=i9" + if j == 168: + vals["x"] = "0" + if j == 183: + vals["x"] = "i10+2" + if j == 185: + vals["x"] = "i10" + if j == 187: + vals["x"] = "i10!=i11" + if j == 196: + vals["y"] = "(i11+11)*(i10!=i11)" + print("after", j, instruction, vals) + if j == 200: + break + + print(inputs, vals["z"]) + +else: + add = lambda a, b: a + b + mul = lambda a, b: a * b + div = lambda a, b: a // b + mod = lambda a, b: a % b + eql = lambda a, b: (a == b) * 1 + + input_value = "92928914999991" if part_to_test == 1 else "91811211611981" + vals = {i: 0 for i in "wxyz"} + inputs = lmap(int, tuple(input_value)) + current_input = 0 + for j, instruction in enumerate(program): + # print ('before', instruction, vals) + if instruction[0] == "inp": + vals[instruction[1]] = inputs[current_input] + current_input += 1 + else: + operands = [] + for i in (1, 2): + if instruction[i].isalpha(): + operands.append(vals[instruction[i]]) + else: + operands.append(int(instruction[i])) + + operation = {"add": add, "mul": mul, "div": div, "mod": mod, "eql": eql}[ + instruction[0] + ] + + vals[instruction[1]] = functools.reduce(operation, operands) + # print (instruction, vals) + if vals["z"] == 0: + puzzle_actual_result = input_value + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-24 11:07:56.259334 +# Part 1: 2021-12-25 02:07:10 +# Part 2: 2021-12-25 02:16:46 From b46f16a46ed5e4c431bcbac8c2b3c06d61f2733a Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Sat, 25 Dec 2021 08:55:18 +0100 Subject: [PATCH 139/143] Added day 2021-25 --- 2021/25-Sea Cucumber.py | 144 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 2021/25-Sea Cucumber.py diff --git a/2021/25-Sea Cucumber.py b/2021/25-Sea Cucumber.py new file mode 100644 index 0000000..ea48280 --- /dev/null +++ b/2021/25-Sea Cucumber.py @@ -0,0 +1,144 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """v...>>.vv> +.vv>>.vv.. +>>.>v>...v +>>v>>.>.v. +v>v.vv.v.. +>.>>..v... +.vv..>.>v. +v.v..>>v.v +....v..v.>""", + "expected": ["Unknown", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Conver integer to 36-character binary +# str_value = "{0:>036b}".format(value) +# Convert binary string to number +# value = int(str_value, 2) + + +@functools.lru_cache +def new_position(position, direction): + if direction == 1: + return (position.real + 1) % width + 1j * position.imag + if direction == -1j: + if -position.imag == height - 1: + return position.real + else: + return position.real + 1j * (position.imag - 1) + + +if part_to_test == 1: + area = grid.Grid() + area.text_to_dots(puzzle_input) + + east_facing = [dot.position for dot in area.dots.values() if dot.terrain == ">"] + south_facing = [dot.position for dot in area.dots.values() if dot.terrain == "v"] + + width, height = area.get_size() + + for generation in range(10 ** 6): + # print('Generation', generation) + + new_area = grid.Grid() + + new_east_facing = set( + new_position(position, 1) + if new_position(position, 1) not in east_facing + and new_position(position, 1) not in south_facing + else position + for position in east_facing + ) + + new_south_facing = set( + new_position(position, -1j) + if new_position(position, -1j) not in south_facing + and new_position(position, -1j) not in new_east_facing + else position + for position in south_facing + ) + + if east_facing == new_east_facing: + if south_facing == new_south_facing: + break + + east_facing = new_east_facing + south_facing = new_south_facing + + puzzle_actual_result = generation + 1 + + +else: + for string in puzzle_input.split("\n"): + if string == "": + continue + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-25 08:15:28.182606 +# Part 1: 2021-12-25 08:53:05 From b34438e2ec71affd253290352313af5732fcfee5 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:06:52 +0100 Subject: [PATCH 140/143] Added day 2021-25 --- 2021/25-Sea Cucumber.py | 1 + 1 file changed, 1 insertion(+) diff --git a/2021/25-Sea Cucumber.py b/2021/25-Sea Cucumber.py index ea48280..0547716 100644 --- a/2021/25-Sea Cucumber.py +++ b/2021/25-Sea Cucumber.py @@ -142,3 +142,4 @@ def new_position(position, direction): print("Actual result : " + str(puzzle_actual_result)) # Date created: 2021-12-25 08:15:28.182606 # Part 1: 2021-12-25 08:53:05 +# Part 2: 2021-12-25 15:00:00 From 2b4089cf169b3add50ac3e3daf97bdf67ba787b7 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:08:27 +0100 Subject: [PATCH 141/143] Added divide in assembly library + fixed issue on opcode --- 2021/assembly.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/2021/assembly.py b/2021/assembly.py index a07534f..f7bf8f0 100644 --- a/2021/assembly.py +++ b/2021/assembly.py @@ -78,7 +78,7 @@ def run(self): ): self.instructions_done += 1 # Get details of current operation - opcode = self.instructions[self.pointer] + opcode = self.instructions[self.pointer][0] current_instr = self.get_instruction(opcode) # Outputs operation details before its execution @@ -103,9 +103,9 @@ def get_instruction(self, opcode): values = [opcode] + [ self.instructions[self.pointer + order + 1] for order in args_order ] - print([self.pointer + order + 1 for order in args_order]) + # print([self.pointer + order + 1 for order in args_order]) - print(args_order, values, self.operation_codes[opcode]) + # print(args_order, values, self.operation_codes[opcode]) return values @@ -216,6 +216,12 @@ def op_multiply(self, instr): instr[1], self.get_register(instr[2]) * self.get_register(instr[3]) ) + # div a b c: store into the division of by " (integer value), + def op_divide(self, instr): + self.set_register( + instr[1], self.get_register(instr[2]) // self.get_register(instr[3]) + ) + # mod a b c: store into the remainder of divided by ", def op_modulo(self, instr): self.set_register( @@ -483,6 +489,7 @@ def custom_commands(self): 9: ["add: {0} = {1}+{2}", 4, op_add, [2, 0, 1]], # This means c = a + b 10: ["mult: {0} = {1}*{2}", 4, op_multiply, [0, 1, 2]], 11: ["mod: {0} = {1}%{2}", 4, op_modulo, [0, 1, 2]], + 17: ["div: {0} = {1}//{2}", 4, op_divide, [0, 1, 2]], 1: ["set: {0} = {1}", 3, op_set, [0, 1]], # Comparisons 4: ["eq: {0} = {1} == {2}", 4, op_equal, [0, 1, 2]], From 2ad007e78b3d4343f66f972c37f3e85b3eeebb10 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:09:04 +0100 Subject: [PATCH 142/143] Graph library - removed useless condition --- 2021/graph.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/2021/graph.py b/2021/graph.py index 1d3652c..0756230 100644 --- a/2021/graph.py +++ b/2021/graph.py @@ -386,10 +386,7 @@ def dijkstra(self, start, end=None): continue # Adding for future examination - if type(neighbor) == complex: - heapq.heappush(frontier, (current_distance + weight, neighbor)) - else: - heapq.heappush(frontier, (current_distance + weight, neighbor)) + heapq.heappush(frontier, (current_distance + weight, neighbor)) # Adding for final search self.distance_from_start[neighbor] = current_distance + weight From 8c275b967440053342d9e6614b175e8ad74bbe65 Mon Sep 17 00:00:00 2001 From: Piratmac <6253139+Piratmac@users.noreply.github.com> Date: Mon, 27 Dec 2021 00:09:32 +0100 Subject: [PATCH 143/143] Added first iterations on day 2021-23 --- 2021/23-Amphipod.v1.py | 368 ++++++ 2021/23-Amphipod.v2.py | 665 ++++++++++ 2021/23-Amphipod.v3.py | 798 ++++++++++++ 2021/23-Amphipod.v4.py | 2569 +++++++++++++++++++++++++++++++++++++ 2021/23-Amphipod.v5.py | 2737 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 7137 insertions(+) create mode 100644 2021/23-Amphipod.v1.py create mode 100644 2021/23-Amphipod.v2.py create mode 100644 2021/23-Amphipod.v3.py create mode 100644 2021/23-Amphipod.v4.py create mode 100644 2021/23-Amphipod.v5.py diff --git a/2021/23-Amphipod.v1.py b/2021/23-Amphipod.v1.py new file mode 100644 index 0000000..fa656cd --- /dev/null +++ b/2021/23-Amphipod.v1.py @@ -0,0 +1,368 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict +from functools import reduce +import heapq + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """############# +#...........# +###B#C#B#D### + #A#D#C#A# + #########""", + "expected": ["12521", "Unknown"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["Unknown", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 1 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# This was the very first attempt to solve it +# It tries to parse the input, the run A* on it to find possible movements +# Basically it's wayyy too slow and buggy + + +# -------------------------------- Actual code execution ----------------------------- # + +dot.Dot.sort_value = dot.Dot.sorting_map["xy"] + + +class NewGrid(grid.Grid): + def text_to_dots(self, text, ignore_terrain="", convert_to_int=False): + self.dots = {} + + y = 0 + self.amphipods = {} + self.position_to_rooms = [] + nb_amphipods = [] + for line in text.splitlines(): + for x in range(len(line)): + if line[x] not in ignore_terrain: + value = line[x] + position = x - y * 1j + + if value == " ": + continue + + if value in "ABCD": + self.position_to_rooms.append(position) + if value in nb_amphipods: + UUID = value + "2" + else: + UUID = value + "1" + nb_amphipods.append(value) + self.amphipods[UUID] = dot.Dot(self, position, value) + + value = "." + + self.dots[position] = dot.Dot(self, position, value) + # self.dots[position].sort_value = self.dots[position].sorting_map['xy'] + if value == ".": + self.dots[position].is_waypoint = True + y += 1 + + +class StateGraph(graph.WeightedGraph): + amphipod_state = ["A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2"] + + def a_star_search(self, start, end=None): + """ + Performs a A* search + + This algorithm is appropriate for "One source, multiple targets" + It takes into account positive weigths / costs of travelling. + Negative weights will make the algorithm fail. + + The exploration path is a mix of Dijkstra and Greedy BFS + It uses the current cost + estimated cost to determine the next element to consider + + Some cases to consider: + - If Estimated cost to complete = 0, A* = Dijkstra + - If Estimated cost to complete <= actual cost to complete, it is exact + - If Estimated cost to complete > actual cost to complete, it is inexact + - If Estimated cost to complete = infinity, A* = Greedy BFS + The higher Estimated cost to complete, the faster it goes + + :param Any start: The start vertex to consider + :param Any end: The target/end vertex to consider + :return: True when the end vertex is found, False otherwise + """ + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + self.visited = [tuple(dot.position for dot in start)] + + i = 0 + while frontier: # and i < 5: + i += 1 + priority, vertex, current_distance = heapq.heappop(frontier) + print(len(frontier), priority, current_distance) + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + if any( + equivalent_position in self.visited + for equivalent_position in self.equivalent_positions(neighbor) + ): + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor, end) + # print (vertex, neighbor, current_distance, priority) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + self.visited.append(tuple(dot.position for dot in neighbor)) + + if self.state_is_final(neighbor): + return self.distance_from_start[neighbor] + + # print (len(frontier)) + + return end in self.distance_from_start + + def neighbors(self, state): + if self.state_is_final(state): + return None + + neighbors = {} + for i, current_dot in enumerate(state): + amphipod_code = self.amphipod_state[i] + dots = self.area_graph.edges[current_dot] + for dot, cost in dots.items(): + new_state = list(state) + new_state[i] = dot + new_state = tuple(new_state) + # print ('Checking', amphipod_code, 'moved from', state[i], 'to', new_state[i]) + if self.state_is_valid(state, new_state, i): + neighbors[new_state] = ( + cost * self.amphipods[amphipod_code].movement_cost + ) + # print ('Movement costs', cost * self.amphipods[amphipod_code].movement_cost) + + return neighbors + + def state_is_final(self, state): + for i, position in enumerate(state): + amphipod_code = self.amphipod_state[i] + amphipod = self.amphipods[amphipod_code] + + if not position in self.room_to_positions[amphipod.terrain]: + return False + return True + + def state_is_valid(self, state, new_state, changed): + # Duplicate = 2 amphipods in the same place + if len(set(new_state)) != len(new_state): + # print ('Duplicate amphipod', new_state[changed]) + return False + + # Check amphipod is not in wrong room + if new_state[i].position in self.position_to_rooms: + room = self.position_to_rooms[new_state[i].position] + # print ('Amphipod may be in wrong place', new_state) + amphipod = self.amphipod_state[i] + if room == self.amphipods[amphipod].initial_room: + return True + else: + # print ('Amphipod is in wrong place', new_state) + return False + + return True + + def estimate_to_complete(self, state, target_vertex): + distance = 0 + for i, dot in enumerate(state): + amphipod_code = self.amphipod_state[i] + amphipod = self.amphipods[amphipod_code] + + if not dot.position in self.room_to_positions[amphipod.terrain]: + room_positions = self.room_to_positions[amphipod.terrain] + targets = [self.dots[position] for position in room_positions] + distance += ( + min( + self.area_graph.all_edges[dot][target] + if target in self.area_graph.all_edges[dot] + else 10 ** 6 + for target in targets + ) + * amphipod.movement_cost + ) + + return distance + + def equivalent_positions(self, state): + state_positions = [dot.position for dot in state] + positions = [ + tuple([state_positions[1]] + [state_positions[0]] + state_positions[2:]), + tuple( + state_positions[0:2] + + [state_positions[3]] + + [state_positions[2]] + + state_positions[4:] + ), + tuple( + state_positions[0:4] + + [state_positions[5]] + + [state_positions[4]] + + state_positions[6:] + ), + tuple(state_positions[0:6] + [state_positions[7]] + [state_positions[6]]), + ] + + for i in range(4): + position = tuple( + state_positions[:i] + + state_positions[i + 1 : i] + + state_positions[i + 2 :] + ) + positions.append(position) + + return positions + + +if part_to_test == 1: + area_map = NewGrid() + area_map.text_to_dots(puzzle_input) + + position_to_rooms = defaultdict(list) + room_to_positions = defaultdict(list) + area_map.position_to_rooms = sorted( + area_map.position_to_rooms, key=lambda a: (a.real, a.imag) + ) + for i in range(4): + position_to_rooms[area_map.position_to_rooms[2 * i]] = "ABCD"[i] + position_to_rooms[area_map.position_to_rooms[2 * i + 1]] = "ABCD"[i] + room_to_positions["ABCD"[i]].append(area_map.position_to_rooms[2 * i]) + room_to_positions["ABCD"[i]].append(area_map.position_to_rooms[2 * i + 1]) + # Forbid to use the dot right outside the room + area_map.dots[area_map.position_to_rooms[2 * i + 1] + 1j].is_waypoint = False + area_map.position_to_rooms = position_to_rooms + area_map.room_to_positions = room_to_positions + + # print (list(dot for dot in area_map.dots if area_map.dots[dot].is_waypoint)) + + for amphipod in area_map.amphipods: + area_map.amphipods[amphipod].initial_room = area_map.position_to_rooms[ + area_map.amphipods[amphipod].position + ] + area_map.amphipods[amphipod].movement_cost = 10 ** ( + ord(area_map.amphipods[amphipod].terrain) - ord("A") + ) + + area_graph = area_map.convert_to_graph() + area_graph.all_edges = area_graph.edges + area_graph.edges = { + dot: { + neighbor: distance + for neighbor, distance in area_graph.edges[dot].items() + if distance <= 2 + } + for dot in area_graph.vertices + } + print(len(area_graph.all_edges)) + + # print (area_graph.vertices) + # print (area_graph.edges) + + state_graph = StateGraph() + state_graph.area_graph = area_graph + state_graph.amphipods = area_map.amphipods + state_graph.position_to_rooms = area_map.position_to_rooms + state_graph.room_to_positions = area_map.room_to_positions + state_graph.dots = area_map.dots + + state = tuple( + area_map.dots[area_map.amphipods[amphipod].position] + for amphipod in sorted(area_map.amphipods.keys()) + ) + # print ('area_map.amphipods', area_map.amphipods) + + print("state", state) + # print ('equivalent', state_graph.equivalent_positions(state)) + print("estimate", state_graph.estimate_to_complete(state, None)) + + print(state_graph.a_star_search(state)) + + # In the example, A is already in the right place + # In all other cases, 1 anphipod per group has to go to the bottom, so 1 move per amphipod + + +else: + for string in puzzle_input.split("\n"): + if string == "": + continue + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-23 08:11:43.693421 diff --git a/2021/23-Amphipod.v2.py b/2021/23-Amphipod.v2.py new file mode 100644 index 0000000..fcd5b51 --- /dev/null +++ b/2021/23-Amphipod.v2.py @@ -0,0 +1,665 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict +from functools import reduce, lru_cache +import heapq + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """############# +#...........# +###B#C#B#D### + #A#D#C#A# + #########""", + "expected": ["12521", "44169"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["18170", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + + +# This is attempt 2, where no parsing happens (hardcoded input) +# It works for part 1, but has no optimization so it's too slow for part 2 + + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +class StateGraph(graph.WeightedGraph): + final_states = [] + valid_states = [] + estimate = [] + + def neighbors(self, state): + neighbors = {} + if is_state_final(state): + return {} + for i in range(len(state)): + for target, distance in amphipods_edges[state[i]].items(): + new_state = list(state) + new_state[i] = target + new_state = tuple(new_state) + if is_state_valid(new_state) and is_movement_valid(state, new_state, i): + neighbors[new_state] = ( + distance * amphipod_costs[amphipod_targets[i]] + ) + # if state not in self.edges: + # self.edges[state] = {} + # self.edges[state][new_state] = distance * amphipod_costs[i] + + # print (state, neighbors) + + return neighbors + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + distance = self.edges[self.came_from[target_vertex]][target_vertex] + target_vertex = self.came_from[target_vertex] + path.append((target_vertex, distance)) + + path.reverse() + + return path + + def estimate_to_complete(self, state): + if state in self.estimate: + return self.estimate[state] + estimate = 0 + for i in range(len(state)): + source = state[i] + target = amphipod_targets[i] + estimate += estimate_to_complete_amphipod(source, target) + + return estimate + + def a_star_search(self, start, end=None): + current_distance = 0 + frontier = [(0, start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {start: 0} + self.came_from = {start: None} + self.min_distance = float("inf") + + while frontier: + estimate_at_completion, vertex, current_distance = heapq.heappop(frontier) + if (len(frontier)) % 10000 == 0: + print( + len(frontier), + self.min_distance, + estimate_at_completion, + current_distance, + ) + + if current_distance > self.min_distance: + continue + + if estimate_at_completion > self.min_distance: + continue + + neighbors = self.neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + # We've already checked that node, and it's not better now + if neighbor in self.distance_from_start and self.distance_from_start[ + neighbor + ] <= (current_distance + weight): + continue + + if current_distance + weight > self.min_distance: + continue + + # Adding for future examination + priority = current_distance + self.estimate_to_complete(neighbor) + heapq.heappush( + frontier, (priority, neighbor, current_distance + weight) + ) + + # Adding for final search + self.distance_from_start[neighbor] = current_distance + weight + self.came_from[neighbor] = vertex + + if is_state_final(neighbor): + self.min_distance = min( + self.min_distance, current_distance + weight + ) + print("Found", self.min_distance, "at", len(frontier)) + # Example, part 1: + # Trouvé vers 340000 + # Commence à converger vers 570000 + + # Real, part 1: + # Trouvé 1e valeur vers 1 290 000 + # Trouvé valeur correcte à 1 856 807 + + return end in self.distance_from_start + + +@lru_cache +def is_state_final(state): + return all(amphipod_targets[i] == state[i][0] for i in range(8)) + + +@lru_cache +def is_state_valid(state): + # print (state) + # Can't have 2 amphipods in the same place + if len(set(state)) != len(state): + # print ('Amphipod superposition') + return False + + for i in range(len(state)): + if state[i][0] in "ABCD": + + # Moved to a room + if state[i][0] != start[i][0]: + # Moved to a room that is not ours + if state[i][0] != amphipod_targets[i]: + # print ('Moved to other room', state, i, start) + return False + + # Moved to a room where there is someone else + room = [ + position + for position, room in enumerate(state) + if room == amphipod_targets[i] and position != i + ] + if len(state) == 8: + if any([position // 2 != i // 2 for position in room]): + # print ('Room occupied', state, i, start) + return False + else: + if any([position // 4 != i // 4 for position in room]): + # print ('Room occupied', state, i, start) + return False + + return True + + +@lru_cache +def estimate_to_complete_amphipod(source, target): + estimate = 0 + amphipod_cost = amphipod_costs[target[0]] + # Not in target place + if target[0] != source[0]: + if source in ("LL", "RR"): + estimate += amphipod_cost + source = "LR" if source[0] == "L" else "RL" + # print ('LL/RR', i, source, amphipod_cost) + + if source[0] in "LX": + # print ('LX', i, source, amphipods_edges[source][target[0]+'1'] * amphipod_cost) + estimate += amphipods_edges[source][target[0] + "1"] * amphipod_cost + else: + # From one room to the other, just count 2 until hallway + 2 per room distance + # print ('Room', i, source, (2+2*abs(ord(source[0])-ord('A') - i//2)) * amphipod_cost) + estimate += (2 + 2 * abs(ord(source[0]) - ord(target[0]))) * amphipod_cost + return estimate + + +@lru_cache +def is_movement_valid(state, new_state, changed): + # Check there are no amphibot in the way + # print ('Moving', changed, 'at', state[changed], 'to', new_state[changed]) + if state[changed] in amphipods_edges_conditions: + if new_state[changed] in amphipods_edges_conditions[state[changed]]: + # print (amphipods_edges_conditions[state[changed]][new_state[changed]]) + if any( + amphi in amphipods_edges_conditions[state[changed]][new_state[changed]] + for amphi in new_state + ): + return False + + return True + + +amphipod_costs = {"A": 1, "B": 10, "C": 100, "D": 1000} + +if part_to_test == 1: + amphipod_targets = ["A", "A", "B", "B", "C", "C", "D", "D"] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": {"A2": 1, "LR": 2, "XAB": 2, "XBC": 4, "XCD": 6, "RL": 8}, + "A2": {"A1": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": {"B2": 1, "LR": 4, "XAB": 2, "XBC": 2, "XCD": 4, "RL": 6}, + "B2": {"B1": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": {"C2": 1, "LR": 6, "XAB": 4, "XBC": 2, "XCD": 2, "RL": 4}, + "C2": {"C1": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": {"D2": 1, "LR": 8, "XAB": 6, "XBC": 4, "XCD": 2, "RL": 2}, + "D2": {"D1": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": {"RL": ["XAB", "XBC", "XCD"], "XBC": ["XAB"], "XCD": ["XAB", "XBC"]}, + "B1": {"LR": ["XAB"], "RL": ["XBC", "XCD"], "XCD": ["XBC"]}, + "C1": {"LR": ["XAB", "XBC"], "RL": ["XCD"], "XAB": ["XBC"]}, + "D1": {"LR": ["XAB", "XBC", "XCD"], "XAB": ["XBC", "XCD"], "XBC": ["XCD"]}, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + if case_to_test == 1: + start = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + else: + start = ("A1", "C2", "C1", "D1", "B1", "D2", "A2", "B2") + + end = tuple("AABBCCDD") + + amphipod_graph = StateGraph() + + state = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + assert is_state_final(state) == False + state = ("A1", "A2", "B1", "B2", "C1", "C2", "D2", "D2") + assert is_state_final(state) == True + state = ("A1", "A2", "B1", "B1", "C1", "C2", "D2", "D2") + assert is_state_valid(state) == False + assert is_state_final(state) == True + + # Can't move from C1 to RL if XBC is occupied + source = ("A2", "D2", "B1", "XCD", "C1", "C2", "XBC", "D1") + target = ("A2", "D2", "B1", "XCD", "RL", "C2", "XBC", "D1") + assert amphipod_graph.is_movement_valid(source, target, 4) == False + + # Can't move to room occupied by someone else + target = ("A2", "B1", "A1", "D2", "C1", "C2", "B2", "D1") + assert is_state_valid(target) == False + + state = ("A2", "D2", "A1", "XBC", "B1", "C2", "B2", "D1") + assert amphipod_graph.estimate_to_complete(state) == 6468 + state = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + assert amphipod_graph.estimate_to_complete(state) == 6488 + + amphipod_graph.a_star_search(start) + + puzzle_actual_result = amphipod_graph.min_distance + +else: + amphipod_targets = [ + "A", + "A", + "A", + "A", + "B", + "B", + "B", + "B", + "C", + "C", + "C", + "C", + "D", + "D", + "D", + "D", + ] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": {"A2": 1, "LR": 2, "XAB": 2, "XBC": 4, "XCD": 6}, + "A2": {"A1": 1, "A3": 1}, + "A3": {"A2": 1, "A4": 1}, + "A4": {"A3": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": {"B2": 1, "LR": 4, "XAB": 2, "XBC": 2, "XCD": 4, "RL": 6}, + "B2": {"B1": 1, "B3": 1}, + "B3": {"B2": 1, "B4": 1}, + "B4": {"B3": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": {"C2": 1, "LR": 6, "XAB": 4, "XBC": 2, "XCD": 2, "RL": 4}, + "C2": {"C1": 1, "C3": 1}, + "C3": {"C2": 1, "C4": 1}, + "C4": {"C3": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": {"D2": 1, "LR": 8, "XAB": 6, "XBC": 4, "XCD": 2, "RL": 2}, + "D2": {"D1": 1, "D3": 1}, + "D3": {"D2": 1, "D4": 1}, + "D4": {"D3": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": {"RL": ["XAB", "XBC", "XCD"], "XBC": ["XAB"], "XCD": ["XAB", "XBC"]}, + "B1": {"LR": ["XAB"], "RL": ["XBC", "XCD"], "XCD": ["XBC"]}, + "C1": {"LR": ["XAB", "XBC"], "RL": ["XCD"], "XAB": ["XBC"]}, + "D1": {"LR": ["XAB", "XBC", "XCD"], "XAB": ["XBC", "XCD"], "XBC": ["XCD"]}, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + start_points = { + 1: ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ), + ############# + # ...........# + ###B#C#B#D### + # D#C#B#A# + # D#B#A#C# + # A#D#C#A# + ######### + "real": ( + "A1", + "C3", + "C4", + "D2", + "B3", + "C1", + "C2", + "D1", + "B1", + "B2", + "D3", + "D4", + "A2", + "A3", + "A4", + "B4", + ) + ############# + # ...........# + ###A#C#B#B### + # D#C#B#A# + # D#B#A#C# + # D#D#A#C# + ######### + } + start = start_points[case_to_test] + + amphipod_graph = StateGraph() + + if True: + # Check initial example start + state = start_points[1] + assert is_state_final(state) == False + assert is_state_valid(state) == True + + # Check final state + state = ( + "A1", + "A2", + "A1", + "A2", + "B1", + "B2", + "B1", + "B2", + "C1", + "C2", + "C1", + "C2", + "D2", + "D2", + "D2", + "D2", + ) + assert is_state_final(state) == True + assert is_state_valid(state) == False + + # Can't move from C1 to RL if XBC is occupied + source = ( + "A2", + "D2", + "B1", + "XCD", + "C1", + "C2", + "XBC", + "D1", + "A2", + "D2", + "B1", + "XCD", + "C1", + "C2", + "XBC", + "D1", + ) + target = ( + "A2", + "D2", + "B1", + "XCD", + "RL", + "C2", + "XBC", + "D1", + "A2", + "D2", + "B1", + "XCD", + "C1", + "C2", + "XBC", + "D1", + ) + assert is_movement_valid(source, target, 4) == False + + # Can't move to room occupied by someone else + state = ( + "A4", + "C1", + "C3", + "C2", + "A1", + "B3", + "XAB", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ) + assert is_state_valid(target) == False + + state = start_points[1] + # print (amphipod_graph.neighbors(state)) + # print (amphipod_graph.estimate_to_complete(state)) + assert amphipod_graph.estimate_to_complete(state) == 23342 + + # Estimate when on target + state = ( + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert amphipod_graph.estimate_to_complete(state) == 0 + + # Estimate when 1 is missing + state = ( + "XAB", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert amphipod_graph.estimate_to_complete(state) == 2 + + # Estimate for other amphipod + state = ( + "A1", + "A2", + "A3", + "A4", + "XCD", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert amphipod_graph.estimate_to_complete(state) == 40 + + # Estimate when 2 are inverted + state = ( + "A1", + "A2", + "A3", + "B1", + "A1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert amphipod_graph.estimate_to_complete(state) == 44 + + # Estimate when start in LL + state = ( + "LL", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert amphipod_graph.estimate_to_complete(state) == 3 + + # amphipod_graph.dijkstra(start) + amphipod_graph.a_star_search(start) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-23 08:11:43.693421 +# Part 1: 2021-12-24 01:44:31 diff --git a/2021/23-Amphipod.v3.py b/2021/23-Amphipod.v3.py new file mode 100644 index 0000000..b4634ce --- /dev/null +++ b/2021/23-Amphipod.v3.py @@ -0,0 +1,798 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools +from collections import Counter, deque, defaultdict +from functools import reduce, lru_cache +import heapq + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """############# +#...........# +###B#C#B#D### + #A#D#C#A# + #########""", + "expected": ["12521", "44169"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": open(input_file, "r+").read(), + "expected": ["18170", "Unknown"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + + +############ Works for part 1, too slow for part 2 ################## +# The number of states considered valid is much, much lower than with the first algorithms +# Below numbers are the maximum count of states in the frontier +# For the example's part 1, it went from 155 000 to 25 000 (correct value went from 115 000 to 19 000) +# For the real input's part 1, it went from 525 000 to 15 000 + + +class StateGraph(graph.WeightedGraph): + final_states = [] + valid_states = [] + estimate = [] + + def path(self, target_vertex): + """ + Reconstructs the path followed to reach a given vertex + + :param Any target_vertex: The vertex to be reached + :return: A list of vertex from start to target + """ + path = [target_vertex] + while self.came_from[target_vertex]: + distance = self.edges[self.came_from[target_vertex]][target_vertex] + target_vertex = self.came_from[target_vertex] + path.append((target_vertex, distance)) + + path.reverse() + + return path + + def a_star_search(self, start, end=None): + current_distance = 0 + frontier = [(0, state_to_tuple(start), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {state_to_tuple(start): 0} + self.came_from = {state_to_tuple(start): None} + self.min_distance = float("inf") + + while frontier: + ( + estimate_at_completion, + vertex_code, + vertex, + current_distance, + ) = heapq.heappop(frontier) + if (len(frontier)) % 5000 == 0: + print( + len(frontier), + self.min_distance, + estimate_at_completion, + current_distance, + ) + + if current_distance > self.min_distance: + continue + + if estimate_at_completion > self.min_distance: + continue + + neighbors = get_neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + neighbor_tuple = state_to_tuple(neighbor) + # We've already checked that node, and it's not better now + if ( + neighbor_tuple in self.distance_from_start + and self.distance_from_start[neighbor_tuple] + <= (current_distance + weight) + ): + continue + + if current_distance + weight > self.min_distance: + continue + + # Adding for future examination + priority = current_distance + estimate_to_complete(neighbor_tuple) + heapq.heappush( + frontier, + (priority, neighbor_tuple, neighbor, current_distance + weight), + ) + + # Adding for final search + self.distance_from_start[neighbor_tuple] = current_distance + weight + self.came_from[neighbor_tuple] = vertex + + if is_state_final(neighbor): + self.min_distance = min( + self.min_distance, current_distance + weight + ) + print("Found", self.min_distance, "at", len(frontier)) + + return end in self.distance_from_start + + +@lru_cache +def state_to_tuple(state): + group_size = len(state) // 4 + return tuple( + tuple(sorted(state[group * group_size : (group + 1) * group_size])) + for group in range(4) + ) + + +@lru_cache +def is_state_final(state): + return all(amphipod_targets[i] == state[i][0] for i in range(8)) + + +@lru_cache +def is_state_valid(state): + # Can't have 2 amphipods in the same place + # print ('start point ', start) + # print ('valid check for', state) + if len(set(state)) != len(state): + # print ('Amphipod superposition') + return False + + for i in range(len(state)): + current_room = state[i][0] + if current_room in "ABCD": + # print (i, state[i], 'is in a room') + + # Moved to a room + if current_room != start[i][0]: + # print (start[i], 'moving to', state[i]) + # Moved to a room that is not ours + if state[i][0] != amphipod_targets[i]: + # print (i, state[i], 'Moved to wrong room', amphipod_targets[i]) + return False + + # Moved to a room where there is another type of amphibot + room = [ + other_pos + for other_i, other_pos in enumerate(state) + if amphipod_targets[other_i] != amphipod_targets[i] + and other_pos[0] == amphipod_targets[i] + ] + if len(room) > 0: + # print (i, state[i], 'Moved to room with other people', amphipod_targets[i]) + return False + + return True + + +@lru_cache +def estimate_to_complete_amphipod(source, target): + estimate = 0 + amphipod_cost = amphipod_costs[target[0]] + # Not in target place + if target[0] != source[0]: + if source in ("LL", "RR"): + estimate += amphipod_cost + source = "LR" if source[0] == "L" else "RL" + # print ('LL/RR', i, source, amphipod_cost) + + if source[0] in "RLX": + # print ('LX', i, source, amphipods_edges[source][target[0]+'1'] * amphipod_cost) + estimate += amphipods_edges[source][target[0] + "1"] * amphipod_cost + else: + # From one room to the other, count 2 until hallway + 2 per room distance + # print ('Room', i, source, (2+2*abs(ord(source[0])-ord('A') - i//2)) * amphipod_cost) + estimate += (2 + 2 * abs(ord(source[0]) - ord(target[0]))) * amphipod_cost + + # Then add vertical moves within rooms + estimate += (int(source[1]) - 1) * amphipod_cost + estimate += (int(target[1]) - 1) * amphipod_cost + return estimate + + +@lru_cache +def is_movement_valid(state, new_state, changed): + # We can only from hallway to our own room + if state[changed][0] in "XLR": + if new_state[changed][0] in "ABCD": + if new_state[changed][0] != amphipod_targets[changed]: + return False + + # Check there are no amphibot in the way + # print ('Moving', changed, 'at', state[changed], 'to', new_state[changed]) + if state[changed] in amphipods_edges_conditions: + if new_state[changed] in amphipods_edges_conditions[state[changed]]: + # print (amphipods_edges_conditions[state[changed]][new_state[changed]]) + if any( + amphi in amphipods_edges_conditions[state[changed]][new_state[changed]] + for amphi in new_state + ): + return False + + # If our room is full and we're in it, don't move + if state[changed][0] == amphipod_targets[changed]: + group_size = len(state) // 4 + group = changed // group_size + if all( + state[group * group_size + i][0] == amphipod_targets[changed] + for i in range(group_size) + ): + return False + + return True + + +@lru_cache +def estimate_to_complete(state): + if len(state) != 4: + state = state_to_tuple(state) + new_state = tuple([s for s in state]) + estimate = 0 + + for group in range(len(state)): + available = [ + "ABCD"[group] + str(i) + for i in range(1, len(state[group]) + 1) + if "ABCD"[group] + str(i) not in state[group] + ] + for i, source in enumerate(state[group]): + if source[0] == "ABCD"[group]: + continue + target = available.pop() + estimate += estimate_to_complete_amphipod(source, target) + + return estimate + + +@lru_cache +def get_neighbors(state): + neighbors = {} + if is_state_final(state): + return {} + for i in range(len(state)): + for target, distance in amphipods_edges[state[i]].items(): + new_state = list(state) + new_state[i] = target + + new_state = tuple(new_state) + if is_state_valid(new_state): + if is_movement_valid(state, new_state, i): + neighbors[new_state] = ( + distance * amphipod_costs[amphipod_targets[i]] + ) + + # print (state, neighbors) + + return neighbors + + +amphipod_costs = {"A": 1, "B": 10, "C": 100, "D": 1000} + +if part_to_test == 1: + amphipod_targets = ["A", "A", "B", "B", "C", "C", "D", "D"] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": {"A2": 1, "LR": 2, "XAB": 2, "XBC": 4, "XCD": 6, "RL": 8}, + "A2": {"A1": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": {"B2": 1, "LR": 4, "XAB": 2, "XBC": 2, "XCD": 4, "RL": 6}, + "B2": {"B1": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": {"C2": 1, "LR": 6, "XAB": 4, "XBC": 2, "XCD": 2, "RL": 4}, + "C2": {"C1": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": {"D2": 1, "LR": 8, "XAB": 6, "XBC": 4, "XCD": 2, "RL": 2}, + "D2": {"D1": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": {"RL": ["XAB", "XBC", "XCD"], "XBC": ["XAB"], "XCD": ["XAB", "XBC"]}, + "B1": {"LR": ["XAB"], "RL": ["XBC", "XCD"], "XCD": ["XBC"]}, + "C1": {"LR": ["XAB", "XBC"], "RL": ["XCD"], "XAB": ["XBC"]}, + "D1": {"LR": ["XAB", "XBC", "XCD"], "XAB": ["XBC", "XCD"], "XBC": ["XCD"]}, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + if case_to_test == 1: + start = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + else: + start = ("A1", "C2", "C1", "D1", "B1", "D2", "A2", "B2") + + end = tuple("AABBCCDD") + + amphipod_graph = StateGraph() + + if True: + state = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + assert is_state_final(state) == False + state = ("A1", "A2", "B1", "B2", "C1", "C2", "D2", "D2") + assert is_state_final(state) == True + state = ("A1", "A2", "B1", "B1", "C1", "C2", "D2", "D2") + assert is_state_valid(state) == False + assert is_state_final(state) == True + + # Can't move from C1 to RL if XBC is occupied + source = ("A2", "D2", "B1", "XCD", "C1", "C2", "XBC", "D1") + target = ("A2", "D2", "B1", "XCD", "RL", "C2", "XBC", "D1") + assert is_movement_valid(source, target, 4) == False + + # Can't move to room occupied by someone else + target = ("A2", "B1", "A1", "D2", "C1", "C2", "B2", "D1") + assert is_state_valid(target) == False + + state = ("A2", "D2", "A1", "XBC", "B1", "C2", "B2", "D1") + assert estimate_to_complete(state) == 8479 + state = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + assert estimate_to_complete(state) == 8499 + + amphipod_graph.a_star_search(start) + + puzzle_actual_result = amphipod_graph.min_distance + +else: + amphipod_targets = [ + "A", + "A", + "A", + "A", + "B", + "B", + "B", + "B", + "C", + "C", + "C", + "C", + "D", + "D", + "D", + "D", + ] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": {"A2": 1, "LR": 2, "XAB": 2, "XBC": 4, "XCD": 6}, + "A2": {"A1": 1, "A3": 1}, + "A3": {"A2": 1, "A4": 1}, + "A4": {"A3": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": {"B2": 1, "LR": 4, "XAB": 2, "XBC": 2, "XCD": 4, "RL": 6}, + "B2": {"B1": 1, "B3": 1}, + "B3": {"B2": 1, "B4": 1}, + "B4": {"B3": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": {"C2": 1, "LR": 6, "XAB": 4, "XBC": 2, "XCD": 2, "RL": 4}, + "C2": {"C1": 1, "C3": 1}, + "C3": {"C2": 1, "C4": 1}, + "C4": {"C3": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": {"D2": 1, "LR": 8, "XAB": 6, "XBC": 4, "XCD": 2, "RL": 2}, + "D2": {"D1": 1, "D3": 1}, + "D3": {"D2": 1, "D4": 1}, + "D4": {"D3": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": {"RL": ["XAB", "XBC", "XCD"], "XBC": ["XAB"], "XCD": ["XAB", "XBC"]}, + "B1": {"LR": ["XAB"], "RL": ["XBC", "XCD"], "XCD": ["XBC"]}, + "C1": {"LR": ["XAB", "XBC"], "RL": ["XCD"], "XAB": ["XBC"]}, + "D1": {"LR": ["XAB", "XBC", "XCD"], "XAB": ["XBC", "XCD"], "XBC": ["XCD"]}, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + start_points = { + 1: ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ), + ############# + # ...........# + ###B#C#B#D### + # D#C#B#A# + # D#B#A#C# + # A#D#C#A# + ######### + "real": ( + "A1", + "C3", + "C4", + "D2", + "B3", + "C1", + "C2", + "D1", + "B1", + "B2", + "D3", + "D4", + "A2", + "A3", + "A4", + "B4", + ) + ############# + # ...........# + ###A#C#B#B### + # D#C#B#A# + # D#B#A#C# + # D#D#A#C# + ######### + } + start = start_points[case_to_test] + + amphipod_graph = StateGraph() + + if True: + # Check initial example start + state = start_points[case_to_test] + assert is_state_final(state) == False + assert is_state_valid(state) == True + + # Check final state + state = ( + "A1", + "A2", + "A1", + "A2", + "B4", + "B2", + "B3", + "B2", + "C1", + "C2", + "C1", + "C2", + "D2", + "D3", + "D2", + "D4", + ) + assert is_state_final(state) == True + assert is_state_valid(state) == False + + assert state_to_tuple(state) == ( + ("A1", "A1", "A2", "A2"), + ("B2", "B2", "B3", "B4"), + ("C1", "C1", "C2", "C2"), + ("D2", "D2", "D3", "D4"), + ) + + # Can't move from C1 to RL if XBC is occupied + source = ( + "A2", + "D2", + "B1", + "XCD", + "C1", + "C2", + "XBC", + "D1", + "A2", + "D2", + "B1", + "XCD", + "C1", + "C2", + "XBC", + "D1", + ) + target = ( + "A2", + "D2", + "B1", + "XCD", + "RL", + "C2", + "XBC", + "D1", + "A2", + "D2", + "B1", + "XCD", + "C1", + "C2", + "XBC", + "D1", + ) + assert is_movement_valid(source, target, 4) == False + + # Can't move out of our room if it's full + source = ( + "A1", + "A2", + "A3", + "A4", + "C1", + "C2", + "C3", + "D1", + "C4", + "D2", + "B1", + "B2", + "B3", + "B4", + "D3", + "D4", + ) + target = ( + "A1", + "A2", + "A3", + "XAB", + "C1", + "C2", + "C3", + "D1", + "C4", + "D2", + "B1", + "B2", + "B3", + "B4", + "D3", + "D4", + ) + assert is_movement_valid(source, target, 3) == False + + # Can't move to room that is not yours + state = ( + "A4", + "C3", + "D2", + "B3", + "A1", + "XAB", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ) + assert is_state_valid(state) == False + + # Can't move to room if there are other people there + state = ( + "A4", + "C3", + "D2", + "A3", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "LR", + "B4", + "D1", + ) + assert is_state_valid(state) == False + + # Can move to room if there is only friends there + if case_to_test == 1: + state = ( + "A4", + "C3", + "D2", + "A3", + "RR", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "RL", + "LR", + "B4", + "D1", + ) + assert is_state_valid(state) == True + + state = start_points[1] + assert estimate_to_complete(state) == 36001 + + # Estimate when on target + state = ( + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state) == 0 + + # Estimate when 1 is missing + state = ( + "XAB", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state) == 2 + + # Estimate for other amphipod + state = ( + "A1", + "A2", + "A3", + "A4", + "XCD", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state) == 40 + + # Estimate when 2 are inverted + state = ( + "A1", + "A2", + "A3", + "B1", + "A1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state) == 47 + + # Estimate when start in LL + state = ( + "LL", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state) == 3 + + # amphipod_graph.dijkstra(start) + amphipod_graph.a_star_search(start) + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-23 08:11:43.693421 +# Part 1: 2021-12-24 01:44:31 diff --git a/2021/23-Amphipod.v4.py b/2021/23-Amphipod.v4.py new file mode 100644 index 0000000..32cdc02 --- /dev/null +++ b/2021/23-Amphipod.v4.py @@ -0,0 +1,2569 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools, time, math +from collections import Counter, deque, defaultdict +from functools import reduce, lru_cache +import heapq +import cProfile + + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """############# +#...........# +###B#C#B#D### + #A#D#C#A# + #########""", + "expected": ["12521", "44169"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": "", # open(input_file, "r+").read(), + "expected": ["18170", "50208"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = 1 +part_to_test = 2 + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# This works, but it takes a lot of time to process +# It has 2-3 advantages: +# - Code is much cleaner AND generates correct results +# - A bunch of unit tests in place +# - Some ideas to improve +# - Performance analysis of code + +# Here's the result of the cProfile analysis: +# 2059350331 function calls in 2249.869 seconds +# +# Ordered by: standard name +# +# ncalls tottime percall cumtime percall filename:lineno(function) +# 30871558 51.725 0.000 269.637 0.000 23.py:142(state_to_tuple) +# 154357790 86.291 0.000 217.911 0.000 23.py:144() +# 15110553 49.878 0.000 65.455 0.000 23.py:146(is_state_final) +# 31939951 10.008 0.000 10.008 0.000 23.py:148() +# 381253063 338.472 0.000 427.089 0.000 23.py:150(is_state_valid) +# 6969 0.033 0.000 0.037 0.000 23.py:159(estimate_to_complete_amphipod) +# 1010047 6.345 0.000 8.105 0.000 23.py:192(estimate_to_complete_group) +# 1010047 1.023 0.000 1.023 0.000 23.py:195() +# 8706672 19.769 0.000 27.874 0.000 23.py:204(estimate_to_complete) +# 122968010 292.357 0.000 611.374 0.000 23.py:212(is_movement_valid) +# 6577962 60.399 0.000 65.168 0.000 23.py:233() +# 11917829 85.827 0.000 94.311 0.000 23.py:262() +# 3702505 0.915 0.000 0.915 0.000 23.py:271() +# 18913206 105.473 0.000 121.591 0.000 23.py:284() +# 91713275 22.672 0.000 22.672 0.000 23.py:293() +# 8118317 630.719 0.000 1699.687 0.000 23.py:306(get_neighbors) +# 1 127.265 127.265 2249.865 2249.865 23.py:85(a_star_search) +# 1 0.000 0.000 2249.865 2249.865 :1() +# 1 0.000 0.000 0.000 0.000 {built-in method _heapq.heapify} +# 8706673 65.600 0.000 65.600 0.000 {built-in method _heapq.heappop} +# 8706672 6.275 0.000 6.275 0.000 {built-in method _heapq.heappush} +# 12175 0.003 0.000 0.003 0.000 {built-in method builtins.abs} +# 15110553 5.568 0.000 12.496 0.000 {built-in method builtins.all} +# 11363777 14.360 0.000 36.068 0.000 {built-in method builtins.any} +# 1 0.004 0.004 2249.869 2249.869 {built-in method builtins.exec} +# 771214381 90.197 0.000 90.197 0.000 {built-in method builtins.len} +# 1 0.000 0.000 0.000 0.000 {built-in method builtins.min} +# 5206 0.001 0.000 0.001 0.000 {built-in method builtins.ord} +# 1584 0.035 0.000 0.035 0.000 {built-in method builtins.print} +# 123486232 131.620 0.000 131.620 0.000 {built-in method builtins.sorted} +# 1 0.000 0.000 0.000 0.000 {method 'append' of 'list' objects} +# 1 0.000 0.000 0.000 0.000 {method 'disable' of '_lsprof.Profiler' objects} +# 90852561 29.371 0.000 29.371 0.000 {method 'index' of 'tuple' objects} +# 138003775 16.963 0.000 16.963 0.000 {method 'items' of 'dict' objects} +# 3708981 0.700 0.000 0.700 0.000 {method 'pop' of 'list' objects} + +# Possible improvements: +# Force move from room to target if possible (= skip hallway) +# If X is in Yn and can go to Y(n-1), force that as a neighbor (since it'll happen anyway) +# If X is in Xn and can go to X(n+1), force that as a neighbor (since it'll happen anyway) + + +class StateGraph(graph.WeightedGraph): + final_states = [] + + def a_star_search(self, start, end=None): + current_distance = 0 + frontier = [(0, state_to_tuple(start), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {state_to_tuple(start): 0} + self.came_from = {state_to_tuple(start): None} + self.min_distance = float("inf") + + print("Starting search") + + while frontier: + ( + estimate_at_completion, + vertex_tuple, + vertex, + current_distance, + ) = heapq.heappop(frontier) + if (len(frontier)) % 5000 == 0: + print( + " Searching", + len(frontier), + self.min_distance, + estimate_at_completion, + current_distance, + ) + + if current_distance > self.min_distance: + continue + + if estimate_at_completion > self.min_distance: + continue + + neighbors = get_neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + neighbor_tuple = state_to_tuple(neighbor) + # We've already checked that node, and it's not better now + if ( + neighbor_tuple in self.distance_from_start + and self.distance_from_start[neighbor_tuple] + <= (current_distance + weight) + ): + continue + + if current_distance + weight > self.min_distance: + continue + + # Adding for future examination + priority = current_distance + estimate_to_complete(neighbor_tuple) + heapq.heappush( + frontier, + (priority, neighbor_tuple, neighbor, current_distance + weight), + ) + + # Adding for final search + self.distance_from_start[neighbor_tuple] = current_distance + weight + self.came_from[neighbor_tuple] = vertex_tuple + + if is_state_final(neighbor): + self.min_distance = min( + self.min_distance, current_distance + weight + ) + self.final_states.append(neighbor) + print( + " Found", + self.min_distance, + "at", + len(frontier), + "for", + neighbor, + ) + + print("Search complete!") + return end in self.distance_from_start + + +@lru_cache +def state_to_tuple(state): + return tuple( + tuple(sorted(state[group * group_size : (group + 1) * group_size])) + for group in range(4) + ) + + +@lru_cache +def is_state_final(state): + return all(amphipod_targets[i] == val[0] for i, val in enumerate(state)) + + +@lru_cache +def is_state_valid(state): + # Can't have 2 amphipods in the same place + if len(set(state)) != len(state): + # print ('Amphipod superposition') + return False + + return True + + +@lru_cache +def estimate_to_complete_amphipod(source, target): + estimate = 0 + amphipod_cost = amphipod_costs[target[0]] + # print ('Estimating', source, 'to', target) + # Not in target place + if source in ("LL", "RR"): + estimate += amphipod_cost + source = "LR" if source[0] == "L" else "RL" + # print ('Source in LL/RR, adding', amphipod_cost) + ##print ('LL/RR', i, source, amphipod_cost) + + if source[0] in "RLX": + ##print ('LX', i, source, amphipods_edges[source][target[0]+'1'] * amphipod_cost) + estimate += amphipods_edges[source][target[0] + "1"] * amphipod_cost + # print ('Source in RLX, adding', amphipods_edges[source][target[0]+'1'] * amphipod_cost) + source = target[0] + "1" + + if target[0] != source[0]: + # print ('Source in wrong ABCD room, adding', (2+2*abs(ord(source[0]) - ord(target[0]))) * amphipod_cost) + # From start to top position in room + estimate += abs(int(source[1]) - 1) * amphipod_cost + # From one room to the other, count 2 until hallway + 2 per room distance + estimate += (2 + 2 * abs(ord(source[0]) - ord(target[0]))) * amphipod_cost + + source = target[0] + "1" + + # Then add vertical moves within rooms + # print ('Adding vertical movements within target', abs(int(source[1]) - int(target[1])) * amphipod_cost) + estimate += abs(int(target[1]) - 1) * amphipod_cost + return estimate + + +@lru_cache +def estimate_to_complete_group(group, positions): + estimate = 0 + available = [x for x in amphipod_all_targets[group] if x not in positions] + for i, source in enumerate(positions): + if source[0] == "ABCD"[group]: + continue + target = available.pop() + estimate += estimate_to_complete_amphipod(source, target) + return estimate + + +# @lru_cache +def estimate_to_complete(state): + estimate = 0 + + for group in range(4): + estimate += estimate_to_complete_group(group, state[group]) + + return estimate + + +@lru_cache +def is_movement_valid(state, new_state, changed): + # print ('Checking', changed, 'from', state) + # print (' to', new_state) + current_position = state[changed] + current_room = current_position[0] + + new_position = new_state[changed] + new_room = new_position[0] + + target_room = amphipod_targets[changed] + target_id = changed // group_size + + # Moving within a room + if new_room == current_room: + # Forbidden: Moving with something in between + # Since all movements are by 1 only: If there was an obstable, 2 amphibots would be in the same place + + # Within my target room + if new_room == target_room: + # Room occupied by friends only (myself included) + amphi_in_target = set( + [ + amphipod_targets[state.index(target_room + str(i + 1))] + for i in range(group_size) + if target_room + str(i + 1) in state + ] + ) + if amphi_in_target == {target_room}: + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # print ('# Allowed: Moving down in target room if full of friends') + return new_position[-1] > current_position[-1] + + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # print ('# Allowed: Moving up in target room if has other people') + return new_position[-1] < current_position[-1] + + # Within a hallway + # Forbidden: Moving from hallway to another hallway + # Moving from X to another X is forbidden via amphipods_edges + + # Allowed: move within L or R spaces + if current_room in "LR": + # print ('# Allowed: move within L or R spaces') + return True + + # Allowed: Moving up in other's room + # print ('# Allowed: Moving up in other\'s room') + return new_position[-1] < current_position[-1] + + ####### + # Move to my room + if new_room == target_room: + # Forbidden: Moving to my room if there are others in it + amphi_in_target = set( + [ + amphipod_targets[state.index(target_room + str(i + 1))] + for i in range(group_size) + if target_room + str(i + 1) in state + ] + ) + if amphi_in_target and amphi_in_target != {target_room}: + # print ('# Forbidden: Moving to my room if there are others in it') + return False + + # Forbidden: Moving with something in between + if current_position in amphipods_edges_conditions: + if new_position in amphipods_edges_conditions[current_position]: + # New position can't be blocking because it's not in the list of blocking ones + if any( + position + in amphipods_edges_conditions[current_position][new_position] + for position in new_state + ): + # print ('# Forbidden: Moving to my room with something in between') + return False + + # Allowed: Moving to my room if only same amphibots are in and no obstacle + # Allowed: Moving to my room if empty and no obstacle + # print ('# Allowed: Moving to my room if (empty OR only same amphibots are in) and no obstacle') + return True + + # Move to hallway from a room + if new_room in "XLR": + # Forbidden: Moving out of my room if it's empty + # Forbidden: Moving out of my room if it's full of friends + amphi_in_target = set( + [ + amphipod_targets[state.index(target_room + str(i + 1))] + for i in range(group_size) + if target_room + str(i + 1) in state + ] + ) + if current_room == target_room and ( + amphi_in_target == {target_room} or amphi_in_target == () + ): + # print ('# Forbidden: Moving out of my room if it\'s empty OR full of friends') + return False + + # Forbidden: Moving with something in between + if current_position in amphipods_edges_conditions: + if new_position in amphipods_edges_conditions[current_position]: + # New position can't be blocking because it's not in the list of blocking ones + if any( + position + in amphipods_edges_conditions[current_position][new_position] + for position in new_state + ): + # print ('# Forbidden: Moving to hallway with something in between') + return False + + # Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other's room is there are no obstacle + # print ('# Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other\'s room is there are no obstacle') + return True + + # Forbidden: Moving to other's room + return False + + +def get_neighbors(state): + neighbors = {} + if is_state_final(state): + # print ('Final state') + return {} + + forced_move = False + for i in range(len_state): + # Forbidden: Moving from hallway to another hallway ==> Through amphipods_edges + for target, distance in amphipods_edges[state[i]].items(): + new_state = state[:i] + (target,) + state[i + 1 :] + # print (i, 'moves from', state[i], 'to', target) + # print ('new state', new_state) + if is_state_valid(new_state): + # print ('State valid') + if is_movement_valid(state, new_state, i): + # print ('Movement valid') + + neighbors[new_state] = ( + distance * amphipod_costs[amphipod_targets[i]] + ) + + # print (state, neighbors) + + return neighbors + + +def tuple_replace(init, source, target): + position = init.index(source) + return position, init[:position] + (target,) + init[position + 1 :] + + +def state_to_text(state): + rows = [ + "#############", + ["#", "LL", "LR", ".", "XAB", ".", "XBC", ".", "XCD", ".", "RL", "RR", "#"], + ["#", "#", "#", "A1", "#", "B1", "#", "C1", "#", "D1", "#", "#", "#"], + [" ", " ", "#", "A2", "#", "B2", "#", "C2", "#", "D2", "#", " ", " "], + [" ", " ", "#", "A3", "#", "B3", "#", "C3", "#", "D3", "#", " ", " "], + [" ", " ", "#", "A4", "#", "B4", "#", "C4", "#", "D4", "#", " ", " "], + [" ", " ", "#", "#", "#", "#", "#", "#", "#", "#", "#", " ", " "], + ] + if group_size == 2: + del rows[4:6] + + text = "" + for row in rows: + text += "".join( + "ABCD"[state.index(i) // group_size] + if i in state + else i + if i in ".# " + else "." + for i in row + ) + text += "\n" + + return text + + +amphipod_costs = {"A": 1, "B": 10, "C": 100, "D": 1000} + + +# Given all the changes, this part probably doesn't work anymore (all the asserts are wrong) +if part_to_test == 1: + len_state = 8 + group_size = len_state // 4 + + amphipod_targets = ["A", "A", "B", "B", "C", "C", "D", "D"] + amphipod_all_targets = [["A1", "A2"], ["B1", "B2"], ["C1", "C2"], ["D1", "D2"]] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": {"A2": 1, "LR": 2, "XAB": 2, "XBC": 4, "XCD": 6, "RL": 8}, + "A2": {"A1": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": {"B2": 1, "LR": 4, "XAB": 2, "XBC": 2, "XCD": 4, "RL": 6}, + "B2": {"B1": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": {"C2": 1, "LR": 6, "XAB": 4, "XBC": 2, "XCD": 2, "RL": 4}, + "C2": {"C1": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": {"D2": 1, "LR": 8, "XAB": 6, "XBC": 4, "XCD": 2, "RL": 2}, + "D2": {"D1": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": {"RL": ["XAB", "XBC", "XCD"], "XBC": ["XAB"], "XCD": ["XAB", "XBC"]}, + "B1": {"LR": ["XAB"], "RL": ["XBC", "XCD"], "XCD": ["XBC"]}, + "C1": {"LR": ["XAB", "XBC"], "RL": ["XCD"], "XAB": ["XBC"]}, + "D1": {"LR": ["XAB", "XBC", "XCD"], "XAB": ["XBC", "XCD"], "XBC": ["XCD"]}, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + start_points = { + 1: ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1"), + ############# + # ...........# + ###B#C#B#D### + # A#D#C#A# + ######### + "real": ("A1", "C2", "C1", "D1", "B1", "D2", "A2", "B2") + ############# + # ...........# + ###A#C#B#B### + # D#D#A#C# + ######### + } + start = start_points[case_to_test] + + if case_to_test == 1: + + ######is_state_valid + if True: + state = start_points[case_to_test] + assert is_state_valid(state) == True + + state = ("A1", "A2", "A1", "A2", "B4", "B2", "B3", "B2") + assert is_state_valid(state) == False + + ######is_state_final + if True: + state = start_points[case_to_test] + assert is_state_final(state) == False + + state = ("A1", "A2", "B4", "B2", "C4", "C2", "D2", "D3") + assert is_state_final(state) == True + + ######is_movement_valid + if True: + # Rule set: + # Move within room + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # Forbidden: Moving from hallway to another hallway : Prevented by amphipods_edges (not tested here) + # Forbidden: Moving from X to another X is forbidden : Prevented by amphipods_edges (not tested here) + # Allowed: move within L or R spaces + # Allowed: Moving up in other's room + # Move to target + # Forbidden: Moving to my room if there are others in it + # Forbidden: Moving to my room with something in between + # Allowed: Moving to my room if only same amphibots are in and no obstacle + # Allowed: Moving to my room if empty and no obstacle + # Move to hallway from a room + # Forbidden: Moving out of my room if it's empty + # Forbidden: Moving out of my room if it's full of friends + # Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other's room if there are no obstacle + # Forbidden: Moving to other's room + + # Move within room + + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # Technically not feasible because there are 2 places only + + # Allowed: move within L or R spaces + _, source = tuple_replace(start, "A2", "LL") + changed, target = tuple_replace(source, "LL", "LR") + assert is_movement_valid(source, target, changed) == True + + # Allowed: Moving up in other's room + _, source = tuple_replace(start, "B1", "LL") + changed, target = tuple_replace(source, "B2", "B1") + assert is_movement_valid(source, target, changed) == True + + # state = ('A2', 'D2', 'A1', 'C1', 'B1', 'C2', 'B2', 'D1') + + # Move to target + + # Forbidden: Moving to my room if there are others in it + _, source = tuple_replace(start, "D1", "LR") + changed, target = tuple_replace(source, "LR", "D1") + assert is_movement_valid(source, target, changed) == False + + # Forbidden: Moving to my room with something in between + _, source = tuple_replace(start, "D1", "XAB") + _, source = tuple_replace(source, "A2", "XBC") + changed, target = tuple_replace(source, "XAB", "D1") + assert is_movement_valid(source, target, changed) == False + + # Allowed: Moving to my room if only same amphibots are in and no obstacle + source = ("A2", "XAB", "LR", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "XAB", "A1") + assert is_movement_valid(source, target, changed) == True + + # Allowed: Moving to my room if empty and no obstacle + source = ("LR", "XAB", "LR", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "XAB", "A1") + assert is_movement_valid(source, target, changed) == True + + # Move to hallway from a room + + # Forbidden: Moving out of my room if it's empty + source = ("A2", "LL", "A1", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == False + + # Forbidden: Moving out of my room if it's full of friends + source = ("A2", "LL", "A1", "C1", "B1", "C2", "D2", "D1") + changed, target = tuple_replace(source, "D1", "XCD") + assert is_movement_valid(source, target, changed) == False + + # Allowed: Moving out of my room if there are other people in it and no obstacle + source = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == True + + # Allowed: Moving out of other's room if there are no obstacle + source = start + changed, target = tuple_replace(source, "A1", "XAB") + assert is_movement_valid(source, target, changed) == True + + # Forbidden: Moving to other's room + source = ("XAB", "D2", "A1", "C1", "LR", "C2", "B2", "D1") + changed, target = tuple_replace(source, "XAB", "B1") + assert is_movement_valid(source, target, changed) == False + + ######estimate_to_complete_amphipod ==> via estimate_to_complete + + ######estimate_to_complete + if True: + # Start ('A2', 'D2', 'A1', 'C1', 'B1', 'C2', 'B2', 'D1') + + # Estimate when on target + state = ("A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 0 + + # Estimate when 1 is missing + state = ("XAB", "A2", "B1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 2 + + # Estimate when 1 is missing for B + state = ("A1", "A2", "XCD", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 40 + + # Estimate when 2 are inverted + state = ("B1", "A2", "A1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 44 + + # Estimate when 2 are inverted in bottom pieces + state = ("B2", "A1", "A2", "B1", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 66 + + # Estimate when start in LL + state = ("LL", "A2", "B1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 3 + + ######Manual testing of solution + if True: + states = [ + start, + ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "RL"), + ("A2", "D1", "A1", "C1", "B1", "C2", "B2", "RL"), + ("A2", "LR", "A1", "C1", "B1", "C2", "B2", "RL"), + ("A2", "LR", "A1", "C1", "B1", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "B1", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "XBC", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "XBC", "C2", "B1", "D1"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "B1", "D1"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "XBC", "D1"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "XBC", "D2"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "D1", "D2"), + ("A2", "LR", "A1", "B1", "C1", "C2", "D1", "D2"), + ("A2", "LR", "XAB", "B1", "C1", "C2", "D1", "D2"), + ("A2", "LR", "XAB", "B2", "C1", "C2", "D1", "D2"), + ("A2", "LR", "B1", "B2", "C1", "C2", "D1", "D2"), + ("A2", "A1", "B1", "B2", "C1", "C2", "D1", "D2"), + ] + + total_cost = 0 + for i in range(len(states) - 1): + # print('Starting from', states[i]) + # print('Getting to ', states[i+1]) + neighbors = get_neighbors(states[i]) + # print (neighbors) + + assert states[i + 1] in neighbors + assert is_state_valid(states[i + 1]) + cost = neighbors[states[i + 1]] + # print (estimate_to_complete(state_to_tuple(states[i])), 12521-total_cost) + # print ('Cost', cost) + total_cost += cost + # print ('Total cost', total_cost) + + +else: + len_state = 16 + group_size = len_state // 4 + + amphipod_targets = [ + "A", + "A", + "A", + "A", + "B", + "B", + "B", + "B", + "C", + "C", + "C", + "C", + "D", + "D", + "D", + "D", + ] + amphipod_all_targets = [ + ["A1", "A2", "A3", "A4"], + ["B1", "B2", "B3", "B4"], + ["C1", "C2", "C3", "C4"], + ["D1", "D2", "D3", "D4"], + ] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": {"A2": 1, "LR": 2, "XAB": 2, "XBC": 4, "XCD": 6, "RL": 8}, + "A2": {"A1": 1, "A3": 1}, + "A3": {"A2": 1, "A4": 1}, + "A4": {"A3": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": {"B2": 1, "LR": 4, "XAB": 2, "XBC": 2, "XCD": 4, "RL": 6}, + "B2": {"B1": 1, "B3": 1}, + "B3": {"B2": 1, "B4": 1}, + "B4": {"B3": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": {"C2": 1, "LR": 6, "XAB": 4, "XBC": 2, "XCD": 2, "RL": 4}, + "C2": {"C1": 1, "C3": 1}, + "C3": {"C2": 1, "C4": 1}, + "C4": {"C3": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": {"D2": 1, "LR": 8, "XAB": 6, "XBC": 4, "XCD": 2, "RL": 2}, + "D2": {"D1": 1, "D3": 1}, + "D3": {"D2": 1, "D4": 1}, + "D4": {"D3": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": {"RL": ["XAB", "XBC", "XCD"], "XBC": ["XAB"], "XCD": ["XAB", "XBC"]}, + "B1": {"LR": ["XAB"], "RL": ["XBC", "XCD"], "XCD": ["XBC"]}, + "C1": {"LR": ["XAB", "XBC"], "RL": ["XCD"], "XAB": ["XBC"]}, + "D1": {"LR": ["XAB", "XBC", "XCD"], "XAB": ["XBC", "XCD"], "XBC": ["XCD"]}, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + start_points = { + 1: ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ), + ############# + # ...........# + ###B#C#B#D### + # D#C#B#A# + # D#B#A#C# + # A#D#C#A# + ######### + "real": ( + "A1", + "C3", + "C4", + "D2", + "B3", + "C1", + "C2", + "D1", + "B1", + "B2", + "D3", + "D4", + "A2", + "A3", + "A4", + "B4", + ) + ############# + # ...........# + ###A#C#B#B### + # D#C#B#A# + # D#B#A#C# + # D#D#A#C# + ######### + } + start = start_points[case_to_test] + + amphipod_graph = StateGraph() + + if case_to_test == 1: + + ######is_state_valid + if True: + + state = start_points[case_to_test] + assert is_state_valid(state) == True + + state = ( + "A1", + "A2", + "A1", + "A2", + "B4", + "B2", + "B3", + "B2", + "C1", + "C2", + "C1", + "C2", + "D2", + "D3", + "D2", + "D4", + ) + assert is_state_valid(state) == False + + ######is_state_final + if True: + state = start_points[case_to_test] + assert is_state_final(state) == False + + state = ( + "A1", + "A2", + "A4", + "A3", + "B4", + "B2", + "B3", + "B1", + "C4", + "C2", + "C1", + "C3", + "D2", + "D3", + "D1", + "D4", + ) + assert is_state_final(state) == True + + ######is_movement_valid + if True: + # Rule set: + # Move within room + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # Forbidden: Moving from hallway to another hallway : Prevented by amphipods_edges (not tested here) + # Forbidden: Moving from X to another X is forbidden : Prevented by amphipods_edges (not tested here) + # Allowed: move within L or R spaces + # Allowed: Moving up in other's room + # Move to target + # Forbidden: Moving to my room if there are others in it + # Forbidden: Moving to my room with something in between + # Allowed: Moving to my room if only same amphibots are in and no obstacle + # Allowed: Moving to my room if empty and no obstacle + # Move to hallway from a room + # Forbidden: Moving out of my room if it's empty + # Forbidden: Moving out of my room if it's full of friends + # Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other's room if there are no obstacle + # Forbidden: Moving to other's room + + # Move within room + + # Allowed: Moving down in target room if full of friends + source = ( + "A4", + "A2", + "D2", + "D4", + "LR", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "LL", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "A2", "A3") + assert is_movement_valid(source, target, changed) == True + # Forbidden: Moving down in target room if full of friends + changed, target = tuple_replace(source, "A2", "A1") + assert is_movement_valid(source, target, changed) == False + + # Allowed: Moving up in target room if has other people + source = ( + "A3", + "LR", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A1", + "LL", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "A3", "A2") + assert is_movement_valid(source, target, changed) == True + # Forbidden: Moving down in target room if has other people + source = ( + "A3", + "LR", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A1", + "LL", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "A3", "A4") + assert is_movement_valid(source, target, changed) == False + + # Allowed: move within L or R spaces + _, source = tuple_replace(start, "A4", "LL") + changed, target = tuple_replace(source, "LL", "LR") + assert is_movement_valid(source, target, changed) == True + + # Allowed: Moving up in other's room + _, source = tuple_replace(start, "A1", "LL") + changed, target = tuple_replace(source, "A2", "A1") + assert is_movement_valid(source, target, changed) == True + + # Move to target + + # Forbidden: Moving to my room if there are others in it + _, source = tuple_replace(start, "D1", "LR") + changed, target = tuple_replace(source, "LR", "D1") + assert is_movement_valid(source, target, changed) == False + + # Forbidden: Moving to my room with something in between + _, source = tuple_replace(start, "D1", "XAB") + _, source = tuple_replace(source, "A4", "XBC") + changed, target = tuple_replace(source, "XAB", "D1") + assert is_movement_valid(source, target, changed) == False + + # Allowed: Moving to my room if only same amphibots are in and no obstacle + source = ( + "A3", + "C3", + "RL", + "D4", + "LL", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "RR", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "RL", "A1") + assert is_movement_valid(source, target, changed) == True + source = ( + "A3", + "A2", + "RL", + "D4", + "LL", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "RR", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "RL", "A1") + assert is_movement_valid(source, target, changed) == True + + # Allowed: Moving to my room if empty and no obstacle + source = ( + "RL", + "C3", + "XCD", + "D4", + "LL", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "RR", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "XCD", "A1") + assert is_movement_valid(source, target, changed) == True + + # Move to hallway from a room + + # Forbidden: Moving out of my room if it's empty + source = ( + "A4", + "C3", + "LL", + "LR", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "RR", + "A2", + "A3", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == False + + # Forbidden: Moving out of my room if it's full of friends + source = ( + "A4", + "C3", + "A2", + "A3", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "XAB", + "D2", + "D4", + "LL", + "D1", + ) + changed, target = tuple_replace(source, "D1", "XCD") + assert is_movement_valid(source, target, changed) == False + + # Allowed: Moving out of my room if there are other people in it and no obstacle + source = ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == True + + # Allowed: Moving out of other's room if there are no obstacle + source = start + changed, target = tuple_replace(source, "A1", "XAB") + assert is_movement_valid(source, target, changed) == True + + # Forbidden: Moving to other's room + source = ( + "A4", + "XAB", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "LR", + ) + changed, target = tuple_replace(source, "XAB", "D1") + assert is_movement_valid(source, target, changed) == False + + ######estimate_to_complete_amphipod ==> via estimate_to_complete + + ######estimate_to_complete + if True: + + # Estimate when on target + state = ( + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 0 + + # Estimate when 1 is missing + state = ( + "XAB", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 2 + + # Estimate for other amphipod + state = ( + "A1", + "A2", + "A3", + "A4", + "XCD", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 40 + + # Estimate when 2 are inverted + state = ( + "A1", + "A2", + "A3", + "B1", + "A1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 47 + + # Estimate when start in LL + state = ( + "LL", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 3 + + ######Manual testing of solution - Also allows to identify possible improvements + if True: + states = [ + start, + ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RL", + ), + ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "C3", + "D1", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LR", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "RL", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "RL", + "C1", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "C2", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C1", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "XBC", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C2", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "B1", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "XBC", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "C1", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B2", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B1", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B3", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B2", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B1", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B1", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B2", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "B1", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "B2", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "B3", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B1", + "B3", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "D2", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "D1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "XCD", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), # From solution + ( + "A4", + "LR", + "LL", + "D3", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D2", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D1", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), # + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D1", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D2", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D3", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D4", + "RR", + ), # + ( + "A4", + "LR", + "LL", + "RL", + "XAB", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D4", + "RR", + ), # + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A1", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "XCD", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D1", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D2", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "A3", + "D4", + "RR", + ), # + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "A2", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "A1", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A1", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A2", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), # + ( + "A4", + "A3", + "LR", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A1", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), # + ( + "A4", + "A3", + "A2", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D1", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "RR", + ), # + ( + "A4", + "A3", + "A2", + "A1", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "RR", + ), # + ( + "A4", + "A3", + "A2", + "A1", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "RL", + ), + ( + "A4", + "A3", + "A2", + "A1", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "D1", + ), ## + ############# + # AA.D.....AD# + ###B#.#C#.### + # D#B#C#.# + # D#B#C#.# + # A#B#C#.# + ######### + ] + + total_cost = 0 + for i in range(len(states) - 1): + print("Starting from", "\n" + state_to_text(states[i])) + neighbors = get_neighbors(states[i]) + print("Neighbors") + text = "" + neighbors_text = [ + state_to_text(neighbor).splitlines() for neighbor in neighbors + ] + + nb_row_per_neighbor = len(neighbors_text[0]) + for row in range( + math.ceil(len(neighbors_text) / 10) * nb_row_per_neighbor + ): + start_neighbor = row // nb_row_per_neighbor * 10 + text += ( + " ".join( + neighbors_text[start_neighbor + i][ + row % nb_row_per_neighbor + ] + for i in range(10) + if start_neighbor + i < len(neighbors_text) + ) + + "\n" + ) + if row % nb_row_per_neighbor == nb_row_per_neighbor - 1: + text += "\n" + + print(text) + print("Getting to ", "\n" + state_to_text(states[i + 1])) + + assert states[i + 1] in neighbors + assert is_state_valid(states[i + 1]) + cost = neighbors[states[i + 1]] + print( + estimate_to_complete(state_to_tuple(states[i])), 44169 - total_cost + ) + total_cost += cost + print("Cost", cost) + input() + # exit() + # print ('Total cost', total_cost) + + +amphipod_graph = StateGraph() + +print("Estimate from start", estimate_to_complete(state_to_tuple(start))) + +cProfile.run("amphipod_graph.a_star_search(start)") +# amphipod_graph.a_star_search(start) +for final_state in amphipod_graph.final_states: + print("Final path", amphipod_graph.path(state_to_tuple(final_state))) + + +puzzle_actual_result = amphipod_graph.min_distance + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-23 08:11:43.693421 +# Part 1: 2021-12-24 01:44:31 +# Part 2: 2021-12-26 15:00:00 diff --git a/2021/23-Amphipod.v5.py b/2021/23-Amphipod.v5.py new file mode 100644 index 0000000..671462f --- /dev/null +++ b/2021/23-Amphipod.v5.py @@ -0,0 +1,2737 @@ +# -------------------------------- Input data ---------------------------------------- # +import os, grid, graph, dot, assembly, re, itertools, copy, functools, time, math +from collections import Counter, deque, defaultdict +from functools import reduce, lru_cache +import heapq +import cProfile + + +from compass import * + + +# This functions come from https://github.com/mcpower/adventofcode - Thanks! +def lmap(func, *iterables): + return list(map(func, *iterables)) + + +def ints(s: str): + return lmap(int, re.findall(r"-?\d+", s)) # thanks mserrano! + + +def positive_ints(s: str): + return lmap(int, re.findall(r"\d+", s)) # thanks mserrano! + + +def floats(s: str): + return lmap(float, re.findall(r"-?\d+(?:\.\d+)?", s)) + + +def positive_floats(s: str): + return lmap(float, re.findall(r"\d+(?:\.\d+)?", s)) + + +def words(s: str): + return re.findall(r"[a-zA-Z]+", s) + + +test_data = {} + +test = 1 +test_data[test] = { + "input": """############# +#...........# +###B#C#B#D### + #A#D#C#A# + #########""", + "expected": ["12521", "44169"], +} + +test = "real" +input_file = os.path.join( + os.path.dirname(__file__), + "Inputs", + os.path.basename(__file__).replace(".py", ".txt"), +) +test_data[test] = { + "input": "", # open(input_file, "r+").read(), + "expected": ["18170", "50208"], +} + + +# -------------------------------- Control program execution ------------------------- # + +case_to_test = "real" +part_to_test = 2 +check_assertions = False + +# -------------------------------- Initialize some variables ------------------------- # + +puzzle_input = test_data[case_to_test]["input"] +puzzle_expected_result = test_data[case_to_test]["expected"][part_to_test - 1] +puzzle_actual_result = "Unknown" + + +# -------------------------------- Actual code execution ----------------------------- # + +# Now runs in a reasonable time +# Goal is to further optimize + +# Possible improvements: +# Major change: +# - Same algo: change positions to be numeric +# - Same algo: use sets for each group of amphipods (avoids having to convert them) +# - Change algo: each zone is a room, and use pop/prepend ro keep track of order + +# Final numbers +# Example part 1: 275619 function calls in 0.242 seconds +# Example part 2: 354914699 function calls in 349.813 seconds +# Real part 1: 726789 function calls in 0.612 seconds +# Real part 2: 120184853 function calls in 112.793 seconds + + +# Initial durations +# Example part 1 +# 771454 function calls in 0.700 seconds + +# Example part 2 +# About 2400 seconds + + +# Improvements done: +# If X is in Yn and can go to Y(n-1), force that as a neighbor (since it'll happen anyway) +# If X is in Xn and can go to X(n+1), force that as a neighbor (since it'll happen anyway) +# Doing both gave 2x gain on part 1, 8x on part 2 +# Example part 1 +# 500664 function calls in 0.466 seconds with the priorities (= multiple neighbors) +# 354634 function calls in 0.327 seconds with a single priority (= take 1st priority neighbor found) +# Example part 2 +# 348213851 function calls in 339.382 seconds with a single priority + + +# Allowing to go from X1 to Y1 (with proper 'blocks' in place if someone is in the way) +# Example part 1 +# 275619 function calls in 0.244 seconds +# Example part 2 +# 352620555 function calls in 339.027 seconds + +# Making it end as soon as a solution is found +# Example part 2 +# 352337447 function calls in 356.088 seconds ==> Probably not representative... + + +# Other attempts +# lru_cache on both estimate to complete & get_neighbors +# Example part 2 +# 352333566 function calls in 393.890 seconds ==> not a good idea + +# Remove lru_cache on state_to_tuple +# Example part 2 +# 354915167 function calls in 346.961 seconds + + +class StateGraph(graph.WeightedGraph): + final_states = [] + + def a_star_search(self, start, end=None): + current_distance = 0 + frontier = [(0, state_to_tuple(start), start, 0)] + heapq.heapify(frontier) + self.distance_from_start = {state_to_tuple(start): 0} + self.came_from = {state_to_tuple(start): None} + self.min_distance = float("inf") + + print("Starting search") + + while frontier: + ( + estimate_at_completion, + vertex_tuple, + vertex, + current_distance, + ) = heapq.heappop(frontier) + if (len(frontier)) % 10000 == 0: + print( + " Searching", + len(frontier), + self.min_distance, + estimate_at_completion, + current_distance, + ) + + if current_distance > self.min_distance: + continue + + if estimate_at_completion > self.min_distance: + continue + + neighbors = get_neighbors(vertex) + if not neighbors: + continue + + for neighbor, weight in neighbors.items(): + neighbor_tuple = state_to_tuple(neighbor) + # We've already checked that node, and it's not better now + if ( + neighbor_tuple in self.distance_from_start + and self.distance_from_start[neighbor_tuple] + <= (current_distance + weight) + ): + continue + + if current_distance + weight > self.min_distance: + continue + + # Adding for future examination + priority = current_distance + estimate_to_complete(neighbor_tuple) + heapq.heappush( + frontier, + (priority, neighbor_tuple, neighbor, current_distance + weight), + ) + + # Adding for final search + self.distance_from_start[neighbor_tuple] = current_distance + weight + self.came_from[neighbor_tuple] = vertex_tuple + + if is_state_final(neighbor): + self.min_distance = min( + self.min_distance, current_distance + weight + ) + print( + " Found", + self.min_distance, + "at", + len(frontier), + "for", + neighbor, + ) + return neighbor + self.final_states.append(neighbor) + + print("Search complete!") + return end in self.distance_from_start + + +# @lru_cache +def state_to_tuple(state): + return tuple( + tuple(sorted(state[group * group_size : (group + 1) * group_size])) + for group in range(4) + ) + + +@lru_cache +def is_state_final(state): + return all(amphipod_targets[i] == val[0] for i, val in enumerate(state)) + + +@lru_cache +def is_state_valid(state): + # Can't have 2 amphipods in the same place + if len(set(state)) != len(state): + # print ('Amphipod superposition') + return False + + return True + + +@lru_cache +def estimate_to_complete_amphipod(source, target): + estimate = 0 + amphipod_cost = amphipod_costs[target[0]] + # print ('Estimating', source, 'to', target) + # Not in target place + if source in ("LL", "RR"): + estimate += amphipod_cost + source = "LR" if source[0] == "L" else "RL" + # print ('Source in LL/RR, adding', amphipod_cost) + ##print ('LL/RR', i, source, amphipod_cost) + + if source[0] in "RLX": + ##print ('LX', i, source, amphipods_edges[source][target[0]+'1'] * amphipod_cost) + estimate += amphipods_edges[source][target[0] + "1"] * amphipod_cost + # print ('Source in RLX, adding', amphipods_edges[source][target[0]+'1'] * amphipod_cost) + source = target[0] + "1" + + if target[0] != source[0]: + # print ('Source in wrong ABCD room, adding', (2+2*abs(ord(source[0]) - ord(target[0]))) * amphipod_cost) + # From start to top position in room + estimate += abs(int(source[1]) - 1) * amphipod_cost + # From one room to the other, count 2 until hallway + 2 per room distance + estimate += (2 + 2 * abs(ord(source[0]) - ord(target[0]))) * amphipod_cost + + source = target[0] + "1" + + # Then add vertical moves within rooms + # print ('Adding vertical movements within target', abs(int(source[1]) - int(target[1])) * amphipod_cost) + estimate += abs(int(target[1]) - 1) * amphipod_cost + return estimate + + +@lru_cache +def estimate_to_complete_group(group, positions): + estimate = 0 + available = [x for x in amphipod_all_targets[group] if x not in positions] + for i, source in enumerate(positions): + if source[0] == "ABCD"[group]: + continue + target = available.pop() + estimate += estimate_to_complete_amphipod(source, target) + return estimate + + +def estimate_to_complete(state): + estimate = 0 + + for group in range(4): + estimate += estimate_to_complete_group(group, state[group]) + + return estimate + + +@lru_cache +def is_movement_valid(state, new_state, changed): + # print ('Checking', changed, 'from', state) + # print (' to', new_state) + current_position = state[changed] + current_room = current_position[0] + + new_position = new_state[changed] + new_room = new_position[0] + + target_room = amphipod_targets[changed] + target_id = changed // group_size + + # Moving within a room + if new_room == current_room: + # Forbidden: Moving with something in between + # Since all movements are by 1 only: If there was an obstable, 2 amphibots would be in the same place + + # Within my target room + if new_room == target_room: + # Room occupied by friends only (myself included) + amphi_in_target = set( + [ + amphipod_targets[state.index(target_room + str(i + 1))] + for i in range(group_size) + if target_room + str(i + 1) in state + ] + ) + if amphi_in_target == {target_room}: + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # print ('# Allowed: Moving down in target room if full of friends') + return new_position[-1] > current_position[-1], False + + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # print ('# Allowed: Moving up in target room if has other people') + return new_position[-1] < current_position[-1], False + + # Within a hallway + # Forbidden: Moving from hallway to another hallway + # Moving from X to another X is forbidden via amphipods_edges + + # Allowed: move within L or R spaces + if current_room in "LR": + # print ('# Allowed: move within L or R spaces') + return True, False + + # Allowed: Moving up in other's room + # print ('# Allowed: Moving up in other\'s room') + return new_position[-1] < current_position[-1], True + + ####### + # Move to my room + if new_room == target_room: + # Forbidden: Moving to my room if there are others in it + amphi_in_target = set( + [ + amphipod_targets[state.index(target_room + str(i + 1))] + for i in range(group_size) + if target_room + str(i + 1) in state + ] + ) + if amphi_in_target and amphi_in_target != {target_room}: + # print ('# Forbidden: Moving to my room if there are others in it') + return False, False + + # Forbidden: Moving with something in between + if current_position in amphipods_edges_conditions: + if new_position in amphipods_edges_conditions[current_position]: + # New position can't be blocking because it's not in the list of blocking ones + if any( + position + in amphipods_edges_conditions[current_position][new_position] + for position in new_state + ): + # print ('# Forbidden: Moving to my room with something in between') + return False, False + + # Allowed: Moving to my room if only same amphibots are in and no obstacle + # Allowed: Moving to my room if empty and no obstacle + # print ('# Allowed: Moving to my room if (empty OR only same amphibots are in) and no obstacle') + return True, True + + # Move to hallway from a room + if new_room in "XLR": + # Forbidden: Moving out of my room if it's empty + # Forbidden: Moving out of my room if it's full of friends + amphi_in_target = set( + [ + amphipod_targets[state.index(target_room + str(i + 1))] + for i in range(group_size) + if target_room + str(i + 1) in state + ] + ) + if current_room == target_room and ( + amphi_in_target == {target_room} or amphi_in_target == () + ): + # print ('# Forbidden: Moving out of my room if it\'s empty OR full of friends') + return False, False + + # Forbidden: Moving with something in between + if current_position in amphipods_edges_conditions: + if new_position in amphipods_edges_conditions[current_position]: + # New position can't be blocking because it's not in the list of blocking ones + if any( + position + in amphipods_edges_conditions[current_position][new_position] + for position in new_state + ): + # print ('# Forbidden: Moving to hallway with something in between') + return False, False + + # Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other's room is there are no obstacle + # print ('# Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other\'s room is there are no obstacle') + return True, False + + # Forbidden: Moving to other's room + return False, False + + +def get_neighbors(state): + neighbors = {} + if is_state_final(state): + # print ('Final state') + return {} + + for i in range(len_state): + # Forbidden: Moving from hallway to another hallway ==> Through amphipods_edges + for target, distance in amphipods_edges[state[i]].items(): + new_state = state[:i] + (target,) + state[i + 1 :] + # print (i, 'moves from', state[i], 'to', target) + # print ('new state', new_state) + if is_state_valid(new_state): + # print ('State valid') + is_valid, is_priority = is_movement_valid(state, new_state, i) + if is_valid: # is_movement_valid(state, new_state, i): + # print ('Movement valid') + if is_priority: + return { + new_state: distance * amphipod_costs[amphipod_targets[i]] + } + neighbors[new_state] = ( + distance * amphipod_costs[amphipod_targets[i]] + ) + + # print (state, neighbors) + + return neighbors + + +def tuple_replace(init, source, target): + position = init.index(source) + return position, init[:position] + (target,) + init[position + 1 :] + + +def state_to_text(state): + rows = [ + "#############", + ["#", "LL", "LR", ".", "XAB", ".", "XBC", ".", "XCD", ".", "RL", "RR", "#"], + ["#", "#", "#", "A1", "#", "B1", "#", "C1", "#", "D1", "#", "#", "#"], + [" ", " ", "#", "A2", "#", "B2", "#", "C2", "#", "D2", "#", " ", " "], + [" ", " ", "#", "A3", "#", "B3", "#", "C3", "#", "D3", "#", " ", " "], + [" ", " ", "#", "A4", "#", "B4", "#", "C4", "#", "D4", "#", " ", " "], + [" ", " ", "#", "#", "#", "#", "#", "#", "#", "#", "#", " ", " "], + ] + if group_size == 2: + del rows[4:6] + + text = "" + for row in rows: + text += "".join( + "ABCD"[state.index(i) // group_size] + if i in state + else i + if i in ".# " + else "." + for i in row + ) + text += "\n" + + return text + + +amphipod_costs = {"A": 1, "B": 10, "C": 100, "D": 1000} + + +if part_to_test == 1: + len_state = 8 + group_size = len_state // 4 + + amphipod_targets = ["A", "A", "B", "B", "C", "C", "D", "D"] + amphipod_all_targets = [["A1", "A2"], ["B1", "B2"], ["C1", "C2"], ["D1", "D2"]] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": { + "B1": 4, + "C1": 6, + "D1": 8, + "A2": 1, + "LR": 2, + "XAB": 2, + "XBC": 4, + "XCD": 6, + "RL": 8, + }, + "A2": {"A1": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": { + "A1": 4, + "C1": 4, + "D1": 6, + "B2": 1, + "LR": 4, + "XAB": 2, + "XBC": 2, + "XCD": 4, + "RL": 6, + }, + "B2": {"B1": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": { + "A1": 6, + "B1": 4, + "D1": 4, + "C2": 1, + "LR": 6, + "XAB": 4, + "XBC": 2, + "XCD": 2, + "RL": 4, + }, + "C2": {"C1": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": { + "A1": 8, + "B1": 6, + "C1": 4, + "D2": 1, + "LR": 8, + "XAB": 6, + "XBC": 4, + "XCD": 2, + "RL": 2, + }, + "D2": {"D1": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": { + "B1": ["XAB"], + "C1": ["XAB", "XBC"], + "D1": ["XAB", "XBC", "XCD"], + "RL": ["XAB", "XBC", "XCD"], + "XBC": ["XAB"], + "XCD": ["XAB", "XBC"], + }, + "B1": { + "A1": ["XAB"], + "C1": ["XBC"], + "D1": ["XBC", "XCD"], + "LR": ["XAB"], + "RL": ["XBC", "XCD"], + "XCD": ["XBC"], + }, + "C1": { + "A1": ["XAB", "XBC"], + "B1": ["XBC"], + "D1": ["XCD"], + "LR": ["XAB", "XBC"], + "RL": ["XCD"], + "XAB": ["XBC"], + }, + "D1": { + "A1": ["XAB", "XBC", "XCD"], + "B1": ["XBC", "XCD"], + "C1": ["XCD"], + "LR": ["XAB", "XBC", "XCD"], + "XAB": ["XBC", "XCD"], + "XBC": ["XCD"], + }, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + start_points = { + 1: ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1"), + ############# + # ...........# + ###B#C#B#D### + # A#D#C#A# + ######### + "real": ("A1", "C2", "C1", "D1", "B1", "D2", "A2", "B2") + ############# + # ...........# + ###A#C#B#B### + # D#D#A#C# + ######### + } + start = start_points[case_to_test] + + if case_to_test == 1: + + ######is_state_valid + if check_assertions: + state = start_points[case_to_test] + assert is_state_valid(state) == True + + state = ("A1", "A2", "A1", "A2", "B4", "B2", "B3", "B2") + assert is_state_valid(state) == False + + ######is_state_final + if check_assertions: + state = start_points[case_to_test] + assert is_state_final(state) == False + + state = ("A1", "A2", "B4", "B2", "C4", "C2", "D2", "D3") + assert is_state_final(state) == True + + ######is_movement_valid + if check_assertions: + # Rule set: + # Move within room + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # Forbidden: Moving from hallway to another hallway : Prevented by amphipods_edges (not tested here) + # Forbidden: Moving from X to another X is forbidden : Prevented by amphipods_edges (not tested here) + # Allowed: move within L or R spaces + # Allowed: Moving up in other's room + # Move to target + # Forbidden: Moving to my room if there are others in it + # Forbidden: Moving to my room with something in between + # Allowed: Moving to my room if only same amphibots are in and no obstacle + # Allowed: Moving to my room if empty and no obstacle + # Move to hallway from a room + # Forbidden: Moving out of my room if it's empty + # Forbidden: Moving out of my room if it's full of friends + # Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other's room if there are no obstacle + # Forbidden: Moving to other's room + + # Move within room + + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # Technically not feasible because there are 2 places only + + # Allowed: move within L or R spaces + _, source = tuple_replace(start, "A2", "LL") + changed, target = tuple_replace(source, "LL", "LR") + assert is_movement_valid(source, target, changed) == (True, False) + + # Allowed: Moving up in other's room + _, source = tuple_replace(start, "B1", "LL") + changed, target = tuple_replace(source, "B2", "B1") + assert is_movement_valid(source, target, changed) == (True, True) + + # state = ('A2', 'D2', 'A1', 'C1', 'B1', 'C2', 'B2', 'D1') + + # Move to target + + # Forbidden: Moving to my room if there are others in it + _, source = tuple_replace(start, "D1", "LR") + changed, target = tuple_replace(source, "LR", "D1") + assert is_movement_valid(source, target, changed) == (False, False) + + # Forbidden: Moving to my room with something in between + _, source = tuple_replace(start, "D1", "XAB") + _, source = tuple_replace(source, "A2", "XBC") + changed, target = tuple_replace(source, "XAB", "D1") + assert is_movement_valid(source, target, changed) == (False, False) + + # Allowed: Moving to my room if only same amphibots are in and no obstacle + source = ("A2", "XAB", "LR", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "XAB", "A1") + assert is_movement_valid(source, target, changed) == (True, True) + + # Allowed: Moving to my room if empty and no obstacle + source = ("LR", "XAB", "LR", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "XAB", "A1") + assert is_movement_valid(source, target, changed) == (True, True) + + # Move to hallway from a room + + # Forbidden: Moving out of my room if it's empty + source = ("A2", "LL", "A1", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == (False, False) + + # Forbidden: Moving out of my room if it's full of friends + source = ("A2", "LL", "A1", "C1", "B1", "C2", "D2", "D1") + changed, target = tuple_replace(source, "D1", "XCD") + assert is_movement_valid(source, target, changed) == (False, False) + + # Allowed: Moving out of my room if there are other people in it and no obstacle + source = ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "D1") + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == (True, False) + + # Allowed: Moving out of other's room if there are no obstacle + source = start + changed, target = tuple_replace(source, "A1", "XAB") + assert is_movement_valid(source, target, changed) == (True, False) + + # Forbidden: Moving to other's room + source = ("XAB", "D2", "A1", "C1", "LR", "C2", "B2", "D1") + changed, target = tuple_replace(source, "XAB", "B1") + assert is_movement_valid(source, target, changed) == (False, False) + + ######estimate_to_complete_amphipod ==> via estimate_to_complete + + ######estimate_to_complete + if check_assertions: + # Start ('A2', 'D2', 'A1', 'C1', 'B1', 'C2', 'B2', 'D1') + + # Estimate when on target + state = ("A1", "A2", "B1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 0 + + # Estimate when 1 is missing + state = ("XAB", "A2", "B1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 2 + + # Estimate when 1 is missing for B + state = ("A1", "A2", "XCD", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 40 + + # Estimate when 2 are inverted + state = ("B1", "A2", "A1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 44 + + # Estimate when 2 are inverted in bottom pieces + state = ("B2", "A1", "A2", "B1", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 66 + + # Estimate when start in LL + state = ("LL", "A2", "B1", "B2", "C1", "C2", "D1", "D2") + assert estimate_to_complete(state_to_tuple(state)) == 3 + + ######Manual testing of solution + if check_assertions: + states = [ + start, + ("A2", "D2", "A1", "C1", "B1", "C2", "B2", "RL"), + ("A2", "D1", "A1", "C1", "B1", "C2", "B2", "RL"), + ("A2", "LR", "A1", "C1", "B1", "C2", "B2", "RL"), + ("A2", "LR", "A1", "C1", "B1", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "B1", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "XBC", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "B2", "D1"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "B1", "D1"), + ("A2", "LR", "A1", "XAB", "C1", "C2", "XBC", "D1"), + ("A2", "LR", "A1", "B1", "C1", "C2", "XBC", "D1"), + ("A2", "LR", "A1", "B1", "C1", "C2", "XBC", "D2"), + ("A2", "LR", "A1", "B1", "C1", "C2", "D1", "D2"), + ("A2", "LR", "XAB", "B1", "C1", "C2", "D1", "D2"), + ("A2", "A1", "XAB", "B1", "C1", "C2", "D1", "D2"), + ("A2", "A1", "XAB", "B2", "C1", "C2", "D1", "D2"), + ("A2", "A1", "B1", "B2", "C1", "C2", "D1", "D2"), + ] + + total_cost = 0 + for i in range(len(states) - 1): + print("Starting from", states[i]) + print(state_to_text(states[i])) + neighbors = get_neighbors(states[i]) + print("Neighbors") + text = "" + neighbors_text = [ + state_to_text(neighbor).splitlines() for neighbor in neighbors + ] + + nb_row_per_neighbor = len(neighbors_text[0]) + for row in range( + math.ceil(len(neighbors_text) / 10) * nb_row_per_neighbor + ): + start_neighbor = row // nb_row_per_neighbor * 10 + text += ( + " ".join( + neighbors_text[start_neighbor + i][ + row % nb_row_per_neighbor + ] + for i in range(10) + if start_neighbor + i < len(neighbors_text) + ) + + "\n" + ) + if row % nb_row_per_neighbor == nb_row_per_neighbor - 1: + text += "\n" + + print(text) + print("Getting to ", "\n" + state_to_text(states[i + 1])) + + assert states[i + 1] in neighbors + assert is_state_valid(states[i + 1]) + cost = neighbors[states[i + 1]] + print( + estimate_to_complete(state_to_tuple(states[i])), 44169 - total_cost + ) + total_cost += cost + print("Cost", cost) + input() + # print ('Total cost', total_cost) + + +else: + len_state = 16 + group_size = len_state // 4 + + amphipod_targets = [ + "A", + "A", + "A", + "A", + "B", + "B", + "B", + "B", + "C", + "C", + "C", + "C", + "D", + "D", + "D", + "D", + ] + amphipod_all_targets = [ + ["A1", "A2", "A3", "A4"], + ["B1", "B2", "B3", "B4"], + ["C1", "C2", "C3", "C4"], + ["D1", "D2", "D3", "D4"], + ] + amphipods_edges = { + "LL": {"LR": 1}, + "LR": {"LL": 1, "A1": 2, "B1": 4, "C1": 6, "D1": 8}, + "A1": { + "B1": 4, + "C1": 6, + "D1": 8, + "A2": 1, + "LR": 2, + "XAB": 2, + "XBC": 4, + "XCD": 6, + "RL": 8, + }, + "A2": {"A1": 1, "A3": 1}, + "A3": {"A2": 1, "A4": 1}, + "A4": {"A3": 1}, + "XAB": {"A1": 2, "B1": 2, "C1": 4, "D1": 6}, + "B1": { + "A1": 4, + "C1": 4, + "D1": 6, + "B2": 1, + "LR": 4, + "XAB": 2, + "XBC": 2, + "XCD": 4, + "RL": 6, + }, + "B2": {"B1": 1, "B3": 1}, + "B3": {"B2": 1, "B4": 1}, + "B4": {"B3": 1}, + "XBC": {"B1": 2, "C1": 2, "A1": 4, "D1": 4}, + "C1": { + "A1": 6, + "B1": 4, + "D1": 4, + "C2": 1, + "LR": 6, + "XAB": 4, + "XBC": 2, + "XCD": 2, + "RL": 4, + }, + "C2": {"C1": 1, "C3": 1}, + "C3": {"C2": 1, "C4": 1}, + "C4": {"C3": 1}, + "XCD": {"C1": 2, "D1": 2, "A1": 6, "B1": 4}, + "D1": { + "A1": 8, + "B1": 6, + "C1": 4, + "D2": 1, + "LR": 8, + "XAB": 6, + "XBC": 4, + "XCD": 2, + "RL": 2, + }, + "D2": {"D1": 1, "D3": 1}, + "D3": {"D2": 1, "D4": 1}, + "D4": {"D3": 1}, + "RL": {"RR": 1, "A1": 8, "B1": 6, "C1": 4, "D1": 2}, + "RR": {"RL": 1}, + } + + amphipods_edges_conditions = { + "XAB": {"C1": ["XBC"], "D1": ["XBC", "XCD"]}, + "XBC": {"A1": ["XAB"], "D1": ["XCD"]}, + "XCD": {"A1": ["XAB", "XBC"], "B1": ["XBC"]}, + "A1": { + "B1": ["XAB"], + "C1": ["XAB", "XBC"], + "D1": ["XAB", "XBC", "XCD"], + "RL": ["XAB", "XBC", "XCD"], + "XBC": ["XAB"], + "XCD": ["XAB", "XBC"], + }, + "B1": { + "A1": ["XAB"], + "C1": ["XBC"], + "D1": ["XBC", "XCD"], + "LR": ["XAB"], + "RL": ["XBC", "XCD"], + "XCD": ["XBC"], + }, + "C1": { + "A1": ["XAB", "XBC"], + "B1": ["XBC"], + "D1": ["XCD"], + "LR": ["XAB", "XBC"], + "RL": ["XCD"], + "XAB": ["XBC"], + }, + "D1": { + "A1": ["XAB", "XBC", "XCD"], + "B1": ["XBC", "XCD"], + "C1": ["XCD"], + "LR": ["XAB", "XBC", "XCD"], + "XAB": ["XBC", "XCD"], + "XBC": ["XCD"], + }, + "LR": {"B1": ["XAB"], "C1": ["XAB", "XBC"], "D1": ["XAB", "XBC", "XCD"]}, + "RL": {"A1": ["XAB", "XBC", "XCD"], "B1": ["XBC", "XCD"], "C1": ["XCD"]}, + } + + start_points = { + 1: ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ), + ############# + # ...........# + ###B#C#B#D### + # D#C#B#A# + # D#B#A#C# + # A#D#C#A# + ######### + "real": ( + "A1", + "C3", + "C4", + "D2", + "B3", + "C1", + "C2", + "D1", + "B1", + "B2", + "D3", + "D4", + "A2", + "A3", + "A4", + "B4", + ) + ############# + # ...........# + ###A#C#B#B### + # D#C#B#A# + # D#B#A#C# + # D#D#A#C# + ######### + } + start = start_points[case_to_test] + + amphipod_graph = StateGraph() + + if case_to_test == 1: + + ######is_state_valid + if check_assertions: + + state = start_points[case_to_test] + assert is_state_valid(state) == True + + state = ( + "A1", + "A2", + "A1", + "A2", + "B4", + "B2", + "B3", + "B2", + "C1", + "C2", + "C1", + "C2", + "D2", + "D3", + "D2", + "D4", + ) + assert is_state_valid(state) == False + + ######is_state_final + if check_assertions: + state = start_points[case_to_test] + assert is_state_final(state) == False + + state = ( + "A1", + "A2", + "A4", + "A3", + "B4", + "B2", + "B3", + "B1", + "C4", + "C2", + "C1", + "C3", + "D2", + "D3", + "D1", + "D4", + ) + assert is_state_final(state) == True + + ######is_movement_valid + if check_assertions: + # Rule set: + # Move within room + # Allowed: Moving down in target room if full of friends + # Forbidden: Moving down in target room if full of friends + # Allowed: Moving up in target room if has other people + # Forbidden: Moving down in target room if has other people + # Forbidden: Moving from hallway to another hallway : Prevented by amphipods_edges (not tested here) + # Forbidden: Moving from X to another X is forbidden : Prevented by amphipods_edges (not tested here) + # Allowed: move within L or R spaces + # Allowed: Moving up in other's room + # Move to target + # Forbidden: Moving to my room if there are others in it + # Forbidden: Moving to my room with something in between + # Allowed: Moving to my room if only same amphibots are in and no obstacle + # Allowed: Moving to my room if empty and no obstacle + # Move to hallway from a room + # Forbidden: Moving out of my room if it's empty + # Forbidden: Moving out of my room if it's full of friends + # Allowed: Moving out of my room if there are other people in it and no obstacle + # Allowed: Moving out of other's room if there are no obstacle + # Forbidden: Moving to other's room + + # Move within room + + # Allowed: Moving down in target room if full of friends + source = ( + "A4", + "A2", + "D2", + "D4", + "LR", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "LL", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "A2", "A3") + assert is_movement_valid(source, target, changed) == (True, False) + # Forbidden: Moving down in target room if full of friends + changed, target = tuple_replace(source, "A2", "A1") + assert is_movement_valid(source, target, changed) == (False, False) + + # Allowed: Moving up in target room if has other people + source = ( + "A3", + "LR", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A1", + "LL", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "A3", "A2") + assert is_movement_valid(source, target, changed) == (True, False) + # Forbidden: Moving down in target room if has other people + source = ( + "A3", + "LR", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A1", + "LL", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "A3", "A4") + assert is_movement_valid(source, target, changed) == (False, False) + + # Allowed: move within L or R spaces + _, source = tuple_replace(start, "A4", "LL") + changed, target = tuple_replace(source, "LL", "LR") + assert is_movement_valid(source, target, changed) == (True, False) + + # Allowed: Moving up in other's room + _, source = tuple_replace(start, "A1", "LL") + changed, target = tuple_replace(source, "A2", "A1") + assert is_movement_valid(source, target, changed) == (True, True) + + # Move to target + + # Forbidden: Moving to my room if there are others in it + _, source = tuple_replace(start, "D1", "LR") + changed, target = tuple_replace(source, "LR", "D1") + assert is_movement_valid(source, target, changed) == (False, False) + + # Forbidden: Moving to my room with something in between + _, source = tuple_replace(start, "D1", "XAB") + _, source = tuple_replace(source, "A4", "XBC") + changed, target = tuple_replace(source, "XAB", "D1") + assert is_movement_valid(source, target, changed) == (False, False) + + # Allowed: Moving to my room if only same amphibots are in and no obstacle + source = ( + "A3", + "C3", + "RL", + "D4", + "LL", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "RR", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "RL", "A1") + assert is_movement_valid(source, target, changed) == (True, True) + source = ( + "A3", + "A2", + "RL", + "D4", + "LL", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "RR", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "RL", "A1") + assert is_movement_valid(source, target, changed) == (True, True) + + # Allowed: Moving to my room if empty and no obstacle + source = ( + "RL", + "C3", + "XCD", + "D4", + "LL", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "LR", + "RR", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "XCD", "A1") + assert is_movement_valid(source, target, changed) == (True, True) + + # Move to hallway from a room + + # Forbidden: Moving out of my room if it's empty + source = ( + "A4", + "C3", + "LL", + "LR", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "RR", + "A2", + "A3", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == (False, False) + + # Forbidden: Moving out of my room if it's full of friends + source = ( + "A4", + "C3", + "A2", + "A3", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "XAB", + "D2", + "D4", + "LL", + "D1", + ) + changed, target = tuple_replace(source, "D1", "XCD") + assert is_movement_valid(source, target, changed) == (False, False) + + # Allowed: Moving out of my room if there are other people in it and no obstacle + source = ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "D1", + ) + changed, target = tuple_replace(source, "D1", "XAB") + assert is_movement_valid(source, target, changed) == (True, False) + + # Allowed: Moving out of other's room if there are no obstacle + source = start + changed, target = tuple_replace(source, "A1", "XAB") + assert is_movement_valid(source, target, changed) == (True, False) + + # Forbidden: Moving to other's room + source = ( + "A4", + "XAB", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "LR", + ) + changed, target = tuple_replace(source, "XAB", "D1") + assert is_movement_valid(source, target, changed) == (False, False) + + ######estimate_to_complete_amphipod ==> via estimate_to_complete + + ######estimate_to_complete + if check_assertions: + + # Estimate when on target + state = ( + "A1", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 0 + + # Estimate when 1 is missing + state = ( + "XAB", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 2 + + # Estimate for other amphipod + state = ( + "A1", + "A2", + "A3", + "A4", + "XCD", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 40 + + # Estimate when 2 are inverted + state = ( + "A1", + "A2", + "A3", + "B1", + "A1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 47 + + # Estimate when start in LL + state = ( + "LL", + "A2", + "A3", + "A4", + "B1", + "B2", + "B3", + "B4", + "C1", + "C2", + "C3", + "C4", + "D1", + "D2", + "D3", + "D4", + ) + assert estimate_to_complete(state_to_tuple(state)) == 3 + + ######Manual testing of solution - Also allows to identify possible improvements + if check_assertions: + states = [ + start, + ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RL", + ), + ( + "A4", + "C3", + "D2", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "D1", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LR", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "C1", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "RL", + "C2", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "RL", + "C1", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C3", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C2", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "C1", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "B1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "XBC", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C1", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C2", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "B2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "B1", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "XBC", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "C1", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B2", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B1", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B3", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B2", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "B1", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "XBC", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B1", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B2", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B3", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "XCD", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "B1", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "B2", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "RL", + "B3", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B1", + "B3", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "D3", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "D2", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "D1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "XCD", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D4", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D3", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D2", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "D1", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "XAB", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D1", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D2", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D3", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "A1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "XAB", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A2", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "A1", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "XCD", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D1", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D2", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "A3", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "A2", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "A1", + "D4", + "RR", + ), + ( + "A4", + "LR", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A1", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A2", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "LL", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "LR", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A1", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "XAB", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D1", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "RL", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "A1", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "RR", + ), + ( + "A4", + "A3", + "A2", + "A1", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "RL", + ), + ( + "A4", + "A3", + "A2", + "A1", + "B1", + "B4", + "B2", + "B3", + "C3", + "C2", + "C4", + "C1", + "D3", + "D2", + "D4", + "D1", + ), + ############# + # AA.D.....AD# + ###B#.#C#.### + # D#B#C#.# + # D#B#C#.# + # A#B#C#.# + ######### + ] + + total_cost = 0 + for i in range(len(states) - 1): + print("Starting from", i, states[i], "\n" + state_to_text(states[i])) + neighbors = get_neighbors(states[i]) + print("Neighbors") + text = "" + neighbors_text = [ + state_to_text(neighbor).splitlines() for neighbor in neighbors + ] + + nb_row_per_neighbor = len(neighbors_text[0]) + for row in range( + math.ceil(len(neighbors_text) / 10) * nb_row_per_neighbor + ): + start_neighbor = row // nb_row_per_neighbor * 10 + text += ( + " ".join( + neighbors_text[start_neighbor + i][ + row % nb_row_per_neighbor + ] + for i in range(10) + if start_neighbor + i < len(neighbors_text) + ) + + "\n" + ) + if row % nb_row_per_neighbor == nb_row_per_neighbor - 1: + text += "\n" + + print(text) + print("Getting to ", "\n" + state_to_text(states[i + 1])) + + assert states[i + 1] in neighbors + assert is_state_valid(states[i + 1]) + cost = neighbors[states[i + 1]] + print( + estimate_to_complete(state_to_tuple(states[i])), 44169 - total_cost + ) + total_cost += cost + print("Cost", cost) + # input() + exit() + # print ('Total cost', total_cost) + + +amphipod_graph = StateGraph() + +print("Estimate from start", estimate_to_complete(state_to_tuple(start))) + +cProfile.run("amphipod_graph.a_star_search(start)") +# amphipod_graph.a_star_search(start) +# for final_state in amphipod_graph.final_states: +# print ('Final path', amphipod_graph.path(state_to_tuple(final_state))) + + +puzzle_actual_result = amphipod_graph.min_distance + + +# -------------------------------- Outputs / results --------------------------------- # + +print("Case :", case_to_test, "- Part", part_to_test) +print("Expected result : " + str(puzzle_expected_result)) +print("Actual result : " + str(puzzle_actual_result)) +# Date created: 2021-12-23 08:11:43.693421 +# Part 1: 2021-12-24 01:44:31 +# Part 2: 2021-12-26 15:00:00