diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 078bdd3..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -Inputs/ -template.py -__pycache__ -parse/ -download.py \ No newline at end of file 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)) 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)) diff --git a/2020/23-Crab Cups.py b/2020/23-Crab Cups.py index 9ad0f81..397d2d2 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! @@ -64,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 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..19389b7 --- /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/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/06-Lanternfish.py b/2021/06-Lanternfish.py new file mode 100644 index 0000000..690c5d5 --- /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) 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..41f9cab --- /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": ["543", "994266"], +} + + +# -------------------------------- 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 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 diff --git a/2021/14-Extended Polymerization.py b/2021/14-Extended Polymerization.py new file mode 100644 index 0000000..60dbbe4 --- /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 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 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 diff --git a/2021/20-Trench Map.py b/2021/20-Trench Map.py new file mode 100644 index 0000000..94de766 --- /dev/null +++ b/2021/20-Trench Map.py @@ -0,0 +1,115 @@ +# -------------------------------- 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 ----------------------------- # + +all_directions = directions_diagonals + + +if part_to_test == 1: + generations = 2 +else: + generations = 50 + +# Parsing algorithm +algorithm = puzzle_input.split("\n")[0] + +rules = {} +for i in range(2 ** 9): + binary = "{0:>09b}".format(i) + text = binary.replace("0", ".").replace("1", "#") + rules[text] = algorithm[i] + +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): + image.evolve(1) + if i % 2 == 1: + image.reduce_grid(2) + image.extend_grid(2) + +image.reduce_grid(2) + +puzzle_actual_result = image.dots_to_text().count("#") + + +# -------------------------------- 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 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 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 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 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 diff --git a/2021/24-Arithmetic Logic Unit.ods b/2021/24-Arithmetic Logic Unit.ods new file mode 100644 index 0000000..f024b9b Binary files /dev/null and b/2021/24-Arithmetic Logic Unit.ods differ 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 diff --git a/2021/25-Sea Cucumber.py b/2021/25-Sea Cucumber.py new file mode 100644 index 0000000..0547716 --- /dev/null +++ b/2021/25-Sea Cucumber.py @@ -0,0 +1,145 @@ +# -------------------------------- 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 +# Part 2: 2021-12-25 15:00:00 diff --git a/2021/assembly.py b/2021/assembly.py new file mode 100644 index 0000000..f7bf8f0 --- /dev/null +++ b/2021/assembly.py @@ -0,0 +1,553 @@ +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][0] + 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]) + ) + + # 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( + 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]], + 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]], + 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..aedbdd3 --- /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 str(self.terrain) + "@" + complex(self.position).__str__() + else: + return ( + str(self.terrain) + + "@" + + complex(self.position).__str__() + + direction_to_text[self.source_direction] + ) + + def __str__(self): + return str(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.terrain_default + self.is_walkable, self.is_waypoint = self.terrain_map.get( + self.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..0756230 --- /dev/null +++ b/2021/graph.py @@ -0,0 +1,595 @@ +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 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 + + 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 + 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..35b5046 --- /dev/null +++ b/2021/grid.py @@ -0,0 +1,1195 @@ +from compass import * +from dot import Dot +from graph import WeightedGraph +from functools import lru_cache, reduce +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="", 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] + if self.is_isotropic: + self.dots[x - y * 1j] = Dot(self, x - y * 1j, value) + else: + for dir in self.possible_source_directions.get( + value, self.direction_default + ): + self.dots[(x - y * 1j, dir)] = Dot( + self, x - y * 1j, value, dir + ) + 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] = Dot(self, 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: + 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 text representing a border + """ + + 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([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("".join(dot.terrain 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 = 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()))) + + 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 = 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()))) + + 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, rows_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 = [] + + 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 = [] + + 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 + + +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 + + 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 + + +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)