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 01/97] 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 02/97] 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 03/97] 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 04/97] 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 05/97] 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 06/97] 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 07/97] 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 08/97] 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 09/97] 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 10/97] 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 11/97] 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 12/97] 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 13/97] 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 14/97] 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 15/97] 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 16/97] 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 17/97] 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 18/97] 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 19/97] 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 20/97] 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 21/97] 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 22/97] 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 23/97] 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 24/97] 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 25/97] 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 26/97] 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 27/97] 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 28/97] 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 29/97] 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 30/97] 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 31/97] 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 32/97] 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 33/97] 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 34/97] 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 35/97] 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 36/97] 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 37/97] 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 38/97] 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 39/97] 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 40/97] 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 41/97] 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 42/97] 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 43/97] 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 44/97] 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 45/97] 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 46/97] 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 47/97] 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 48/97] 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 49/97] 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 50/97] 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 51/97] 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 52/97] 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 53/97] 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 54/97] 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 55/97] 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 56/97] 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 57/97] 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 58/97] 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 59/97] 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 60/97] 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 61/97] 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 62/97] 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 63/97] 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 64/97] 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 65/97] 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 66/97] 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 67/97] 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 68/97] 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 69/97] 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 70/97] 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 71/97] 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 72/97] 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 73/97] 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 74/97] 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 75/97] 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 76/97] 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 77/97] 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 78/97] 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 79/97] 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 80/97] 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 81/97] 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 82/97] 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 83/97] 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 84/97] 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 85/97] 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 86/97] 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 87/97] 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 88/97] 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 89/97] 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 90/97] 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 91/97] 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 92/97] 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 93/97] 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 94/97] 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 95/97] 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 96/97] 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 97/97] 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