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 01/32] 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 02/32] 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 03/32] 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 04/32] 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 05/32] 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 06/32] 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 07/32] 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 08/32] 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 09/32] 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 10/32] 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 11/32] 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 12/32] 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 13/32] 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 14/32] 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 15/32] 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 16/32] 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 17/32] 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 18/32] 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 19/32] 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 20/32] 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 21/32] 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 22/32] 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 23/32] 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 24/32] 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 25/32] 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 26/32] 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 27/32] 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 28/32] 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 29/32] 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 30/32] 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 31/32] 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 32/32] 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 pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy

Alternative Proxy